@@ -2904,12 +2904,14 @@ fn fail_quiescent_action_on_channel_close() {
29042904
29052905#[ test]
29062906fn abandon_splice_quiescent_action_on_shutdown ( ) {
2907- do_abandon_splice_quiescent_action_on_shutdown ( true ) ;
2908- do_abandon_splice_quiescent_action_on_shutdown ( false ) ;
2907+ do_abandon_splice_quiescent_action_on_shutdown ( true , false ) ;
2908+ do_abandon_splice_quiescent_action_on_shutdown ( false , false ) ;
2909+ do_abandon_splice_quiescent_action_on_shutdown ( true , true ) ;
2910+ do_abandon_splice_quiescent_action_on_shutdown ( false , true ) ;
29092911}
29102912
29112913#[ cfg( test) ]
2912- fn do_abandon_splice_quiescent_action_on_shutdown ( local_shutdown : bool ) {
2914+ fn do_abandon_splice_quiescent_action_on_shutdown ( local_shutdown : bool , pending_splice : bool ) {
29132915 let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
29142916 let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
29152917 let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
@@ -2923,6 +2925,19 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool) {
29232925 let ( _, _, channel_id, _) =
29242926 create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_capacity, 0 ) ;
29252927
2928+ // When testing with a prior pending splice, complete splice A first so that
2929+ // `quiescent_action_into_error` filters against `pending_splice.contributed_inputs/outputs`.
2930+ if pending_splice {
2931+ let funding_contribution = do_initiate_splice_in (
2932+ & nodes[ 0 ] ,
2933+ & nodes[ 1 ] ,
2934+ channel_id,
2935+ Amount :: from_sat ( initial_channel_capacity / 2 ) ,
2936+ ) ;
2937+ let ( _splice_tx, _new_funding_script) =
2938+ splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
2939+ }
2940+
29262941 // Since we cannot close after having sent `stfu`, send an HTLC so that when we attempt to
29272942 // splice, the `stfu` message is held back.
29282943 let payment_amount = 1_000_000 ;
@@ -2935,17 +2950,22 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool) {
29352950 check_added_monitors ( & nodes[ 0 ] , 1 ) ;
29362951
29372952 nodes[ 1 ] . node . handle_update_add_htlc ( node_id_0, & update. update_add_htlcs [ 0 ] ) ;
2938- nodes[ 1 ] . node . handle_commitment_signed ( node_id_0, & update. commitment_signed [ 0 ] ) ;
2953+ // After a splice, commitment_signed messages are batched across funding scopes.
2954+ nodes[ 1 ] . node . handle_commitment_signed_batch_test ( node_id_0, & update. commitment_signed ) ;
29392955 check_added_monitors ( & nodes[ 1 ] , 1 ) ;
29402956 let ( revoke_and_ack, _) = get_revoke_commit_msgs ( & nodes[ 1 ] , & node_id_0) ;
29412957
29422958 nodes[ 0 ] . node . handle_revoke_and_ack ( node_id_1, & revoke_and_ack) ;
29432959 check_added_monitors ( & nodes[ 0 ] , 1 ) ;
29442960
29452961 // Attempt the splice. `stfu` should not go out yet as the state machine is pending.
2946- let splice_in_amount = initial_channel_capacity / 2 ;
2962+ // Use a different amount when there's a prior splice so the change output differs.
2963+ let splice_in_amount =
2964+ if pending_splice { initial_channel_capacity / 4 } else { initial_channel_capacity / 2 } ;
29472965 let funding_contribution =
29482966 initiate_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, Amount :: from_sat ( splice_in_amount) ) ;
2967+ let splice_b_change_output =
2968+ if pending_splice { funding_contribution. change_output ( ) . cloned ( ) } else { None } ;
29492969 assert ! ( nodes[ 0 ] . node. get_and_clear_pending_msg_events( ) . is_empty( ) ) ;
29502970
29512971 // Close the channel. We should see a `SpliceFailed` event for the pending splice
@@ -2959,7 +2979,33 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool) {
29592979 let shutdown = get_event_msg ! ( closer_node, MessageSendEvent :: SendShutdown , closee_node_id) ;
29602980 closee_node. node . handle_shutdown ( closer_node_id, & shutdown) ;
29612981
2962- expect_splice_failed_events ( & nodes[ 0 ] , & channel_id, funding_contribution) ;
2982+ if pending_splice {
2983+ // With a prior pending splice, contributions are filtered against committed inputs/outputs.
2984+ let events = nodes[ 0 ] . node . get_and_clear_pending_events ( ) ;
2985+ assert_eq ! ( events. len( ) , 2 , "{events:?}" ) ;
2986+ match & events[ 0 ] {
2987+ Event :: SpliceFailed { channel_id : cid, .. } => {
2988+ assert_eq ! ( * cid, channel_id) ;
2989+ } ,
2990+ other => panic ! ( "Expected SpliceFailed, got {:?}" , other) ,
2991+ }
2992+ match & events[ 1 ] {
2993+ Event :: DiscardFunding {
2994+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
2995+ ..
2996+ } => {
2997+ // The UTXO was filtered: it's still committed to the prior splice.
2998+ assert ! ( inputs. is_empty( ) , "Expected empty inputs (filtered), got {:?}" , inputs) ;
2999+ // The change output was NOT filtered: different splice-in amount produces a
3000+ // different change.
3001+ let expected_outputs: Vec < _ > = splice_b_change_output. into_iter ( ) . collect ( ) ;
3002+ assert_eq ! ( * outputs, expected_outputs) ;
3003+ } ,
3004+ other => panic ! ( "Expected DiscardFunding with Contribution, got {:?}" , other) ,
3005+ }
3006+ } else {
3007+ expect_splice_failed_events ( & nodes[ 0 ] , & channel_id, funding_contribution) ;
3008+ }
29633009 let _ = get_event_msg ! ( closee_node, MessageSendEvent :: SendShutdown , closer_node_id) ;
29643010}
29653011
@@ -5318,3 +5364,74 @@ fn test_splice_rbf_sequential() {
53185364 node. chain_source . remove_watched_txn_and_outputs ( outpoint_1, new_funding_script. clone ( ) ) ;
53195365 }
53205366}
5367+
5368+ #[ test]
5369+ fn test_splice_rbf_disconnect_filters_prior_contributions ( ) {
5370+ // When disconnecting during an RBF round that reuses the same UTXOs as a prior round,
5371+ // the SpliceFundingFailed event should filter out inputs/outputs still committed to the prior
5372+ // round. This exercises the `reset_pending_splice_state` → `maybe_create_splice_funding_failed`
5373+ // macro path.
5374+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
5375+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
5376+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
5377+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
5378+
5379+ let node_id_0 = nodes[ 0 ] . node . get_our_node_id ( ) ;
5380+ let node_id_1 = nodes[ 1 ] . node . get_our_node_id ( ) ;
5381+
5382+ let initial_channel_value_sat = 100_000 ;
5383+ let ( _, _, channel_id, _) =
5384+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_value_sat, 0 ) ;
5385+
5386+ let added_value = Amount :: from_sat ( 50_000 ) ;
5387+ // Provide exactly 1 UTXO per node so coin selection is deterministic.
5388+ provide_utxo_reserves ( & nodes, 1 , added_value * 2 ) ;
5389+
5390+ // --- Round 0: Initial splice-in at floor feerate (253). ---
5391+ let funding_contribution = do_initiate_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value) ;
5392+ let ( _splice_tx_0, _new_funding_script) =
5393+ splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
5394+
5395+ // --- Round 1: RBF at higher feerate without providing new UTXOs. ---
5396+ // The wallet reselects the same UTXO since the splice tx hasn't been mined.
5397+ let feerate_1_sat_per_kwu = ( FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23 ) / 24 ;
5398+ let rbf_feerate = FeeRate :: from_sat_per_kwu ( feerate_1_sat_per_kwu) ;
5399+ let funding_contribution_1 =
5400+ do_initiate_rbf_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value, rbf_feerate) ;
5401+ let rbf_change_output = funding_contribution_1. change_output ( ) . cloned ( ) ;
5402+
5403+ // STFU exchange + RBF handshake to start interactive TX.
5404+ complete_rbf_handshake ( & nodes[ 0 ] , & nodes[ 1 ] ) ;
5405+
5406+ // Disconnect mid-negotiation. Stale interactive TX messages are cleared by peer_disconnected.
5407+ nodes[ 0 ] . node . peer_disconnected ( node_id_1) ;
5408+ nodes[ 1 ] . node . peer_disconnected ( node_id_0) ;
5409+
5410+ // The initiator should get SpliceFailed + DiscardFunding with filtered contributions.
5411+ let events = nodes[ 0 ] . node . get_and_clear_pending_events ( ) ;
5412+ assert_eq ! ( events. len( ) , 2 , "{events:?}" ) ;
5413+ match & events[ 0 ] {
5414+ Event :: SpliceFailed { channel_id : cid, .. } => {
5415+ assert_eq ! ( * cid, channel_id) ;
5416+ } ,
5417+ other => panic ! ( "Expected SpliceFailed, got {:?}" , other) ,
5418+ }
5419+ match & events[ 1 ] {
5420+ Event :: DiscardFunding {
5421+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
5422+ ..
5423+ } => {
5424+ // The UTXO was filtered out: it's still committed to round 0's splice.
5425+ assert ! ( inputs. is_empty( ) , "Expected empty inputs (filtered), got {:?}" , inputs) ;
5426+ // The change output was NOT filtered: different feerate produces a different amount.
5427+ let expected_outputs: Vec < _ > = rbf_change_output. into_iter ( ) . collect ( ) ;
5428+ assert_eq ! ( * outputs, expected_outputs) ;
5429+ } ,
5430+ other => panic ! ( "Expected DiscardFunding with Contribution, got {:?}" , other) ,
5431+ }
5432+
5433+ // Reconnect. After a completed splice, channel_ready is not re-sent.
5434+ let mut reconnect_args = ReconnectArgs :: new ( & nodes[ 0 ] , & nodes[ 1 ] ) ;
5435+ reconnect_args. send_announcement_sigs = ( true , true ) ;
5436+ reconnect_nodes ( reconnect_args) ;
5437+ }
0 commit comments