diff options
Diffstat (limited to 'tools/ark-htlc-held.test.js')
| -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 +}); |
