mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-20 23:04:46 -07:00
global: fixes
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user