diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 01:43:15 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 01:43:15 -0700 |
| commit | 817cc4a2dc0c7d343864da594aeb3c32b3459ffb (patch) | |
| tree | 2da18d52b3a47f6ff56e49dac30a6e96febc32b6 /tools | |
| parent | de8cb7a6f2272fa0b601a155dd244fe3a3a3f331 (diff) | |
| download | coinosv3-817cc4a2dc0c7d343864da594aeb3c32b3459ffb.tar.gz coinosv3-817cc4a2dc0c7d343864da594aeb3c32b3459ffb.zip | |
Ark: a Lightning payment's HTLC vtxo is never a coin to spend. The reconcile pass held an in-flight action's inputs but not its HTLC outputs, so between two quick zaps it took the first zap's HTLC coin for an unheld 'pending' orphan, the server called it unspent, and it came back 'spendable' — the second zap chose it as its input and the server refused the policy ("not spendable as a round, offboard or arkoor input (policy: server-htlc-send-v1)"). HTLC outputs are now held with their action, _free() refuses any non-pubkey coin whatever its state (records learn their policy from their bytes), and a pass heals HTLC coins an earlier pass freed
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/ark-htlc-held.test.js | 46 |
1 files changed, 46 insertions, 0 deletions
diff --git a/tools/ark-htlc-held.test.js b/tools/ark-htlc-held.test.js new file mode 100644 index 0000000..b2fd07e --- /dev/null +++ b/tools/ark-htlc-held.test.js @@ -0,0 +1,46 @@ +// bun test tools/ark-htlc-held.test.js — a Lightning payment's HTLC vtxo is +// never a coin to spend: the reconcile pass must not free it while its +// payment is in flight (quick zaps: the second one picked the first one's +// HTLC coin as its input and the server refused the policy), and _free() +// refuses one however it is labelled. +import { test, expect, mock } from 'bun:test'; +import * as proto from '../src/ark/proto.js'; +mock.module('../src/ark/proto.js', () => ({ ...proto, getVtxoStatus: async () => proto.VTXO_STATE_SPENDABLE })); +const { ArkManager } = await import('../src/ark/manager.js'); +const make = () => { + const m = Object.create(ArkManager.prototype); + m.state = { + vtxos: [ + { id: 'h', state: 'pending', amountSat: 23, bytes: 'x', policy: 'serverHtlcSend' }, // zap 1's HTLC, in flight + { id: 'c', state: 'pending', amountSat: 5, bytes: 'x', policy: 'pubkey' }, // a real orphan + { id: 'old', state: 'spendable', amountSat: 23, bytes: 'x', policy: 'serverHtlcSend' }, // freed by a pass before the fix, payment long done + { id: 'legacy', state: 'pending', amountSat: 7, bytes: 'x' }, // record from before the policy field + ], + actions: [ + { type: 'ln-pay', step: 'initiated', parts: [{ inputId: 'in1' }], htlcVtxoIds: ['h'] }, + { type: 'ln-pay', step: 'done', parts: [{ inputId: 'in0' }], htlcVtxoIds: ['old'] }, + ], + scheduled: [], + }; + m.arkUrl = 'x'; + m._decoded = (v) => ({ point: { raw: new Uint8Array(36) }, policy: { type: v.id === 'legacy' ? 'serverHtlcSend' : 'pubkey' } }); + m._keyForVtxo = () => ({ privkey: new Uint8Array(32) }); + m._save = () => {}; m._movement = () => {}; m._remoteBusy = () => new Set(); + return m; +}; +test('an in-flight HTLC coin stays held through a reconcile pass', async () => { + const m = make(); + await m.reconcile(); + const st = Object.fromEntries(m.state.vtxos.map((v) => [v.id, v.state])); + expect(st.h).toBe('pending'); // held by its payment, not an orphan + expect(st.c).toBe('spendable'); // a real orphan is still freed + expect(st.old).toBe('spent'); // healed: its payment is done + expect(st.legacy).toBe('pending'); // learned its policy from the bytes, not freed + expect(m.state.vtxos[3].policy).toBe('serverHtlcSend'); +}); +test('_free never offers an HTLC coin, whatever state it carries', () => { + const m = make(); + expect(m._free({ id: 'z', state: 'spendable', policy: 'serverHtlcSend' })).toBe(false); + expect(m._free({ id: 'z', state: 'spendable', policy: 'pubkey' })).toBe(true); + expect(m._free({ id: 'legacy', state: 'spendable', bytes: 'x' })).toBe(false); // decoded: serverHtlcSend +}); |
