Trustless cross-chain swap: RXD ↔ ETH on local chains¶
Trade a Radiant asset for ETH with no bridge, no custodian, and no trusted third party —
and watch it settle end to end on chains running entirely on your own machine. This is the
flagship of what pyrxd’s gravity module does: a hash-timelock (HTLC) atomic swap driven by a
chain-neutral coordinator.
Pre-audit — local/testnet only. This primitive has not had an external security audit. It’s the right tool to build and demo cross-chain swaps on regtest/testnet, but do not move real value with it until the audit gate clears. An atomic swap’s whole job is to be safe against a hostile counterparty; that property is what an audit certifies.
The idea in one breath¶
Two parties, two chains, one secret:
The maker holds the Radiant asset and wants ETH. The taker holds ETH and wants the asset.
The maker picks a random 32-byte secret
pand publishesH = SHA256(p). The sameHlocks both legs.Revealing
pto claim one leg hands the counterparty the secret to claim the other. So either both legs settle, or neither does (after a timeout both refund). That’s the atomicity — enforced by each chain’s own consensus, not by anyone’s good behaviour.
one secret p, H = SHA256(p), locks BOTH legs:
Radiant covenant ◀──── same H ────▶ ETH HTLC
(holds the asset) (holds the ETH)
reveal p to claim one leg → the counterparty scrapes p and claims the other.
You’ll do this twice: first see the on-chain pieces with no setup at all, then run a full swap on local Anvil + a Radiant regtest node.
Part 1 — see the building blocks (30 seconds, no setup)¶
The Radiant side of the swap is an on-chain HTLC covenant: it holds the asset and can be
spent two ways — the taker’s hashlock claim (reveal p) or the maker’s CSV refund (after
a timelock). Build a real one, with the real production builders, with no network:
$ python examples/htlc_swap_demo.py
It generates (p, H), builds an NFT HTLC covenant, and builds both spend paths — the claim
(which embeds p) and the timelocked refund — printing the maker/taker roles and the one safety
invariant you’ll meet again below. Nothing connects out; it’s pure construction. Read it
alongside build a cross-chain swap, which catalogues the
pieces.
Part 2 — run a full swap on local chains¶
Now the whole thing: a real EthLeg (deploying the real EthHtlc contract on a local Anvil) and a
real RadiantCovenantLeg (a radiant-core regtest node), driven through the production
SwapCoordinator from negotiated to completed. No real value moves — Anvil is a local devnet
with public deterministic keys, and the Radiant node is a throwaway regtest container.
Prerequisites (one-time)¶
$ pip install "pyrxd[eth]" # the ETH leg needs web3
$ pyrxd regtest setup # builds the radiant-core regtest image (downloads the
# official release + verifies its checksum); same Step 0 as
# the quickstart
$ curl -L https://foundry.paradigm.xyz | bash && foundryup # installs `anvil`
(Need Docker on your PATH too — the regtest node runs in a container.)
The flow, step by step¶
The coordinator is chain-neutral: it drives a state machine over two legs and never trusts a
counterparty’s claims — every step re-verifies against on-chain truth. Here’s the happy path
(the method names say btc for historical reasons; they drive whichever counter-leg you wired —
here, ETH):
# build the covenant + the two real legs + the coordinator (see the e2e test for the full setup)
coord = SwapCoordinator(record=SwapRecord(state=SwapState.NEGOTIATED, terms=terms),
counter_leg=eth_leg, radiant_leg=rxd_leg, indexer=indexer,
seen_store=seen, config=config)
# 1. TAKER locks the ETH first — deploys + funds the EthHtlc on Anvil, bound to H.
rec = await coord.taker_funds_btc(terms) # NEGOTIATED → BTC_LOCKED
# 2. MAKER locks the asset second — funds the Radiant HTLC covenant; the taker re-validates the
# covenant SPK, the Glyph ref, and the cross-clock timelock margin before trusting it.
rec = await coord.post_asset_lock_revalidate(cov.funded_spk) # → BOTH_LOCKED
# 3. MAKER claims the ETH — revealing p on-chain (the EthHtlc emits Claimed(p)).
rec = await coord.maker_claims_btc(p) # → SECRET_REVEALED
# 4. TAKER scrapes p off the maker's ETH claim and claims the Radiant asset — but only once the
# reorg-finality gate says the ETH claim is reorg-SAFE (see "why it's trustless" below).
rec = await coord.taker_scrape_and_claim_asset(claim_tx, ...) # → COMPLETED
That’s it — the secret revealed on one chain settles the other, and the asset covenant is spent.
Watch it actually settle¶
The end-to-end proof is wired up and maintained as an integration test — it runs this exact flow through the real coordinator, for three asset kinds (native RXD, a Glyph NFT, and a Glyph FT), against a live local Anvil + regtest node:
$ XCHAIN_ETH_REGTEST=1 pytest tests/test_xchain_eth_swap_regtest_e2e.py::TestEthRxdSwap \
-m integration -s
...
tests/test_xchain_eth_swap_regtest_e2e.py ....
======================= 4 passed in ~7s =======================
Three trustless cross-chain swaps — RXD, an NFT, and an FT — plus the mutual-refund failure path, settling on real local ETH + Radiant consensus in under ten seconds, no real value at risk. The same file also runs the alert-only watchtower against the swap, proving it pages the right action at the right moment (and never pages a claim against a not-yet-final ETH reveal).
If a prerequisite is missing (Docker, the regtest image,
anvil, orweb3), the test skips rather than fails — so a green/ skipped result tells you exactly what’s set up.
Why it’s trustless (the safety, briefly)¶
The swap never asks you to trust the counterparty — it leans on each chain’s consensus plus three rules the coordinator enforces fail-closed:
Ordering + timelock margin. The taker locks ETH first, the maker locks the asset second, and the maker reveals
pfirst. The leg claimed second (Radiant) carries the shorter refund window, so the taker always has time to scrapepand claim before its own refund opens. The coordinator refuses to proceed unlesst_counter > t_rxd + margin.The reorg-finality gate. The taker doesn’t claim the asset the instant
pappears — it waits until the ETH claim is reorg-safe (post-Mergefinalized-checkpoint final, and the Radiant claim buried deep enough that reversing it would cost more than the value at stake). A premature reveal returnsWAIT/SQUEEZED, never a silent claim.No counterparty inputs trusted. Every step re-derives the covenant, re-checks the Glyph ref, and reads values from on-chain truth — never from what the other party asserts.
If the maker stalls (locks the asset but never reveals p), nobody is stuck: after the timelocks
mature, mutual_refund returns both legs to their owners.
Next steps¶
The pieces, as reference: build a cross-chain swap — the coordinator, the legs,
MarginPolicy, and how to add another counter-chain.Other chains, no new code: the same machinery runs against BTC (Taproot-HTLC) and the EVM family — Base, Optimism, Arbitrum, Linea (
pyrxd.eth_wallet.chains). Only the per-chain finality window changes.Two operators, for real adversarial testing: run a two-host swap dry-run splits the swap across two separate operators exchanging only the public envelope — the genuine adversarial exercise.
Before any real value: the external security audit is the hard gate. See
SECURITY.md.