@@ -652,9 +652,15 @@ pub fn splice_channel<'a, 'b, 'c, 'd>(
652652 ( splice_tx, new_funding_script)
653653}
654654
655+ pub struct SpliceLockedResult {
656+ pub stfu : Option < MessageSendEvent > ,
657+ pub node_a_discarded : Vec < ( Vec < bitcoin:: OutPoint > , Vec < TxOut > ) > ,
658+ pub node_b_discarded : Vec < ( Vec < bitcoin:: OutPoint > , Vec < TxOut > ) > ,
659+ }
660+
655661pub fn lock_splice_after_blocks < ' a , ' b , ' c , ' d > (
656662 node_a : & ' a Node < ' b , ' c , ' d > , node_b : & ' a Node < ' b , ' c , ' d > , num_blocks : u32 ,
657- ) -> Option < MessageSendEvent > {
663+ ) -> SpliceLockedResult {
658664 connect_blocks ( node_a, num_blocks) ;
659665 connect_blocks ( node_b, num_blocks) ;
660666
@@ -667,7 +673,7 @@ pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>(
667673pub fn lock_splice < ' a , ' b , ' c , ' d > (
668674 node_a : & ' a Node < ' b , ' c , ' d > , node_b : & ' a Node < ' b , ' c , ' d > ,
669675 splice_locked_for_node_b : & msgs:: SpliceLocked , is_0conf : bool , expected_discard_txids : & [ Txid ] ,
670- ) -> Option < MessageSendEvent > {
676+ ) -> SpliceLockedResult {
671677 let prev_funding_txid = node_a
672678 . chain_monitor
673679 . chain_monitor
@@ -704,29 +710,23 @@ pub fn lock_splice<'a, 'b, 'c, 'd>(
704710 }
705711 }
706712
707- let mut all_discard_txids = Vec :: new ( ) ;
708- let expected_num_events = 1 + expected_discard_txids . len ( ) ;
709- for node in [ node_a, node_b] {
713+ let mut node_a_discarded = Vec :: new ( ) ;
714+ let mut node_b_discarded = Vec :: new ( ) ;
715+ for ( idx , node) in [ node_a, node_b] . into_iter ( ) . enumerate ( ) {
710716 let events = node. node . get_and_clear_pending_events ( ) ;
711- assert_eq ! ( events. len ( ) , expected_num_events , " {events:?}") ;
717+ assert ! ( ! events. is_empty ( ) , "Expected at least ChannelReady, got {events:?}") ;
712718 assert ! ( matches!( events[ 0 ] , Event :: ChannelReady { .. } ) ) ;
713- let discard_txids: Vec < _ > = events[ 1 ..]
714- . iter ( )
715- . map ( |e| match e {
716- Event :: DiscardFunding { funding_info : FundingInfo :: Tx { transaction } , .. } => {
717- transaction. compute_txid ( )
718- } ,
719+ let discarded = if idx == 0 { & mut node_a_discarded } else { & mut node_b_discarded } ;
720+ for event in & events[ 1 ..] {
721+ match event {
719722 Event :: DiscardFunding {
720- funding_info : FundingInfo :: OutPoint { outpoint } , ..
721- } => outpoint. txid ,
722- other => panic ! ( "Expected DiscardFunding, got {:?}" , other) ,
723- } )
724- . collect ( ) ;
725- for txid in expected_discard_txids {
726- assert ! ( discard_txids. contains( txid) , "Missing DiscardFunding for txid {}" , txid) ;
727- }
728- if all_discard_txids. is_empty ( ) {
729- all_discard_txids = discard_txids;
723+ funding_info : FundingInfo :: Contribution { inputs, outputs } ,
724+ ..
725+ } => {
726+ discarded. push ( ( inputs. clone ( ) , outputs. clone ( ) ) ) ;
727+ } ,
728+ other => panic ! ( "Expected DiscardFunding with Contribution, got {:?}" , other) ,
729+ }
730730 }
731731 check_added_monitors ( node, 1 ) ;
732732 }
@@ -764,18 +764,18 @@ pub fn lock_splice<'a, 'b, 'c, 'd>(
764764 // old funding as it is no longer being tracked.
765765 for node in [ node_a, node_b] {
766766 node. chain_source . remove_watched_by_txid ( prev_funding_txid) ;
767- for txid in & all_discard_txids {
767+ for txid in expected_discard_txids {
768768 node. chain_source . remove_watched_by_txid ( * txid) ;
769769 }
770770 }
771771
772- node_a_stfu. or ( node_b_stfu)
772+ SpliceLockedResult { stfu : node_a_stfu. or ( node_b_stfu) , node_a_discarded , node_b_discarded }
773773}
774774
775775pub fn lock_rbf_splice_after_blocks < ' a , ' b , ' c , ' d > (
776776 node_a : & ' a Node < ' b , ' c , ' d > , node_b : & ' a Node < ' b , ' c , ' d > , tx : & Transaction , num_blocks : u32 ,
777777 expected_discard_txids : & [ Txid ] ,
778- ) -> Option < MessageSendEvent > {
778+ ) -> SpliceLockedResult {
779779 mine_transaction ( node_a, tx) ;
780780 mine_transaction ( node_b, tx) ;
781781
@@ -1366,7 +1366,7 @@ fn fails_initiating_concurrent_splices(reconnect: bool) {
13661366
13671367 mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
13681368 mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
1369- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
1369+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
13701370 // Node 0 had called splice_channel (line above) but never funding_contributed, so no stfu
13711371 // is expected from node 0 at this point.
13721372 assert ! ( stfu. is_none( ) ) ;
@@ -1394,7 +1394,7 @@ fn test_initiating_splice_holds_stfu_with_pending_splice() {
13941394 // Mine and lock the splice.
13951395 mine_transaction ( & nodes[ 0 ] , & splice_tx) ;
13961396 mine_transaction ( & nodes[ 1 ] , & splice_tx) ;
1397- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , 5 ) ;
1397+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , 5 ) . stfu ;
13981398 assert ! ( stfu. is_none( ) ) ;
13991399}
14001400
@@ -1630,7 +1630,7 @@ fn do_test_splice_tiebreak(
16301630 mine_transaction ( & nodes[ 1 ] , & tx) ;
16311631
16321632 // After splice_locked, node 1's preserved QuiescentAction triggers STFU for retry.
1633- let node_1_stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
1633+ let node_1_stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
16341634 let stfu_1 = if let Some ( MessageSendEvent :: SendStfu { msg, .. } ) = node_1_stfu {
16351635 assert ! ( msg. initiator) ;
16361636 msg
@@ -4672,13 +4672,109 @@ fn test_splice_rbf_acceptor_basic() {
46724672 expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
46734673
46744674 // Step 11: Mine, lock, and verify DiscardFunding for the replaced splice candidate.
4675- lock_rbf_splice_after_blocks (
4675+ let result = lock_rbf_splice_after_blocks (
46764676 & nodes[ 0 ] ,
46774677 & nodes[ 1 ] ,
46784678 & rbf_tx,
46794679 ANTI_REORG_DELAY - 1 ,
46804680 & [ first_splice_tx. compute_txid ( ) ] ,
46814681 ) ;
4682+
4683+ // The test wallet reuses the same UTXO across RBF rounds (the wallet doesn't track
4684+ // in-flight spends), so all contributed inputs are in the promoted tx. No unique
4685+ // contributions to discard.
4686+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
4687+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
4688+ }
4689+
4690+ #[ test]
4691+ fn test_splice_rbf_discard_unique_contribution ( ) {
4692+ // Verify that DiscardFunding events contain the correct unique inputs and outputs when the
4693+ // RBF round uses different UTXOs than the initial splice. By clearing the wallet between
4694+ // rounds and providing fresh UTXOs, we force distinct inputs per round. Round 0 also
4695+ // includes a splice-out output with a unique script_pubkey not present in the RBF tx.
4696+ // When the RBF is promoted, round 0's inputs and splice-out output should appear in
4697+ // DiscardFunding. The change output is filtered because it shares a script_pubkey with the
4698+ // promoted tx's change output.
4699+ let chanmon_cfgs = create_chanmon_cfgs ( 2 ) ;
4700+ let node_cfgs = create_node_cfgs ( 2 , & chanmon_cfgs) ;
4701+ let node_chanmgrs = create_node_chanmgrs ( 2 , & node_cfgs, & [ None , None ] ) ;
4702+ let nodes = create_network ( 2 , & node_cfgs, & node_chanmgrs) ;
4703+
4704+ let node_id_0 = nodes[ 0 ] . node . get_our_node_id ( ) ;
4705+ let node_id_1 = nodes[ 1 ] . node . get_our_node_id ( ) ;
4706+
4707+ let initial_channel_value_sat = 100_000 ;
4708+ let ( _, _, channel_id, _) =
4709+ create_announced_chan_between_nodes_with_value ( & nodes, 0 , 1 , initial_channel_value_sat, 0 ) ;
4710+
4711+ let added_value = Amount :: from_sat ( 50_000 ) ;
4712+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
4713+
4714+ // Round 0: Splice-in-and-out from node 0 with a splice-out output.
4715+ let splice_out_output = TxOut {
4716+ value : Amount :: from_sat ( 5_000 ) ,
4717+ script_pubkey : ScriptBuf :: new_p2wpkh ( & WPubkeyHash :: all_zeros ( ) ) ,
4718+ } ;
4719+ let funding_contribution = do_initiate_splice_in_and_out (
4720+ & nodes[ 0 ] ,
4721+ & nodes[ 1 ] ,
4722+ channel_id,
4723+ added_value,
4724+ vec ! [ splice_out_output. clone( ) ] ,
4725+ ) ;
4726+ let round_0_inputs: Vec < _ > = funding_contribution. contributed_inputs ( ) . collect ( ) ;
4727+ assert ! ( !round_0_inputs. is_empty( ) ) ;
4728+
4729+ let ( first_splice_tx, new_funding_script) =
4730+ splice_channel ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, funding_contribution) ;
4731+
4732+ // Clear node 0's wallet so round 1 must use different UTXOs.
4733+ nodes[ 0 ] . wallet_source . clear_utxos ( ) ;
4734+ provide_utxo_reserves ( & nodes, 2 , added_value * 2 ) ;
4735+
4736+ // Round 1: RBF with fresh UTXOs, splice-in only (no splice-out output).
4737+ let rbf_feerate = FeeRate :: from_sat_per_kwu ( FEERATE_FLOOR_SATS_PER_KW as u64 + 25 ) ;
4738+ let funding_contribution =
4739+ do_initiate_rbf_splice_in ( & nodes[ 0 ] , & nodes[ 1 ] , channel_id, added_value, rbf_feerate) ;
4740+ let round_1_inputs: Vec < _ > = funding_contribution. contributed_inputs ( ) . collect ( ) ;
4741+ assert_ne ! ( round_0_inputs, round_1_inputs, "Rounds must use different UTXOs" ) ;
4742+
4743+ complete_rbf_handshake ( & nodes[ 0 ] , & nodes[ 1 ] ) ;
4744+
4745+ complete_interactive_funding_negotiation (
4746+ & nodes[ 0 ] ,
4747+ & nodes[ 1 ] ,
4748+ channel_id,
4749+ funding_contribution,
4750+ new_funding_script. clone ( ) ,
4751+ ) ;
4752+
4753+ let ( rbf_tx, splice_locked) = sign_interactive_funding_tx ( & nodes[ 0 ] , & nodes[ 1 ] , false ) ;
4754+ assert ! ( splice_locked. is_none( ) ) ;
4755+
4756+ expect_splice_pending_event ( & nodes[ 0 ] , & node_id_1) ;
4757+ expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
4758+
4759+ let result = lock_rbf_splice_after_blocks (
4760+ & nodes[ 0 ] ,
4761+ & nodes[ 1 ] ,
4762+ & rbf_tx,
4763+ ANTI_REORG_DELAY - 1 ,
4764+ & [ first_splice_tx. compute_txid ( ) ] ,
4765+ ) ;
4766+
4767+ // Node 0's round 0 inputs are NOT in the promoted tx (which uses round 1's fresh UTXOs),
4768+ // so they appear as unique contributions to discard. The splice-out output also survives
4769+ // because its script_pubkey is not in the promoted tx. The change output is filtered
4770+ // because it shares a script_pubkey with the promoted tx's change output.
4771+ assert_eq ! ( result. node_a_discarded. len( ) , 1 ) ;
4772+ let ( ref inputs, ref outputs) = result. node_a_discarded [ 0 ] ;
4773+ assert_eq ! ( * inputs, round_0_inputs) ;
4774+ assert_eq ! ( * outputs, vec![ splice_out_output] ) ;
4775+
4776+ // Node 1 (non-contributing acceptor) has no contributions to discard.
4777+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
46824778}
46834779
46844780#[ test]
@@ -5604,13 +5700,18 @@ pub fn do_test_splice_rbf_tiebreak(
56045700 expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
56055701
56065702 // Mine, lock, and verify DiscardFunding for the replaced splice candidate.
5607- lock_rbf_splice_after_blocks (
5703+ let result = lock_rbf_splice_after_blocks (
56085704 & nodes[ 0 ] ,
56095705 & nodes[ 1 ] ,
56105706 & rbf_tx,
56115707 ANTI_REORG_DELAY - 1 ,
56125708 & [ first_splice_tx. compute_txid ( ) ] ,
56135709 ) ;
5710+
5711+ // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs
5712+ // are in the promoted tx and nothing is unique to discard.
5713+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
5714+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
56145715 } else {
56155716 // Acceptor does not contribute — complete with only node 0's inputs/outputs.
56165717 complete_interactive_funding_negotiation_for_both (
@@ -5635,14 +5736,14 @@ pub fn do_test_splice_rbf_tiebreak(
56355736 // Mine, lock, and verify DiscardFunding for the replaced splice candidate.
56365737 // Node 1's QuiescentAction was preserved, so after splice_locked it re-initiates
56375738 // quiescence to retry its contribution in a future splice.
5638- let node_b_stfu = lock_rbf_splice_after_blocks (
5739+ let result = lock_rbf_splice_after_blocks (
56395740 & nodes[ 0 ] ,
56405741 & nodes[ 1 ] ,
56415742 & rbf_tx,
56425743 ANTI_REORG_DELAY - 1 ,
56435744 & [ first_splice_tx. compute_txid ( ) ] ,
56445745 ) ;
5645- let stfu_1 = if let Some ( MessageSendEvent :: SendStfu { msg, .. } ) = node_b_stfu {
5746+ let stfu_1 = if let Some ( MessageSendEvent :: SendStfu { msg, .. } ) = result . stfu {
56465747 msg
56475748 } else {
56485749 panic ! ( "Expected SendStfu from node 1" ) ;
@@ -5916,13 +6017,18 @@ fn test_splice_rbf_acceptor_recontributes() {
59166017 expect_splice_pending_event ( & nodes[ 1 ] , & node_id_0) ;
59176018
59186019 // Step 12: Mine, lock, and verify DiscardFunding for the replaced splice candidate.
5919- lock_rbf_splice_after_blocks (
6020+ let result = lock_rbf_splice_after_blocks (
59206021 & nodes[ 0 ] ,
59216022 & nodes[ 1 ] ,
59226023 & rbf_tx,
59236024 ANTI_REORG_DELAY - 1 ,
59246025 & [ first_splice_tx. compute_txid ( ) ] ,
59256026 ) ;
6027+
6028+ // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs
6029+ // are in the promoted tx and nothing is unique to discard.
6030+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
6031+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
59266032}
59276033
59286034#[ test]
@@ -6241,13 +6347,18 @@ fn test_splice_rbf_sequential() {
62416347 // --- Mine and lock the final RBF, verifying DiscardFunding for both replaced candidates. ---
62426348 let splice_tx_0_txid = splice_tx_0. compute_txid ( ) ;
62436349 let splice_tx_1_txid = splice_tx_1. compute_txid ( ) ;
6244- lock_rbf_splice_after_blocks (
6350+ let result = lock_rbf_splice_after_blocks (
62456351 & nodes[ 0 ] ,
62466352 & nodes[ 1 ] ,
62476353 & rbf_tx_final,
62486354 ANTI_REORG_DELAY - 1 ,
62496355 & [ splice_tx_0_txid, splice_tx_1_txid] ,
62506356 ) ;
6357+
6358+ // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs
6359+ // are in the promoted tx and nothing is unique to discard.
6360+ assert ! ( result. node_a_discarded. is_empty( ) ) ;
6361+ assert ! ( result. node_b_discarded. is_empty( ) ) ;
62516362}
62526363
62536364#[ test]
@@ -6838,7 +6949,7 @@ fn test_funding_contributed_rbf_adjustment_exceeds_max_feerate() {
68386949 // Mine and lock the pending splice → pending_splice is cleared.
68396950 mine_transaction ( & nodes[ 0 ] , & _splice_tx) ;
68406951 mine_transaction ( & nodes[ 1 ] , & _splice_tx) ;
6841- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
6952+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
68426953
68436954 // STFU is sent during lock — the splice proceeds as a fresh splice (not RBF).
68446955 let stfu = match stfu {
@@ -6915,7 +7026,7 @@ fn test_funding_contributed_rbf_adjustment_insufficient_budget() {
69157026 // Mine and lock the pending splice → pending_splice is cleared.
69167027 mine_transaction ( & nodes[ 0 ] , & _splice_tx) ;
69177028 mine_transaction ( & nodes[ 1 ] , & _splice_tx) ;
6918- let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) ;
7029+ let stfu = lock_splice_after_blocks ( & nodes[ 0 ] , & nodes[ 1 ] , ANTI_REORG_DELAY - 1 ) . stfu ;
69197030
69207031 // STFU is sent during lock — the splice proceeds as a fresh splice (not RBF).
69217032 let stfu = match stfu {
0 commit comments