diff options
| author | Adam Soltys <[email protected]> | 2026-06-29 15:31:59 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-29 15:31:59 -0700 |
| commit | 052e9c714092d8f4a5a9bb29bb85277630c1926d (patch) | |
| tree | fdf7cc771f458df5834e34048a0c9870a9e55121 /src/wallet.js | |
| parent | 87fcb80de91f539891cc2d635b0013d07826dd60 (diff) | |
| download | coinosv3-052e9c714092d8f4a5a9bb29bb85277630c1926d.tar.gz coinosv3-052e9c714092d8f4a5a9bb29bb85277630c1926d.zip | |
Swaps: don't reuse a burned swap index (deterministic-preimage collision)
A reverse swap's preimage is sha256(swapNode(idx).privateKey) — deterministic per
index. nextSwapIndex was derived only from *saved* swaps, so an attempt that
created the swap at Boltz but threw before saving (e.g. the lockup-address-mismatch
bug) never advanced the index; the next try reused the same preimage and Boltz
rejected it ('a swap with this preimage hash exists already').
Make nextSwapIndex a persisted, monotonic counter (reserves the index on every
call, seeded from saved swaps), and have startReverse retry with the next index
when Boltz reports the preimage already exists — which also recovers wallets
already stuck on a burned index.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/wallet.js')
| -rw-r--r-- | src/wallet.js | 13 |
1 files changed, 12 insertions, 1 deletions
diff --git a/src/wallet.js b/src/wallet.js index b3e3d40..c16bd66 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1537,8 +1537,19 @@ export class Wallet { saveSwaps(list) { try { localStorage.setItem(this._swapKey(), JSON.stringify(list)); } catch {} } + _swapIdxKey() { return this._cacheKey() + ':swapidx'; } + // Reserve the next swap index: monotonic + persisted on every call. A reverse/ + // submarine attempt that fails AFTER Boltz created the swap (a later validation + // throw, a dropped response) still burns its index, so a retry can't reuse the + // same deterministic preimage (Boltz rejects "swap with this preimage exists"). + // Seeded from saved swaps so existing wallets don't regress. nextSwapIndex() { - return this.loadSwaps().reduce((m, s) => Math.max(m, (s.swapIndex ?? -1) + 1), 0); + const fromSaved = this.loadSwaps().reduce((m, s) => Math.max(m, (s.swapIndex ?? -1) + 1), 0); + let stored = 0; + try { stored = parseInt(localStorage.getItem(this._swapIdxKey()) || '0', 10) || 0; } catch {} + const idx = Math.max(fromSaved, stored); + try { localStorage.setItem(this._swapIdxKey(), String(idx + 1)); } catch {} + return idx; } // Deterministic swap keypair on a dedicated chain (2) so swap keys never // collide with receive (0) / change (1). Returns the HDKey node (has |
