aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark-htlc-held.test.js
blob: b2fd07eb5109883a354a4b5eef76d4d193f74f33 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
});