@@ -5154,6 +5154,104 @@ fn test_splice_rbf_after_splice_locked() {
51545154 }
51555155}
51565156
5157+ #[ test]
5158+ fn test_splice_rbf_stfu_after_splice_locked ( ) {
5159+ // Test that we don't send tx_init_rbf when we've already sent splice_locked.
5160+ //
5161+ // Scenario: node 0 initiates an RBF and sends STFU, but before receiving the counterparty's
5162+ // STFU response, it mines enough blocks to send splice_locked (setting sent_funding_txid).
5163+ // When node 1's STFU arrives, the stfu() handler should detect that RBF is no longer valid
5164+ // and return WarnAndDisconnect instead of sending tx_init_rbf.
5165+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
5166+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
5167+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
5168+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
5169+
5170+ let node_id_0 = nodes[ 0 ] . node . get_our_node_id ( ) ;
5171+ let node_id_1 = nodes[ 1 ] . node . get_our_node_id ( ) ;
5172+
5173+ let initial_channel_value_sat = 100_000 ;
5174+ let ( _, _, channel_id, _) =
5175+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_value_sat, 0 ) ;
5176+
5177+ let added_value = Amount :: from_sat ( 50_000 ) ;
5178+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
5179+
5180+ // Complete a splice-in from node 0.
5181+ let funding_contribution = do_initiate_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value) ;
5182+ let ( splice_tx, _) = splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
5183+
5184+ // Mine the splice tx on both nodes (not enough for splice_locked yet).
5185+ mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
5186+ mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
5187+
5188+ // Provide more UTXOs for the RBF attempt.
5189+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
5190+
5191+ // Initiate RBF from node 0.
5192+ let rbf_feerate = FeeRate :: from_sat_per_kwu ( FEERATE_FLOOR_SATS_PER_KW as u64 + 25 ) ;
5193+ let _funding_contribution =
5194+ do_initiate_rbf_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value, rbf_feerate) ;
5195+
5196+ // Node 0 sends STFU (can_initiate_rbf passes since no splice_locked yet).
5197+ let stfu_init = get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendStfu , node_id_1) ;
5198+
5199+ // Deliver STFU to node 1; extract node 1's STFU response but don't deliver it yet.
5200+ nodes[ 1 ] . node . handle_stfu ( node_id_0, & stfu_init) ;
5201+ let stfu_ack = get_event_msg ! ( nodes[ 1 ] , MessageSendEvent :: SendStfu , node_id_0) ;
5202+
5203+ // Mine enough blocks on node 0 so it sends splice_locked (sets sent_funding_txid).
5204+ connect_blocks ( & nodes[ 0 ] , ANTI_REORG_DELAY - 1 ) ;
5205+ let _splice_locked = get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendSpliceLocked , node_id_1) ;
5206+
5207+ // Now deliver node 1's STFU to node 0. The stfu() handler should detect that RBF is no
5208+ // longer valid (we already sent splice_locked) and return WarnAndDisconnect.
5209+ nodes[ 0 ] . node . handle_stfu ( node_id_1, & stfu_ack) ;
5210+
5211+ let msg_events = nodes[ 0 ] . node . get_and_clear_pending_msg_events ( ) ;
5212+ assert_eq ! ( msg_events. len( ) , 1 , "{msg_events:?}" ) ;
5213+ match & msg_events[ 0 ] {
5214+ MessageSendEvent :: HandleError { action, .. } => {
5215+ assert_eq ! (
5216+ * action,
5217+ msgs:: ErrorAction :: DisconnectPeerWithWarning {
5218+ msg: msgs:: WarningMessage {
5219+ channel_id,
5220+ data: format!(
5221+ "Channel {} already sent splice_locked, cannot RBF" ,
5222+ channel_id,
5223+ ) ,
5224+ } ,
5225+ }
5226+ ) ;
5227+ } ,
5228+ _ => panic ! ( "Expected HandleError, got {:?}" , msg_events[ 0 ] ) ,
5229+ }
5230+
5231+ // Node 0 should emit DiscardFunding + SpliceNegotiationFailed for the RBF contribution.
5232+ // The change output is filtered (same script_pubkey as the first splice's change output),
5233+ // but the input survives because it's a different UTXO from the first splice.
5234+ let events = nodes[ 0 ] . node . get_and_clear_pending_events ( ) ;
5235+ assert_eq ! ( events. len( ) , 2 , "{events:?}" ) ;
5236+ match & events[ 0 ] {
5237+ Event :: DiscardFunding {
5238+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
5239+ ..
5240+ } => {
5241+ assert ! ( !inputs. is_empty( ) ) ;
5242+ assert ! ( outputs. is_empty( ) ) ;
5243+ } ,
5244+ other => panic ! ( "Expected DiscardFunding, got {:?}" , other) ,
5245+ }
5246+ match & events[ 1 ] {
5247+ Event :: SpliceNegotiationFailed { channel_id : cid, reason, .. } => {
5248+ assert_eq ! ( * cid, channel_id) ;
5249+ assert_eq ! ( * reason, NegotiationFailureReason :: CannotInitiateRbf ) ;
5250+ } ,
5251+ other => panic ! ( "Expected SpliceNegotiationFailed, got {:?}" , other) ,
5252+ }
5253+ }
5254+
51575255#[ test]
51585256fn test_splice_zeroconf_no_rbf_feerate ( ) {
51595257 // Test that splice_channel returns a FundingTemplate with min_rbf_feerate = None for a
0 commit comments