pyrxd.gravity — Cross-chain atomic swaps¶
pyrxd.gravity — Gravity protocol covenant transaction builders and orchestrator.
Phase 3a implements the Radiant-side covenant transaction builders that
correspond to the JS prototype’s claim_tx.js, finalize_tx.js, and
forfeit_tx.js.
Phase 3b adds the high-level GravityTrade orchestrator that wraps the
full 4-step BTC↔RXD swap into a single async class.
Public surface¶
GravityOffer— all Maker-committed parameters for a covenantClaimResult— output ofbuild_claim_tx()FinalizeResult— output ofbuild_finalize_tx()ForfeitResult— output ofbuild_forfeit_tx()build_claim_tx— spend MakerOffer → create MakerClaimed UTXObuild_finalize_tx— spend MakerClaimed → release photons to Takerbuild_forfeit_tx— Maker reclaims after claimDeadlinecompute_p2sh_code_hash— derive the expectedClaimedCodeHash a covenant checksGravityTrade— high-level async swap orchestrator (Phase 3b)TradeConfig— tunable parameters for GravityTradeConfirmationStatus— BTC confirmation poll result
- class pyrxd.gravity.ActiveOffer[source]¶
Bases:
objectState of a live Gravity MakerOffer on Radiant.
Returned by
GravityMakerSession.create_offer()and required by all subsequent lifecycle methods.- offer¶
The original
GravityOffercovenant parameters.
- maker_offer_result¶
Raw tx details from
build_maker_offer_tx.
- __init__(offer, maker_offer_result, offer_txid, offer_vout, offer_photons)¶
- Parameters:
offer (GravityOffer)
maker_offer_result (MakerOfferResult)
offer_txid (str)
offer_vout (int)
offer_photons (int)
- Return type:
None
- offer: GravityOffer¶
- maker_offer_result: MakerOfferResult¶
- class pyrxd.gravity.CappedFeeWalletSource[source]¶
Bases:
objectA capped
FeeUtxoSourceover a fixed pre-funded pool.- Parameters:
pool – The pre-funded inventory: small plain-RXD
FeeInputUTXOs the capped-pool wallet owns. Each must be a bare P2PKH UTXO whose pkh matches its own WIF (validated). Must be non-empty and free of duplicate outpoints (a duplicate would double-spend).total_cap_photons – Hard cumulative ceiling on dispensed value. Dispensing stops once the next input would push the running total over this — before handing it out.
max_per_input_photons – Optional per-input ceiling. If given, construction fails when any pool UTXO exceeds it, keeping the “a fee input is small” invariant structural rather than assumed.
- property funded_photons: int¶
Total value of the pre-funded pool. This is the ceiling only if the pool key is isolated from the operator’s main wallet (a deployment property this class cannot verify — see the module docstring and the design note’s residuals).
- next_fee_input()[source]¶
Dispense (commit) the next pool UTXO.
Raises
FeePoolExhaustedError— fail-closed — when the pool is empty or the next input would exceedtotal_cap_photons. Dispense-once: the returned UTXO is never returned again.- Return type:
- property remaining_inputs: int¶
Count of pool UTXOs not yet dispensed (physical inventory; some may be blocked by the cap — see
remaining_photonsfor the actually-spendable budget).
- property remaining_photons: int¶
Photons that
next_fee_input()will actually dispense from here — the in-order prefix of remaining inputs that fits under the cap. Dispensing is in-order and stops at the first input that would exceed the cap (head-of-line), so this is 0 once the next input no longer fits, giving a tower an honest “page now” signal that matches dispense behaviour.
- class pyrxd.gravity.ClaimResult[source]¶
Bases:
objectClaimResult(tx_hex: ‘str’, txid: ‘str’, tx_size: ‘int’, offer_p2sh: ‘str’, claimed_p2sh: ‘str’, fee_sats: ‘int’, output_photons: ‘int’)
- __init__(tx_hex, txid, tx_size, offer_p2sh, claimed_p2sh, fee_sats, output_photons)¶
- class pyrxd.gravity.ConfirmationStatus[source]¶
Bases:
objectStatus returned by
GravityTrade.wait_confirmations().- __init__(txid, confirmations, confirmed, block_height)¶
- class pyrxd.gravity.CovenantArtifact[source]¶
Bases:
objectA loaded, pre-compiled covenant artifact with parameter substitution.
- __init__(contract, hex_template, abi)¶
- classmethod from_json(json_text, *, allow_legacy=False)[source]¶
Load from raw artifact JSON (e.g. from a custom compiled artifact).
- Parameters:
- Return type:
- classmethod load(name, *, allow_legacy=False)[source]¶
Load a bundled artifact by stem name (without
.artifact.json).Available artifacts: -
maker_offer-maker_covenant_6x12_p2wpkh-maker_covenant_flat_6x12_p2wpkh-maker_covenant_trade- Parameters:
- Return type:
- substitute(params)[source]¶
Substitute constructor params into the hex template.
Returns the full redeem script bytes. Raises
ValidationErrorif any required param is missing, any placeholder remains unfilled, or any fixed-width typed param (Ripemd160/Sha256/PubKey) has the wrong byte length — these would silently encode as the wrong push and produce an on-chain-rejected covenant.Values: -
intparams: pass Pythonint-bytes/Ripemd160/Sha256/PubKeyparams: pass hex string
- class pyrxd.gravity.FeeInput[source]¶
Bases:
objectA plain-RXD P2PKH UTXO that pays the miner fee for an HTLC spend.
The single covenant output carries the asset and cannot also pay the fee, so every HTLC spend joins a fee input the spender owns. The whole surplus (
value - out0_value) is consumed as the miner fee — there is no change output (the covenant forbids a second output), so sizevalueupstream so the surplus clears the per-kB min-relay fee but is not wastefully large.- __init__(txid, vout, value, scriptpubkey, wif)¶
- class pyrxd.gravity.FinalizeResult[source]¶
Bases:
objectFinalizeResult(tx_hex: ‘str’, txid: ‘str’, tx_size: ‘int’, fee_sats: ‘int’, output_photons: ‘int’)
- __init__(tx_hex, txid, tx_size, fee_sats, output_photons)¶
- class pyrxd.gravity.ForfeitResult[source]¶
Bases:
objectForfeitResult(tx_hex: ‘str’, txid: ‘str’, tx_size: ‘int’, fee_sats: ‘int’, output_photons: ‘int’)
- __init__(tx_hex, txid, tx_size, fee_sats, output_photons)¶
- class pyrxd.gravity.GravityMakerSession[source]¶
Bases:
objectManage the full lifecycle of a Gravity BTC↔RXD atomic swap offer.
This class handles the Maker’s side of the swap:
Build and broadcast the MakerOffer tx (
create_offer).Poll for the Taker’s claim (
wait_for_claim).Broadcast a cancel tx if the Taker never claims (
cancel_offer).Query current state (
check_status).
- Parameters:
rxd_client – Connected
ElectrumXClientfor Radiant chain operations (broadcast, query UTXOs).btc_source – A
BtcDataSource— used only by subclasses / extensions that need BTC confirmation data. May beNonefor pure Radiant operations.maker_priv – Maker’s secp256k1 private key wrapped in
PrivateKeyMaterial.poll_interval_seconds – Seconds between UTXO polls in
wait_for_claim. Default 30.
Examples
Typical Maker flow:
async with ElectrumXClient(["wss://electrumx.example.com"]) as rxd: session = GravityMakerSession(rxd_client=rxd, maker_priv=priv) params = GravityOfferParams( offer=offer, funding_txid="...", funding_vout=0, funding_photons=5_100_000, fee_sats=100_000, ) active = await session.create_offer(params) claim_txid = await session.wait_for_claim(active, timeout_seconds=3600) if claim_txid is None: cancel_txid = await session.cancel_offer(active)
- __init__(rxd_client, maker_priv, btc_source=None, poll_interval_seconds=30)[source]¶
- Parameters:
rxd_client (ElectrumXClient)
maker_priv (PrivateKeyMaterial)
btc_source (BtcDataSource | None)
poll_interval_seconds (int)
- Return type:
None
- async cancel_offer(offer, fee_sats=1000, maker_address='')[source]¶
Broadcast the cancel (MakerOffer.cancel()) transaction.
Reclaims the MakerOffer UTXO before the claim deadline using
build_cancel_tx. This is only valid if the Taker has NOT yet claimed the UTXO.- Parameters:
offer (ActiveOffer) – The
ActiveOfferto cancel.fee_sats (int) – Miner fee in photons for the cancel tx. Default 1000.
maker_address (str) – Maker’s Radiant P2PKH address to receive the reclaimed photons. Required — must be a valid Radiant address.
- Returns:
The cancel tx’s txid.
- Return type:
- Raises:
ValidationError – If
maker_addressis empty or the offer redeem is invalid.NetworkError – On broadcast failure.
- async check_status(offer)[source]¶
Return the current status of the offer UTXO.
Queries the Radiant ElectrumX server for the MakerOffer P2SH UTXO.
Returns one of:
"open"— UTXO is still unspent (offer not yet claimed)."claimed"— UTXO no longer in unspent set (Taker has claimed)."expired"— claim_deadline has passed and UTXO is unspent(Maker can now forfeit).
"unknown"— UTXO not found and not yet past deadline(may be unconfirmed or already finalized/cancelled).
- Parameters:
offer (ActiveOffer) – The
ActiveOfferto check.- Returns:
One of
"open","claimed","expired","unknown".- Return type:
- Raises:
NetworkError – On ElectrumX query failure.
- async create_offer(offer_params)[source]¶
Build and broadcast the MakerOffer funding tx.
The offer UTXO is a P2SH output locked to
offer_params.offer’s MakerOffer covenant. Once broadcast, the Taker can claim it by spending it withbuild_claim_tx.- Parameters:
offer_params (GravityOfferParams) – Funding-UTXO details and the
GravityOffercovenant.- Returns:
Populated with the resulting txid and UTXO details.
- Return type:
- Raises:
ValidationError – On any parameter format or covenant validation error.
NetworkError – On broadcast failure.
- async wait_for_claim(offer, timeout_seconds=3600)[source]¶
Poll for the Taker’s claim transaction.
Polls
get_utxos()on the MakerOffer P2SH script hash. When the UTXO disappears from the unspent set the Taker has claimed it.This method cannot directly return the claim txid — ElectrumX’s
listunspentAPI only reports which UTXOs are currently unspent. Once the offer UTXO is spent (claimed), we return the offer’s txid as a sentinel so the caller knows which offer was claimed. Callers that need the actual claim txid should fetch the spending tx separately (e.g. viaget_transactionon the address history).- Parameters:
offer (ActiveOffer) – The
ActiveOfferreturned bycreate_offer.timeout_seconds (int) – Maximum seconds to wait. Returns
Noneon timeout.
- Returns:
The offer txid (as a claimed-sentinel) on success, or
Noneon timeout.- Return type:
str or None
- class pyrxd.gravity.GravityOffer[source]¶
Bases:
objectAll parameters a Maker commits into a MakerOffer covenant.
Mirrors
CovenantParamsinpyrxd.spv.proofbut adds Radiant-side fields and the two precomputed redeem scripts.- __init__(btc_receive_hash, btc_receive_type, btc_satoshis, chain_anchor, anchor_height, merkle_depth, taker_radiant_pkh, claim_deadline, photons_offered, offer_redeem_hex, claimed_redeem_hex, expected_code_hash_hex, expected_nbits=None, expected_nbits_next=None)¶
- Parameters:
btc_receive_hash (bytes)
btc_receive_type (str)
btc_satoshis (int)
chain_anchor (bytes)
anchor_height (int)
merkle_depth (int)
taker_radiant_pkh (bytes)
claim_deadline (int)
photons_offered (int)
offer_redeem_hex (str)
claimed_redeem_hex (str)
expected_code_hash_hex (str)
expected_nbits (bytes | None)
expected_nbits_next (bytes | None)
- Return type:
None
- validate_deadline_from_now(accept_short_deadline=False)[source]¶
Check that
claim_deadlineis at leastMIN_DEADLINE_FROM_NOW_HOURSfrom now.Raises
ValidationErrorunless accept_short_deadline isTrue(audit 04-S1 guard: Taker needs time to confirm BTC + build SPV proof + finalize on Radiant).- Parameters:
accept_short_deadline (bool)
- Return type:
None
- class pyrxd.gravity.GravityOfferParams[source]¶
Bases:
objectParameters required to create a new Gravity MakerOffer.
These are the funding-UTXO details for the Maker’s side. The
GravityOfferitself (covenant bytecode, BTC-side params, etc.) is built externally (e.g. viabuild_gravity_offer) and passed asoffer.- offer¶
Fully populated
GravityOfferwithoffer_redeem_hexset.
- change_address¶
Optional Radiant P2PKH address for change output. See
build_maker_offer_txfor semantics.- Type:
str | None
- __init__(offer, funding_txid, funding_vout, funding_photons, fee_sats, change_address=None)¶
- offer: GravityOffer¶
- class pyrxd.gravity.GravityTrade[source]¶
Bases:
objectOrchestrate a complete Gravity BTC↔RXD atomic swap.
- Parameters:
radiant_network – Connected
ElectrumXClientfor Radiant chain operations (broadcast, fetch tx/block).bitcoin_source – A
BtcDataSourcefor Bitcoin chain data (tx fetch, Merkle proof, block headers).config – Optional
TradeConfig. Uses defaults if not provided.
Examples
Typical Taker flow:
async with ElectrumXClient(["wss://electrumx.example.com"]) as rxd: trade = GravityTrade(radiant_network=rxd, bitcoin_source=btc_src) claim = await trade.claim( offer=offer, offer_txid="...", offer_vout=0, offer_photons=10_000_000, fee_sats=1000, taker_privkey=privkey, ) btc_txid = "..." # broadcast BTC payment externally status = await trade.wait_confirmations(btc_txid) result = await trade.finalize( btc_txid=btc_txid, offer=offer, claimed_txid=claim.txid, claimed_vout=0, claimed_photons=claim.output_photons, taker_address="...", fee_sats=1000, )
- __init__(*, radiant_network, bitcoin_source, config=None)[source]¶
- Parameters:
radiant_network (ElectrumXClient)
bitcoin_source (BtcDataSource)
config (TradeConfig | None)
- Return type:
None
- async claim(offer, offer_txid, offer_vout, offer_photons, fee_sats, taker_privkey)[source]¶
Spend the MakerOffer UTXO, creating a MakerClaimed UTXO.
Broadcasts the claim transaction to the Radiant network and returns a
ClaimResult.The claim transaction requires Taker’s signature (audit 04-S3).
build_claim_txindependently verifies the code hash before signing (audit 05-F-13).- Parameters:
offer (GravityOffer) – The
GravityOfferposted by the Maker.offer_txid (str) – Radiant txid of the MakerOffer funding output.
offer_vout (int) – Output index of the MakerOffer UTXO.
offer_photons (int) – Value of the MakerOffer UTXO in photons.
fee_sats (int) – Radiant miner fee in photons.
taker_privkey (PrivateKeyMaterial) – Taker’s secp256k1 private key.
- Return type:
- async finalize(btc_txid, offer, claimed_txid, claimed_vout, claimed_photons, taker_address, fee_sats, btc_tx_height=None)[source]¶
Fetch the BTC SPV proof, verify it, and broadcast the finalize tx.
This method always runs the full
SpvProofBuilderverifier chain — there is no way to bypass verification at this level.- Parameters:
btc_txid (str) – Bitcoin transaction ID of the Taker’s BTC payment.
offer (GravityOffer) – The
GravityOfferoriginally posted by the Maker. Used to constructCovenantParamsfor SPV proof verification.claimed_txid (str) – Radiant txid of the MakerClaimed UTXO (output of
claim()).claimed_vout (int) – Output index of the MakerClaimed UTXO.
claimed_photons (int) – Value of the MakerClaimed UTXO in photons.
taker_address (str) – Taker’s Radiant P2PKH address to receive the photons.
fee_sats (int) – Radiant miner fee in photons.
btc_tx_height (int | None) – Optional: Bitcoin block height where btc_txid was confirmed. If not provided, the orchestrator will determine it automatically.
- Raises:
SpvVerificationError – If any SPV verifier rejects the proof.
NetworkError – On any network failure fetching BTC data.
ValidationError – On any parameter format error.
- Return type:
- async wait_confirmations(btc_txid, min_confirmations=None)[source]¶
Poll Bitcoin until btc_txid reaches the required confirmations.
- Parameters:
- Returns:
Always has
confirmed=Trueon return (raises on timeout).- Return type:
- Raises:
NetworkError – If polling exceeds
config.max_poll_attempts.ValidationError – If btc_txid is not a valid 64-char hex string.
- class pyrxd.gravity.HtlcCovenant[source]¶
Bases:
objectA built HTLC covenant: the funded SPK + the bindings a spend must satisfy.
- prologue_len¶
Length of the compiled body (==
len(funded_spk)for NFT/RXD; the offset of the FT epilogue weld for FT). The bare-0xbd guard pins to this.- Type:
- taker_holder_script / maker_holder_script
The holder scripts
output[0]of a claim (taker) / refund (maker) must equal; the covenant bindshash256of each.
- expected_taker_hash / expected_maker_hash
hash256(taker_holder_script)/hash256(maker_holder_script)— the values baked into the covenant.
- __init__(variant, funded_spk, prologue_len, taker_holder_script, maker_holder_script, expected_taker_hash, expected_maker_hash, genesis_ref, hashlock, refund_csv)¶
- class pyrxd.gravity.MakerOfferResult[source]¶
Bases:
objectOutput of
build_maker_offer_tx()— the MakerOffer funding tx.- __init__(tx_hex, txid, tx_size, offer_p2sh, fee_sats, output_photons)¶
- class pyrxd.gravity.OfferReceive[source]¶
Bases:
objectA per-offer BTC receive destination derived from a maker account xpub.
Persist
offer_indexwith the offer: the maker needs it to (a) spend the received BTC via the matching child key and (b) never reuse it for another live offer.- __init__(btc_receive_hash, btc_receive_type, offer_index)¶
- class pyrxd.gravity.RadiantChainIO[source]¶
Bases:
objectThin chain helper over an
ElectrumXClient-like object.Provides exactly what the leg needs: broadcast, confirmation depth, and the on-chain value of a covenant output. NOT unified with
GravityTrade(that drives the SPV-oracle finalize swap, a different protocol).The injected
clientmust exposebroadcast(raw)->txid,get_transaction_verbose(txid)->dict(withconfirmations), andget_utxos(script_hash)->list(records withtx_hash/tx_pos/value).- async covenant_unspent_incl_mempool(outpoint)[source]¶
Mempool-AWARE liveness of a covenant outpoint — the complement to
find_covenant_utxo’s mempool-BLIND scantxoutset scan.True= unspent considering the mempool;False= spent (confirmed OR by a PENDING mempool tx);None= the client cannot answer (the caller keeps its own idempotency guard). Lets the autonomous claim executor treat a covenant already spent IN THE MEMPOOL as claimed — killing the per-tick re-carve drain WITHOUT a durable cross-restart store and WITHOUT the SeenStore’s eviction blind spot (a truly-unspent covenant, e.g. after a claim is evicted by a reorg, correctly re-fires).
- async find_covenant_utxo(spk, *, expected_value=None)[source]¶
Locate the funded covenant UTXO for
spk->(outpoint, value, height).Scans the UTXO set of the covenant scriptPubKey (ElectrumX script-hash =
sha256(spk)reversed). The covenant funds exactly one output, so there is one matching UTXO; ifexpected_valueis given, the match must equal it (a wrong value is a mis-funded covenant -> fail-closed). The returned value is the ON-CHAIN value, never a self-report.
- class pyrxd.gravity.RadiantCovenantLeg[source]¶
Bases:
objectThe concrete Radiant
radiant_leg(HTLC covenant claim/refund).- Parameters:
network – Radiant network tag (regtest test chains bypass the audit gate).
maker_pkh (taker_pkh /) – The taker (claim) and maker (refund) Radiant holder pubkey-hashes. The covenant binds
hash256(holder(pkh)); these must reproduce the terms’taker_dest_hash/maker_dest_hash(asserted inexpected_covenant_scriptpubkey()).chain_io – A
RadiantChainIO(broadcast + confirmations + UTXO value).fee_source – A
FeeUtxoSourcesupplying the fee input for each spend.min_confirmations – Confirmations required before the funded covenant value is trusted.
audit_cleared – Explicit opt-in for a value-bearing
network(seepyrxd.btc_wallet.htlc_leg.require_audit_cleared()).
- __init__(*, network, taker_pkh, maker_pkh, chain_io, fee_source, min_confirmations=1, audit_cleared=False)[source]¶
- async claim_asset(record, preimage)[source]¶
Build + broadcast the TAKER’s claim spend (reveals
p). Returns the txid.
- async covenant_outpoint(terms)[source]¶
Locate the funded covenant UTXO
txid:voutby scanning its SPK’s UTXO set.The maker locks the asset into the covenant SPK (a pure function of the terms); the leg finds that single funded UTXO on-chain via ElectrumX. The carrier value is bound to
terms.radiant_amountso a mis-funded covenant fails closed.- Parameters:
terms (NegotiatedTerms)
- Return type:
- class pyrxd.gravity.RefAuthenticityIndexer[source]¶
Bases:
ProtocolThe minimal indexer surface needed to verify a genesis REF is real.
Implementations resolve a genesis-outpoint ref to its on-chain reveal as a
ResolvedRef.resolve_refis async (the underlyingglyph.get_tokenRPC is async) and MUST raise (not returnNoneoptimistically) when the indexer cannot reach a definitive answer — the caller treatsNone, any missing/invalid field, or any exception as fail-closed. ReturningNonemeans “no such token” (also fail-closed).- __init__(*args, **kwargs)¶
- async resolve_ref(genesis_ref)[source]¶
Resolve
genesis_refto its reveal, orNoneif unknown.- Parameters:
genesis_ref (bytes)
- Return type:
ResolvedRef | None
- class pyrxd.gravity.ResolvedRef[source]¶
Bases:
objectA trusted indexer’s resolution of a genesis ref to its on-chain reveal.
This is the inspectable surface the gate binds against — the gate does NOT trust a bare boolean from the indexer; it re-checks each field against what the taker agreed to. A real adapter populates this from the indexer’s
glyph.get_tokenresponse (ref_outpoint/payload_hash/confirmations + a decodedglymarker); a test fake constructs it directly.- genesis_outpoint¶
The 36-byte genesis outpoint (txid||vout) the indexer says this token was minted at. Binding (a)/(d): MUST equal the advertised
genesis_ref.- Type:
- has_gly_marker¶
True iff the reveal carries a
glyenvelope (binding (b)). A bare singleton on a plain wallet UTXO has none — the exact R1 forgery.- Type:
- payload_hash¶
The reveal’s payload-commitment hash (binding (c)), or
b""if the indexer did not supply one.- Type:
- confirmations¶
Confirmations on the genesis tx (binding (e)). A negative/None value is treated as fail-closed by the gate.
- Type:
- __init__(genesis_outpoint, has_gly_marker, payload_hash, confirmations)¶
- class pyrxd.gravity.RxinDexerRefAdapter[source]¶
Bases:
objectResolve a genesis ref to a
ResolvedRefvia RXinDexerglyph.get_token.Implements the
RefAuthenticityIndexerprotocol the pre-lock gate awaits. Maps the indexer’s token dict to the inspectable fields the gate binds:genesis_outpoint — from the token’s
ref_outpoint(txid:vout), re-encoded to the 36-byte wire ref so it compares equal to the advertisedgenesis_ref. (glyph.get_tokenonly returns genuinely-minted Glyph tokens, so a resolvable token IS aglyreveal — seehas_gly_marker.)has_gly_marker —
Truewhenever the indexer returned a token dict for the ref (the indexer only indexes realglyenvelopes). A bare wallet-UTXO singleton (the R1 forgery) resolves toNoneand the gate fails closed.payload_hash — from
payload_hash(bytes), orb""if absent.confirmations — read separately from the genesis tx via
chain_io(glyph.get_tokendoes not carry confs).
NOTE (T7 plan D3): a single indexer is a SPOF, and decoding a token dict is NOT SPV authenticity (no Merkle/header binding). For the regtest milestone the local node is ground truth; SPV-bound / multi-source cross-checking is the audit-gated track. This adapter is the single-indexer regtest backend.
- __init__(indexer, chain_io)[source]¶
- Parameters:
chain_io (RadiantChainIO)
- Return type:
None
- async resolve_ref(genesis_ref)[source]¶
- Parameters:
genesis_ref (bytes)
- Return type:
ResolvedRef | None
- class pyrxd.gravity.SeenStore[source]¶
Bases:
objectIn-memory H-freshness store (the coordinator’s
reserve/has_seen).Records every hashlock H the coordinator has committed to funding, so a reused H is rejected for BOTH reasons: economic (free-option replay) and cross-swap preimage replay.
reserve(H)is the authoritative atomic test-and-set the coordinator calls PRE-broadcast;has_seenis a read-only advisory probe (the pre-lock gate’s cheap early-reject), never the binding decision.NON-DURABLE (
durable = False): a plainset, so freshness does NOT survive a restart or a second process. That is acceptable only for a single-process, single-shot run that mints a fresh H per swap (the dust runbook); the coordinator’s construct-time guard refuses this store on a value-bearing network unless the operator passesCoordinatorConfig(accept_nondurable_seen=True). A durable replacement (SQLiteINSERT OR IGNOREkeyed on H, declaringdurable = True) is deferred to the external-audit track; it MUST stay non-blocking (asyncio.to_threadbehind an asyncreserve) and fsync the reservation BEFORE the BTC broadcast. The method shape is duck-compatible so that durable store drops in unchanged.- durable = False¶
- class pyrxd.gravity.TradeConfig[source]¶
Bases:
objectTunable parameters for GravityTrade.
- min_btc_confirmations¶
Minimum on-chain BTC confirmations before finalizing. MUST equal the covenant’s header-depth N (the finalize path verifies exactly N consecutive headers from the anchor; a proof with fewer is rejected). Default 6 — matches the default N=6 covenant and Bitcoin’s standard finality convention (~1h). N is a per-offer MAKER knob: raise it (e.g. 12) for high-value/irreversible assets to roughly double the reorg cost, at the price of a longer wait. When using a covenant built with a different N, set this to that N (audit 2026-05-24: the two must match).
- Type:
- max_poll_attempts¶
Maximum number of polls before
wait_confirmationsraisesNetworkError. Default 120 (= 2 hours at 60s).- Type:
- accept_short_deadline¶
If
True, suppress the 24h deadline guard (audit 04-S1). Only for testing — do NOT set in production.- Type:
- deadline_warning_seconds¶
Emit a WARNING log in
finalize()when the Maker’s claim deadline is less than this many seconds away. Default 7200 (2 hours). Set to 0 to disable. Takers should finalize immediately if this fires (audit 04-S1 forfeit race).- Type:
- __init__(min_btc_confirmations=6, poll_interval_seconds=60, max_poll_attempts=120, accept_short_deadline=False, deadline_warning_seconds=7200)¶
- pyrxd.gravity.build_claim_tx(offer, funding_txid, funding_vout, funding_photons, fee_sats, taker_privkey, accept_short_deadline=False)[source]¶
Build the Radiant
claim()spending tx: MakerOffer → MakerClaimed.Requires Taker’s private key to produce a Radiant signature satisfying
MakerOffer.claim(takerSig)— prevents third-party state-advance grief (audit 04-S3).Audit 05-F-13: verifies
claimedRedeemHexmatchesexpectedClaimedCodeHashbefore building, so the tx won’t be rejected on-chain.scriptSig layout:
<takerSig+hashtype> OP_1 <offer redeem script>
- Parameters:
offer (GravityOffer) – Fully populated
GravityOffer(validated in__post_init__).funding_txid (str) – Hex txid of the MakerOffer UTXO being spent.
funding_vout (int) – Output index of the MakerOffer UTXO.
funding_photons (int) – Value of the MakerOffer UTXO in photons.
fee_sats (int) – Miner fee in photons (== satoshis on Radiant).
taker_privkey (PrivateKeyMaterial) – Taker’s secp256k1 private key (wrapped in
PrivateKeyMaterial).accept_short_deadline (bool) – If
True, suppress the 24-hour deadline guard (audit 04-S1).
- Return type:
- pyrxd.gravity.build_finalize_tx(spv_proof, claimed_redeem_hex, funding_txid, funding_vout, funding_photons, to_address, fee_sats, minimum_output_photons=0, header_slots=None, branch_slots=None)[source]¶
Build the Radiant
finalize()tx: MakerClaimed → Taker’s address.The
spv_proofmust be a fully-verifiedSpvProofproduced bySpvProofBuilder.build()— this is the only way to construct one.No Radiant signature is required — the covenant accepts the scriptSig based on the SPV proof data alone. Output routing is enforced by the covenant’s committed
takerRadiantPkhstate.scriptSig layout (pushed bottom-to-top; last push is TOP at exec):
<h1> <h2> ... <hN> <branch> <rawTx> OP_0 <claimed redeem script>
OP_0(empty push = selector 0) selects thefinalize()function.- Parameters:
spv_proof (SpvProof) – Fully-verified SPV proof (only obtainable from
SpvProofBuilder).claimed_redeem_hex (str) – Hex of MakerClaimed locking bytecode.
funding_txid (str) – Txid of the MakerClaimed UTXO being spent.
funding_vout (int) – Output index of the MakerClaimed UTXO.
funding_photons (int) – Value of the MakerClaimed UTXO in photons.
to_address (str) – Taker’s Radiant P2PKH address.
fee_sats (int) – Miner fee in photons.
minimum_output_photons (int) – The covenant’s
totalPhotonsInOutputfloor — baked in at offer creation time. The finalize tx is rejected on-chain ifoutput[0].value < totalPhotonsInOutput, so we validate here before burning relay fees. Passoffer.photons_offeredwhen calling fromGravityTrade. Defaults to 0 (no floor check) for callers that have already verified externally.header_slots (int | None)
branch_slots (int | None)
- Return type:
- pyrxd.gravity.build_forfeit_tx(offer, funding_txid, funding_vout, funding_photons, maker_address, fee_sats)[source]¶
Build the Radiant
forfeit()tx: Maker reclaims afterclaimDeadline.Can only be built once
offer.claim_deadlinehas passed (i.e. the current wall-clock time is >=claim_deadline).Sets
nLockTime = claim_deadlineforOP_CHECKLOCKTIMEVERIFY. Sets input sequence to0xFFFFFFFE(<0xFFFFFFFF— required for CLTV to be evaluated).scriptSig layout:
OP_1 <claimed redeem script>
OP_1(selector 1) selects theforfeit()function.- Parameters:
offer (GravityOffer) –
GravityOfferwhoseclaim_deadlinehas already passed.funding_txid (str) – Txid of the MakerClaimed UTXO being forfeited.
funding_vout (int) – Output index of the MakerClaimed UTXO.
funding_photons (int) – Value of the MakerClaimed UTXO in photons.
maker_address (str) – Maker’s Radiant P2PKH address to receive the reclaimed photons.
fee_sats (int) – Miner fee in photons.
- Return type:
- pyrxd.gravity.build_gravity_offer(maker_pkh, maker_pk, taker_pk, taker_radiant_pkh, btc_receive_hash, btc_receive_type, btc_satoshis, btc_chain_anchor, expected_nbits, anchor_height, merkle_depth, claim_deadline, photons_offered, expected_nbits_next=None, accept_short_deadline=False, covenant_artifact_name='maker_covenant_flat_12x20_sentinel_all', offer_artifact_name='maker_offer', used_btc_receive_hashes=None, reject_low_difficulty=True, min_difficulty_nbits=None)[source]¶
Build a
GravityOfferwith real covenant redeem scripts generated from the bundled artifacts.This is the top-level entry point for Maker-side offer construction. Internally it:
Validates the claim deadline (S1 guard).
Loads the MakerClaimed covenant artifact and substitutes code-section params.
Computes
expectedClaimedCodeHash = hash256(P2SH_scriptPubKey)from the substituted redeem script.Loads the MakerOffer artifact and substitutes its params (including the code hash from step 3).
Returns a
GravityOfferwith both redeem scripts populated.
- Parameters:
accept_short_deadline (bool) – Override the 24h deadline guard. Set True only for test harnesses you control — never because a counterparty asks.
covenant_artifact_name (str) – Override the MakerClaimed artifact stem.
offer_artifact_name (str) – Override the MakerOffer artifact stem.
used_btc_receive_hashes (set[bytes] | None) – Optional set of
btc_receive_hashvalues already committed to other LIVE offers by this Maker. If the newbtc_receive_hashis in this set, the call is rejected.reject_low_difficulty (bool) – Enforce a difficulty FLOOR on the committed nBits (audit 2026-05-29 F-02). The covenant only pins
nBits == committed, so a min-difficulty commit (e.g. theffff001dfootgun) lets an attacker mine a fake SPV header chain off the real anchor for ~$0. With this True the committed target must be strictly harder than the floor. Covenant-less retained uses (bridge-in / oracle / gate) MUST set this True — and SHOULD also passmin_difficulty_nbits, because the default floor (difficulty-1) only blocks the difficulty-1 class, not a target merely easier than mainnet. Defaults True (secure-by-default, audit 2026-05-29 F-02 follow-up): a real mainnet nBits is far harder than difficulty-1 so it passes the default floor; only a difficulty-1-class commit (ffff001d) is rejected. Passreject_low_difficulty=Falsefor regtest/test offers that useffff001d.min_difficulty_nbits (bytes | None) – Optional 4-byte wire nBits defining the difficulty floor used when
reject_low_difficultyis True. Source this from the live block header atanchor_heightfor a real network-difficulty floor; if omitted, the floor defaults to difficulty-1 (a coarse footgun guard only).maker_pkh (bytes)
maker_pk (bytes)
taker_pk (bytes)
taker_radiant_pkh (bytes)
btc_receive_hash (bytes)
btc_receive_type (str)
btc_satoshis (int)
btc_chain_anchor (bytes)
expected_nbits (bytes)
anchor_height (int)
merkle_depth (int)
claim_deadline (int)
photons_offered (int)
expected_nbits_next (bytes | None)
- Return type:
Warning
CROSS-OFFER REPLAY (audit 2026-05-24 C-ECON-1). A Bitcoin payment cannot reference a Radiant offer, so the covenant binds the payment only by
btc_receive_hash+btc_satoshis+btc_chain_anchor. If the samebtc_receive_hash(BTC receive address) + amount is reused across two offers with overlapping anchor windows, one BTC payment + one SPV proof can finalize BOTH offers — a taker pays once and takes two assets. There is NO on-chain or automatic defense; the earlier “per-offer-derived btcReceiveHash (H1)” control described in the design notes was never implemented. The Maker MUST use a fresh, unique BTC receive address per offer. Passused_btc_receive_hashesto have this function reject reuse it can see; offers built by separate processes are the caller’s responsibility.PREFER
build_gravity_offer_derived(), which derives a distinct receive address per offer from the maker’s account xpub — that is the structural fix (distinct address ⇒ distinct code hash ⇒ replay impossible) and needs no caller-side live-set tracking. This raw-hash entry point remains for callers that manage receive addresses themselves; theused_btc_receive_hashesguard is only best-effort.
- pyrxd.gravity.build_gravity_offer_derived(account_xpub, offer_index, *, maker_pkh, maker_pk, taker_pk, taker_radiant_pkh, btc_satoshis, btc_chain_anchor, expected_nbits, anchor_height, merkle_depth, claim_deadline, photons_offered, expected_nbits_next=None, accept_short_deadline=False, covenant_artifact_name='maker_covenant_flat_12x20_sentinel_all', offer_artifact_name='maker_offer', reject_low_difficulty=True, min_difficulty_nbits=None)[source]¶
Build an offer whose BTC receive address is DERIVED per-offer (replay-safe).
This is the structural fix for the cross-offer replay (C-ECON-1 / “H1”): the receive hash is derived from
account_xpubatoffer_indexviapyrxd.gravity.receive.derive_offer_btc_receive(), so every offer commits to a DISTINCT BTC address. A payment to one offer’s address cannot satisfy another offer’s covenant (differentbtcReceiveHash⇒ different code hash), so one BTC payment can finalize at most one offer — no caller-supplied live-set bookkeeping required.Prefer this over passing a raw
btc_receive_hashtobuild_gravity_offer(). The caller MUST allocate a fresh, never-reusedoffer_indexper offer (a persistent monotonic counter per account) and hold the matching xprv to spend received BTC.- Returns:
(GravityOffer, OfferReceive)— persistOfferReceive.offer_indexwith the offer so the maker can later spend the received BTC and never reuse it.- Parameters:
account_xpub (Any)
offer_index (int)
maker_pkh (bytes)
maker_pk (bytes)
taker_pk (bytes)
taker_radiant_pkh (bytes)
btc_satoshis (int)
btc_chain_anchor (bytes)
expected_nbits (bytes)
anchor_height (int)
merkle_depth (int)
claim_deadline (int)
photons_offered (int)
expected_nbits_next (bytes | None)
accept_short_deadline (bool)
covenant_artifact_name (str)
offer_artifact_name (str)
reject_low_difficulty (bool)
min_difficulty_nbits (bytes | None)
- Return type:
- pyrxd.gravity.build_htlc_claim_tx(*, covenant, covenant_outpoint, carrier_value, preimage, fee)[source]¶
Build the TAKER’s claim spend: reveal
p, pay the single output to the taker.Covenant scriptSig =
<preimage push> <OP_0>(preimage first/under, selector last/on top). The single output pays the covenant’s pinned TAKER holder script at the carrier value; the fee input’s full surplus is the miner fee (no change).- Parameters:
covenant (HtlcCovenant)
covenant_outpoint (str)
carrier_value (int)
preimage (bytes)
fee (FeeInput)
- Return type:
- pyrxd.gravity.build_htlc_covenant_ft(*, genesis_txid, genesis_vout, amount, taker_pkh, maker_pkh, hashlock, refund_csv)[source]¶
Build the FT-variant HTLC covenant (genesis ref bound via the FT epilogue weld).
- pyrxd.gravity.build_htlc_covenant_nft(*, genesis_txid, genesis_vout, nft_carrier_value, taker_pkh, maker_pkh, hashlock, refund_csv)[source]¶
Build the NFT-variant HTLC covenant (singleton
d8<ref>inside the body).
- pyrxd.gravity.build_htlc_covenant_rxd(*, amount, taker_pkh, maker_pkh, hashlock, refund_csv)[source]¶
Build the RXD-variant HTLC covenant (native RXD: NO genesis ref, NO ref ops).
- pyrxd.gravity.build_htlc_refund_tx(*, covenant, covenant_outpoint, carrier_value, fee)[source]¶
Build the MAKER’s CSV refund spend (function selector OP_1, after the timelock).
Covenant scriptSig =
<OP_1>ONLY (no preimage, no sig — gated by the relative timelock). The covenant input’snSequence=covenant.refund_csvandtx.version = 2so BIP68 engages. The single output pays the covenant’s pinned MAKER holder script; the fee input’s full surplus is the miner fee.- Parameters:
covenant (HtlcCovenant)
covenant_outpoint (str)
carrier_value (int)
fee (FeeInput)
- Return type:
- pyrxd.gravity.build_maker_offer_tx(offer, funding_txid, funding_vout, funding_photons, fee_sats, maker_privkey, change_address=None)[source]¶
Build the Radiant funding tx that deploys a MakerOffer P2SH UTXO.
Spends a plain P2PKH UTXO owned by the Maker and creates a P2SH output locked to the MakerOffer redeem script. Once confirmed, the Taker can spend it with
build_claim_tx().The P2SH scriptPubKey is:
OP_HASH160 <hash160(offer_redeem)> OP_EQUAL
Signing uses standard BIP143 P2PKH sighash (the input is a plain P2PKH UTXO, not a covenant) with Radiant’s
hashOutputHashesextension. The scriptCode for signing is the P2PKH scriptPubKey of the funding input, derived from the Maker’s compressed public key.- Parameters:
offer (GravityOffer) – Fully populated
GravityOfferwithoffer_redeem_hexset.funding_txid (str) – Hex txid of the Maker’s P2PKH UTXO being spent.
funding_vout (int) – Output index of the Maker’s P2PKH UTXO.
funding_photons (int) – Value of the Maker’s P2PKH UTXO in photons.
fee_sats (int) – Miner fee in photons. The offer output receives
funding_photons - fee_satsphotons.maker_privkey (PrivateKeyMaterial) – Maker’s secp256k1 private key (
PrivateKeyMaterial). Used to sign the P2PKH input and derive the P2PKH scriptCode for hashing.change_address (str | None) – Default
None(single-output): the fullfunding_photons - fee_satsis locked in the P2SH, so surplus aboveoffer.photons_offeredstays with the covenant to fund the later claim/finalize tx fees. When set (two-output): the P2SH receives exactlyoffer.photons_offeredand the remainder goes to a P2PKH output atchange_address. Use the two-output form only whenoffer.photons_offeredalready includes a buffer for downstream claim/finalize fees — otherwise the covenant will reject those txs.
- Return type:
- pyrxd.gravity.compute_p2sh_code_hash(redeem_script)[source]¶
Compute expectedClaimedCodeHash:
hash256of the P2SH scriptPubKey.This is what MakerOffer checks on-chain:
hash256(tx.outputs[0].codeScript) == expectedClaimedCodeHash
For P2SH outputs the
codeScriptis the 23-byteOP_HASH160 <hash> OP_EQUALscriptPubKey.Audit 05-F-13 fix: caller passes the claimed redeem script; we derive the hash independently rather than trusting a caller-supplied value.
- pyrxd.gravity.derive_offer_btc_receive(account_xpub, offer_index)[source]¶
Derive a unique P2WPKH receive hash for one offer.
- Parameters:
account_xpub (str | bytes | Xpub) – The maker’s BIP32 account-level xpub (e.g. the public form of
m/84'/0'/0'). Child derivation is non-hardened so it is reproducible from the xpub; the maker holds the matching xprv to spend received BTC.offer_index (int) – A per-offer, never-reused non-hardened index. The caller owns allocation (a monotonic counter) — this function is pure and does NOT track which indices have been issued.
- Returns:
OfferReceivewith the derived 20-byte receive hash and the index.- Raises:
ValidationError – on an out-of-range index or an unusable xpub.
- Return type:
- pyrxd.gravity.validate_claim_deadline(claim_deadline, *, min_future_seconds=86400, bypass=False)[source]¶
Raise
ValidationErrorifclaim_deadlineis not at leastmin_future_secondsfrom now (default: 24h).This is the Python port of the S1 check in
extract_p2sh_code_hash.js(audit 04 finding S1: a short deadline lets Maker race-snipe Taker’s claim).
- async pyrxd.gravity.verify_ref_authenticity(indexer, genesis_ref, *, asset_variant, min_confirmations, expected_payload_hash=None)[source]¶
Hard pre-payment gate: confirm the covenant’s REF is a real minted asset.
awaitthis BEFORE the taker pays any BTC for an FT/NFT swap. Plain-RXD swaps carry no ref and are skipped. Enforces the five bindings (a)-(e) documented at module level and fails closed on EVERY uncertain outcome: indexer unreachable/error,None(unknown token), a missing/invalid field, genesis-outpoint ≠ ref, absentglymarker, payload mismatch, or a genesis shallower thanmin_confirmations.- Parameters:
indexer (RefAuthenticityIndexer) – a trusted
RefAuthenticityIndexer. A lying or attacker-controlled indexer defeats this gate — the taker must use an indexer they trust (the audit-gated track adds SPV/multi-source cross-checking; a single indexer is a SPOF, see T7 plan D3).genesis_ref (bytes) – the 36-byte genesis outpoint ref baked into the covenant. This IS the advertised asset’s identity (binding d).
asset_variant (str) – “rxd” | “ft” | “nft”. Only ft/nft carry a ref to verify.
min_confirmations (int) – required confirmations on the genesis tx (binding e). Must be a non-negative int.
expected_payload_hash (bytes | None) – if the taker agreed to a specific payload, the reveal’s payload hash MUST match it (binding c).
Noneskips this single binding (the others still apply).
- Raises:
ValidationError – if the ref is not provably the advertised authentic asset. The caller MUST NOT pay the counter-leg (BTC or ETH) when this raises.
- Return type:
None