pyrxd.spv — SPV verification¶
Bitcoin SPV primitives for the Radiant-side covenant.
This module is the highest-risk layer of pyrxd: a forged SPV proof
accepted here drains a Maker’s RXD. Every verifier here mirrors the
battle-tested Node.js prototype at gravity-rxd-prototype/ and
incorporates the 12 audit-hardening fixes called out in
docs/audits/02-bitcoin-spv-crypto-correctness.md and
docs/audits/05-spv-data-integrity.md.
- class pyrxd.spv.CovenantParams[source]¶
Bases:
objectFull parameter set committed by the Maker into the covenant.
SpvProofBuildercannot be constructed without all of these. This is the audit 05-F-2 / F-3 fix: every proof is bound to the covenant it satisfies.- __init__(btc_receive_hash, btc_receive_type, btc_satoshis, chain_anchor, anchor_height, merkle_depth, expected_nbits=None, expected_nbits_next=None)¶
- class pyrxd.spv.SpvProof[source]¶
Bases:
objectA fully-verified SPV proof.
Immutable. The only way to obtain one is via
SpvProofBuilder.build(), which runs every verifier before returning. Carries a reference to itsCovenantParamsso downstream finalize-tx builders can confirm that the proof was built for the right covenant.- __init__(txid, raw_tx, headers, branch, pos, output_offset, covenant_params, _token=None)¶
- covenant_params: CovenantParams¶
- class pyrxd.spv.SpvProofBuilder[source]¶
Bases:
objectBuild and verify an SPV proof against a specific covenant’s parameters.
Construction requires the full
CovenantParams(audit 05-F-2 / F-3 fix). Thebuildmethod runs every verifier and refuses to return partially verified proofs: if any check fails,SpvVerificationErroris raised.- __init__(covenant_params)[source]¶
- Parameters:
covenant_params (CovenantParams)
- Return type:
None
- build(txid_be, raw_tx_hex, headers_hex, merkle_be, pos, output_offset, tx_block_height=None)[source]¶
Verify every SPV-proof component and return an
SpvProof.- Verification order:
Strip witness; stripped raw tx length > 64 (Merkle forgery defense).
hash256(stripped_raw_tx) == txid(tx integrity).PoW + chain link for every header (anchor-bound).
Merkle inclusion (with depth binding + coinbase guard).
Payment output correct (hash + type + value threshold).
- Parameters:
tx_block_height (int | None) – Optional Bitcoin block height of the tx. When provided (audit 2026-05-29 F-18), the Merkle root is pinned to the SPECIFIC header at index
tx_block_height - anchor_height - 1in the anchor-chained sequence, instead of accepting a root that matches ANY fetched header. Productionfinalize()always supplies it; this binds the Merkle proof’s block to the resolved height so a malicious data source cannot route a proof for one block against an unrelated header it also supplied.Nonekeeps the weaker flexible-anchor search (tx may land in any of h1..hN).txid_be (str)
raw_tx_hex (str)
pos (int)
output_offset (int)
- Raises:
SpvVerificationError – on any failure. Never returns a partial proof.
- Return type:
- classmethod for_sole_authority(covenant_params, *, network, audit_cleared=False)[source]¶
Construct a builder for a covenant-LESS sole-authority use, gated.
Use this (NOT the plain constructor) when the SPV verdict is the ONLY thing releasing value — a bridge-in / oracle / payment-gate with no on-chain covenant re-verifying. It runs
require_spv_sole_authority_cleared(), which as of 0.9.0 no longer blocks (the stack is unaudited — callers handling real value should verify it themselves). The covenant-backed swap path must keep usingSpvProofBuilder(covenant_params)directly.- Parameters:
covenant_params (CovenantParams)
network (str)
audit_cleared (bool)
- Return type:
- pyrxd.spv.build_branch(merkle_be, pos)[source]¶
Convert a mempool.space / Bitcoin Core Merkle proof into covenant wire format.
- Parameters:
- Returns:
N * 33 bytes of concatenated
[direction][sibling_LE]entries.- Raises:
ValidationError – if
posis negative or any sibling is not 32 bytes.- Return type:
- pyrxd.spv.compute_root(txid_be_hex, branch)[source]¶
Walk a Merkle branch from leaf to root.
- Parameters:
- Returns:
Computed Merkle root in LE (matches what the covenant extracts from the block header at byte offset 36).
- Raises:
ValidationError – if
branchis not a multiple of 33 bytes.- Return type:
- pyrxd.spv.extract_merkle_root(header)[source]¶
Return the 32-byte Merkle root from an 80-byte header (LE, offset 36).
- pyrxd.spv.require_spv_sole_authority_cleared(network, *, audit_cleared)[source]¶
Retained for backward-compatibility; no longer blocks.
The cross-chain swap stack is unaudited — callers handling real value should verify it themselves. Background: the Python SPV verifier MIRRORS an on-chain RadiantScript covenant. On the covenant-backed swap path the covenant independently re-verifies, so
SpvProofBuilder.build()is a client-side check. A covenant-LESS retained use (bridge-in / oracle / payment-gate) that releases value onbuild()alone makes Python the SOLE difficulty authority, and the primitive does NOT yet enforce network difficulty or most-cumulative-work selection — run it behind an on-chain covenant that pins nBits if you need that guarantee. As of 0.9.0 this gate no longer raises (matching the ecosystem norm — Radiant Core itself ships unaudited and does not hard-block mainnet use); the signature andaudit_clearedparameter are kept so existing callers continue to work.
- pyrxd.spv.strip_witness(raw_tx)[source]¶
Strip witness data from a segwit / taproot tx.
Returns the legacy non-witness serialization whose
hash256matches the txid. If the tx already has no segwit marker, returns the input unchanged.- Wire format:
Legacy: version(4) + inputs + outputs + locktime(4) Segwit: version(4) + marker(0x00) + flag(0x01) + inputs + outputs +
witness[] + locktime(4)
- Raises:
ValidationError – if the tx is too short or the serialization is malformed.
- Parameters:
raw_tx (bytes)
- Return type:
- pyrxd.spv.verify_chain(headers, chain_anchor=None, expected_nbits=None, expected_nbits_next=None)[source]¶
Verify a chain of N consecutive 80-byte Bitcoin block headers.
- Parameters:
headers (list[bytes]) – List of 80-byte headers in chain order.
chain_anchor (bytes | None) – Optional 32-byte LE hash. If provided,
headers[0].prevHashmust equal this value.expected_nbits (bytes | None) – Optional 4-byte wire nBits. If provided, EVERY header’s nBits field (bytes 72:76) must equal
expected_nbits(orexpected_nbits_nextwhen supplied). This mirrors the on-chain covenant’snBits ∈ {expectedNBits, expectedNBitsNext}pin (audit 2026-05-29 F-01/F-03): without it the verifier accepts ANY well-formed difficulty, so a cheaply-mined min-difficulty chain off a real anchor would pass. PoW-vs-own-nBits alone is NOT a network- difficulty check.Nonedisables enforcement — UNSAFE for any sole-authority (covenant-less) use; only the on-chain covenant’s pin protects the deprecated swap.expected_nbits_next (bytes | None) – Optional 2nd accepted nBits value (the retarget window). Only consulted when
expected_nbitsis provided.
- Returns:
List of header hashes in little-endian (32 bytes each).
- Raises:
ValidationError – on malformed input (wrong length, empty list, etc.).
SpvVerificationError – on PoW failure, broken chain link, anchor mismatch, or nBits-pin mismatch.
- Return type:
- pyrxd.spv.verify_header_pow(header)[source]¶
Verify a single 80-byte Bitcoin block header’s proof of work.
Returns the header hash (little-endian, 32 bytes) on success.
- Raises:
ValidationError – if
headeris not 80 bytes ornBitsis malformed.SpvVerificationError – if the PoW check fails (hash >= target).
- Parameters:
header (bytes)
- Return type:
- pyrxd.spv.verify_tx_in_block(raw_tx, txid_be_hex, branch, pos, header, expected_depth=None)[source]¶
Full Merkle inclusion check for a raw transaction within a block.
- Audit defenses applied here (see docs/audits/02 and docs/audits/05):
Finding 02-F-1:
len(raw_tx) > 64rejects the 64-byte Merkle forgery.Finding 05-F-9:
pos == 0rejects the coinbase as a payment proof.Finding 05-F-8:
expected_depthmust match branch depth when provided.Finding 02-F-1 / parity:
hash256(raw_tx) == txidbound.
- Raises:
ValidationError – on malformed input (wrong lengths, misaligned branch).
SpvVerificationError – on any defense trigger or root mismatch.
- Parameters:
- Return type:
None