global: fixes

This commit is contained in:
nym21
2026-05-02 00:42:16 +02:00
parent 6f879a5551
commit 2b8a0a8cf7
99 changed files with 4308 additions and 1525 deletions

View File

@@ -2,14 +2,51 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
def test_mining_blocks_fee_rates(brk, mempool, period):
"""Block fee-rate percentiles must have the same element structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
PERCENTILES = ["avgFee_0", "avgFee_10", "avgFee_25", "avgFee_50", "avgFee_75", "avgFee_90", "avgFee_100"]
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_fee_rates_structure(brk, mempool, period):
"""Block fee-rate percentiles envelope must match across all periods."""
path = f"/api/v1/mining/blocks/fee-rates/{period}"
b = brk.get_json(path)
b = brk.get_block_fee_rates(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_blocks_fee_rates_invariants(brk):
"""Series ordering, percentile monotonicity, non-negative rates (period=1m)."""
period = "1m"
b = brk.get_block_fee_rates(period)
show("GET", f"/api/v1/mining/blocks/fee-rates/{period}", summary(b), "-")
assert len(b) > 0, "expected non-empty fee-rates series for 1m"
heights = [entry["avgHeight"] for entry in b]
timestamps = [entry["timestamp"] for entry in b]
assert heights == sorted(heights), "avgHeight not ascending"
assert timestamps == sorted(timestamps), "timestamps not ascending"
assert len(set(heights)) == len(heights), "duplicate avgHeight in series"
for entry in b:
values = [entry[k] for k in PERCENTILES]
assert values == sorted(values), (
f"percentiles not monotonically non-decreasing at height {entry['avgHeight']}: {values}"
)
for k in PERCENTILES:
assert entry[k] >= 0, f"negative fee rate {k}={entry[k]} at {entry['avgHeight']}"
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_blocks_fee_rates_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/blocks/fee-rates/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,46 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
def test_mining_blocks_fees(brk, mempool, period):
"""Average block fees must have the same element structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_fees_structure(brk, mempool, period):
"""Average block fees envelope must match across all periods."""
path = f"/api/v1/mining/blocks/fees/{period}"
b = brk.get_json(path)
b = brk.get_block_fees(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_blocks_fees_invariants(brk):
"""Series ascending by height and timestamp, fees and USD non-negative (period=1m)."""
period = "1m"
b = brk.get_block_fees(period)
show("GET", f"/api/v1/mining/blocks/fees/{period}", summary(b), "-")
assert len(b) > 0, "expected non-empty fees series for 1m"
heights = [entry["avgHeight"] for entry in b]
timestamps = [entry["timestamp"] for entry in b]
assert heights == sorted(heights), "avgHeight not ascending"
assert timestamps == sorted(timestamps), "timestamps not ascending"
assert len(set(heights)) == len(heights), "duplicate avgHeight in series"
for entry in b:
assert entry["avgFees"] >= 0, f"negative avgFees: {entry}"
assert entry["USD"] >= 0, f"negative USD: {entry}"
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_blocks_fees_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/blocks/fees/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,46 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
def test_mining_blocks_rewards(brk, mempool, period):
"""Average block rewards must have the same element structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_rewards_structure(brk, mempool, period):
"""Average block rewards envelope must match across all periods."""
path = f"/api/v1/mining/blocks/rewards/{period}"
b = brk.get_json(path)
b = brk.get_block_rewards(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_blocks_rewards_invariants(brk):
"""Series ascending by height and timestamp, rewards positive, USD non-negative (period=1m)."""
period = "1m"
b = brk.get_block_rewards(period)
show("GET", f"/api/v1/mining/blocks/rewards/{period}", summary(b), "-")
assert len(b) > 0, "expected non-empty rewards series for 1m"
heights = [entry["avgHeight"] for entry in b]
timestamps = [entry["timestamp"] for entry in b]
assert heights == sorted(heights), "avgHeight not ascending"
assert timestamps == sorted(timestamps), "timestamps not ascending"
assert len(set(heights)) == len(heights), "duplicate avgHeight in series"
for entry in b:
assert entry["avgRewards"] > 0, f"non-positive avgRewards: {entry}"
assert entry["USD"] >= 0, f"negative USD: {entry}"
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_blocks_rewards_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/blocks/rewards/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,60 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y"])
def test_mining_blocks_sizes_weights(brk, mempool, period):
"""Block sizes and weights must have the same structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
MAX_BLOCK_WEIGHT = 4_000_000
@pytest.mark.parametrize("period", PERIODS)
def test_mining_blocks_sizes_weights_structure(brk, mempool, period):
"""Combined sizes/weights envelope must match across all periods."""
path = f"/api/v1/mining/blocks/sizes-weights/{period}"
b = brk.get_json(path)
b = brk.get_block_sizes_weights(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, dict) and isinstance(m, dict)
assert_same_structure(b, m)
def test_mining_blocks_sizes_weights_invariants(brk):
"""Parallel arrays, ascending order, positive size, weight in (0, 4M] (period=1m)."""
period = "1m"
b = brk.get_block_sizes_weights(period)
sizes = b["sizes"]
weights = b["weights"]
show("GET", f"/api/v1/mining/blocks/sizes-weights/{period}", summary(b), "-")
assert len(sizes) > 0, "expected non-empty sizes series for 1m"
assert len(sizes) == len(weights), (
f"sizes/weights array lengths diverge: {len(sizes)} vs {len(weights)}"
)
size_heights = [e["avgHeight"] for e in sizes]
size_ts = [e["timestamp"] for e in sizes]
assert size_heights == sorted(size_heights), "size avgHeights not ascending"
assert size_ts == sorted(size_ts), "size timestamps not ascending"
assert len(set(size_heights)) == len(size_heights), "duplicate avgHeight in sizes"
for s, w in zip(sizes, weights):
assert s["avgHeight"] == w["avgHeight"], (
f"size/weight height misalignment: {s['avgHeight']} vs {w['avgHeight']}"
)
assert s["timestamp"] == w["timestamp"], (
f"size/weight timestamp misalignment at height {s['avgHeight']}"
)
assert s["avgSize"] > 0, f"non-positive avgSize at {s['avgHeight']}: {s['avgSize']}"
assert 0 < w["avgWeight"] <= MAX_BLOCK_WEIGHT, (
f"avgWeight out of range at {w['avgHeight']}: {w['avgWeight']}"
)
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_blocks_sizes_weights_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/blocks/sizes-weights/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -1,15 +1,52 @@
"""GET /api/v1/mining/blocks/timestamp/{timestamp}"""
from _lib import assert_same_structure, show
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, assert_same_values, show
def test_mining_blocks_timestamp(brk, mempool, live):
"""Block lookup by timestamp must have the same structure for various eras."""
GENESIS_TIMESTAMP = 1231006505
def test_mining_blocks_timestamp_structure_and_parity(brk, mempool, live):
"""For each live era, brk and mempool must resolve the same block."""
for block in live.blocks:
info = brk.get_json(f"/api/block/{block.hash}")
ts = info["timestamp"]
path = f"/api/v1/mining/blocks/timestamp/{ts}"
b = brk.get_json(path)
b = brk.get_block_by_timestamp(ts)
m = mempool.get_json(path)
show("GET", path, b, m)
assert_same_structure(b, m)
assert_same_values(b, m)
def test_mining_blocks_timestamp_round_trip(brk, live):
"""Looking up a block's own timestamp must return that block (or an earlier one with same ts)."""
for block in live.blocks:
info = brk.get_json(f"/api/block/{block.hash}")
ts = info["timestamp"]
b = brk.get_block_by_timestamp(ts)
show("GET", f"/api/v1/mining/blocks/timestamp/{ts}", b, "-")
assert b["height"] <= block.height, (
f"resolved height {b['height']} > requested block height {block.height}"
)
def test_mining_blocks_timestamp_genesis(brk):
"""Genesis Unix timestamp must resolve to genesis (height 0)."""
b = brk.get_block_by_timestamp(GENESIS_TIMESTAMP)
show("GET", f"/api/v1/mining/blocks/timestamp/{GENESIS_TIMESTAMP}", b, "-")
assert b["height"] == 0, f"genesis ts must resolve to height 0, got {b['height']}"
@pytest.mark.parametrize("bad", ["abc", "-1"])
def test_mining_blocks_timestamp_malformed(brk, bad):
"""Non-numeric or negative timestamp must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/blocks/timestamp/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,53 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"])
def test_mining_difficulty_adjustments(brk, mempool, period):
"""Historical difficulty adjustments must have the same structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
RETARGET_INTERVAL = 2016
@pytest.mark.parametrize("period", PERIODS)
def test_mining_difficulty_adjustments_structure(brk, mempool, period):
"""Historical difficulty adjustments envelope must match across all periods."""
path = f"/api/v1/mining/difficulty-adjustments/{period}"
b = brk.get_json(path)
b = brk.get_difficulty_adjustments_by_period(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_difficulty_adjustments_invariants(brk):
"""Tip-first ordering, retarget-aligned heights, genesis sentinel (period=all)."""
period = "all"
b = brk.get_difficulty_adjustments_by_period(period)
show("GET", f"/api/v1/mining/difficulty-adjustments/{period}", summary(b), "-")
assert len(b) > 0, "expected non-empty difficulty adjustments for period=all"
heights = [entry[1] for entry in b]
assert heights == sorted(heights, reverse=True), "entries not descending by height"
assert len(set(heights)) == len(heights), "duplicate heights in series"
assert heights[-1] == 0, f"last entry must be genesis (height 0), got {heights[-1]}"
assert heights.count(0) == 1, "expected exactly one genesis entry"
for entry in b[:-1]:
timestamp, height, difficulty, change_ratio = entry
assert height % RETARGET_INTERVAL == 0, (
f"non-genesis height {height} not on retarget boundary"
)
assert difficulty > 0, f"non-positive difficulty: {difficulty} at height {height}"
assert change_ratio > 0, f"non-positive change ratio: {change_ratio} at height {height}"
genesis = b[-1]
assert genesis[2] == 1.0, f"genesis difficulty must be 1.0, got {genesis[2]}"
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_difficulty_adjustments_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/difficulty-adjustments/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,52 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y"])
def test_mining_hashrate(brk, mempool, period):
"""Network hashrate + difficulty must have the same structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
@pytest.mark.parametrize("period", PERIODS)
def test_mining_hashrate_structure(brk, mempool, period):
"""Network hashrate envelope must match across all periods."""
path = f"/api/v1/mining/hashrate/{period}"
b = brk.get_json(path)
b = brk.get_hashrate_by_period(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert_same_structure(b, m)
def test_mining_hashrate_invariants(brk):
"""Series ascending, values positive, current* fields populated (period=1m)."""
period = "1m"
b = brk.get_hashrate_by_period(period)
show("GET", f"/api/v1/mining/hashrate/{period}", summary(b), "-")
assert isinstance(b["currentHashrate"], int) and b["currentHashrate"] > 0
assert isinstance(b["currentDifficulty"], (int, float)) and b["currentDifficulty"] > 0
hashrates = b["hashrates"]
assert len(hashrates) > 0, "expected non-empty hashrates list for 1m"
timestamps = [h["timestamp"] for h in hashrates]
assert timestamps == sorted(timestamps), "hashrate timestamps not ascending"
assert len(set(timestamps)) == len(timestamps), "duplicate hashrate timestamps"
for h in hashrates:
assert isinstance(h["avgHashrate"], int) and h["avgHashrate"] > 0
difficulty = b["difficulty"]
times = [d["time"] for d in difficulty]
heights = [d["height"] for d in difficulty]
assert times == sorted(times), "difficulty entries not ascending by time"
assert heights == sorted(heights), "difficulty entries not ascending by height"
for d in difficulty:
assert d["difficulty"] > 0, f"non-positive difficulty: {d}"
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_hashrate_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/hashrate/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,48 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "1y"])
def test_mining_hashrate_pools(brk, mempool, period):
"""Per-pool hashrate must have the same structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
@pytest.mark.parametrize("period", PERIODS)
def test_mining_hashrate_pools_structure(brk, mempool, period):
"""Per-pool hashrate snapshot envelope must match across all periods."""
path = f"/api/v1/mining/hashrate/pools/{period}"
b = brk.get_json(path)
b = brk.get_pools_hashrate_by_period(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_hashrate_pools_invariants(brk):
"""Snapshot has single timestamp, valid shares summing to <=1, unique pool names (period=1w)."""
period = "1w"
b = brk.get_pools_hashrate_by_period(period)
show("GET", f"/api/v1/mining/hashrate/pools/{period}", summary(b), "-")
assert len(b) > 0, "expected non-empty per-pool hashrate snapshot for 1w"
timestamps = {entry["timestamp"] for entry in b}
assert len(timestamps) == 1, f"expected single snapshot timestamp, got {timestamps}"
pool_names = [entry["poolName"] for entry in b]
assert len(set(pool_names)) == len(pool_names), "duplicate poolName in snapshot"
for entry in b:
assert entry["poolName"], "empty poolName"
assert isinstance(entry["avgHashrate"], int) and entry["avgHashrate"] >= 0
assert isinstance(entry["share"], (int, float)) and 0.0 <= entry["share"] <= 1.0
total_share = sum(entry["share"] for entry in b)
assert total_share <= 1.0001, f"share sum > 1: {total_share}"
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_hashrate_pools_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/hashrate/pools/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -1,13 +1,73 @@
"""GET /api/v1/mining/pool/{slug}"""
from _lib import assert_same_structure, show, summary
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, assert_same_values, show, summary
def test_mining_pool_detail(brk, mempool, pool_slugs):
"""Pool detail must have the same structure for top pools."""
# Tip-race / mempool-only / int-vs-str fields excluded from value equality.
VOLATILE = {
"blockCount", "blockShare", "estimatedHashrate", "reportedHashrate",
"totalReward", "avgBlockHealth", "avgMatchRate", "avgFeeDelta",
}
# Digit/punctuation slugs that previously diverged between brk and mempool.
# Pinning them here lets the slug rename fixes regress loudly if reverted.
SLUG_RENAME_REGRESSION_GUARD = ["1thash", "175btc", "21inc", "1hash", "58coin", "7pool"]
def test_mining_pool_detail_structure(brk, mempool, pool_slugs):
"""Pool detail envelope must match mempool for the top active pools."""
for slug in pool_slugs:
path = f"/api/v1/mining/pool/{slug}"
b = brk.get_json(path)
b = brk.get_pool(slug)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert_same_structure(b, m)
def test_mining_pool_detail_static_fields(brk, mempool, pool_slug):
"""The pool registry fields (id, name, link, slug, unique_id) must value-match."""
path = f"/api/v1/mining/pool/{pool_slug}"
b = brk.get_pool(pool_slug)
m = mempool.get_json(path)
show("GET", path, b["pool"], m["pool"])
assert_same_values(b["pool"], m["pool"], path=f"{path}.pool")
def test_mining_pool_detail_invariants(brk, pool_slug):
"""blockCount monotonic by window; blockShare in [0,1]; pool.slug round-trips."""
b = brk.get_pool(pool_slug)
show("GET", f"/api/v1/mining/pool/{pool_slug}", summary(b), "-")
assert b["pool"]["slug"] == pool_slug, (
f"response.pool.slug={b['pool']['slug']!r} vs URL slug={pool_slug!r}"
)
bc = b["blockCount"]
assert bc["all"] >= bc["1w"] >= bc["24h"] >= 0, f"blockCount not monotonic: {bc}"
bs = b["blockShare"]
for window, value in bs.items():
assert 0.0 <= value <= 1.0, f"blockShare[{window}]={value} out of [0,1]"
assert isinstance(b["estimatedHashrate"], int) and b["estimatedHashrate"] >= 0
@pytest.mark.parametrize("slug", SLUG_RENAME_REGRESSION_GUARD)
def test_mining_pool_detail_slug_renames(brk, mempool, slug):
"""Pools whose slugs were renamed to match mempool must remain reachable."""
path = f"/api/v1/mining/pool/{slug}"
b = brk.get_pool(slug)
m = mempool.get_json(path)
show("GET", path, b["pool"], m["pool"])
assert b["pool"]["slug"] == slug
assert_same_values(b["pool"], m["pool"], path=f"{path}.pool")
@pytest.mark.parametrize("bad", ["notapool", "FoundryUSA", ""])
def test_mining_pool_detail_malformed(brk, bad):
"""Unknown slug must produce BrkError(status=400 or 404)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/pool/{bad}")
assert exc_info.value.status in (400, 404), (
f"expected 400 or 404 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -1,15 +1,47 @@
"""GET /api/v1/mining/pool/{slug}/blocks"""
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show
def test_mining_pool_blocks(brk, mempool, pool_slugs):
"""Recent blocks by pool must have the same element structure."""
PAGE_SIZE = 100
def test_mining_pool_blocks_structure(brk, mempool, pool_slugs):
"""Per-pool block list element schema must match for top active pools."""
for slug in pool_slugs:
path = f"/api/v1/mining/pool/{slug}/blocks"
b = brk.get_json(path)
b = brk.get_pool_blocks(slug)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
assert isinstance(b, list) and isinstance(m, list)
if b and m:
assert_same_structure(b[0], m[0])
assert_same_structure(b, m)
def test_mining_pool_blocks_invariants(brk, pool_slug):
"""Page is descending, capped at 100, all blocks attributed to the requested pool."""
b = brk.get_pool_blocks(pool_slug)
show("GET", f"/api/v1/mining/pool/{pool_slug}/blocks", f"({len(b)} blocks)", "-")
assert 0 < len(b) <= PAGE_SIZE, f"unexpected length: {len(b)}"
heights = [blk["height"] for blk in b]
assert heights == sorted(heights, reverse=True), f"not tip-first: {heights[:5]}..."
assert len(set(heights)) == len(heights), "duplicate heights in page"
for blk in b:
assert blk["stale"] is False, f"stale block in page: {blk['id']}"
assert blk["extras"]["pool"]["slug"] == pool_slug, (
f"block {blk['id']} attributed to {blk['extras']['pool']['slug']}, "
f"expected {pool_slug}"
)
@pytest.mark.parametrize("bad", ["notapool", "FoundryUSA"])
def test_mining_pool_blocks_malformed(brk, bad):
"""Unknown slug must produce BrkError(status=400 or 404)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/pool/{bad}/blocks")
assert exc_info.value.status in (400, 404), (
f"expected 400 or 404 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -1,15 +1,61 @@
"""GET /api/v1/mining/pool/{slug}/blocks/{height}"""
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show
def test_mining_pool_blocks_at_height(brk, mempool, pool_slug, live):
"""Pool blocks before various heights must have the same element structure."""
for block in live.blocks[::2]: # every other block, to keep run-time bounded
path = f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}"
b = brk.get_json(path)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
assert isinstance(b, list) and isinstance(m, list)
if b and m:
assert_same_structure(b[0], m[0])
PAGE_SIZE = 100
def test_mining_pool_blocks_from_height_structure(brk, mempool, pool_slug, block):
"""Per-pool block list before a height must match mempool's element schema."""
path = f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}"
b = brk.get_pool_blocks_from(pool_slug, block.height)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} blocks)", f"({len(m)} blocks)")
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_pool_blocks_from_height_invariants(brk, pool_slug, block):
"""Page is descending, capped at 100, height-bounded, attributed to the pool."""
b = brk.get_pool_blocks_from(pool_slug, block.height)
show("GET", f"/api/v1/mining/pool/{pool_slug}/blocks/{block.height}", f"({len(b)} blocks)", "-")
assert 0 <= len(b) <= PAGE_SIZE, f"unexpected length: {len(b)}"
if not b:
return
heights = [blk["height"] for blk in b]
assert heights == sorted(heights, reverse=True), f"not descending: {heights[:5]}..."
assert max(heights) <= block.height, (
f"page contains height > requested {block.height}: max={max(heights)}"
)
assert len(set(heights)) == len(heights), "duplicate heights in page"
for blk in b:
assert blk["stale"] is False, f"stale block in page: {blk['id']}"
assert blk["extras"]["pool"]["slug"] == pool_slug, (
f"block {blk['id']} attributed to {blk['extras']['pool']['slug']}, "
f"expected {pool_slug}"
)
@pytest.mark.parametrize("bad_slug", ["notapool", "FoundryUSA"])
def test_mining_pool_blocks_from_height_malformed_slug(brk, bad_slug):
"""Unknown slug must produce BrkError(status=400 or 404)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/pool/{bad_slug}/blocks/100000")
assert exc_info.value.status in (400, 404), (
f"expected 400 or 404 for slug {bad_slug!r}, got {exc_info.value.status}"
)
@pytest.mark.parametrize("bad_height", ["-1", "abc"])
def test_mining_pool_blocks_from_height_malformed_height(brk, pool_slug, bad_height):
"""Negative or non-numeric height must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/pool/{pool_slug}/blocks/{bad_height}")
assert exc_info.value.status == 400, (
f"expected 400 for height {bad_height!r}, got {exc_info.value.status}"
)

