global: fixes

This commit is contained in:
nym21
2026-05-03 12:44:18 +02:00
parent 9cb5f2c880
commit 4663d13194
46 changed files with 1058 additions and 544 deletions

View File

@@ -4,7 +4,7 @@ from _lib import assert_same_structure, show
MAX_PROJECTED_BLOCKS = 8
BRK_FEE_RANGE_LEN = 7
FEE_RANGE_LEN = 7
def test_fees_mempool_blocks_structure(brk, mempool):
@@ -36,8 +36,8 @@ def test_fees_mempool_blocks_invariants(brk):
assert block["totalFees"] >= 0, f"block {i} has negative totalFees"
assert block["medianFee"] > 0, f"block {i} has non-positive medianFee"
fr = block["feeRange"]
assert len(fr) == BRK_FEE_RANGE_LEN, (
f"block {i} feeRange has {len(fr)} items, expected {BRK_FEE_RANGE_LEN}"
assert len(fr) == FEE_RANGE_LEN, (
f"block {i} feeRange has {len(fr)} items, expected {FEE_RANGE_LEN}"
)
assert fr == sorted(fr), f"block {i} feeRange not ascending: {fr}"
assert fr[0] <= block["medianFee"] <= fr[-1], (

View File

@@ -28,8 +28,10 @@ def test_mempool_info_invariants(brk):
f"histogram entry {i} not a 2-element list: {entry}"
)
rate, bvs = entry
assert isinstance(rate, (int, float)) and rate > 0, (
f"non-positive rate at bin {i}: {rate}"
# Zero-rate bins are legitimate (CPFP/package-relay anchors with
# zero-fee parents); mempool.space's API returns them too.
assert isinstance(rate, (int, float)) and rate >= 0, (
f"negative rate at bin {i}: {rate}"
)
assert isinstance(bvs, int) and bvs > 0, f"non-positive vsize at bin {i}: {bvs}"
rates.append(rate)

View File

@@ -1,4 +1,10 @@
"""GET /api/v1/mining/blocks/fee-rates/{time_period}"""
"""GET /api/v1/mining/blocks/fee-rates/{time_period}
Note: there is no values_match test here. brk emits float bucket means; mempool
stores integer per-block percentiles, averages, then casts to INT. Beyond
rounding, the per-block max (avgFee_100) also drifts by several sat/vB,
indicating a real methodology difference (effective fee-rate definition,
RBF/ancestor-fee handling) that no test tolerance should hide."""
import pytest

View File

@@ -45,3 +45,30 @@ def test_mining_blocks_fees_malformed(brk, bad):
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_fees_values_match(brk, mempool, period):
"""For shared buckets (keyed by timestamp), avgHeight and avgFees must equal mempool.space.
USD is sourced from different price oracles and is intentionally not compared."""
path = f"/api/v1/mining/blocks/fees/{period}"
b = brk.get_block_fees(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
m_by_ts = {e["timestamp"]: e for e in m}
matched = 0
for be in b:
me = m_by_ts.get(be["timestamp"])
if me is None:
continue
matched += 1
assert be["avgHeight"] == me["avgHeight"], (
f"avgHeight drift at timestamp {be['timestamp']}: "
f"brk={be['avgHeight']} mempool={me['avgHeight']}"
)
assert be["avgFees"] == me["avgFees"], (
f"avgFees mismatch at timestamp {be['timestamp']}: "
f"brk={be['avgFees']} mempool={me['avgFees']}"
)
assert matched > 0, "no overlapping bucket timestamps between brk and mempool"

View File

@@ -45,3 +45,29 @@ def test_mining_blocks_rewards_malformed(brk, bad):
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_rewards_values_match(brk, mempool, period):
"""For shared buckets (keyed by timestamp), avgHeight and avgRewards must equal mempool.space."""
path = f"/api/v1/mining/blocks/rewards/{period}"
b = brk.get_block_rewards(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
m_by_ts = {e["timestamp"]: e for e in m}
matched = 0
for be in b:
me = m_by_ts.get(be["timestamp"])
if me is None:
continue
matched += 1
assert be["avgHeight"] == me["avgHeight"], (
f"avgHeight drift at timestamp {be['timestamp']}: "
f"brk={be['avgHeight']} mempool={me['avgHeight']}"
)
assert be["avgRewards"] == me["avgRewards"], (
f"avgRewards mismatch at timestamp {be['timestamp']}: "
f"brk={be['avgRewards']} mempool={me['avgRewards']}"
)
assert matched > 0, "no overlapping bucket timestamps between brk and mempool"

View File

@@ -59,3 +59,34 @@ def test_mining_blocks_sizes_weights_malformed(brk, bad):
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_sizes_weights_values_match(brk, mempool, period):
"""For shared buckets (keyed by timestamp), avgSize and avgWeight must equal mempool.space."""
path = f"/api/v1/mining/blocks/sizes-weights/{period}"
b = brk.get_block_sizes_weights(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
sizes_by_ts = {e["timestamp"]: e for e in m["sizes"]}
weights_by_ts = {e["timestamp"]: e for e in m["weights"]}
matched = 0
for s, w in zip(b["sizes"], b["weights"]):
ts = s["timestamp"]
ms = sizes_by_ts.get(ts)
mw = weights_by_ts.get(ts)
if ms is None or mw is None:
continue
matched += 1
assert s["avgHeight"] == ms["avgHeight"], (
f"size avgHeight drift at timestamp {ts}: brk={s['avgHeight']} mempool={ms['avgHeight']}"
)
assert s["avgSize"] == ms["avgSize"], (
f"avgSize mismatch at timestamp {ts}: brk={s['avgSize']} mempool={ms['avgSize']}"
)
assert w["avgWeight"] == mw["avgWeight"], (
f"avgWeight mismatch at timestamp {ts}: brk={w['avgWeight']} mempool={mw['avgWeight']}"
)
assert matched > 0, "no overlapping bucket timestamps between brk and mempool"

View File

@@ -52,3 +52,30 @@ def test_mining_difficulty_adjustments_malformed(brk, bad):
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)
# `all`: mempool.space's `difficulty_adjustments` table begins from when their tracker
# started, not genesis, so series length and earliest entries diverge by construction.
@pytest.mark.parametrize("period", [p for p in PERIODS if p != "all"])
def test_mining_difficulty_adjustments_values_match(brk, mempool, period):
"""For every bounded period, every retarget entry must match mempool.space:
same height, same timestamp, and difficulty/change-ratio within float tolerance."""
path = f"/api/v1/mining/difficulty-adjustments/{period}"
b = brk.get_difficulty_adjustments_by_period(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert len(b) == len(m), f"length mismatch: brk={len(b)} mempool={len(m)}"
for be, me in zip(b, m):
bt, bh, bd, br = be
mt, mh, md, mr = me
assert bh == mh, f"height mismatch at retarget: brk={bh} mempool={mh}"
assert bt == mt, f"timestamp mismatch at height {bh}: brk={bt} mempool={mt}"
# mempool.space serializes difficulty/change_ratio with limited precision,
# so only require parity within mempool.space's ~6-decimal rounding window.
assert abs(bd - md) / max(md, 1.0) < 1e-5, (
f"difficulty drift at height {bh}: brk={bd} mempool={md}"
)
assert abs(br - mr) < 1e-5, (
f"change_ratio drift at height {bh}: brk={br} mempool={mr}"
)

View File

@@ -1,10 +1,15 @@
"""GET /api/v1/mining/reward-stats/{block_count}"""
"""GET /api/v1/mining/reward-stats/{block_count}
Note: there is no values_match test here. mempool.space's reward-stats endpoint
serves results anchored to a cached/precomputed block that lags real-time tip
non-deterministically across counts, so any direct numeric comparison is flaky.
The invariants test below covers structural correctness."""
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, assert_same_values, show
from _lib import assert_same_structure, show
COUNTS = [1, 10, 100, 500, 1000]
@@ -20,16 +25,6 @@ def test_mining_reward_stats_structure(brk, mempool, count):
assert_same_structure(b, m)
@pytest.mark.parametrize("count", [100, 1000])
def test_mining_reward_stats_values_match(brk, mempool, count):
"""brk and mempool must agree exactly on aggregated stats."""
path = f"/api/v1/mining/reward-stats/{count}"
b = brk.get_reward_stats(count)
m = mempool.get_json(path)
show("GET", path, b, m)
assert_same_values(b, m)
def test_mining_reward_stats_invariants(brk):
"""Range alignment, reward >= fee, totalTx >= block count (count=1000)."""
count = 1000

View File

@@ -1,5 +1,7 @@
"""GET /api/v1/transaction-times?txId[]=..."""
import time
import pytest
from brk_client import BrkError
@@ -65,3 +67,39 @@ def test_transaction_times_malformed_short(brk):
with pytest.raises(BrkError) as exc_info:
brk.get_transaction_times(["abc"])
assert exc_info.value.status == 400
def test_transaction_times_mempool_unconfirmed(brk, mempool):
"""Unconfirmed mempool tx: first-seen timestamp must be a plausible
Unix-second value (post-genesis, not in the future). Cross-observer
agreement is not asserted: each server records when *it* first saw
the tx, and rebroadcasts/restarts can put two independent observers
days or weeks apart on the same txid."""
txids = mempool.get_json("/api/mempool/txids")
if not txids:
pytest.skip("mempool.space mempool currently empty")
GENESIS_TS = 1231006505
now = int(time.time())
skew = 5 * 60
for txid in txids[:25]:
try:
b = brk.get_transaction_times([txid])
except BrkError:
continue
if not b or b[0] == 0:
continue
try:
m = mempool.get_json(
"/api/v1/transaction-times", params=[("txId[]", txid)]
)
except Exception:
continue
if not m or m[0] == 0:
continue
show("GET", f"/api/v1/transaction-times?txId[]={txid[:16]}...", b, m)
assert GENESIS_TS <= b[0] <= now + skew, f"brk first-seen out of plausible range: {b[0]}"
assert GENESIS_TS <= m[0] <= now + skew, f"mempool first-seen out of plausible range: {m[0]}"
return
pytest.skip("no shared unconfirmed tx between brk and mempool.space")