@@ -2812,12 +2812,14 @@ fn fail_quiescent_action_on_channel_close() {
28122812
28132813#[ test]
28142814fn abandon_splice_quiescent_action_on_shutdown ( ) {
2815- do_abandon_splice_quiescent_action_on_shutdown ( true ) ;
2816- do_abandon_splice_quiescent_action_on_shutdown ( false ) ;
2815+ do_abandon_splice_quiescent_action_on_shutdown ( true , false ) ;
2816+ do_abandon_splice_quiescent_action_on_shutdown ( false , false ) ;
2817+ do_abandon_splice_quiescent_action_on_shutdown ( true , true ) ;
2818+ do_abandon_splice_quiescent_action_on_shutdown ( false , true ) ;
28172819}
28182820
28192821#[ cfg( test) ]
2820- fn do_abandon_splice_quiescent_action_on_shutdown ( local_shutdown : bool ) {
2822+ fn do_abandon_splice_quiescent_action_on_shutdown ( local_shutdown : bool , pending_splice : bool ) {
28212823 let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
28222824 let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
28232825 let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
@@ -2831,6 +2833,19 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool) {
28312833 let ( _, _, channel_id, _) =
28322834 create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_capacity, 0 ) ;
28332835
2836+ // When testing with a prior pending splice, complete splice A first so that
2837+ // `quiescent_action_into_error` filters against `pending_splice.contributed_inputs/outputs`.
2838+ if pending_splice {
2839+ let funding_contribution = do_initiate_splice_in (
2840+ & nodes[ 0 ] ,
2841+ & nodes[ 1 ] ,
2842+ channel_id,
2843+ Amount :: from_sat ( initial_channel_capacity / 2 ) ,
2844+ ) ;
2845+ let ( _splice_tx, _new_funding_script) =
2846+ splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
2847+ }
2848+
28342849 // Since we cannot close after having sent `stfu`, send an HTLC so that when we attempt to
28352850 // splice, the `stfu` message is held back.
28362851 let payment_amount = 1_000_000 ;
@@ -2843,17 +2858,22 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool) {
28432858 check_added_monitors ( & nodes[ 0 ] , 1 ) ;
28442859
28452860 nodes[ 1 ] . node . handle_update_add_htlc ( node_id_0, & update. update_add_htlcs [ 0 ] ) ;
2846- nodes[ 1 ] . node . handle_commitment_signed ( node_id_0, & update. commitment_signed [ 0 ] ) ;
2861+ // After a splice, commitment_signed messages are batched across funding scopes.
2862+ nodes[ 1 ] . node . handle_commitment_signed_batch_test ( node_id_0, & update. commitment_signed ) ;
28472863 check_added_monitors ( & nodes[ 1 ] , 1 ) ;
28482864 let ( revoke_and_ack, _) = get_revoke_commit_msgs ( & nodes[ 1 ] , & node_id_0) ;
28492865
28502866 nodes[ 0 ] . node . handle_revoke_and_ack ( node_id_1, & revoke_and_ack) ;
28512867 check_added_monitors ( & nodes[ 0 ] , 1 ) ;
28522868
28532869 // Attempt the splice. `stfu` should not go out yet as the state machine is pending.
2854- let splice_in_amount = initial_channel_capacity / 2 ;
2870+ // Use a different amount when there's a prior splice so the change output differs.
2871+ let splice_in_amount =
2872+ if pending_splice { initial_channel_capacity / 4 } else { initial_channel_capacity / 2 } ;
28552873 let funding_contribution =
28562874 initiate_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, Amount :: from_sat ( splice_in_amount) ) ;
2875+ let splice_b_change_output =
2876+ if pending_splice { funding_contribution. change_output ( ) . cloned ( ) } else { None } ;
28572877 assert ! ( nodes[ 0 ] . node. get_and_clear_pending_msg_events( ) . is_empty( ) ) ;
28582878
28592879 // Close the channel. We should see a `SpliceFailed` event for the pending splice
@@ -2867,7 +2887,33 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool) {
28672887 let shutdown = get_event_msg ! ( closer_node, MessageSendEvent :: SendShutdown , closee_node_id) ;
28682888 closee_node. node . handle_shutdown ( closer_node_id, & shutdown) ;
28692889
2870- expect_splice_failed_events ( & nodes[ 0 ] , & channel_id, funding_contribution) ;
2890+ if pending_splice {
2891+ // With a prior pending splice, contributions are filtered against committed inputs/outputs.
2892+ let events = nodes[ 0 ] . node . get_and_clear_pending_events ( ) ;
2893+ assert_eq ! ( events. len( ) , 2 , "{events:?}" ) ;
2894+ match & events[ 0 ] {
2895+ Event :: SpliceFailed { channel_id : cid, .. } => {
2896+ assert_eq ! ( * cid, channel_id) ;
2897+ } ,
2898+ other => panic ! ( "Expected SpliceFailed, got {:?}" , other) ,
2899+ }
2900+ match & events[ 1 ] {
2901+ Event :: DiscardFunding {
2902+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
2903+ ..
2904+ } => {
2905+ // The UTXO was filtered: it's still committed to the prior splice.
2906+ assert ! ( inputs. is_empty( ) , "Expected empty inputs (filtered), got {:?}" , inputs) ;
2907+ // The change output was NOT filtered: different splice-in amount produces a
2908+ // different change.
2909+ let expected_outputs: Vec < _ > = splice_b_change_output. into_iter ( ) . collect ( ) ;
2910+ assert_eq ! ( * outputs, expected_outputs) ;
2911+ } ,
2912+ other => panic ! ( "Expected DiscardFunding with Contribution, got {:?}" , other) ,
2913+ }
2914+ } else {
2915+ expect_splice_failed_events ( & nodes[ 0 ] , & channel_id, funding_contribution) ;
2916+ }
28712917 let _ = get_event_msg ! ( closee_node, MessageSendEvent :: SendShutdown , closer_node_id) ;
28722918}
28732919
@@ -5344,3 +5390,89 @@ fn test_splice_rbf_sequential() {
53445390 node. chain_source . remove_watched_txn_and_outputs ( outpoint_1, new_funding_script. clone ( ) ) ;
53455391 }
53465392}
5393+
5394+ #[ test]
5395+ fn test_splice_rbf_disconnect_filters_prior_contributions ( ) {
5396+ // When disconnecting during an RBF round that reuses the same UTXOs as a prior round,
5397+ // the SpliceFundingFailed event should filter out inputs/outputs still committed to the prior
5398+ // round. This exercises the `reset_pending_splice_state` → `maybe_create_splice_funding_failed`
5399+ // macro path.
5400+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
5401+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
5402+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
5403+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
5404+
5405+ let node_id_0 = nodes[ 0 ] . node . get_our_node_id ( ) ;
5406+ let node_id_1 = nodes[ 1 ] . node . get_our_node_id ( ) ;
5407+
5408+ let initial_channel_value_sat = 100_000 ;
5409+ let ( _, _, channel_id, _) =
5410+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_value_sat, 0 ) ;
5411+
5412+ let added_value = Amount :: from_sat ( 50_000 ) ;
5413+ // Provide exactly 1 UTXO per node so coin selection is deterministic.
5414+ provide_utxo_reserves ( & nodes, 1 , added_value * 2 ) ;
5415+
5416+ // --- Round 0: Initial splice-in at floor feerate (253). ---
5417+ let funding_contribution = do_initiate_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value) ;
5418+ let ( _splice_tx_0, _new_funding_script) =
5419+ splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
5420+
5421+ // --- Round 1: RBF at higher feerate without providing new UTXOs. ---
5422+ // The wallet reselects the same UTXO since the splice tx hasn't been mined.
5423+ let feerate_1_sat_per_kwu = ( FEERATE_FLOOR_SATS_PER_KW as u64 * 25 + 23 ) / 24 ;
5424+ let rbf_feerate = FeeRate :: from_sat_per_kwu ( feerate_1_sat_per_kwu) ;
5425+ let funding_template = nodes[ 0 ] . node . rbf_channel ( & channel_id, & node_id_1, rbf_feerate) . unwrap ( ) ;
5426+ let wallet = WalletSync :: new ( Arc :: clone ( & nodes[ 0 ] . wallet_source ) , nodes[ 0 ] . logger ) ;
5427+ let funding_contribution_1 = funding_template. splice_in_sync ( added_value, & wallet) . unwrap ( ) ;
5428+ let rbf_change_output = funding_contribution_1. change_output ( ) . cloned ( ) ;
5429+ nodes[ 0 ]
5430+ . node
5431+ . funding_contributed ( & channel_id, & node_id_1, funding_contribution_1, None )
5432+ . unwrap ( ) ;
5433+
5434+ // STFU exchange.
5435+ let stfu_a = get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendStfu , node_id_1) ;
5436+ nodes[ 1 ] . node . handle_stfu ( node_id_0, & stfu_a) ;
5437+ let stfu_b = get_event_msg ! ( nodes[ 1 ] , MessageSendEvent :: SendStfu , node_id_0) ;
5438+ nodes[ 0 ] . node . handle_stfu ( node_id_1, & stfu_b) ;
5439+
5440+ // RBF handshake to start interactive TX.
5441+ let tx_init_rbf = get_event_msg ! ( nodes[ 0 ] , MessageSendEvent :: SendTxInitRbf , node_id_1) ;
5442+ assert_eq ! ( tx_init_rbf. feerate_sat_per_1000_weight, feerate_1_sat_per_kwu as u32 ) ;
5443+ nodes[ 1 ] . node . handle_tx_init_rbf ( node_id_0, & tx_init_rbf) ;
5444+ let tx_ack_rbf = get_event_msg ! ( nodes[ 1 ] , MessageSendEvent :: SendTxAckRbf , node_id_0) ;
5445+ nodes[ 0 ] . node . handle_tx_ack_rbf ( node_id_1, & tx_ack_rbf) ;
5446+
5447+ // Disconnect mid-negotiation. Stale interactive TX messages are cleared by peer_disconnected.
5448+ nodes[ 0 ] . node . peer_disconnected ( node_id_1) ;
5449+ nodes[ 1 ] . node . peer_disconnected ( node_id_0) ;
5450+
5451+ // The initiator should get SpliceFailed + DiscardFunding with filtered contributions.
5452+ let events = nodes[ 0 ] . node . get_and_clear_pending_events ( ) ;
5453+ assert_eq ! ( events. len( ) , 2 , "{events:?}" ) ;
5454+ match & events[ 0 ] {
5455+ Event :: SpliceFailed { channel_id : cid, .. } => {
5456+ assert_eq ! ( * cid, channel_id) ;
5457+ } ,
5458+ other => panic ! ( "Expected SpliceFailed, got {:?}" , other) ,
5459+ }
5460+ match & events[ 1 ] {
5461+ Event :: DiscardFunding {
5462+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
5463+ ..
5464+ } => {
5465+ // The UTXO was filtered out: it's still committed to round 0's splice.
5466+ assert ! ( inputs. is_empty( ) , "Expected empty inputs (filtered), got {:?}" , inputs) ;
5467+ // The change output was NOT filtered: different feerate produces a different amount.
5468+ let expected_outputs: Vec < _ > = rbf_change_output. into_iter ( ) . collect ( ) ;
5469+ assert_eq ! ( * outputs, expected_outputs) ;
5470+ } ,
5471+ other => panic ! ( "Expected DiscardFunding with Contribution, got {:?}" , other) ,
5472+ }
5473+
5474+ // Reconnect. After a completed splice, channel_ready is not re-sent.
5475+ let mut reconnect_args = ReconnectArgs :: new ( & nodes[ 0 ] , & nodes[ 1 ] ) ;
5476+ reconnect_args. send_announcement_sigs = ( true , true ) ;
5477+ reconnect_nodes ( reconnect_args) ;
5478+ }
0 commit comments