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: object

Full parameter set committed by the Maker into the covenant.

SpvProofBuilder cannot 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)
Parameters:
  • btc_receive_hash (bytes)

  • btc_receive_type (str)

  • btc_satoshis (int)

  • chain_anchor (bytes)

  • anchor_height (int)

  • merkle_depth (int)

Return type:

None

btc_receive_hash: bytes
btc_receive_type: str
btc_satoshis: int
chain_anchor: bytes
anchor_height: int
merkle_depth: int
class pyrxd.spv.SpvProof[source]

Bases: object

A 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 its CovenantParams so 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)
Parameters:
Return type:

None

txid: str
raw_tx: bytes
headers: list[bytes]
branch: bytes
pos: int
output_offset: int
covenant_params: CovenantParams
class pyrxd.spv.SpvProofBuilder[source]

Bases: object

Build and verify an SPV proof against a specific covenant’s parameters.

Construction requires the full CovenantParams (audit 05-F-2 / F-3 fix). The build method runs every verifier and refuses to return partially verified proofs: if any check fails, SpvVerificationError is 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)[source]

Verify every SPV-proof component and return an SpvProof.

Verification order:
  1. Strip witness; stripped raw tx length > 64 (Merkle forgery defense).

  2. hash256(stripped_raw_tx) == txid (tx integrity).

  3. PoW + chain link for every header (anchor-bound).

  4. Merkle inclusion (with depth binding + coinbase guard).

  5. Payment output correct (hash + type + value threshold).

Raises:

SpvVerificationError – on any failure. Never returns a partial proof.

Parameters:
Return type:

SpvProof

pyrxd.spv.build_branch(merkle_be, pos)[source]

Convert a mempool.space / Bitcoin Core Merkle proof into covenant wire format.

Parameters:
  • merkle_be (list[str]) – Sibling hashes in BE display order (hex strings, as returned by mempool.space /tx/:txid/merkle-proof).

  • pos (int) – Zero-indexed tx position within the block’s flat leaf list.

Returns:

N * 33 bytes of concatenated [direction][sibling_LE] entries.

Raises:

ValidationError – if pos is negative or any sibling is not 32 bytes.

Return type:

bytes

pyrxd.spv.compute_root(txid_be_hex, branch)[source]

Walk a Merkle branch from leaf to root.

Parameters:
  • txid_be_hex (str) – Transaction id in BE display format (mempool.space style).

  • branch (bytes) – N * 33 bytes in covenant wire format (from build_branch).

Returns:

Computed Merkle root in LE (matches what the covenant extracts from the block header at byte offset 36).

Raises:

ValidationError – if branch is not a multiple of 33 bytes.

Return type:

bytes

pyrxd.spv.extract_merkle_root(header)[source]

Return the 32-byte Merkle root from an 80-byte header (LE, offset 36).

Parameters:

header (bytes)

Return type:

bytes

pyrxd.spv.hash256(data)[source]

Double SHA-256, Bitcoin’s standard hash function.

Parameters:

data (bytes)

Return type:

bytes

pyrxd.spv.strip_witness(raw_tx)[source]

Strip witness data from a segwit / taproot tx.

Returns the legacy non-witness serialization whose hash256 matches 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:

bytes

pyrxd.spv.verify_chain(headers, chain_anchor=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].prevHash must equal this value.

Returns:

List of header hashes in little-endian (32 bytes each).

Raises:
Return type:

list[bytes]

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:
Parameters:

header (bytes)

Return type:

bytes

pyrxd.spv.verify_payment(raw_tx, output_offset, expected_hash, output_type, min_satoshis)[source]

Verify a specific output in raw_tx pays expected_hash at least min_satoshis.

Parameters:
  • raw_tx (bytes) – Full raw transaction bytes (witness-stripped if segwit).

  • output_offset (int) – Byte offset within raw_tx where this output begins.

  • expected_hash (bytes) – 20 bytes for P2PKH / P2WPKH / P2SH; 32 bytes for P2TR.

  • output_type (str) – One of the module constants above.

  • min_satoshis (int) – Minimum acceptable value (must be > 0 and <= value).

Raises:
Return type:

None

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) > 64 rejects the 64-byte Merkle forgery.

  • Finding 05-F-9: pos == 0 rejects the coinbase as a payment proof.

  • Finding 05-F-8: expected_depth must match branch depth when provided.

  • Finding 02-F-1 / parity: hash256(raw_tx) == txid bound.

Raises:
Parameters:
Return type:

None