View File

@@ -1,13 +1,43 @@
"""GET /api/v1/mining/pool/{slug}/hashrate"""
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
def test_mining_pool_hashrate(brk, mempool, pool_slugs):
"""Pool hashrate history must have the same structure for top pools."""
def test_mining_pool_hashrate_structure(brk, mempool, pool_slugs):
"""Pool hashrate history element schema must match for top active pools."""
for slug in pool_slugs:
path = f"/api/v1/mining/pool/{slug}/hashrate"
b = brk.get_json(path)
b = brk.get_pool_hashrate(slug)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert isinstance(b, list) and isinstance(m, list)
assert_same_structure(b, m)
def test_mining_pool_hashrate_invariants(brk, pool_slug):
"""Series must be non-empty, ascending in time, with valid hashrate/share/poolName."""
b = brk.get_pool_hashrate(pool_slug)
show("GET", f"/api/v1/mining/pool/{pool_slug}/hashrate", summary(b), "-")
assert len(b) > 0, f"empty hashrate history for {pool_slug}"
timestamps = [entry["timestamp"] for entry in b]
assert timestamps == sorted(timestamps), "timestamps not ascending"
assert len(set(timestamps)) == len(timestamps), "duplicate timestamps"
pool_names = {entry["poolName"] for entry in b}
assert len(pool_names) == 1, f"poolName not consistent across series: {pool_names}"
for entry in b:
assert isinstance(entry["avgHashrate"], int) and entry["avgHashrate"] >= 0
assert isinstance(entry["share"], (int, float)) and 0.0 <= entry["share"] <= 1.0
@pytest.mark.parametrize("bad", ["notapool", "FoundryUSA"])
def test_mining_pool_hashrate_malformed(brk, bad):
"""Unknown slug must produce BrkError(status=400 or 404)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/pool/{bad}/hashrate")
assert exc_info.value.status in (400, 404), (
f"expected 400 or 404 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -3,43 +3,78 @@
from _lib import assert_same_structure, show
# Slugs present in brk's vendored pools-v2.json but reported under a different
# slug (or missing) by mempool.space. Currently the duplicate-pool collision
# case where brk preserves both `bitcoinindia` (variant 80) and
# `bitcoinindiapool` (variant 134), while mempool emits both as `bitcoinindia`.
KNOWN_BRK_ONLY_SLUGS = {"bitcoinindiapool"}
# Pools added upstream after brk's vendored pools-v2.json snapshot. Refresh
# the vendored file (and update this set) when bumping the snapshot.
KNOWN_MEMPOOL_ONLY_SLUGS = {
"drdetroit", "emzy", "knorrium", "mononaut", "nymkappa", "rijndael",
}
EXPECTED_MIN_POOLS = 165
def test_mining_pools_list_structure(brk, mempool):
"""Pool list must have the same element structure."""
"""Pool list element schema must match (flat list, {name, slug, unique_id})."""
path = "/api/v1/mining/pools"
b = brk.get_json(path)
b = brk.get_pools()
m = mempool.get_json(path)
show(
"GET", path,
b[:3] if isinstance(b, list) else b,
m[:3] if isinstance(m, list) else m,
)
show("GET", path, f"({len(b)} pools)", f"({len(m)} pools)", max_lines=4)
assert isinstance(b, list) and isinstance(m, list), "both must be flat lists"
assert_same_structure(b, m)
def _pools(data):
"""`pools` may live at the root or inside an envelope across versions."""
if isinstance(data, list):
return data
return data.get("pools", []) if isinstance(data, dict) else []
def test_mining_pools_list_fields(brk):
"""Each pool entry must carry slug and name (period-less endpoint omits stats)."""
b = _pools(brk.get_json("/api/v1/mining/pools"))
show("GET", "/api/v1/mining/pools", f"({len(b)} pools)", "")
assert b, "no pools in brk's response"
required = {"slug", "name"}
for p in b[:5]:
missing = required - set(p.keys())
assert not missing, f"pool {p.get('slug', '?')} missing fields: {missing}"
assert isinstance(p["name"], str) and p["name"]
"""Every pool entry must carry a non-empty slug + name + non-negative unique_id."""
b = brk.get_pools()
show("GET", "/api/v1/mining/pools", f"({len(b)} pools)", "-")
assert len(b) >= EXPECTED_MIN_POOLS, f"expected >= {EXPECTED_MIN_POOLS} pools, got {len(b)}"
for p in b:
assert isinstance(p["slug"], str) and p["slug"], f"bad slug: {p!r}"
assert isinstance(p["name"], str) and p["name"], f"bad name: {p!r}"
assert isinstance(p["unique_id"], int) and p["unique_id"] >= 0, (
f"bad unique_id: {p!r}"
)
def test_mining_pools_slugs_unique(brk):
"""Pool slugs must be unique across the response."""
b = _pools(brk.get_json("/api/v1/mining/pools"))
b = brk.get_pools()
slugs = [p["slug"] for p in b]
show("GET", "/api/v1/mining/pools", f"({len(slugs)} slugs)", "")
show("GET", "/api/v1/mining/pools", f"({len(slugs)} slugs)", "-")
assert len(slugs) == len(set(slugs)), (
f"duplicate slugs: {len(slugs) - len(set(slugs))}"
)
def test_mining_pools_unique_ids_unique(brk):
"""Pool unique_ids must be unique across the response."""
b = brk.get_pools()
ids = [p["unique_id"] for p in b]
show("GET", "/api/v1/mining/pools", f"({len(ids)} unique_ids)", "-")
assert len(ids) == len(set(ids)), (
f"duplicate unique_ids: {len(ids) - len(set(ids))}"
)
def test_mining_pools_slugs_match_mempool(brk, mempool):
"""brk's slug set must equal mempool's, modulo documented exceptions."""
b_slugs = {p["slug"] for p in brk.get_pools()}
m_slugs = {p["slug"] for p in mempool.get_json("/api/v1/mining/pools")}
show(
"GET", "/api/v1/mining/pools",
f"brk-only={sorted(b_slugs - m_slugs)}",
f"mempool-only={sorted(m_slugs - b_slugs)}",
)
unexpected_brk_only = (b_slugs - m_slugs) - KNOWN_BRK_ONLY_SLUGS
unexpected_mempool_only = (m_slugs - b_slugs) - KNOWN_MEMPOOL_ONLY_SLUGS
assert not unexpected_brk_only, (
f"undocumented brk-only slugs (likely format divergence): {unexpected_brk_only}"
)
assert not unexpected_mempool_only, (
f"undocumented mempool-only slugs (refresh pools-v2.json?): {unexpected_mempool_only}"
)

View File

@@ -2,14 +2,53 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show, summary
@pytest.mark.parametrize("period", ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"])
def test_mining_pools_by_period(brk, mempool, period):
"""Pool stats for a time period must have the same structure."""
PERIODS = ["24h", "3d", "1w", "1m", "3m", "6m", "1y", "2y", "3y", "all"]
@pytest.mark.parametrize("period", PERIODS)
def test_mining_pools_by_period_structure(brk, mempool, period):
"""Pool stats envelope must structurally match mempool across all periods."""
path = f"/api/v1/mining/pools/{period}"
b = brk.get_json(path)
b = brk.get_pool_stats(period)
m = mempool.get_json(path)
show("GET", path, summary(b), summary(m))
assert_same_structure(b, m)
def test_mining_pools_by_period_invariants(brk):
"""A single deep-period sanity pass on `1w`."""
period = "1w"
b = brk.get_pool_stats(period)
show("GET", f"/api/v1/mining/pools/{period}", summary(b), "-")
assert isinstance(b["blockCount"], int) and b["blockCount"] > 0
assert isinstance(b["lastEstimatedHashrate"], int) and b["lastEstimatedHashrate"] > 0
pools = b["pools"]
assert pools, "expected non-empty pools list for 1w"
slugs = [p["slug"] for p in pools]
assert len(slugs) == len(set(slugs)), "duplicate slugs in pools list"
ranks = [p["rank"] for p in pools]
assert ranks == list(range(1, len(pools) + 1)), f"ranks not 1..N: {ranks}"
block_total = 0
for p in pools:
assert isinstance(p["blockCount"], int) and p["blockCount"] >= 0
assert isinstance(p["emptyBlocks"], int) and p["emptyBlocks"] >= 0
assert 0.0 <= p["share"] <= 1.0, f"share out of range for {p['slug']}: {p['share']}"
block_total += p["blockCount"]
assert block_total <= b["blockCount"], (
f"sum(pool.blockCount)={block_total} exceeds envelope.blockCount={b['blockCount']}"
)
@pytest.mark.parametrize("bad", ["9000y", "abc", "1d"])
def test_mining_pools_by_period_malformed(brk, bad):
"""Unknown time period must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/pools/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)

