query: fixes

This commit is contained in:
nym21
2026-04-30 19:19:09 +02:00
parent 9b42b40a36
commit 1068ad4e8f
10 changed files with 165 additions and 35 deletions

View File

@@ -0,0 +1 @@
*md

View File

@@ -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:

View File

@@ -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)"
)