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,47 +2,130 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show
@pytest.fixture(params=[
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # P2PKH — early block reward
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # P2SH
], ids=["p2pkh", "p2sh"])
def static_addr(request):
"""Well-known addresses that always exist."""
return request.param
KNOWN_ADDR_TYPES = {
"p2pk", "p2pkh", "p2sh", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr",
"multisig", "op_return", "p2a", "empty", "unknown",
}
# Static fixtures: stable addresses with known shapes.
STATIC_ADDRS = [
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # genesis coinbase, p2pkh — heavy path
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # p2pkh — exercises tx_count divergence
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # p2sh
]
# Satoshi's genesis pubkey (uncompressed). Brk-only: mempool returns 400.
SATOSHI_GENESIS_PUBKEY = (
"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"
)
def test_address_info_static(brk, mempool, static_addr):
"""Address stats structure must match for well-known addresses."""
path = f"/api/address/{static_addr}"
b = brk.get_json(path)
def _tx_count_tolerance(m_tx_count: int) -> int:
"""Allow drift between brk's distinct-tx and mempool's output-count semantics."""
import math
return max(5, math.ceil(0.05 * m_tx_count))
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_info_shape(brk, mempool, addr):
"""Typed brk response must structurally match mempool and echo the input address."""
path = f"/api/address/{addr}"
b = brk.get_address(addr)
m = mempool.get_json(path)
show("GET", path, b, m)
assert_same_structure(b, m)
assert b["address"] == m["address"]
assert b["address"] == addr
assert "addr_type" in b
assert "type_index" in b["chain_stats"]
assert "realized_price" in b["chain_stats"]
def test_address_info_discovered(brk, mempool, live_addrs):
"""Address stats structure must match for each discovered type."""
def test_address_info_shape_dynamic(brk, mempool, live_addrs):
"""Same shape contract over each live-discovered scriptpubkey type."""
assert live_addrs, "no live addresses discovered"
for atype, addr in live_addrs:
path = f"/api/address/{addr}"
b = brk.get_json(path)
b = brk.get_address(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", b, m)
assert_same_structure(b, m)
assert b["address"] == m["address"]
assert b["address"] == addr
def test_address_chain_stats_close(brk, mempool, live_addrs):
"""Chain stats values must be close for each discovered address."""
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_chain_stats_match(brk, mempool, addr):
"""Funded/spent counts and sums must match exactly; tx_count tolerated within 5% (min 5)."""
path = f"/api/address/{addr}"
b = brk.get_address(addr)["chain_stats"]
m = mempool.get_json(path)["chain_stats"]
show("GET", f"{path} [chain_stats]", b, m)
for key in ("funded_txo_count", "funded_txo_sum", "spent_txo_count", "spent_txo_sum"):
assert b[key] == m[key], (
f"{addr} {key}: brk={b[key]} vs mempool={m[key]}"
)
tol = _tx_count_tolerance(m["tx_count"])
assert abs(b["tx_count"] - m["tx_count"]) <= tol, (
f"{addr} tx_count drift {abs(b['tx_count'] - m['tx_count'])} > tol {tol}: "
f"brk={b['tx_count']} vs mempool={m['tx_count']}"
)
def test_address_chain_stats_match_dynamic(brk, mempool, live_addrs):
"""Same equality/tolerance contract on dynamically discovered addresses."""
assert live_addrs, "no live addresses discovered"
for atype, addr in live_addrs:
path = f"/api/address/{addr}"
b = brk.get_json(path)["chain_stats"]
b = brk.get_address(addr)["chain_stats"]
m = mempool.get_json(path)["chain_stats"]
show("GET", f"{path} [chain_stats, {atype}]", b, m)
assert_same_structure(b, m)
assert abs(b["tx_count"] - m["tx_count"]) <= 5, (
f"{atype} tx_count: brk={b['tx_count']} vs mempool={m['tx_count']}"
for key in ("funded_txo_count", "funded_txo_sum", "spent_txo_count", "spent_txo_sum"):
assert b[key] == m[key], (
f"{atype} {addr} {key}: brk={b[key]} vs mempool={m[key]}"
)
tol = _tx_count_tolerance(m["tx_count"])
assert abs(b["tx_count"] - m["tx_count"]) <= tol, (
f"{atype} {addr} tx_count drift {abs(b['tx_count'] - m['tx_count'])} > tol {tol}: "
f"brk={b['tx_count']} vs mempool={m['tx_count']}"
)
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_brk_extras(brk, addr):
"""Brk-only extras must be coherent: known addr_type, non-negative type_index/realized_price."""
b = brk.get_address(addr)
assert b["addr_type"] in KNOWN_ADDR_TYPES, (
f"unknown addr_type {b['addr_type']!r} for {addr}"
)
cs = b["chain_stats"]
assert cs["type_index"] >= 0, f"negative type_index for {addr}: {cs['type_index']}"
assert cs["realized_price"] >= 0, (
f"negative realized_price for {addr}: {cs['realized_price']}"
)
if cs["tx_count"] == 0:
assert cs["realized_price"] == 0, (
f"unfunded address {addr} must have realized_price=0, got {cs['realized_price']}"
)
def test_address_invalid(brk):
"""Garbage input must produce a BrkError carrying HTTP 400."""
with pytest.raises(BrkError) as exc_info:
brk.get_address("abc")
assert exc_info.value.status == 400, (
f"expected status=400, got {exc_info.value.status}"
)
def test_address_pubkey_as_address(brk):
"""Brk-only: hex-encoded pubkey is accepted as a P2PK address."""
b = brk.get_address(SATOSHI_GENESIS_PUBKEY)
assert b["addr_type"] == "p2pk", f"expected p2pk, got {b['addr_type']!r}"
assert b["chain_stats"]["funded_txo_count"] >= 1, (
f"genesis pubkey must have at least one funded output, got "
f"{b['chain_stats']['funded_txo_count']}"
)

View File

@@ -2,33 +2,44 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show
@pytest.fixture(params=[
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r",
], ids=["p2pkh", "p2sh"])
def static_addr(request):
return request.param
# Heavy address (recently active) — stresses the 50-cap path; cannot be ordered
# exactly against mempool.space because the two indexers drift at the chain tip.
ACTIVE_ADDR = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
# Inactive historical addresses — both indexers agree exactly on first-page
# ordering and on pagination.
STABLE_ADDRS = [
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # p2pkh, ~125 txs
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # p2sh, ~5700 txs (heavy pagination)
]
STATIC_ADDRS = [ACTIVE_ADDR] + STABLE_ADDRS
def test_address_txs_static(brk, mempool, static_addr):
"""Confirmed+mempool tx list structure must match for well-known addresses."""
path = f"/api/address/{static_addr}/txs"
b = brk.get_json(path)
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_shape(brk, mempool, addr):
"""Typed list response must structurally match mempool; brk's `index` extra is allowed."""
path = f"/api/address/{addr}/txs"
b = brk.get_address_txs(addr)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
assert isinstance(b, list) and isinstance(m, list)
if b and m:
assert_same_structure(b[0], m[0])
assert "index" in b[0], "brk-only `index` field missing"
def test_address_txs_discovered(brk, mempool, live_addrs):
"""Confirmed+mempool tx list structure must match for each discovered type."""
def test_address_txs_shape_dynamic(brk, mempool, live_addrs):
"""Same shape contract over each live-discovered scriptpubkey type."""
assert live_addrs, "no live addresses discovered"
for atype, addr in live_addrs:
path = f"/api/address/{addr}/txs"
b = brk.get_json(path)
b = brk.get_address_txs(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", f"({len(b)} txs)", f"({len(m)} txs)")
assert isinstance(b, list) and isinstance(m, list)
@@ -36,14 +47,76 @@ def test_address_txs_discovered(brk, mempool, live_addrs):
assert_same_structure(b[0], m[0])
def test_address_txs_fields(brk, mempool, live):
"""Every tx in the list must carry the core mempool.space fields."""
path = f"/api/address/{live.sample_address}/txs"
b = brk.get_json(path)
show("GET", path, f"({len(b)} txs)", "")
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_ordering(brk, addr):
"""All entries must be confirmed and heights monotonically non-increasing."""
b = brk.get_address_txs(addr)
if not b:
pytest.skip("address has no txs in brk")
required = {"txid", "version", "locktime", "vin", "vout", "size", "weight", "fee", "status"}
for tx in b[:5]:
missing = required - set(tx.keys())
assert not missing, f"tx {tx.get('txid', '?')} missing fields: {missing}"
pytest.skip(f"{addr} has no txs in brk")
for tx in b:
assert tx["status"]["confirmed"] is True, (
f"{addr} returned unconfirmed tx {tx['txid']} (this endpoint is chain-only on brk)"
)
heights = [tx["status"]["block_height"] for tx in b]
assert heights == sorted(heights, reverse=True), (
f"{addr} not newest-first by height: {heights[:5]}..."
)
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_limit(brk, addr):
"""Hard cap of 50 confirmed txs per call."""
b = brk.get_address_txs(addr)
assert len(b) <= 50, f"{addr} returned {len(b)} txs, exceeds 50-cap"
@pytest.mark.parametrize("addr", STABLE_ADDRS)
def test_address_txs_top_match_stable(brk, mempool, addr):
"""For inactive historical addresses, brk and mempool agree on first-page order."""
b_txids = [t["txid"] for t in brk.get_address_txs(addr)]
m_txids = [t["txid"] for t in mempool.get_json(f"/api/address/{addr}/txs")]
assert b_txids == m_txids, (
f"{addr} first-page txid order diverges:\n"
f" brk: {b_txids[:5]}...\n"
f" mempool: {m_txids[:5]}..."
)
def test_address_txs_pagination(brk, mempool):
"""`after_txid` returns a fresh, strictly-older page; matches mempool.space."""
addr = "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r"
first = brk.get_address_txs(addr)
assert len(first) == 50, f"expected full first page, got {len(first)}"
last_txid = first[-1]["txid"]
last_height = first[-1]["status"]["block_height"]
second = brk.get_address_txs(addr, after_txid=last_txid)
assert second, "second page must be non-empty for a 5700-tx address"
first_txids = {t["txid"] for t in first}
second_txids = {t["txid"] for t in second}
assert not (first_txids & second_txids), "pagination must not return overlapping txs"
for tx in second:
assert tx["status"]["block_height"] <= last_height, (
f"page 2 tx {tx['txid']} at height {tx['status']['block_height']} "
f"exceeds page-1 tail height {last_height}"
)
m_second = mempool.get_json(f"/api/address/{addr}/txs?after_txid={last_txid}")
b_ids = [t["txid"] for t in second]
m_ids = [t["txid"] for t in m_second]
assert b_ids == m_ids, (
f"page-2 order diverges from mempool:\n"
f" brk: {b_ids[:5]}...\n"
f" mempool: {m_ids[:5]}..."
)
def test_address_txs_invalid(brk):
"""Garbage input must produce a BrkError carrying HTTP 400."""
with pytest.raises(BrkError) as exc_info:
brk.get_address_txs("abc")
assert exc_info.value.status == 400, (
f"expected status=400, got {exc_info.value.status}"
)

View File

@@ -1,34 +1,43 @@
"""GET /api/address/{address}/txs/chain"""
"""GET /api/address/{address}/txs/chain (and /txs/chain/{after_txid})"""
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show
@pytest.fixture(params=[
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r",
], ids=["p2pkh", "p2sh"])
def static_addr(request):
return request.param
# Heavy active address (chain-tip drift expected, no exact-order assertion)
ACTIVE_ADDR = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
# Inactive historical addresses — both indexers agree exactly on first-page ordering
STABLE_ADDRS = [
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", # p2pkh, ~125 txs
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r", # p2sh, ~5700 txs (heavy pagination)
]
STATIC_ADDRS = [ACTIVE_ADDR] + STABLE_ADDRS
def test_address_txs_chain_static(brk, mempool, static_addr):
"""Confirmed-only tx list structure must match for well-known addresses."""
path = f"/api/address/{static_addr}/txs/chain"
b = brk.get_json(path)
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_chain_shape(brk, mempool, addr):
"""Typed list response must structurally match mempool; brk's `index` extra is allowed."""
path = f"/api/address/{addr}/txs/chain"
b = brk.get_address_confirmed_txs(addr)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
assert isinstance(b, list) and isinstance(m, list)
if b and m:
assert_same_structure(b[0], m[0])
assert "index" in b[0], "brk-only `index` field missing"
def test_address_txs_chain_discovered(brk, mempool, live_addrs):
"""Confirmed-only tx list structure must match for each discovered type."""
def test_address_txs_chain_shape_dynamic(brk, mempool, live_addrs):
"""Same shape contract over each live-discovered scriptpubkey type."""
assert live_addrs, "no live addresses discovered"
for atype, addr in live_addrs:
path = f"/api/address/{addr}/txs/chain"
b = brk.get_json(path)
b = brk.get_address_confirmed_txs(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", f"({len(b)} txs)", f"({len(m)} txs)")
assert isinstance(b, list) and isinstance(m, list)
@@ -36,14 +45,88 @@ def test_address_txs_chain_discovered(brk, mempool, live_addrs):
assert_same_structure(b[0], m[0])
def test_address_txs_chain_all_confirmed(brk, live):
"""Every tx returned by /txs/chain must have confirmed=True in its status."""
path = f"/api/address/{live.sample_address}/txs/chain"
b = brk.get_json(path)
show("GET", path, f"({len(b)} txs)", "")
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_chain_all_confirmed(brk, addr):
"""Every entry must have `status.confirmed == True`."""
b = brk.get_address_confirmed_txs(addr)
if not b:
pytest.skip("address has no confirmed txs in brk")
unconfirmed = [t for t in b if not t.get("status", {}).get("confirmed", False)]
pytest.skip(f"{addr} has no confirmed txs in brk")
unconfirmed = [t for t in b if not t["status"]["confirmed"]]
assert not unconfirmed, (
f"{len(unconfirmed)} unconfirmed tx(s) returned by /txs/chain"
f"{addr}: {len(unconfirmed)} unconfirmed tx(s) returned: "
f"{[t['txid'] for t in unconfirmed[:3]]}"
)
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_chain_ordering(brk, addr):
"""Heights must be monotonically non-increasing (newest first)."""
b = brk.get_address_confirmed_txs(addr)
if not b:
pytest.skip(f"{addr} has no confirmed txs in brk")
heights = [t["status"]["block_height"] for t in b]
assert heights == sorted(heights, reverse=True), (
f"{addr} not newest-first by height: {heights[:5]}..."
)
@pytest.mark.parametrize("addr", STATIC_ADDRS)
def test_address_txs_chain_limit(brk, addr):
"""Hard cap of 25 confirmed txs per call."""
b = brk.get_address_confirmed_txs(addr)
assert len(b) <= 25, f"{addr} returned {len(b)} txs, exceeds 25-cap"
@pytest.mark.parametrize("addr", STABLE_ADDRS)
def test_address_txs_chain_top_match_stable(brk, mempool, addr):
"""For inactive historical addresses, brk and mempool agree on first-page order."""
b_txids = [t["txid"] for t in brk.get_address_confirmed_txs(addr)]
m_txids = [t["txid"] for t in mempool.get_json(f"/api/address/{addr}/txs/chain")]
assert b_txids == m_txids, (
f"{addr} first-page txid order diverges:\n"
f" brk: {b_txids[:5]}...\n"
f" mempool: {m_txids[:5]}..."
)
def test_address_txs_chain_pagination(brk, mempool):
"""Path-style pagination must match mempool.space's Esplora-canonical form exactly."""
addr = "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r"
first = brk.get_address_confirmed_txs(addr)
assert len(first) == 25, f"expected full first page (25), got {len(first)}"
last_txid = first[-1]["txid"]
last_height = first[-1]["status"]["block_height"]
second = brk.get_address_confirmed_txs_after(addr, last_txid)
assert second, "second page must be non-empty for a 5700-tx address"
assert len(second) <= 25, f"page 2 exceeds 25-cap: {len(second)}"
first_txids = {t["txid"] for t in first}
second_txids = {t["txid"] for t in second}
assert not (first_txids & second_txids), "pagination must not return overlapping txs"
for tx in second:
assert tx["status"]["confirmed"] is True, f"page 2 has unconfirmed tx {tx['txid']}"
assert tx["status"]["block_height"] <= last_height, (
f"page 2 tx {tx['txid']} at height {tx['status']['block_height']} "
f"exceeds page-1 tail height {last_height}"
)
# Cross-check against mempool.space's path-style form.
m_second = mempool.get_json(f"/api/address/{addr}/txs/chain/{last_txid}")
b_ids = [t["txid"] for t in second]
m_ids = [t["txid"] for t in m_second]
assert b_ids == m_ids, (
f"page-2 order diverges from mempool path-style:\n"
f" brk: {b_ids[:5]}...\n"
f" mempool: {m_ids[:5]}..."
)
def test_address_txs_chain_invalid(brk):
"""Garbage input must produce a BrkError carrying HTTP 400."""
with pytest.raises(BrkError) as exc_info:
brk.get_address_confirmed_txs("abc")
assert exc_info.value.status == 400, (
f"expected status=400, got {exc_info.value.status}"
)

View File

@@ -1,33 +1,55 @@
"""GET /api/address/{address}/txs/mempool"""
from _lib import show
import pytest
from brk_client import BrkError
from _lib import assert_same_structure, show
def test_address_txs_mempool_sample(brk, mempool, live):
"""Mempool tx list must be an array (contents are volatile)."""
path = f"/api/address/{live.sample_address}/txs/mempool"
b = brk.get_json(path)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} txs)", f"({len(m)} txs)")
assert isinstance(b, list) and isinstance(m, list)
def test_address_txs_mempool_discovered(brk, mempool, live_addrs):
"""Mempool tx list must be a (possibly empty) array for each discovered type."""
def test_address_txs_mempool_shape_dynamic(brk, mempool, live_addrs):
"""Shape contract over each live-discovered scriptpubkey type."""
assert live_addrs, "no live addresses discovered"
for atype, addr in live_addrs:
path = f"/api/address/{addr}/txs/mempool"
b = brk.get_json(path)
b = brk.get_address_mempool_txs(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", f"({len(b)} txs)", f"({len(m)} txs)")
assert isinstance(b, list) and isinstance(m, list)
if b and m:
assert_same_structure(b[0], m[0])
def test_address_txs_mempool_all_unconfirmed(brk, live):
"""Every tx returned by /txs/mempool must have confirmed=False (if any)."""
path = f"/api/address/{live.sample_address}/txs/mempool"
b = brk.get_json(path)
show("GET", path, f"({len(b)} txs)", "")
confirmed = [t for t in b if t.get("status", {}).get("confirmed", False)]
assert not confirmed, (
f"{len(confirmed)} confirmed tx(s) returned by /txs/mempool"
def test_address_txs_mempool_limit(brk, live_addrs):
"""Hard cap of 50 mempool txs per call."""
for _atype, addr in live_addrs:
b = brk.get_address_mempool_txs(addr)
assert len(b) <= 50, f"{addr} returned {len(b)} txs, exceeds 50-cap"
def test_address_txs_mempool_all_unconfirmed(brk, live_addrs):
"""Every entry must have status.confirmed == False."""
for _atype, addr in live_addrs:
b = brk.get_address_mempool_txs(addr)
confirmed = [t for t in b if t["status"]["confirmed"]]
assert not confirmed, (
f"{addr}: {len(confirmed)} confirmed tx(s) returned by /txs/mempool: "
f"{[t['txid'] for t in confirmed[:3]]}"
)
def test_address_txs_mempool_unique_txids(brk, live_addrs):
"""No duplicate txids within a single response."""
for _atype, addr in live_addrs:
b = brk.get_address_mempool_txs(addr)
txids = [t["txid"] for t in b]
assert len(txids) == len(set(txids)), f"{addr}: duplicate txids in response"
def test_address_txs_mempool_invalid(brk):
"""Garbage input must produce a BrkError carrying HTTP 400."""
with pytest.raises(BrkError) as exc_info:
brk.get_address_mempool_txs("abc")
assert exc_info.value.status == 400, (
f"expected status=400, got {exc_info.value.status}"
)

View File

@@ -2,51 +2,70 @@
import pytest
from brk_client import BrkError
from _lib import assert_same_values, show
@pytest.fixture(params=[
"12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S",
"3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r",
], ids=["p2pkh", "p2sh"])
def static_addr(request):
return request.param
# Inactive historical addresses with stable, comparable UTXO sets.
STABLE_ADDRS = [
("p2pkh", "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S"),
("p2sh", "3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r"),
]
# Genesis pubkey-hash address: tens of thousands of dust UTXOs — exceeds both
# brk's 1000-cap and mempool.space's 500-cap, so both indexers must 400.
HEAVY_ADDR = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
def test_address_utxo_static(brk, mempool, static_addr):
"""UTXO list must match — same txids, values, and statuses."""
path = f"/api/address/{static_addr}/utxo"
b = brk.get_json(path)
@pytest.mark.parametrize("atype,addr", STABLE_ADDRS, ids=[a for a, _ in STABLE_ADDRS])
def test_address_utxo_static(brk, mempool, atype, addr):
"""Exact UTXO parity (txid+vout+value+status) for stable historical addresses."""
path = f"/api/address/{addr}/utxo"
b = brk.get_address_utxos(addr)
m = mempool.get_json(path)
show("GET", path, f"({len(b)} utxos)", f"({len(m)} utxos)")
assert isinstance(b, list) and isinstance(m, list)
key = lambda u: (u.get("txid", ""), u.get("vout", 0))
b_sorted = sorted(b, key=key)
m_sorted = sorted(m, key=key)
assert_same_values(b_sorted, m_sorted)
show("GET", f"{path} [{atype}]", f"({len(b)} utxos)", f"({len(m)} utxos)")
key = lambda u: (u["txid"], u["vout"])
assert_same_values(sorted(b, key=key), sorted(m, key=key))
def test_address_utxo_discovered(brk, mempool, live_addrs):
"""UTXO list must match for each discovered address type — same txids, values, and statuses."""
"""Same exact-parity contract over each live-discovered scriptpubkey type."""
for atype, addr in live_addrs:
path = f"/api/address/{addr}/utxo"
b = brk.get_json(path)
b = brk.get_address_utxos(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", f"({len(b)} utxos)", f"({len(m)} utxos)")
assert isinstance(b, list) and isinstance(m, list)
key = lambda u: (u.get("txid", ""), u.get("vout", 0))
key = lambda u: (u["txid"], u["vout"])
assert_same_values(sorted(b, key=key), sorted(m, key=key))
def test_address_utxo_fields(brk, live):
"""Every utxo must carry the core mempool.space fields."""
path = f"/api/address/{live.sample_address}/utxo"
b = brk.get_json(path)
show("GET", path, f"({len(b)} utxos)", "")
@pytest.mark.parametrize("atype,addr", STABLE_ADDRS, ids=[a for a, _ in STABLE_ADDRS])
def test_address_utxo_all_confirmed(brk, atype, addr):
"""brk's /utxo only returns confirmed UTXOs (mempool-funded ones are excluded by design)."""
b = brk.get_address_utxos(addr)
if not b:
pytest.skip("address has no utxos in brk")
required = {"txid", "vout", "value", "status"}
for u in b[:5]:
missing = required - set(u.keys())
assert not missing, f"utxo {u.get('txid', '?')}:{u.get('vout', '?')} missing fields: {missing}"
assert isinstance(u["value"], int) and u["value"] > 0
pytest.skip(f"{addr} has no utxos in brk")
unconfirmed = [u for u in b if not u["status"]["confirmed"]]
assert not unconfirmed, (
f"{addr}: {len(unconfirmed)} unconfirmed UTXO(s) returned: "
f"{[(u['txid'], u['vout']) for u in unconfirmed[:3]]}"
)
def test_address_utxo_too_many(brk):
"""Heavy address (>1000 UTXOs) must produce BrkError(status=400, code=too_many_utxos)."""
with pytest.raises(BrkError) as exc_info:
brk.get_address_utxos(HEAVY_ADDR)
assert exc_info.value.status == 400, (
f"expected status=400, got {exc_info.value.status}"
)
def test_address_utxo_invalid(brk):
"""Garbage input must produce a BrkError carrying HTTP 400."""
with pytest.raises(BrkError) as exc_info:
brk.get_address_utxos("abc")
assert exc_info.value.status == 400, (
f"expected status=400, got {exc_info.value.status}"
)

View File

@@ -5,49 +5,79 @@ import pytest
from _lib import assert_same_structure, assert_same_values, show
def test_validate_address_discovered(brk, mempool, live_addrs):
"""Validation of each discovered address type must match exactly."""
for atype, addr in live_addrs:
path = f"/api/v1/validate-address/{addr}"
b = brk.get_json(path)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", b, m)
assert_same_values(b, m)
assert b["isvalid"] is True
VALID_ADDRS = [
("p2pkh-genesis", "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"),
("p2sh", "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy"),
("p2wpkh", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"),
("p2wsh", "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"),
("p2tr", "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"),
]
INVALID_ADDRS = [
("garbage", "notanaddress123"),
("bad-checksum-p2pkh", "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb"),
("bad-checksum-p2sh", "3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz"),
("bad-checksum-p2wpkh", "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5"),
("wrong-network-bech32", "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"),
("mixed-case-bech32", "bc1QRP33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"),
]
# Satoshi's genesis-coinbase pubkey: brk validates this as p2pk; mempool.space
# rejects all raw-pubkey-hex inputs. Documents the intentional brk superset.
GENESIS_PUBKEY = (
"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6"
"49f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f"
)
@pytest.mark.parametrize("addr,kind", [
("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "p2pkh-genesis"),
("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy", "p2sh"),
("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", "p2wpkh"),
("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", "p2tr"),
])
def test_validate_address_static_valid(brk, mempool, addr, kind):
@pytest.mark.parametrize("kind,addr", VALID_ADDRS, ids=[k for k, _ in VALID_ADDRS])
def test_validate_address_static_valid(brk, mempool, kind, addr):
"""Well-known addresses across all script types must validate identically."""
path = f"/api/v1/validate-address/{addr}"
b = brk.get_json(path)
b = brk.validate_address(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{kind}]", b, m)
assert_same_values(b, m)
assert b["isvalid"] is True
assert_same_values(b, m)
@pytest.mark.parametrize("addr,kind", [
("notanaddress123", "garbage"),
("", "empty"),
("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNb", "bad-checksum-p2pkh"),
("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", "bad-checksum-p2wpkh"),
("3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLz", "bad-checksum-p2sh"),
])
def test_validate_address_invalid(brk, mempool, addr, kind):
"""Invalid addresses must produce the same rejection structure."""
def test_validate_address_discovered(brk, mempool, live_addrs):
"""Validation of each live-discovered scriptpubkey type must match exactly."""
for atype, addr in live_addrs:
path = f"/api/v1/validate-address/{addr}"
b = brk.validate_address(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{atype}]", b, m)
assert b["isvalid"] is True
assert_same_values(b, m)
@pytest.mark.parametrize("kind,addr", INVALID_ADDRS, ids=[k for k, _ in INVALID_ADDRS])
def test_validate_address_invalid(brk, mempool, kind, addr):
"""Invalid addresses produce isvalid=false; structure must match (error strings differ by impl)."""
path = f"/api/v1/validate-address/{addr}"
if kind == "empty":
# An empty path segment routes to a different endpoint — skip.
pytest.skip("empty address routes to a different endpoint")
b = brk.get_json(path)
b = brk.validate_address(addr)
m = mempool.get_json(path)
show("GET", f"{path} [{kind}]", b, m)
assert b["isvalid"] is False
assert m["isvalid"] is False
assert_same_structure(b, m)
def test_validate_address_pubkey_hex_brk_only(brk, mempool):
"""Raw pubkey hex: brk accepts as p2pk (superset); mempool.space rejects (non-2xx or no isvalid:true)."""
path = f"/api/v1/validate-address/{GENESIS_PUBKEY}"
b = brk.validate_address(GENESIS_PUBKEY)
m_resp = mempool.get_raw(path)
show("GET", path, b, f"<HTTP {m_resp.status_code}> {m_resp.text[:200]}")
assert b["isvalid"] is True, "brk must accept raw pubkey hex as p2pk"
assert b.get("isscript") is False
assert b.get("iswitness") is False
if 200 <= m_resp.status_code < 300:
try:
m = m_resp.json()
except ValueError:
m = None
assert not (isinstance(m, dict) and m.get("isvalid") is True), (
"mempool.space must not validate raw pubkey hex as a real address"
)