fix(asb): serialize Monero locks and refund unfundable swaps - #1193
Open
louislaugier wants to merge 1 commit into
Open
fix(asb): serialize Monero locks and refund unfundable swaps#1193louislaugier wants to merge 1 commit into
louislaugier wants to merge 1 commit into
Conversation
louislaugier
force-pushed
the
fix/serialize-monero-lock-phase
branch
from
August 19, 2026 01:11
be1a996 to
a415bd3
Compare
This was referenced Aug 19, 2026
Closed
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
force-pushed
the
fix/serialize-monero-lock-phase
branch
from
August 19, 2026 19:10
a415bd3 to
f732309
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Testing
concurrent_bobs_before_xmr_lock_proof_sentandconcurrent_bobs_after_xmr_lock_proof_sent(both swaps funded, both redeem), a newconcurrent_bobs_insufficient_xmr(maker funded for only one of two concurrent swaps; the winner redeems, the loser refunds early rather than wedging), andhappy_pathall 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.