lightningd: don't force-close when fulfilled HTLC removal is in progress - #8940
Closed
vincenzopalazzo wants to merge 4 commits into
Closed
Conversation
vincenzopalazzo
force-pushed
the
test/reproduce-issue-8899-fulfilled-htlc-deadline
branch
from
March 13, 2026 21:04
d6d8693 to
e316362
Compare
…e force-close Add test_fulfilled_htlc_deadline_no_force_close to reproduce the bug where CLN force-closes a channel with "Fulfilled HTLC SENT_REMOVE_HTLC cltv hit deadline" even though it has the preimage and just needs to reconnect to send update_fulfill_htlc upstream. The test sets up l1->l2->l3, sends a payment, and disconnects l2 from l1 right before update_fulfill_htlc is sent (-WIRE_UPDATE_FULFILL_HTLC). This leaves the incoming HTLC on the l1-l2 channel stuck in SENT_REMOVE_HTLC (or SENT_REMOVE_COMMIT under Valgrind). Mining blocks to the deadline triggers the buggy force-close. Reproduces: ElementsProject/lightning#8899 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
vincenzopalazzo
force-pushed
the
test/reproduce-issue-8899-fulfilled-htlc-deadline
branch
from
March 15, 2026 00:25
3ad64fb to
cbcc032
Compare
vincenzopalazzo
force-pushed
the
test/reproduce-issue-8899-fulfilled-htlc-deadline
branch
from
March 15, 2026 00:35
4cf3776 to
066b3c7
Compare
When an incoming HTLC has been fulfilled (preimage known) and is in SENT_REMOVE_HTLC or later state, the removal is already in progress: channeld has been told to send update_fulfill_htlc upstream (or will be told on reconnect). Force-closing the channel is counterproductive because: 1. The preimage is persisted to DB and onchaind can claim on-chain 2. The cooperative path (reconnect + fulfill) is cheaper and faster 3. If the peer goes on-chain themselves, onchaind handles it Instead of force-closing, log a warning and let the removal complete through the normal state machine. CI run of the reproducer test (cbcc032) confirms the bug exists. Before this fix, the force-close fires: lightningd: Peer permanent failure in CHANNELD_NORMAL: Fulfilled HTLC 0 SENT_REMOVE_HTLC cltv 119 hit deadline After this fix, the force-close is skipped and a warning is logged: lightningd: UNUSUAL: Fulfilled HTLC 0 SENT_REMOVE_HTLC cltv 119 hit deadline, but removal already in progress Also updates test_htlc_no_force_close and test_htlc_in_timeout which depended on the old force-close behavior: l3/l2 no longer force-close for fulfilled HTLCs past deadline; instead the offering peer (l2/l1) force-closes for the offered HTLC timeout, and the fulfilling node claims on-chain via onchaind using the preimage. Changelog-Fixed: Don't force-close channel when fulfilled HTLC hits deadline but removal is already in progress. Fixes: ElementsProject/lightning#8899 Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
vincenzopalazzo
force-pushed
the
test/reproduce-issue-8899-fulfilled-htlc-deadline
branch
from
March 15, 2026 10:21
5ec99ea to
c95850f
Compare
When a BOLT 12 invoice is created via createinvoice and is associated with a local offer, include the offer_id in the invoice_creation event notification. This allows plugins to determine which offer triggered an invoice creation without having to call listinvoices for every created invoice. For bolt11 invoices and bolt12 invoices not associated with a local offer, the field is omitted. Fixes #8191 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ting test_fulfilled_htlc_deadline_no_force_close
Andezion
reviewed
Aug 6, 2026
| * on-chain, onchaind will claim using the | ||
| * preimage. | ||
| * See https://github.com/ElementsProject/lightning/issues/8899 */ | ||
| if (hin->hstate >= SENT_REMOVE_HTLC) { |
Collaborator
There was a problem hiding this comment.
Isnt the new guard hin->hstate >= SENT_REMOVE_HTLC is unconditionally true at the point it's checked, so channel_fail_permanent() for a fulfilled HTLC becomes dead code - not just for the #8899 race, but for every case?
I mean, from what i see on master:
htlcs_notify_new_block()already requireshin->preimage != NULLto reach this code (lightningd/peer_htlcs.c at line 2998 -if (!hin->preimage) continue;)hin->preimageis set in exactly one place,fulfill_htlc()(lightningd/peer_htlcs.c at line 397), and the very next line unconditionally advances state -htlc_in_update_state(channel, hin, SENT_REMOVE_HTLC)(line 400), with the comment "We update state now to signal it's in progress, for persistence." No other call site assignshin->preimage(the only other match, htlc_end.c at line 158, sets it to NULL).
Sohin->preimage != NULLimplieshin->hstate >= SENT_REMOVE_HTLCalways? The newifblock will always be taken, and the originalchannel_fail_permanent("Fulfilled HTLC /// hit deadline")can never fire again
|
|
||
|
|
||
| @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd anchors unsupported') | ||
| def test_fulfilled_htlc_deadline_no_force_close(node_factory, bitcoind): |
Collaborator
There was a problem hiding this comment.
And after the forst comment, test_fulfilled_htlc_deadline_no_force_close and test_fulfilled_htlc_deadline_reconnect don't actually test "removal in progress -> skip" vs. "removal stuck -> still force-close". Once comment is fixed with a real distinguishing condition, these tests should be extended to also cover the case where the peer never reconnects and the channel should still eventually force-close
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #8899: CLN force-closes a channel with
Fulfilled HTLC SENT_REMOVE_HTLC cltv hit deadlineeven though the node has the preimage and the fulfill is already in progress.The bug
In
htlcs_notify_new_block(), when a fulfilled incoming HTLC hits the cltv deadline, CLN force-closes the channel without checking whether the HTLC removal is already in progress. The HTLC state machine shows the fulfill was queued to channeld (SENT_REMOVE_HTLC), but the force-close fires before the upstream peer receivesupdate_fulfill_htlc.This was reported in the wild: the upstream peer was connected and exchanging pings, but CLN force-closed instead of sending the fulfill message.
The fix
When the HTLC is in
SENT_REMOVE_HTLCor later state (>= SENT_REMOVE_HTLC), skip the force-close and log a warning instead. This is safe because:onchaindcan claim on-chain if neededupdate_fulfill_htlconchaindhandles it with the known preimageCommits
Fulfilled HTLC $ID SENT_REMOVE_HTLC cltv $CLTV hit deadlinewithout attempt to claim #8899 — Test that triggers the buggy force-close (l2 disconnects from l1 before sendingupdate_fulfill_htlc, then mining blocks to the deadline)hin->hstate >= SENT_REMOVE_HTLC, loglog_unusualinsteadChangelog-Fixed: Don't force-close channel when fulfilled HTLC hits deadline but removal is already in progress.
Test plan
test_fulfilled_htlc_deadline_no_force_closereproduces the bug (first commit)🤖 Generated with Claude Code