@@ -3957,6 +3957,91 @@ 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+ # Wait for channeld on the l1-l2 channel to die after the disconnect.
4007+ # Note: "Peer transient failure" appears just before the dev_disconnect
4008+ # log in connectd, so we must wait for it first to avoid advancing the
4009+ # log search position past it.
4010+ l2 .daemon .wait_for_log (r'chan#1: Peer transient failure in CHANNELD_NORMAL' )
4011+
4012+ # Get the incoming HTLC's cltv_expiry from listpeerchannels.
4013+ htlc = only_one (only_one (l2 .rpc .listpeerchannels (l1 .info ['id' ])['channels' ])['htlcs' ])
4014+ # Under Valgrind, the state may advance to SENT_REMOVE_COMMIT before
4015+ # the disconnect fully takes effect. Both states reproduce the bug.
4016+ assert htlc ['state' ] in ('SENT_REMOVE_HTLC' , 'SENT_REMOVE_COMMIT' ), \
4017+ f"Expected SENT_REMOVE_HTLC or SENT_REMOVE_COMMIT, got { htlc ['state' ]} "
4018+ cltv_expiry = htlc ['expiry' ]
4019+
4020+ # Compute the deadline dynamically from the actual HTLC cltv_expiry.
4021+ # htlc_in_deadline = cltv_expiry - (cltv_expiry_delta + 1)/2
4022+ # With regtest cltv_expiry_delta=6: deadline = cltv_expiry - 3
4023+ deadline = cltv_expiry - (6 + 1 ) // 2
4024+ current_height = bitcoind .rpc .getblockcount ()
4025+
4026+ # Mine up to one block before the deadline — should NOT trigger force-close.
4027+ blocks_to_deadline = deadline - current_height
4028+ assert blocks_to_deadline > 1 , f"Not enough room: deadline={ deadline } , height={ current_height } "
4029+ bitcoind .generate_block (blocks_to_deadline - 1 )
4030+ sync_blockheight (bitcoind , [l2 ])
4031+ assert not l2 .daemon .is_in_log ('hit deadline' )
4032+
4033+ # Mine one more block to hit the deadline.
4034+ #
4035+ # BUG: l2 force-closes with "Fulfilled HTLC 0 SENT_REMOVE_HTLC cltv ... hit deadline"
4036+ # even though it has the preimage and just needs to reconnect to send it upstream.
4037+ # After a fix, this line should be changed to assert the force-close
4038+ # does NOT happen:
4039+ # assert not l2.daemon.is_in_log('Fulfilled HTLC 0 SENT_REMOVE_HTLC')
4040+ bitcoind .generate_block (1 )
4041+ sync_blockheight (bitcoind , [l2 ])
4042+ l2 .daemon .wait_for_log (r'Fulfilled HTLC 0 SENT_REMOVE_(HTLC|COMMIT) cltv .* hit deadline' )
4043+
4044+
39604045def test_closing_tx_valid (node_factory , bitcoind ):
39614046 l1 , l2 = node_factory .line_graph (2 , opts = {'may_reconnect' : True ,
39624047 'dev-no-reconnect' : None })
0 commit comments