diff options
| author | Adam Soltys <[email protected]> | 2026-07-31 23:23:03 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-31 23:23:03 -0700 |
| commit | b524be4331cbcb6ff68c27894b6e8143b556b85f (patch) | |
| tree | 1561fd40af72c963675d71945b2ecdb7d0cef1e0 /lib/utils.ts | |
| parent | fb09303ff274a241c28cccc0cd29550f6e59d0b2 (diff) | |
| download | coinos-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
