Skip to content

lightningd: don't force-close when fulfilled HTLC removal is in progress#8940

Open
vincenzopalazzo wants to merge 2 commits intoElementsProject:masterfrom
vincenzopalazzo:test/reproduce-issue-8899-fulfilled-htlc-deadline
Open

lightningd: don't force-close when fulfilled HTLC removal is in progress#8940
vincenzopalazzo wants to merge 2 commits intoElementsProject:masterfrom
vincenzopalazzo:test/reproduce-issue-8899-fulfilled-htlc-deadline

Conversation

@vincenzopalazzo
Copy link
Collaborator

@vincenzopalazzo vincenzopalazzo commented Mar 13, 2026

Summary

Fixes #8899: CLN force-closes a channel with Fulfilled HTLC SENT_REMOVE_HTLC cltv hit deadline even 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 receives update_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_HTLC or later state (>= SENT_REMOVE_HTLC), skip the force-close and log a warning instead. This is safe because:

  1. The preimage is persisted to DB — onchaind can claim on-chain if needed
  2. On reconnect, channeld will resend update_fulfill_htlc
  3. If the peer goes on-chain themselves, onchaind handles it with the known preimage
  4. The cooperative path (reconnect + fulfill) is cheaper and preserves the channel

Commits

  1. pytest: reproduce issue FC due to Fulfilled HTLC $ID SENT_REMOVE_HTLC cltv $CLTV hit deadline without attempt to claim #8899 — Test that triggers the buggy force-close (l2 disconnects from l1 before sending update_fulfill_htlc, then mining blocks to the deadline)
  2. lightningd: fix — Skip force-close when hin->hstate >= SENT_REMOVE_HTLC, log log_unusual instead

Changelog-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_close reproduces the bug (first commit)
  • With the fix, l2 logs "but removal already in progress" instead of force-closing
  • CI passes (Valgrind, ASan, integration tests)

🤖 Generated with Claude Code

@vincenzopalazzo vincenzopalazzo force-pushed the test/reproduce-issue-8899-fulfilled-htlc-deadline branch from d6d8693 to e316362 Compare March 13, 2026 21:04
@vincenzopalazzo vincenzopalazzo changed the title pytest: reproduce issue #8899 fulfilled HTLC SENT_REMOVE_HTLC deadline force-close pytest: reproduce issue #8899 fulfilled HTLC deadline force-close Mar 14, 2026
…VE_HTLC deadline 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#8899

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vincenzopalazzo vincenzopalazzo force-pushed the test/reproduce-issue-8899-fulfilled-htlc-deadline branch from 3ad64fb to cbcc032 Compare March 15, 2026 00:25
@vincenzopalazzo vincenzopalazzo changed the title pytest: reproduce issue #8899 fulfilled HTLC deadline force-close lightningd: don't force-close when fulfilled HTLC removal is in progress Mar 15, 2026
@vincenzopalazzo vincenzopalazzo force-pushed the test/reproduce-issue-8899-fulfilled-htlc-deadline branch from 4cf3776 to 066b3c7 Compare March 15, 2026 00:35
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#8899

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vincenzopalazzo vincenzopalazzo force-pushed the test/reproduce-issue-8899-fulfilled-htlc-deadline branch from 5ec99ea to c95850f Compare March 15, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FC due to Fulfilled HTLC $ID SENT_REMOVE_HTLC cltv $CLTV hit deadline without attempt to claim

1 participant