@@ -3661,6 +3661,85 @@ def test_close_twice(node_factory, executor):
36613661 assert fut2 .result (TIMEOUT )['type' ] == 'mutual'
36623662
36633663
3664+ @pytest .mark .xfail (
3665+ strict = True ,
3666+ reason = "Bug: channel stuck in CLOSINGD_COMPLETE if funding never confirms"
3667+ )
3668+ def test_closingd_complete_stuck_no_funding (node_factory , bitcoind ):
3669+ """Mutual close pre-lockin + funding never confirms → permanent CLOSINGD_COMPLETE.
3670+
3671+ BOLT 2 explicitly permits sending `shutdown` before `channel_ready`
3672+ (i.e. before the funding tx has reached `minimum_depth`). Both sides
3673+ happily complete the mutual close negotiation and persist a
3674+ fully-signed close tx. If the funding tx then never confirms, the
3675+ close tx is permanently invalid — its only input is a 2-of-2 funding
3676+ output that does not exist on chain. The state machine has no path
3677+ from CLOSINGD_COMPLETE to FUNDING_SPEND_SEEN / ONCHAIN, and there is
3678+ no cleanup. The channel record sits in CLOSINGD_COMPLETE
3679+ indefinitely.
3680+
3681+ This test demonstrates the stuck state. It is marked xfail-strict
3682+ because no fix yet exists; once fixed, the marker should be removed.
3683+ """
3684+ l1 , l2 = node_factory .line_graph (2 , fundchannel = False )
3685+
3686+ # Fund l1's on-chain wallet
3687+ l1 .fundwallet (10 ** 7 )
3688+
3689+ # Open the channel: funding tx is broadcast to bitcoind's mempool,
3690+ # but we do NOT mine it.
3691+ res = l1 .rpc .fundchannel (l2 .info ['id' ], 10 ** 6 )
3692+ funding_txid = res ['txid' ]
3693+
3694+ # Both sides reach CHANNELD_AWAITING_LOCKIN
3695+ wait_for (lambda : only_one (l1 .rpc .listpeerchannels ()['channels' ])['state' ]
3696+ == 'CHANNELD_AWAITING_LOCKIN' )
3697+ wait_for (lambda : only_one (l2 .rpc .listpeerchannels ()['channels' ])['state' ]
3698+ == 'CHANNELD_AWAITING_LOCKIN' )
3699+
3700+ # Confirm the funding tx is in the mempool but NOT yet in any block
3701+ assert funding_txid in bitcoind .rpc .getrawmempool ()
3702+
3703+ # Push funding's effective fee far below any block-min-fee so future
3704+ # generated blocks do not include it. pyln-testing uses this same
3705+ # trick (utils.py:629–635).
3706+ bitcoind .rpc .prioritisetransaction (funding_txid , None , - 10 ** 8 )
3707+
3708+ # Initiate mutual close while still in CHANNELD_AWAITING_LOCKIN
3709+ # (BOLT 2 §"Closing Initiation: shutdown" permits this).
3710+ l1 .rpc .close (l2 .info ['id' ])
3711+
3712+ # Both sides should reach CLOSINGD_COMPLETE
3713+ wait_for (lambda : only_one (l1 .rpc .listpeerchannels ()['channels' ])['state' ]
3714+ == 'CLOSINGD_COMPLETE' )
3715+ wait_for (lambda : only_one (l2 .rpc .listpeerchannels ()['channels' ])['state' ]
3716+ == 'CLOSINGD_COMPLETE' )
3717+
3718+ # Advance the chain. CLN waits for an on-chain funding-spend event
3719+ # (not a block count), so 100 blocks is sufficient to demonstrate the
3720+ # stuck state.
3721+ bitcoind .generate_block (100 )
3722+ sync_blockheight (bitcoind , [l1 , l2 ])
3723+
3724+ # Sanity: funding really never confirmed
3725+ assert l1 .rpc .listpeerchannels ()['channels' ][0 ].get ('short_channel_id' ) is None
3726+ assert l2 .rpc .listpeerchannels ()['channels' ][0 ].get ('short_channel_id' ) is None
3727+
3728+ # Expected behavior under fix: channel record has moved beyond
3729+ # CLOSINGD_COMPLETE (transitioned to ONCHAIN with proof-of-give-up,
3730+ # been auto-forgotten, or some other resolved terminal state).
3731+ chans_l1 = l1 .rpc .listpeerchannels ()['channels' ]
3732+ chans_l2 = l2 .rpc .listpeerchannels ()['channels' ]
3733+ assert all (c ['state' ] != 'CLOSINGD_COMPLETE' for c in chans_l1 ), (
3734+ f"l1 still has channel in CLOSINGD_COMPLETE after 100 blocks: "
3735+ f"{ [c ['state' ] for c in chans_l1 ]} "
3736+ )
3737+ assert all (c ['state' ] != 'CLOSINGD_COMPLETE' for c in chans_l2 ), (
3738+ f"l2 still has channel in CLOSINGD_COMPLETE after 100 blocks: "
3739+ f"{ [c ['state' ] for c in chans_l2 ]} "
3740+ )
3741+
3742+
36643743def test_close_weight_estimate (node_factory , bitcoind ):
36653744 """closingd uses the expected closing tx weight to constrain fees; make sure that lightningd agrees
36663745 once it has the actual agreed tx"""
0 commit comments