diff options
| author | Adam Soltys <[email protected]> | 2026-10-04 08:58:34 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-04 08:58:34 -0700 |
| commit | 7421218e987b0053f1ddbefaddc2c9ecfd664954 (patch) | |
| tree | 4d04e81afa32c230e7f2d8beb1f723662a4747c2 /tools/ark-reconcile-race.test.js | |
| parent | 7c6ca4d70b91dc2d050cb18e7978b65bc1db2d59 (diff) | |
| download | coinosv3-7421218e987b0053f1ddbefaddc2c9ecfd664954.tar.gz coinosv3-7421218e987b0053f1ddbefaddc2c9ecfd664954.zip | |
Ark: a double-tapped zap no longer spends one coin twice
A reconcile pass that began before the first zap took its coin handed
it back as spendable (the server still said so) while the zap was in
flight; the second zap picked it and the server refused: 'vtxo …
already spent'. Coins whose state changed during the pass are left
alone. And a payment refused for an already-spent coin now reconciles
and pays once more instead of failing with the raw error.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
Diffstat (limited to 'tools/ark-reconcile-race.test.js')
| -rw-r--r-- | tools/ark-reconcile-race.test.js | 22 |
1 files changed, 22 insertions, 0 deletions
diff --git a/tools/ark-reconcile-race.test.js b/tools/ark-reconcile-race.test.js new file mode 100644 index 0000000..165eb58 --- /dev/null +++ b/tools/ark-reconcile-race.test.js @@ -0,0 +1,22 @@ +// bun test tools/ark-reconcile-race.test.js — a reconcile pass must not hand a +// coin back to spendable after a payment took it mid-pass (double-tapped zap). +import { test, expect, mock } from 'bun:test'; +import * as proto from '../src/ark/proto.js'; +let release; +const gate = new Promise((r) => { release = r; }); +mock.module('../src/ark/proto.js', () => ({ ...proto, getVtxoStatus: async () => { await gate; return proto.VTXO_STATE_SPENDABLE; } })); +const { ArkManager } = await import('../src/ark/manager.js'); +test('a coin taken mid-reconcile stays taken', async () => { + const m = Object.create(ArkManager.prototype); + m.state = { vtxos: [{ id: 'a', state: 'spendable', amountSat: 85, bytes: 'x' }, { id: 'b', state: 'pending', amountSat: 5, bytes: 'x' }], actions: [], scheduled: [] }; + m.arkUrl = 'x'; m._decoded = () => ({ point: { raw: new Uint8Array(36) } }); m._keyForVtxo = () => ({ privkey: new Uint8Array(32) }); + m._save = () => {}; m._movement = () => {}; + const p = m.reconcile(); + await new Promise((r) => setTimeout(r, 10)); + m.state.vtxos[0].state = 'pending'; // the zap takes coin a + m.state.actions.push({ type: 'ln-pay', step: 'created', parts: [{ inputId: 'a' }] }); + release(); + await p; + expect(m.state.vtxos[0].state).toBe('pending'); + expect(m.state.vtxos[1].state).toBe('spendable'); // a real orphan is still freed +}); |
