mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-19 14:24:47 -07:00
global: fixes
This commit is contained in:
@@ -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], (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user