Skip to content

fix(asb): serialize Monero locks and refund unfundable swaps - #1193

Open
louislaugier wants to merge 1 commit into
eigenwallet:masterfrom
louislaugier:fix/serialize-monero-lock-phase
Open

fix(asb): serialize Monero locks and refund unfundable swaps#1193
louislaugier wants to merge 1 commit into
eigenwallet:masterfrom
louislaugier:fix/serialize-monero-lock-phase

Conversation

@louislaugier

@louislaugier louislaugier commented Aug 19, 2026

Copy link
Copy Markdown

Problem

Two swaps running at the same time can both select the same Monero output while building their lock transactions. wallet2 only marks an output spent after its lock transaction is relayed, and monero-sys has no reserve/freeze API, so the second lock becomes a permanent double-spend that monerod rejects, and that swap hangs until the cancel timelock (the recurring "xmr is refundable" reports).

A related case: when concurrent swaps together need more XMR than the maker holds, even a serialized second swap can be forced to reselect the first swap's freshly spent outputs, build a lock that double-spends, and then retry the rejected publish forever (the publish retry has no deadline), wedging until the cancel timelock.

Fix

  1. Serialize the construct-to-first-publish window with a process-wide async mutex, so at most one unpublished lock exists at a time. Overlapping swaps that are each fundable then pick different outputs and both succeed. A max-hold deadline releases the mutex if a swap wedges on a rejected publish, so one stuck swap cannot starve the others.
  2. Under that guard, before constructing, check the wallet's unlocked balance against what the lock needs. If a concurrent swap already consumed the shared balance, fail with a permanent error that routes to an early Bitcoin refund (no Monero is locked, so this is safe) instead of building a doomed double-spend that wedges the swap.

Testing

concurrent_bobs_before_xmr_lock_proof_sent and concurrent_bobs_after_xmr_lock_proof_sent (both swaps funded, both redeem), a new concurrent_bobs_insufficient_xmr (maker funded for only one of two concurrent swaps; the winner redeems, the loser refunds early rather than wedging), and happy_path all pass locally.

Scope / related

The balance pre-check is best-effort: it is bounded by how promptly wallet2 reflects a sibling's freshly relayed (still unconfirmed) spend. A fully reliable insufficient-funds check under an untrusted daemon needs a daemon-side spent-key-image query, which is what #1142 adds; this change is complementary prevention, not a substitute for that.


AI disclosure: I used Claude Fable 5 to help investigate and write this change. I understand the code, tested it, and am responsible for it.

Two overlapping swaps could select the same Monero output when building
their lock transactions. wallet2 only marks an output spent once its
lock tx is relayed, and monero-sys has no reserve API, so the second
swap's lock tx becomes a permanent double-spend that monerod rejects
forever -- the swap then hangs until the cancel timelock and the taker
is refunded ("xmr is refundable").

A related case: when concurrent swaps together need more XMR than the
maker holds, even a serialized swap can be forced to reselect a sibling's
freshly spent outputs, build a double-spend, and then retry the rejected
publish forever, since the publish has no deadline.

Serialize the construct-through-first-publish window with a process-wide
async mutex so at most one unpublished lock tx exists at a time;
overlapping swaps that are each fundable pick different outputs and both
succeed. A max-hold deadline releases the mutex if a swap wedges on a
rejected publish. Under the guard, before constructing, check that the
unlocked balance covers the lock and fail with a permanent error (an
early Bitcoin refund; no Monero was locked, so it is safe) if a sibling
already took the shared balance, instead of building a doomed lock.

Verified with the concurrent_bobs_before_xmr_lock_proof_sent,
concurrent_bobs_after_xmr_lock_proof_sent, a new
concurrent_bobs_insufficient_xmr, and happy_path integration tests.

Refs eigenwallet#120

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@louislaugier
louislaugier force-pushed the fix/serialize-monero-lock-phase branch from a415bd3 to f732309 Compare August 19, 2026 19:10
@louislaugier louislaugier changed the title fix(asb): serialize the Monero lock phase across swaps fix(asb): serialize Monero locks and refund unfundable swaps Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant