From 1d0e26f5815f817f489c709bdf25f4708d18cd7d Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 18 Aug 2026 20:11:36 -0700 Subject: A many-coined receive keeps the server's own order: the claim step stops alphabetizing HTLC vtxos the ASP matches positionally, and the expiry guard reads the true earliest deadline instead of whoever sorted first Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012u4oe1KPx9faCsDBLfZYqi --- src/ark/manager.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/ark/manager.js b/src/ark/manager.js index e5e04d4..c1121db 100644 --- a/src/ark/manager.js +++ b/src/ark/manager.js @@ -1152,13 +1152,16 @@ export class ArkManager { } if (action.step === 'htlcsReady') { const keys = this._key(action.keyIndex); - const decoded = action.htlcBytes.map((b) => decodeVtxo(hex.decode(b))) - .sort((a, b) => (a.id < b.id ? -1 : 1)); + // Server order, verbatim: ClaimLightningReceive matches cosign parts to + // its HTLC list positionally, so a re-sorted list is rejected as + // "cosign inputs do not match htlcs" (bites only multi-vtxo receives). + const decoded = action.htlcBytes.map((b) => decodeVtxo(hex.decode(b))); // preimage not revealed yet + HTLCs near expiry: abandon rather than // commit (the inbound HTLC times out and the sender is refunded) if (!action.preimageRevealed) { const tip = await this.chain.tipHeight(); - if (tip > decoded[0].policy.htlcExpiry - this._expiryMargin()) { + const minExpiry = Math.min(...decoded.map((v) => v.policy.htlcExpiry)); + if (tip > minExpiry - this._expiryMargin()) { action.step = 'failed'; action.error = 'HTLCs near expiry — abandoned before preimage reveal'; this._movement({ type: 'ln-receive', amountSat: action.amountSat, status: 'failed', detail: action.error }); -- cgit v1.2.3