mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 14:24:47 -07:00
query: fixes
This commit is contained in:
1
packages/brk_client/tests/mempool_compat/.gitignore
vendored
Normal file
1
packages/brk_client/tests/mempool_compat/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*md
|
||||
@@ -28,6 +28,8 @@ from typing import Any
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from brk_client import BrkClient
|
||||
|
||||
# Make `_lib` and `_endpoints` importable from any nested test file.
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
@@ -118,7 +120,16 @@ class LiveData:
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def brk():
|
||||
return ApiClient(BRK_BASE, "brk")
|
||||
"""Typed brk_client SDK pointed at the brk server under test.
|
||||
|
||||
All tests must go through this fixture's typed methods (e.g.
|
||||
`brk.get_difficulty_adjustment()`). If a typed method is missing or
|
||||
broken, that's an SDK finding — fix the bindgen before adapting the
|
||||
test. Never reach into raw HTTP from a compat test.
|
||||
"""
|
||||
client = BrkClient(BRK_BASE)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@@ -127,12 +138,12 @@ def mempool():
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def check_servers(brk, mempool):
|
||||
def check_servers(mempool):
|
||||
"""Fail fast if either server is unreachable."""
|
||||
try:
|
||||
brk.get("/api/blocks/tip/height")
|
||||
BrkClient(BRK_BASE).get_text("/api/blocks/tip/height")
|
||||
except Exception as e:
|
||||
pytest.exit(f"brk server not reachable at {brk.base_url}: {e}")
|
||||
pytest.exit(f"brk server not reachable at {BRK_BASE}: {e}")
|
||||
try:
|
||||
mempool.get("/api/blocks/tip/height")
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,35 +1,103 @@
|
||||
"""GET /api/v1/difficulty-adjustment"""
|
||||
|
||||
from _lib import assert_same_structure, show
|
||||
import time
|
||||
|
||||
from _lib import assert_same_structure, assert_same_values, show
|
||||
|
||||
|
||||
DIFFICULTY_KEYS = [
|
||||
REQUIRED_KEYS = {
|
||||
"progressPercent", "difficultyChange", "estimatedRetargetDate",
|
||||
"remainingBlocks", "remainingTime", "previousRetarget",
|
||||
"previousTime", "nextRetargetHeight", "timeAvg",
|
||||
"adjustedTimeAvg", "timeOffset", "expectedBlocks",
|
||||
]
|
||||
}
|
||||
|
||||
# Fields derived purely from chain state (heights and confirmed block
|
||||
# timestamps) — these must match mempool.space within float tolerance.
|
||||
# All other fields depend on the wall clock at request time and will drift.
|
||||
CHAIN_DETERMINISTIC_FIELDS = {
|
||||
"progressPercent", "remainingBlocks", "nextRetargetHeight",
|
||||
"previousTime", "previousRetarget",
|
||||
}
|
||||
|
||||
|
||||
def test_difficulty_adjustment(brk, mempool):
|
||||
"""Difficulty adjustment must have the same structure."""
|
||||
def test_difficulty_adjustment_shape(brk, mempool):
|
||||
"""Response must have every key mempool.space returns, with matching types."""
|
||||
path = "/api/v1/difficulty-adjustment"
|
||||
b = brk.get_json(path)
|
||||
b = brk.get_difficulty_adjustment()
|
||||
m = mempool.get_json(path)
|
||||
show("GET", path, b, m)
|
||||
assert_same_structure(b, m)
|
||||
for key in DIFFICULTY_KEYS:
|
||||
assert key in b, f"brk missing '{key}'"
|
||||
missing = REQUIRED_KEYS - set(b.keys())
|
||||
assert not missing, f"brk missing keys: {missing}"
|
||||
|
||||
|
||||
def test_difficulty_adjustment_values_sane(brk, mempool):
|
||||
"""Progress must be 0-100 %, remaining blocks must be 0-2016."""
|
||||
path = "/api/v1/difficulty-adjustment"
|
||||
for label, client in [("brk", brk), ("mempool", mempool)]:
|
||||
d = client.get_json(path)
|
||||
assert 0 <= d["progressPercent"] <= 100, (
|
||||
f"{label} progressPercent out of range: {d['progressPercent']}"
|
||||
)
|
||||
assert 0 <= d["remainingBlocks"] <= 2016, (
|
||||
f"{label} remainingBlocks out of range: {d['remainingBlocks']}"
|
||||
def test_difficulty_adjustment_chain_values_match_mempool(brk, mempool):
|
||||
"""Chain-derived fields must equal mempool.space's within float tolerance."""
|
||||
b = brk.get_difficulty_adjustment()
|
||||
m = mempool.get_json("/api/v1/difficulty-adjustment")
|
||||
assert_same_values(
|
||||
{k: b[k] for k in CHAIN_DETERMINISTIC_FIELDS},
|
||||
{k: m[k] for k in CHAIN_DETERMINISTIC_FIELDS},
|
||||
)
|
||||
|
||||
|
||||
def test_difficulty_adjustment_invariants(brk):
|
||||
"""Cross-field invariants and protocol-level bounds."""
|
||||
d = brk.get_difficulty_adjustment()
|
||||
now_ms = int(time.time() * 1000)
|
||||
|
||||
assert 0 <= d["progressPercent"] <= 100
|
||||
assert 0 <= d["remainingBlocks"] <= 2016
|
||||
assert d["nextRetargetHeight"] % 2016 == 0
|
||||
|
||||
blocks_done = 2016 - d["remainingBlocks"]
|
||||
assert abs(d["progressPercent"] - blocks_done / 2016 * 100) < 1e-6
|
||||
|
||||
# Bitcoin protocol clamps a single retarget to a 4× factor in either
|
||||
# direction → difficulty change ∈ [-75%, +300%]. previousRetarget reports
|
||||
# the same quantity historically, so the same bound applies.
|
||||
assert -75.0 <= d["difficultyChange"] <= 300.0
|
||||
assert -75.0 <= d["previousRetarget"] <= 300.0
|
||||
|
||||
# expectedBlocks: wall-clock-derived count of blocks that should have
|
||||
# arrived in the current epoch. Bounded above by ~2 epochs in any sane
|
||||
# state (a much larger value would mean clock skew or epoch-start bug).
|
||||
assert 0 <= d["expectedBlocks"] <= 2 * 2016
|
||||
|
||||
# timeAvg in milliseconds. Sanity: between 1s and 1h per block.
|
||||
assert 1_000 <= d["timeAvg"] <= 3_600_000
|
||||
assert 1_000 <= d["adjustedTimeAvg"] <= 3_600_000
|
||||
|
||||
# remainingTime is constructed as remainingBlocks * timeAvg in brk.
|
||||
assert d["remainingTime"] == d["remainingBlocks"] * d["timeAvg"]
|
||||
|
||||
assert d["estimatedRetargetDate"] > now_ms
|
||||
assert d["previousTime"] * 1000 < now_ms
|
||||
|
||||
|
||||
def test_difficulty_adjustment_previous_time_matches_chain(brk):
|
||||
"""previousTime must be the timestamp of the block at the most recent retarget."""
|
||||
d = brk.get_difficulty_adjustment()
|
||||
epoch_start_height = d["nextRetargetHeight"] - 2016
|
||||
epoch_start_hash = brk.get_block_by_height(epoch_start_height)
|
||||
epoch_start_block = brk.get_block(epoch_start_hash)
|
||||
assert d["previousTime"] == epoch_start_block["timestamp"], (
|
||||
f"previousTime={d['previousTime']} but block at height "
|
||||
f"{epoch_start_height} has timestamp={epoch_start_block['timestamp']}"
|
||||
)
|
||||
|
||||
|
||||
def test_difficulty_adjustment_next_retarget_aligned_with_tip(brk):
|
||||
"""The tip must sit inside the epoch ending at nextRetargetHeight."""
|
||||
d = brk.get_difficulty_adjustment()
|
||||
tip = int(brk.get_block_tip_height())
|
||||
assert d["nextRetargetHeight"] - 2016 <= tip < d["nextRetargetHeight"], (
|
||||
f"tip={tip} not in current epoch "
|
||||
f"[{d['nextRetargetHeight'] - 2016}, {d['nextRetargetHeight']})"
|
||||
)
|
||||
assert d["remainingBlocks"] == d["nextRetargetHeight"] - tip - 1 \
|
||||
or d["remainingBlocks"] == d["nextRetargetHeight"] - tip, (
|
||||
"remainingBlocks must equal blocks left to next retarget "
|
||||
"(off-by-one tolerated for tip-race during request)"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user