Capital-lockup griefing: accepted, not bonded¶
Decision: ACCEPT the residual and document it. Do NOT add a bond or deposit. Revisit only on the trigger in §5.
1. The attack, and why it is structural¶
Verified against the shipped state machine, not inferred:
The taker locks first.
gravity/swap_state.py:NEGOTIATED --> BTC_LOCKED : taker funds BTC P2TR HTLC (locks FIRST).The taker’s own refund is the LONGER timelock.
assert_timelock_marginenforcest_btc - t_rxd >= margin(gravity/swap_coordinator.py:475). This direction is correct and deliberate — it is what prevents the far worse one-sided-loss race — but it means the party who commits first is also the party who waits longest to get out.The recourse works, and is slow.
taker_refund_btcis “valid from BTC_LOCKED (maker never locked,t_btcelapsed)”. The taker is made whole; they simply cannot leave early.
So the cost table is:
attacker (malicious maker) |
victim (honest taker) |
|
|---|---|---|
on-chain transactions |
none |
fund + refund (2) |
capital immobilised |
none |
full swap value, for |
marginal cost to repeat |
~zero |
linear in swaps accepted |
The attacker’s move is inaction. There is nothing to slash, nothing to observe on-chain, and no protocol step they fail to perform — they never started one.
2. Why the test suite cannot see it¶
This is the part worth internalising. Every safety oracle in the adversarial matrix asks a
principal question: did anyone end with less than they started? Here, nobody does. The
invariant holds, the refund fires, the record reaches ABORTED cleanly. A griefing campaign and a
counterparty with flaky connectivity are indistinguishable to every check we have.
That is why it survived a red-team pass, a security panel, and an eight-reviewer fan-out without being flagged as a defect: it is not a defect. It is a property of HTLC swaps in general, and naming it is the only honest way to close it.
3. Why a bond is the wrong fix here¶
A bond or deposit is the textbook answer, and it is wrong for pyrxd now:
It changes the protocol for everyone to price a threat with no observed instance. Every honest swap would pay setup cost and UX friction to deter an attack nobody has run.
A bond needs its own escrow, and escrow needs its own adjudication. Who holds it? On which chain? Who decides the abort was malicious rather than a stalled node or a genuine reorg? Each answer is new consensus-adjacent surface on a stack whose existing surface is explicitly unaudited. Adding an unaudited mechanism to deter a liveness nuisance is a poor trade.
It cannot distinguish malice from failure. The honest counterparty whose node dies mid-swap is punished identically to the attacker. A bond that slashes on abort penalises exactly the users least able to absorb it.
Cheaper mitigations exist that need no protocol change (§4).
4. What to do instead¶
These are ordinary operational controls, available today, none of which touch the protocol:
Order the legs by trust. The taker-locks-first ordering is a default, not a consensus rule. Against an untrusted counterparty, negotiate the maker locking first — the griefing cost then lands on the party who chose to grief.
Size
t_btcdeliberately. The margin has a floor for safety, but everything above it is chosen. A longer-than-necessaryt_btcbuys nothing and directly lengthens the griefing window.Keep counterparty state at the negotiation layer. An allowlist or a simple abort-rate memory is trivially cheap and sits entirely outside the swap protocol.
Do not automate acceptance. The RSWP orderbook makes accepting swaps programmatic; an operator auto-accepting from unknown counterparties converts a manual nuisance into an automated drain. That is the actual risk multiplier — not the attack itself.
5. The trigger to revisit¶
Accepting a residual is only honest with a named condition that reverses it. Revisit when either:
An actual griefing incident occurs — one observed campaign beats any amount of speculation about whether it will happen; or
The orderbook carries untrusted counterparties at volume. Griefing scales with automation. Today RSWP is experimental and swaps are operator-driven. If accepting an order becomes unattended and open to anyone, the cost asymmetry stops being theoretical and the mitigations in §4 stop being sufficient.
An external auditor flagging it as blocking is a third trigger, and would supersede this decision.
6. What is being accepted, stated plainly¶
pyrxd’s cross-chain swap stack defends SAFETY, not LIVENESS. It will not let a counterparty take your funds. It will not stop a counterparty from wasting your time and immobilising your capital for the duration of a timelock at near-zero cost to themselves. Those are different guarantees, and only the first is engineered for.
Anyone running swaps against untrusted counterparties should read that sentence as a design constraint, not a bug report.
See also¶
docs/threat-model.md— S22 (this residual, registered).docs/plans/2026-08-09-gap-closure-plan.md§A3 — the plan item this closes.docs/solutions/design-decisions/spv-oracle-swap-is-not-atomic-use-htlc.md— why the HTLC binding exists at all; griefing is the residual left after one-sided loss was engineered out.