aboutsummaryrefslogtreecommitdiffstats
path: root/lib/utils.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-31 23:23:03 -0700
committerAdam Soltys <[email protected]>2026-07-31 23:23:03 -0700
commitb524be4331cbcb6ff68c27894b6e8143b556b85f (patch)
tree1561fd40af72c963675d71945b2ecdb7d0cef1e0 /lib/utils.ts
parentfb09303ff274a241c28cccc0cd29550f6e59d0b2 (diff)
downloadcoinos-server-b524be4331cbcb6ff68c27894b6e8143b556b85f.tar.gz
coinos-server-b524be4331cbcb6ff68c27894b6e8143b556b85f.zip
sendKeysend: verify a thrown keysend by label, not the caller's hash (COINOS-1)
sendKeysend debited, ran ln.keysend, and on a throw decided whether to refund by calling listpays({payment_hash: hash}) with the CALLER's id (a Nostr ev.id on the NWC path). keysend randomizes its own payment hash — the CLN command takes no preimage arg — so that lookup could never match: it returned [], `failed` was always true, and reverse() ran on EVERY throw. A keysend that threw but actually settled was re-credited while the sats had left the node, a repeatable hot-wallet drain. Nothing corrected it either: unlike sendLightning, keysends are never enrolled in `pending`, so check() never revisits them. Recovering the real hash from the error payload doesn't work — verified on CLN v26.06.2 that keysend errors carry no payment_hash under any key (the error is {code, message, attempts}), so that approach never reverses and instead eats the user's money on every ordinary routing failure. Instead tag the payment with our own unique label and record where the sendpays index stands first (`wait sendpays created 0` returns the current index without blocking), so a throw can list only sendpays created since — no full scan — and match ours by label whatever the error looks like. CLN writes the sendpay record BEFORE sending the HTLC, so no record at all proves nothing left the node and the debit is safe to reverse. Verified both directions on regtest: an unroutable keysend is refunded, and a settled one is correctly detected and left alone. Known gap: a keysend still in flight when the RPC throws stays debited, since keysends are not enrolled in `pending` for check() to reconcile. Reported-by: Muhammed Shekho (@SYR-ROOT) <[email protected]> Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'lib/utils.ts')
0 files changed, 0 insertions, 0 deletions