View File

@@ -2,14 +2,62 @@
import pytest
from _lib import assert_same_structure, show
from brk_client import BrkError
from _lib import assert_same_structure, assert_same_values, show
@pytest.mark.parametrize("block_count", [10, 100, 500])
def test_mining_reward_stats(brk, mempool, block_count):
"""Reward stats must have the same structure."""
path = f"/api/v1/mining/reward-stats/{block_count}"
b = brk.get_json(path)
COUNTS = [1, 10, 100, 500, 1000]
@pytest.mark.parametrize("count", COUNTS)
def test_mining_reward_stats_structure(brk, mempool, count):
"""Reward stats envelope must match across counts."""
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_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
b = brk.get_reward_stats(count)
show("GET", f"/api/v1/mining/reward-stats/{count}", b, "-")
start = int(b["startBlock"])
end = int(b["endBlock"])
total_reward = int(b["totalReward"])
total_fee = int(b["totalFee"])
total_tx = int(b["totalTx"])
assert start <= end, f"startBlock {start} > endBlock {end}"
assert end - start + 1 == count, (
f"range mismatch: {end} - {start} + 1 = {end - start + 1}, expected {count}"
)
assert total_fee >= 0, f"negative totalFee: {total_fee}"
assert total_reward >= total_fee, (
f"totalReward {total_reward} < totalFee {total_fee} (subsidy must be non-negative)"
)
assert total_tx >= count, (
f"totalTx {total_tx} < block_count {count} (each block has >=1 coinbase tx)"
)
@pytest.mark.parametrize("bad", ["abc", "-1"])
def test_mining_reward_stats_malformed(brk, bad):
"""Non-numeric or negative block_count must produce BrkError(status=400)."""
with pytest.raises(BrkError) as exc_info:
brk.get_text(f"/api/v1/mining/reward-stats/{bad}")
assert exc_info.value.status == 400, (
f"expected status=400 for {bad!r}, got {exc_info.value.status}"
)