The SPV-oracle swap is not atomic — use an HTLC¶
The finding¶
The original Gravity cross-chain swap design was a one-directional SPV oracle:
Maker locks the asset in a Radiant covenant.
Taker pays BTC to the maker’s plain Bitcoin address.
Taker submits an SPV proof (block headers + Merkle inclusion) to Radiant’s covenant
finalizepath to release the asset.If the taker doesn’t finalize before a deadline, the maker’s
forfeitpath reclaims the asset.
An adversarial review panel (four independent reviewers, writing blind — see
docs/brainstorms/gravity-ref-spike/DEADLINE_RACE_PANEL_2026-05-24.md)
unanimously concluded this design is not atomically secure.
Why (root cause)¶
The irreversibility is on the Bitcoin side. The BTC payment goes to a plain address with no refund path — once sent, it is gone regardless of what Radiant does. The SPV proof only answers “did this payment happen?”; it is a one-directional oracle, not a binding between the two legs. Therefore:
If BTC blocks are slow, the payment is mined late, or the maker set a tight deadline, the taker can fail to finalize in time → the maker’s
forfeitreclaims the asset → the taker loses the BTC and gets no asset (one-sided loss).No Radiant-side deadline change closes this hole, because the loss is on the Bitcoin side where Radiant has no authority. Every deadline-tuning option leaves the same hole.
Two related facts the panel surfaced:
tx.time/ OP_CHECKLOCKTIMEVERIFY is a>=-only consensus lower bound (a “not-before”), so an in-script upper-bound deadline on the taker is impossible. The “deadline” is really “the moment the maker’s forfeit becomes spendable.”Rejected non-fixes: maker-signature-gated forfeit (introduces permanent stranding
an extortion lever; removes the permissionless liveness backstop) and bonds (a maker prices a bond below a unique asset’s subjective value). These do not restore atomicity.
The decision¶
Bind both legs with one secret using an HTLC (hashlock + relative timelock).
The BTC goes into a script-controlled output (a Taproot tapscript HTLC) with two paths: claim-with-preimage and refund-after-timeout. The asset releases on the preimage reveal; each side can refund via a relative timelock if the other never proceeds.
Both legs share
H = sha256(p). Radiant’sOP_SHA256matches Bitcoin’s SHA-256, so a plain SHA-256 hashlock works on both chains — no adaptor signatures needed for v1 (those are an optional later privacy optimization).No structural one-sided loss: the HTLC converts the SPV oracle’s unconditional, no-taker-fault loss into a bounded residual conditioned on taker liveness failure or a successful pinning/eviction attack during the
[secret-reveal, t_RXD CSV-maturity]window — bounded by the timelock margin + CSV burial, but not autonomously closed and never fully eliminated. (The original “both refund and walk away whole — never one-sided loss” headline overstated this: the shipped FSM has a reachableONE_SIDED_LOSS_TAKERterminal viaSECRET_REVEALED → ASSET_VULNERABLE. The residual is strictly smaller than under any SPV hybrid — a preimage scrapes from the mempool, whereas an SPV proof needs N-deep burial plus an ~8–11 KB covenant spend.) Caveat (red-team 2026-06-12; watchtower status as of 2026-09-24): the watchtower is alert-only for the claim path — it pages a human (PAGE_CLAIM) but broadcasts no asset claim. AClaimExecutoris built, but no entrypoint can arm it (#519). So closing this residual today depends on taker/operator liveness withint_RXD, not autonomous watchtower action. Two related gaps the same red-team reported: the Radiant claim-finality gate uses a fixed burial depth not scaled to value-at-risk (assess_claim_finality,swap_coordinator.py) — economically reversible above ~the cost of a shallow Radiant reorg — and the Radiant claim tx is non-RBF / CPFP-incapable, so a fee spike in the window can strand it. Both are pre-real-value blockers.
Load-bearing constraint: timelock ordering¶
The party holding p locks the leg with the longer refund window and claims
the leg with the shorter one (Herlihy, Atomic Cross-Chain Swaps, arXiv:1801.09515 §1:
the secret generator locks at 6∆ and claims a 4∆ leg). The settled, incentive-aligned
direction (MAKER_SECRET_TAKER_LOCKS_BTC_FIRST): the maker locks the Radiant asset,
then claims the BTC first, revealing p in the Bitcoin witness, and the taker
scrapes p and claims the Radiant asset second. So the Radiant refund timeout must
exceed the Bitcoin one — the leg claimed second carries the longer window — by a
margin covering reorg depth + relay + congestion. The taker’s client MUST verify, in
wall clock, that t_RXD · i_RXD ≥ t_BTC · i_BTC + margin · i_BTC before funding, or
a malicious maker can mis-set it. The margin is an ESTIMATE until derived from observed
inter-block data on the relevant chains.
⚠ This paragraph stated the exact reverse until 2026-09-02 — “the leg claimed second must have the shorter refund window”, “the Bitcoin refund timeout must exceed the Radiant”, and a
MUST verify t_BTC − t_RXD ≥ margin. That is the layout in which the maker refunds its own leg whilepis secret and still claims the other, taking both.
Cost of the fix (be honest about it)¶
Atomicity is not free: it introduces a retained-state obligation — the refunding
party (or a watchtower) must keep the refund key + script and broadcast the refund if
the happy path stalls. Removing that obligation re-introduces custodial trust and the
one-sided-loss hole. The funding UX stays paste-and-send (a normal send to a bc1p
address), so the cost is operational (a watchtower), not user-facing friction.
Status¶
The HTLC mechanism (BTC Taproot HTLC ↔ Radiant covenant, one secret, secret scraped from the BTC witness to claim the asset) has been demonstrated end-to-end on mainnet as a proof-of-mechanism for RXD/FT/NFT. External audit of cross-chain atomicity is a hard gate before any real-value use — a working demo is not an audit. SPV proof code itself remains sound and in use elsewhere; what was unsafe was the swap built on a plain-address payment, not SPV verification as a technology.
Lesson¶
A cross-chain “swap” that verifies a payment on one chain but cannot refund that
payment is a payment oracle, not an atomic swap. Atomicity requires binding both legs
by one secret with symmetric refund paths — verify the refund path actually exists and
fires on both chains before calling it atomic. (See also
radiant-covenant-amount-pin-must-match-funded-carrier.md
and the refund-leaf execution-test lesson: prove the refund on-chain, don’t assume it.)