@@ -4871,6 +4871,104 @@ fn test_splice_rbf_after_splice_locked() {
48714871 }
48724872}
48734873
4874+ #[ test]
4875+ fn test_splice_rbf_stfu_after_splice_locked ( ) {
4876+ // Test that we don't send tx_init_rbf when we've already sent splice_locked.
4877+ //
4878+ // Scenario: node 0 initiates an RBF and sends STFU, but before receiving the counterparty's
4879+ // STFU response, it mines enough blocks to send splice_locked (setting sent_funding_txid).
4880+ // When node 1's STFU arrives, the stfu() handler should detect that RBF is no longer valid
4881+ // and return WarnAndDisconnect instead of sending tx_init_rbf.
4882+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
4883+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
4884+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
4885+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
4886+
4887+ let node_id_0 = nodes[ 0 ] . node . get_our_node_id ( ) ;
4888+ let node_id_1 = nodes[ 1 ] . node . get_our_node_id ( ) ;
4889+
4890+ let initial_channel_value_sat = 100_000 ;
4891+ let ( _, _, channel_id, _) =
4892+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_value_sat, 0 ) ;
4893+
4894+ let added_value = Amount :: from_sat ( 50_000 ) ;
4895+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
4896+
4897+ // Complete a splice-in from node 0.
4898+ let funding_contribution = do_initiate_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value) ;
4899+ let ( splice_tx, _) = splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
4900+
4901+ // Mine the splice tx on both nodes (not enough for splice_locked yet).
4902+ mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
4903+ mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
4904+
4905+ // Provide more UTXOs for the RBF attempt.
4906+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
4907+
4908+ // Initiate RBF from node 0.
4909+ let rbf_feerate = FeeRate :: from_sat_per_kwu ( FEERATE_FLOOR_SATS_PER_KW as u64 + 25 ) ;
4910+ let funding_contribution =
4911+ do_initiate_rbf_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value, rbf_feerate) ;
4912+
4913+ // Node 0 sends STFU (can_initiate_rbf passes since no splice_locked yet).
4914+ let stfu_init = get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendStfu , node_id_1) ;
4915+
4916+ // Deliver STFU to node 1; extract node 1's STFU response but don't deliver it yet.
4917+ nodes[ 1 ] . node . handle_stfu ( node_id_0, & stfu_init) ;
4918+ let stfu_ack = get_event_msg ! ( nodes[ 1 ] , MessageSendEvent :: SendStfu , node_id_0) ;
4919+
4920+ // Mine enough blocks on node 0 so it sends splice_locked (sets sent_funding_txid).
4921+ connect_blocks ( & nodes[ 0 ] , ANTI_REORG_DELAY - 1 ) ;
4922+ let _splice_locked = get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendSpliceLocked , node_id_1) ;
4923+
4924+ // Now deliver node 1's STFU to node 0. The stfu() handler should detect that RBF is no
4925+ // longer valid (we already sent splice_locked) and return WarnAndDisconnect.
4926+ nodes[ 0 ] . node . handle_stfu ( node_id_1, & stfu_ack) ;
4927+
4928+ let msg_events = nodes[ 0 ] . node . get_and_clear_pending_msg_events ( ) ;
4929+ assert_eq ! ( msg_events. len( ) , 1 , "{msg_events:?}" ) ;
4930+ match & msg_events[ 0 ] {
4931+ MessageSendEvent :: HandleError { action, .. } => {
4932+ assert_eq ! (
4933+ * action,
4934+ msgs:: ErrorAction :: DisconnectPeerWithWarning {
4935+ msg: msgs:: WarningMessage {
4936+ channel_id,
4937+ data: format!(
4938+ "Channel {} already sent splice_locked, cannot RBF" ,
4939+ channel_id,
4940+ ) ,
4941+ } ,
4942+ }
4943+ ) ;
4944+ } ,
4945+ _ => panic ! ( "Expected HandleError, got {:?}" , msg_events[ 0 ] ) ,
4946+ }
4947+
4948+ // Node 0 should emit DiscardFunding + SpliceNegotiationFailed for the RBF contribution.
4949+ // The change output is filtered (same script_pubkey as the first splice's change output),
4950+ // but the input survives because it's a different UTXO from the first splice.
4951+ let events = nodes[ 0 ] . node . get_and_clear_pending_events ( ) ;
4952+ assert_eq ! ( events. len( ) , 2 , "{events:?}" ) ;
4953+ match & events[ 0 ] {
4954+ Event :: DiscardFunding {
4955+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
4956+ ..
4957+ } => {
4958+ assert ! ( !inputs. is_empty( ) ) ;
4959+ assert ! ( outputs. is_empty( ) ) ;
4960+ } ,
4961+ other => panic ! ( "Expected DiscardFunding, got {:?}" , other) ,
4962+ }
4963+ match & events[ 1 ] {
4964+ Event :: SpliceNegotiationFailed { channel_id : cid, reason, .. } => {
4965+ assert_eq ! ( * cid, channel_id) ;
4966+ assert_eq ! ( * reason, NegotiationFailureReason :: CannotInitiateRbf ) ;
4967+ } ,
4968+ other => panic ! ( "Expected SpliceNegotiationFailed, got {:?}" , other) ,
4969+ }
4970+ }
4971+
48744972#[ test]
48754973fn test_splice_zeroconf_no_rbf_feerate ( ) {
48764974 // Test that splice_channel returns a FundingTemplate with min_rbf_feerate = None for a
0 commit comments