Skip to content

Commit d6d8693

Browse files
pytest: reproduce issue #8899 fulfilled HTLC SENT_REMOVE_HTLC deadline force-close
Add test that reproduces the bug where CLN force-closes a channel with "Fulfilled HTLC SENT_REMOVE_HTLC cltv hit deadline" even though the node already has the preimage and just needs to send update_fulfill_htlc to the upstream peer. The test sets up l1->l2->l3 routing where l2 disconnects from l1 right before sending update_fulfill_htlc, leaving the incoming HTLC stuck in SENT_REMOVE_HTLC state. When blocks advance past the deadline, l2 force-closes instead of attempting to reconnect and complete the fulfill. Ref: #8899 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 376468f commit d6d8693

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

tests/test_closing.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3957,6 +3957,82 @@ def censoring_sendrawtx(r):
39573957
# FIXME: l2 should complain!
39583958

39593959

3960+
@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd anchors unsupported')
3961+
def test_fulfilled_htlc_deadline_no_force_close(node_factory, bitcoind):
3962+
"""Test that l2 does not force-close when fulfilled HTLC is in
3963+
SENT_REMOVE_HTLC state (preimage known, fulfill queued to channeld
3964+
but not yet sent to upstream peer).
3965+
3966+
Reproduces https://github.com/ElementsProject/lightning/issues/8899:
3967+
CLN force-closed with 'Fulfilled HTLC SENT_REMOVE_HTLC cltv hit deadline'
3968+
without attempting to send update_fulfill_htlc upstream first.
3969+
"""
3970+
# l1 -> l2 -> l3 topology.
3971+
# l2 disconnects from l1 right before sending update_fulfill_htlc,
3972+
# so the incoming HTLC on l1-l2 stays in SENT_REMOVE_HTLC.
3973+
# l2 cannot reconnect (dev-no-reconnect), simulating the scenario where
3974+
# the upstream peer appears connected but isn't processing messages.
3975+
#
3976+
# Use identical feerates to avoid gratuitous commits to update them.
3977+
opts = [{'dev-no-reconnect': None,
3978+
'feerates': (7500, 7500, 7500, 7500)},
3979+
{'disconnect': ['-WIRE_UPDATE_FULFILL_HTLC'],
3980+
'dev-no-reconnect': None,
3981+
'feerates': (7500, 7500, 7500, 7500)},
3982+
{'feerates': (7500, 7500, 7500, 7500)}]
3983+
3984+
l1, l2, l3 = node_factory.line_graph(3, opts=opts, wait_for_announce=True)
3985+
3986+
amt = 12300000
3987+
inv = l3.rpc.invoice(amt, 'test_fulfilled_deadline', 'desc')
3988+
3989+
# Use explicit route with known delays to have predictable cltv_expiry.
3990+
# delay=16 for first hop (cltv_delta=6 + cltv_final=10),
3991+
# delay=10 for second hop (cltv_final=10).
3992+
route = [{'amount_msat': amt + 1 + amt * 10 // 1000000,
3993+
'id': l2.info['id'],
3994+
'delay': 16,
3995+
'channel': first_scid(l1, l2)},
3996+
{'amount_msat': amt,
3997+
'id': l3.info['id'],
3998+
'delay': 10,
3999+
'channel': first_scid(l2, l3)}]
4000+
l1.rpc.sendpay(route, inv['payment_hash'],
4001+
payment_secret=inv['payment_secret'])
4002+
4003+
# l3 fulfills the HTLC, preimage flows back to l2.
4004+
# l2 transitions the incoming HTLC (from l1) to SENT_REMOVE_HTLC,
4005+
# then tries to send update_fulfill_htlc to l1 but disconnects.
4006+
l2.daemon.wait_for_log('dev_disconnect: -WIRE_UPDATE_FULFILL_HTLC')
4007+
4008+
# Verify the HTLC is stuck: l2 has the preimage but can't send it
4009+
# to l1 because of the disconnect.
4010+
wait_for(lambda: only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])['htlcs'] != [])
4011+
4012+
# Now mine blocks up to the deadline.
4013+
# Default regtest cltv_expiry_delta=6, so deadline = cltv_expiry - (6+1)/2 = cltv_expiry - 3.
4014+
# With delay=16 on first hop, cltv_expiry should be 119 (starting height ~103 + 16).
4015+
# Deadline = 119 - 3 = 116. We're at ~108 after mine_funding_to_announce,
4016+
# so we need ~8 blocks to hit it.
4017+
#
4018+
# BUG: l2 will force-close with "Fulfilled HTLC 0 SENT_REMOVE_HTLC cltv ... hit deadline"
4019+
# even though it has the preimage and just needs to reconnect to send it upstream.
4020+
# The correct behavior would be to NOT force-close when the HTLC is in
4021+
# SENT_REMOVE_HTLC (preimage known, fulfill already queued to channeld).
4022+
bitcoind.generate_block(7)
4023+
sync_blockheight(bitcoind, [l2])
4024+
assert not l2.daemon.is_in_log('hit deadline')
4025+
4026+
bitcoind.generate_block(1)
4027+
sync_blockheight(bitcoind, [l2])
4028+
4029+
# This is the bug: l2 force-closes with SENT_REMOVE_HTLC.
4030+
# After a fix, this line should be changed to assert the force-close
4031+
# does NOT happen:
4032+
# assert not l2.daemon.is_in_log('Fulfilled HTLC 0 SENT_REMOVE_HTLC')
4033+
l2.daemon.wait_for_log('Fulfilled HTLC 0 SENT_REMOVE_HTLC cltv .* hit deadline')
4034+
4035+
39604036
def test_closing_tx_valid(node_factory, bitcoind):
39614037
l1, l2 = node_factory.line_graph(2, opts={'may_reconnect': True,
39624038
'dev-no-reconnect': None})

0 commit comments

Comments
 (0)