@@ -660,9 +660,15 @@ pub fn splice_channel<'a, 'b, 'c, 'd>(
660660 ( splice_tx, new_funding_script)
661661}
662662
663+ pub struct SpliceLockedResult {
664+ pub stfu : Option < MessageSendEvent > ,
665+ pub node_a_discarded : Vec < ( Vec < bitcoin:: OutPoint > , Vec < TxOut > ) > ,
666+ pub node_b_discarded : Vec < ( Vec < bitcoin:: OutPoint > , Vec < TxOut > ) > ,
667+ }
668+
663669pub fn lock_splice_after_blocks < ' a , ' b , ' c , ' d > (
664670 node_a : & ' a Node < ' b , ' c , ' d > , node_b : & ' a Node < ' b , ' c , ' d > , num_blocks : u32 ,
665- ) -> Option < MessageSendEvent > {
671+ ) -> SpliceLockedResult {
666672 connect_blocks ( node_a, num_blocks) ;
667673 connect_blocks ( node_b, num_blocks) ;
668674
@@ -675,7 +681,7 @@ pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>(
675681pub fn lock_splice < ' a , ' b , ' c , ' d > (
676682 node_a : & ' a Node < ' b , ' c , ' d > , node_b : & ' a Node < ' b , ' c , ' d > ,
677683 splice_locked_for_node_b : & msgs:: SpliceLocked , is_0conf : bool , expected_discard_txids : & [ Txid ] ,
678- ) -> Option < MessageSendEvent > {
684+ ) -> SpliceLockedResult {
679685 let prev_funding_txid = node_a
680686 . chain_monitor
681687 . chain_monitor
@@ -712,29 +718,23 @@ pub fn lock_splice<'a, 'b, 'c, 'd>(
712718 }
713719 }
714720
715- let mut all_discard_txids = Vec :: new ( ) ;
716- let expected_num_events = 1 + expected_discard_txids . len ( ) ;
717- for node in [ node_a, node_b] {
721+ let mut node_a_discarded = Vec :: new ( ) ;
722+ let mut node_b_discarded = Vec :: new ( ) ;
723+ for ( idx , node) in [ node_a, node_b] . into_iter ( ) . enumerate ( ) {
718724 let events = node. node . get_and_clear_pending_events ( ) ;
719- assert_eq ! ( events. len ( ) , expected_num_events , " {events:?}") ;
725+ assert ! ( ! events. is_empty ( ) , "Expected at least ChannelReady, got {events:?}") ;
720726 assert ! ( matches!( events[ 0 ] , Event :: ChannelReady { .. } ) ) ;
721- let discard_txids: Vec < _ > = events[ 1 ..]
722- . iter ( )
723- . map ( |e| match e {
724- Event :: DiscardFunding { funding_info : FundingInfo :: Tx { transaction } , .. } => {
725- transaction. compute_txid ( )
726- } ,
727+ let discarded = if idx == 0 { & mut node_a_discarded } else { & mut node_b_discarded } ;
728+ for event in & events[ 1 ..] {
729+ match event {
727730 Event :: DiscardFunding {
728- funding_info : FundingInfo :: OutPoint { outpoint } , ..
729- } => outpoint. txid ,
730- other => panic ! ( "Expected DiscardFunding, got {:?}" , other) ,
731- } )
732- . collect ( ) ;
733- for txid in expected_discard_txids {
734- assert ! ( discard_txids. contains( txid) , "Missing DiscardFunding for txid {}" , txid) ;
735- }
736- if all_discard_txids. is_empty ( ) {
737- all_discard_txids = discard_txids;
731+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
732+ ..
733+ } => {
734+ discarded. push ( ( inputs. clone ( ) , outputs. clone ( ) ) ) ;
735+ } ,
736+ other => panic ! ( "Expected DiscardFunding with Contribution, got {:?}" , other) ,
737+ }
738738 }
739739 check_added_monitors ( node, 1 ) ;
740740 }
@@ -772,18 +772,18 @@ pub fn lock_splice<'a, 'b, 'c, 'd>(
772772 // old funding as it is no longer being tracked.
773773 for node in [ node_a, node_b] {
774774 node. chain_source . remove_watched_by_txid ( prev_funding_txid) ;
775- for txid in & all_discard_txids {
775+ for txid in expected_discard_txids {
776776 node. chain_source . remove_watched_by_txid ( * txid) ;
777777 }
778778 }
779779
780- node_a_stfu. or ( node_b_stfu)
780+ SpliceLockedResult { stfu : node_a_stfu. or ( node_b_stfu) , node_a_discarded , node_b_discarded }
781781}
782782
783783pub fn lock_rbf_splice_after_blocks < ' a , ' b , ' c , ' d > (
784784 node_a : & ' a Node < ' b , ' c , ' d > , node_b : & ' a Node < ' b , ' c , ' d > , tx : & Transaction , num_blocks : u32 ,
785785 expected_discard_txids : & [ Txid ] ,
786- ) -> Option < MessageSendEvent > {
786+ ) -> SpliceLockedResult {
787787 mine_transaction ( node_a, tx) ;
788788 mine_transaction ( node_b, tx) ;
789789
@@ -1374,7 +1374,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
13741374
13751375 mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
13761376 mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
1377- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
1377+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
13781378 // Node 0 had called splice_channel (line above) but never funding_contributed, so no stfu
13791379 // is expected from node 0 at this point.
13801380 assert ! ( stfu. is_none( ) ) ;
@@ -1402,7 +1402,7 @@ fn test_initiating_splice_holds_stfu_with_pending_splice() {
14021402 // Mine and lock the splice.
14031403 mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
14041404 mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
1405- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , 5 ) ;
1405+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , 5 ) . stfu ;
14061406 assert ! ( stfu. is_none( ) ) ;
14071407}
14081408
@@ -1638,7 +1638,7 @@ fn do_test_splice_tiebreak(
16381638 mine_transaction ( & nodes[ 1 ] , & tx) ;
16391639
16401640 // After splice_locked, node 1's preserved QuiescentAction triggers STFU for retry.
1641- let node_1_stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
1641+ let node_1_stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
16421642 let stfu_1 = if let Some ( MessageSendEvent :: SendStfu { msg, .. } ) = node_1_stfu {
16431643 assert ! ( msg. initiator) ;
16441644 msg
@@ -4680,13 +4680,109 @@ fn test_splice_rbf_acceptor_basic() {
46804680 expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
46814681
46824682 // Step 11: Mine, lock, and verify DiscardFunding for the replaced splice candidate.
4683- lock_rbf_splice_after_blocks (
4683+ let result = lock_rbf_splice_after_blocks (
46844684 & nodes[ 0 ] ,
46854685 & nodes[ 1 ] ,
46864686 & rbf_tx,
46874687 ANTI_REORG_DELAY - 1 ,
46884688 & [ first_splice_tx. compute_txid ( ) ] ,
46894689 ) ;
4690+
4691+ // The test wallet reuses the same UTXO across RBF rounds (the wallet doesn't track
4692+ // in-flight spends), so all contributed inputs are in the promoted tx. No unique
4693+ // contributions to discard.
4694+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
4695+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
4696+ }
4697+
4698+ #[ test]
4699+ fn test_splice_rbf_discard_unique_contribution ( ) {
4700+ // Verify that DiscardFunding events contain the correct unique inputs and outputs when the
4701+ // RBF round uses different UTXOs than the initial splice. By clearing the wallet between
4702+ // rounds and providing fresh UTXOs, we force distinct inputs per round. Round 0 also
4703+ // includes a splice-out output with a unique script_pubkey not present in the RBF tx.
4704+ // When the RBF is promoted, round 0's inputs and splice-out output should appear in
4705+ // DiscardFunding. The change output is filtered because it shares a script_pubkey with the
4706+ // promoted tx's change output.
4707+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
4708+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
4709+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
4710+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
4711+
4712+ let node_id_0 = nodes[ 0 ] . node . get_our_node_id ( ) ;
4713+ let node_id_1 = nodes[ 1 ] . node . get_our_node_id ( ) ;
4714+
4715+ let initial_channel_value_sat = 100_000 ;
4716+ let ( _, _, channel_id, _) =
4717+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_value_sat, 0 ) ;
4718+
4719+ let added_value = Amount :: from_sat ( 50_000 ) ;
4720+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
4721+
4722+ // Round 0: Splice-in-and-out from node 0 with a splice-out output.
4723+ let splice_out_output = TxOut {
4724+ value : Amount :: from_sat ( 5_000 ) ,
4725+ script_pubkey : ScriptBuf :: new_p2wpkh ( & WPubkeyHash :: all_zeros ( ) ) ,
4726+ } ;
4727+ let funding_contribution = do_initiate_splice_in_and_out (
4728+ & nodes[ 0 ] ,
4729+ & nodes[ 1 ] ,
4730+ channel_id,
4731+ added_value,
4732+ vec ! [ splice_out_output. clone( ) ] ,
4733+ ) ;
4734+ let round_0_inputs: Vec < _ > = funding_contribution. contributed_inputs ( ) . collect ( ) ;
4735+ assert ! ( !round_0_inputs. is_empty( ) ) ;
4736+
4737+ let ( first_splice_tx, new_funding_script) =
4738+ splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
4739+
4740+ // Clear node 0's wallet so round 1 must use different UTXOs.
4741+ nodes[ 0 ] . wallet_source . clear_utxos ( ) ;
4742+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
4743+
4744+ // Round 1: RBF with fresh UTXOs, splice-in only (no splice-out output).
4745+ let rbf_feerate = FeeRate :: from_sat_per_kwu ( FEERATE_FLOOR_SATS_PER_KW as u64 + 25 ) ;
4746+ let funding_contribution =
4747+ do_initiate_rbf_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value, rbf_feerate) ;
4748+ let round_1_inputs: Vec < _ > = funding_contribution. contributed_inputs ( ) . collect ( ) ;
4749+ assert_ne ! ( round_0_inputs, round_1_inputs, "Rounds must use different UTXOs" ) ;
4750+
4751+ complete_rbf_handshake ( & nodes[ 0 ] , & nodes[ 1 ] ) ;
4752+
4753+ complete_interactive_funding_negotiation (
4754+ & nodes[ 0 ] ,
4755+ & nodes[ 1 ] ,
4756+ channel_id,
4757+ funding_contribution,
4758+ new_funding_script. clone ( ) ,
4759+ ) ;
4760+
4761+ let ( rbf_tx, splice_locked) = sign_interactive_funding_tx ( & nodes[ 0 ] , & nodes[ 1 ] , false ) ;
4762+ assert ! ( splice_locked. is_none( ) ) ;
4763+
4764+ expect_splice_pending_event ( & nodes[ 0 ] , & node_id_1) ;
4765+ expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
4766+
4767+ let result = lock_rbf_splice_after_blocks (
4768+ & nodes[ 0 ] ,
4769+ & nodes[ 1 ] ,
4770+ & rbf_tx,
4771+ ANTI_REORG_DELAY - 1 ,
4772+ & [ first_splice_tx. compute_txid ( ) ] ,
4773+ ) ;
4774+
4775+ // Node 0's round 0 inputs are NOT in the promoted tx (which uses round 1's fresh UTXOs),
4776+ // so they appear as unique contributions to discard. The splice-out output also survives
4777+ // because its script_pubkey is not in the promoted tx. The change output is filtered
4778+ // because it shares a script_pubkey with the promoted tx's change output.
4779+ assert_eq ! ( result. node_a_discarded. len( ) , 1 ) ;
4780+ let ( ref inputs, ref outputs) = result. node_a_discarded [ 0 ] ;
4781+ assert_eq ! ( * inputs, round_0_inputs) ;
4782+ assert_eq ! ( * outputs, vec![ splice_out_output] ) ;
4783+
4784+ // Node 1 (non-contributing acceptor) has no contributions to discard.
4785+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
46904786}
46914787
46924788#[ test]
@@ -5623,13 +5719,18 @@ pub fn do_test_splice_rbf_tiebreak(
56235719 expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
56245720
56255721 // Mine, lock, and verify DiscardFunding for the replaced splice candidate.
5626- lock_rbf_splice_after_blocks (
5722+ let result = lock_rbf_splice_after_blocks (
56275723 & nodes[ 0 ] ,
56285724 & nodes[ 1 ] ,
56295725 & rbf_tx,
56305726 ANTI_REORG_DELAY - 1 ,
56315727 & [ first_splice_tx. compute_txid ( ) ] ,
56325728 ) ;
5729+
5730+ // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs
5731+ // are in the promoted tx and nothing is unique to discard.
5732+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
5733+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
56335734 } else {
56345735 // Acceptor does not contribute — complete with only node 0's inputs/outputs.
56355736 complete_interactive_funding_negotiation_for_both (
@@ -5654,14 +5755,14 @@ pub fn do_test_splice_rbf_tiebreak(
56545755 // Mine, lock, and verify DiscardFunding for the replaced splice candidate.
56555756 // Node 1's QuiescentAction was preserved, so after splice_locked it re-initiates
56565757 // quiescence to retry its contribution in a future splice.
5657- let node_b_stfu = lock_rbf_splice_after_blocks (
5758+ let result = lock_rbf_splice_after_blocks (
56585759 & nodes[ 0 ] ,
56595760 & nodes[ 1 ] ,
56605761 & rbf_tx,
56615762 ANTI_REORG_DELAY - 1 ,
56625763 & [ first_splice_tx. compute_txid ( ) ] ,
56635764 ) ;
5664- let stfu_1 = if let Some ( MessageSendEvent :: SendStfu { msg, .. } ) = node_b_stfu {
5765+ let stfu_1 = if let Some ( MessageSendEvent :: SendStfu { msg, .. } ) = result . stfu {
56655766 msg
56665767 } else {
56675768 panic ! ( "Expected SendStfu from node 1" ) ;
@@ -5935,13 +6036,18 @@ fn test_splice_rbf_acceptor_recontributes() {
59356036 expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
59366037
59376038 // Step 12: Mine, lock, and verify DiscardFunding for the replaced splice candidate.
5938- lock_rbf_splice_after_blocks (
6039+ let result = lock_rbf_splice_after_blocks (
59396040 & nodes[ 0 ] ,
59406041 & nodes[ 1 ] ,
59416042 & rbf_tx,
59426043 ANTI_REORG_DELAY - 1 ,
59436044 & [ first_splice_tx. compute_txid ( ) ] ,
59446045 ) ;
6046+
6047+ // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs
6048+ // are in the promoted tx and nothing is unique to discard.
6049+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
6050+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
59456051}
59466052
59476053#[ test]
@@ -6260,13 +6366,18 @@ fn test_splice_rbf_sequential() {
62606366 // --- Mine and lock the final RBF, verifying DiscardFunding for both replaced candidates. ---
62616367 let splice_tx_0_txid = splice_tx_0. compute_txid ( ) ;
62626368 let splice_tx_1_txid = splice_tx_1. compute_txid ( ) ;
6263- lock_rbf_splice_after_blocks (
6369+ let result = lock_rbf_splice_after_blocks (
62646370 & nodes[ 0 ] ,
62656371 & nodes[ 1 ] ,
62666372 & rbf_tx_final,
62676373 ANTI_REORG_DELAY - 1 ,
62686374 & [ splice_tx_0_txid, splice_tx_1_txid] ,
62696375 ) ;
6376+
6377+ // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs
6378+ // are in the promoted tx and nothing is unique to discard.
6379+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
6380+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
62706381}
62716382
62726383#[ test]
@@ -6856,7 +6967,7 @@ fn test_funding_contributed_rbf_adjustment_exceeds_max_feerate() {
68566967 // Mine and lock the pending splice → pending_splice is cleared.
68576968 mine_transaction ( & nodes[ 0 ] , & _splice_tx) ;
68586969 mine_transaction ( & nodes[ 1 ] , & _splice_tx) ;
6859- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
6970+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
68606971
68616972 // STFU is sent during lock — the splice proceeds as a fresh splice (not RBF).
68626973 let stfu = match stfu {
@@ -6933,7 +7044,7 @@ fn test_funding_contributed_rbf_adjustment_insufficient_budget() {
69337044 // Mine and lock the pending splice → pending_splice is cleared.
69347045 mine_transaction ( & nodes[ 0 ] , & _splice_tx) ;
69357046 mine_transaction ( & nodes[ 1 ] , & _splice_tx) ;
6936- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
7047+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
69377048
69387049 // STFU is sent during lock — the splice proceeds as a fresh splice (not RBF).
69397050 let stfu = match stfu {
0 commit comments