@@ -186,24 +186,42 @@ impl BroadcasterInterface for TestBroadcaster {
186186struct ChainState {
187187 blocks : Vec < ( Header , Vec < Transaction > ) > ,
188188 confirmed_txids : HashSet < Txid > ,
189+ /// Unconfirmed transactions (e.g., splice txs). Conflicting RBF candidates may coexist;
190+ /// `confirm_pending_txs` determines which one confirms.
191+ pending_txs : Vec < ( Txid , Transaction ) > ,
189192}
190193
191194impl ChainState {
192195 fn new ( ) -> Self {
193196 let genesis_hash = genesis_block ( Network :: Bitcoin ) . block_hash ( ) ;
194197 let genesis_header = create_dummy_header ( genesis_hash, 42 ) ;
195- Self { blocks : vec ! [ ( genesis_header, Vec :: new( ) ) ] , confirmed_txids : HashSet :: new ( ) }
198+ Self {
199+ blocks : vec ! [ ( genesis_header, Vec :: new( ) ) ] ,
200+ confirmed_txids : HashSet :: new ( ) ,
201+ pending_txs : Vec :: new ( ) ,
202+ }
196203 }
197204
198205 fn tip_height ( & self ) -> u32 {
199206 ( self . blocks . len ( ) - 1 ) as u32
200207 }
201208
209+ fn is_outpoint_spent ( & self , outpoint : & bitcoin:: OutPoint ) -> bool {
210+ self . blocks . iter ( ) . any ( |( _, txs) | {
211+ txs. iter ( ) . any ( |tx| {
212+ tx. input . iter ( ) . any ( |input| input. previous_output == * outpoint)
213+ } )
214+ } )
215+ }
216+
202217 fn confirm_tx ( & mut self , tx : Transaction ) -> bool {
203218 let txid = tx. compute_txid ( ) ;
204219 if self . confirmed_txids . contains ( & txid) {
205220 return false ;
206221 }
222+ if tx. input . iter ( ) . any ( |input| self . is_outpoint_spent ( & input. previous_output ) ) {
223+ return false ;
224+ }
207225 self . confirmed_txids . insert ( txid) ;
208226
209227 let prev_hash = self . blocks . last ( ) . unwrap ( ) . 0 . block_hash ( ) ;
@@ -218,6 +236,53 @@ impl ChainState {
218236 true
219237 }
220238
239+ /// Add a transaction to the pending pool (mempool). Multiple conflicting transactions (RBF
240+ /// candidates) may coexist; `confirm_pending_txs` selects which one to confirm.
241+ fn add_pending_tx ( & mut self , tx : Transaction ) {
242+ self . pending_txs . push ( ( tx. compute_txid ( ) , tx) ) ;
243+ }
244+
245+ /// Confirm pending transactions in a single block, selecting deterministically among
246+ /// conflicting RBF candidates. Sorting by txid ensures the winner is determined by fuzz input
247+ /// content. Transactions that double-spend an already-confirmed outpoint are skipped.
248+ fn confirm_pending_txs ( & mut self ) {
249+ let mut txs = std:: mem:: take ( & mut self . pending_txs ) ;
250+ txs. sort_by_key ( |( txid, _) | * txid) ;
251+
252+ let mut confirmed = Vec :: new ( ) ;
253+ let mut spent_outpoints = Vec :: new ( ) ;
254+ for ( txid, tx) in txs {
255+ if self . confirmed_txids . contains ( & txid) {
256+ continue ;
257+ }
258+ if tx. input . iter ( ) . any ( |input| {
259+ self . is_outpoint_spent ( & input. previous_output )
260+ || spent_outpoints. contains ( & input. previous_output )
261+ } ) {
262+ continue ;
263+ }
264+ self . confirmed_txids . insert ( txid) ;
265+ for input in & tx. input {
266+ spent_outpoints. push ( input. previous_output ) ;
267+ }
268+ confirmed. push ( tx) ;
269+ }
270+
271+ if confirmed. is_empty ( ) {
272+ return ;
273+ }
274+
275+ let prev_hash = self . blocks . last ( ) . unwrap ( ) . 0 . block_hash ( ) ;
276+ let header = create_dummy_header ( prev_hash, 42 ) ;
277+ self . blocks . push ( ( header, confirmed) ) ;
278+
279+ for _ in 0 ..5 {
280+ let prev_hash = self . blocks . last ( ) . unwrap ( ) . 0 . block_hash ( ) ;
281+ let header = create_dummy_header ( prev_hash, 42 ) ;
282+ self . blocks . push ( ( header, Vec :: new ( ) ) ) ;
283+ }
284+ }
285+
221286 fn block_at ( & self , height : u32 ) -> & ( Header , Vec < Transaction > ) {
222287 & self . blocks [ height as usize ]
223288 }
@@ -862,11 +927,15 @@ fn send_mpp_hop_payment(
862927fn assert_action_timeout_awaiting_response ( action : & msgs:: ErrorAction ) {
863928 // Since sending/receiving messages may be delayed, `timer_tick_occurred` may cause a node to
864929 // disconnect their counterparty if they're expecting a timely response.
865- assert ! ( matches!(
930+ assert ! (
931+ matches!(
932+ action,
933+ msgs:: ErrorAction :: DisconnectPeerWithWarning { msg }
934+ if msg. data. contains( "Disconnecting due to timeout awaiting response" )
935+ ) ,
936+ "Expected timeout disconnect, got: {:?}" ,
866937 action,
867- msgs:: ErrorAction :: DisconnectPeerWithWarning { msg }
868- if msg. data. contains( "Disconnecting due to timeout awaiting response" )
869- ) ) ;
938+ ) ;
870939}
871940
872941enum ChanType {
@@ -2033,15 +2102,16 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
20332102 assert!( txs. len( ) >= 1 ) ;
20342103 let splice_tx = txs. remove( 0 ) ;
20352104 assert_eq!( new_funding_txo. txid, splice_tx. compute_txid( ) ) ;
2036- chain_state. confirm_tx ( splice_tx) ;
2105+ chain_state. add_pending_tx ( splice_tx) ;
20372106 } ,
20382107 events:: Event :: SpliceFailed { .. } => { } ,
20392108 events:: Event :: DiscardFunding {
2040- funding_info: events:: FundingInfo :: Contribution { .. } ,
2109+ funding_info: events:: FundingInfo :: Contribution { .. }
2110+ | events:: FundingInfo :: Tx { .. } ,
20412111 ..
20422112 } => { } ,
20432113
2044- _ => panic!( "Unhandled event" ) ,
2114+ _ => panic!( "Unhandled event: {:?}" , event ) ,
20452115 }
20462116 }
20472117 while nodes[ $node] . needs_pending_htlc_processing( ) {
@@ -2505,13 +2575,31 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
25052575 } ,
25062576
25072577 // Sync node by 1 block to cover confirmation of a transaction.
2508- 0xa8 => sync_with_chain_state ( & mut chain_state, & nodes[ 0 ] , & mut node_height_a, Some ( 1 ) ) ,
2509- 0xa9 => sync_with_chain_state ( & mut chain_state, & nodes[ 1 ] , & mut node_height_b, Some ( 1 ) ) ,
2510- 0xaa => sync_with_chain_state ( & mut chain_state, & nodes[ 2 ] , & mut node_height_c, Some ( 1 ) ) ,
2578+ 0xa8 => {
2579+ chain_state. confirm_pending_txs ( ) ;
2580+ sync_with_chain_state ( & mut chain_state, & nodes[ 0 ] , & mut node_height_a, Some ( 1 ) ) ;
2581+ } ,
2582+ 0xa9 => {
2583+ chain_state. confirm_pending_txs ( ) ;
2584+ sync_with_chain_state ( & mut chain_state, & nodes[ 1 ] , & mut node_height_b, Some ( 1 ) ) ;
2585+ } ,
2586+ 0xaa => {
2587+ chain_state. confirm_pending_txs ( ) ;
2588+ sync_with_chain_state ( & mut chain_state, & nodes[ 2 ] , & mut node_height_c, Some ( 1 ) ) ;
2589+ } ,
25112590 // Sync node to chain tip to cover confirmation of a transaction post-reorg-risk.
2512- 0xab => sync_with_chain_state ( & mut chain_state, & nodes[ 0 ] , & mut node_height_a, None ) ,
2513- 0xac => sync_with_chain_state ( & mut chain_state, & nodes[ 1 ] , & mut node_height_b, None ) ,
2514- 0xad => sync_with_chain_state ( & mut chain_state, & nodes[ 2 ] , & mut node_height_c, None ) ,
2591+ 0xab => {
2592+ chain_state. confirm_pending_txs ( ) ;
2593+ sync_with_chain_state ( & mut chain_state, & nodes[ 0 ] , & mut node_height_a, None ) ;
2594+ } ,
2595+ 0xac => {
2596+ chain_state. confirm_pending_txs ( ) ;
2597+ sync_with_chain_state ( & mut chain_state, & nodes[ 1 ] , & mut node_height_b, None ) ;
2598+ } ,
2599+ 0xad => {
2600+ chain_state. confirm_pending_txs ( ) ;
2601+ sync_with_chain_state ( & mut chain_state, & nodes[ 2 ] , & mut node_height_c, None ) ;
2602+ } ,
25152603
25162604 0xb0 | 0xb1 | 0xb2 => {
25172605 // Restart node A, picking among the in-flight `ChannelMonitor`s to use based on
0 commit comments