@@ -2150,6 +2150,129 @@ async fn splice_channel() {
21502150 ) ;
21512151}
21522152
2153+ /// A splice on a 0conf channel locks before its funding transaction confirms: LDK promotes the
2154+ /// new funding immediately and then re-broadcasts the still-unconfirmed transaction — re-typed as
2155+ /// a generic funding transaction with wallet-view figures and no contribution data — on every
2156+ /// monitor-update completion until it confirms. The re-broadcasts must neither disturb the
2157+ /// contribution-derived record on the contributing side nor mint spurious records on either side.
2158+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2159+ async fn zero_conf_splice_survives_funding_rebroadcasts ( ) {
2160+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
2161+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2162+
2163+ // Node A's log collector synchronizes the record checks below with the re-broadcasts.
2164+ // `setup_two_nodes` wires file loggers, so build the pair manually with a collector, Node B
2165+ // trusting Node A for 0conf so channels and splices lock without confirmations.
2166+ let logger_a = Arc :: new ( CollectingLogWriter :: new ( ) ) ;
2167+ let mut config_a = random_config ( ) ;
2168+ config_a. log_writer = TestLogWriter :: Custom ( logger_a. clone ( ) ) ;
2169+ let node_a = setup_node ( & chain_source, config_a) ;
2170+
2171+ let mut config_b = random_config ( ) ;
2172+ config_b. node_config . trusted_peers_0conf . push ( node_a. node_id ( ) ) ;
2173+ let node_b = setup_node ( & chain_source, config_b) ;
2174+
2175+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2176+ let premine_amount_sat = 5_000_000 ;
2177+ premine_and_distribute_funds (
2178+ & bitcoind. client ,
2179+ & electrsd. client ,
2180+ vec ! [ address_a] ,
2181+ Amount :: from_sat ( premine_amount_sat) ,
2182+ )
2183+ . await ;
2184+ node_a. sync_wallets ( ) . unwrap ( ) ;
2185+
2186+ open_channel ( & node_a, & node_b, 2_000_000 , false , & electrsd) . await ;
2187+
2188+ // 0conf: the channel is ready without any confirmations.
2189+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2190+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2191+
2192+ // Confirm the original funding so the splice below is the only unconfirmed funding and Node
2193+ // A's change from the open is spendable for the splice contribution.
2194+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2195+ node_a. sync_wallets ( ) . unwrap ( ) ;
2196+ node_b. sync_wallets ( ) . unwrap ( ) ;
2197+
2198+ node_a. splice_in ( & user_channel_id_a, node_b. node_id ( ) , 1_000_000 ) . unwrap ( ) ;
2199+ let txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2200+ wait_for_classified_funding_payment ( & node_a, txo. txid ) . await ;
2201+
2202+ // The 0conf splice locks without confirmations, re-signaled as `ChannelReady`.
2203+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2204+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2205+
2206+ let payment = funding_payment ( & node_a, txo. txid ) ;
2207+ let recorded_amount_msat = payment. amount_msat ;
2208+ let recorded_fee_paid_msat = payment. fee_paid_msat ;
2209+ assert ! ( matches!(
2210+ payment. kind,
2211+ PaymentKind :: Onchain { tx_type: Some ( TransactionType :: InteractiveFunding { .. } ) , .. }
2212+ ) ) ;
2213+
2214+ // Locking the splice completed monitor updates that re-offered the unconfirmed funding
2215+ // transaction; a payment drives further monitor updates and thus further re-broadcasts.
2216+ let amount_msat = 1_000_000 ;
2217+ let payment_id =
2218+ node_a. spontaneous_payment ( ) . send ( amount_msat, node_b. node_id ( ) , None ) . unwrap ( ) ;
2219+ expect_payment_successful_event ! ( node_a, payment_id, None ) ;
2220+ expect_payment_received_event ! ( node_b, amount_msat) ;
2221+
2222+ // Wait until the classification pipeline has demonstrably processed a re-offer against the
2223+ // interactive-funding record. The broadcast loop classifies sequentially, so by the second
2224+ // arrival the first re-offer's store write has completed and the checks below are
2225+ // deterministic rather than racing the queue.
2226+ let rebroadcast = format ! ( "funding-typed rebroadcast {}" , txo. txid) ;
2227+ assert ! (
2228+ logger_a. wait_for_count( & rebroadcast, 2 ) . await ,
2229+ "no funding re-broadcast reached Node A's classification"
2230+ ) ;
2231+
2232+ let splice_payments = |node : & Node | {
2233+ node. list_payments_with_filter (
2234+ |p| matches ! ( p. kind, PaymentKind :: Onchain { txid, .. } if txid == txo. txid) ,
2235+ )
2236+ } ;
2237+
2238+ // The record must keep the splice-time id, classification, and contribution-derived figures
2239+ // through the re-broadcasts.
2240+ let payments = splice_payments ( & node_a) ;
2241+ assert_eq ! ( payments. len( ) , 1 ) ;
2242+ let payment = & payments[ 0 ] ;
2243+ assert_eq ! ( payment. amount_msat, recorded_amount_msat) ;
2244+ assert_eq ! ( payment. fee_paid_msat, recorded_fee_paid_msat) ;
2245+ assert ! ( matches!(
2246+ payment. kind,
2247+ PaymentKind :: Onchain { tx_type: Some ( TransactionType :: InteractiveFunding { .. } ) , .. }
2248+ ) ) ;
2249+
2250+ // Node B contributed nothing and its wallet sees no activity in the splice; the
2251+ // re-broadcasts must not mint a spurious zero-amount record for it.
2252+ assert ! ( splice_payments( & node_b) . is_empty( ) ) ;
2253+
2254+ // Confirmation and graduation must land on that same record.
2255+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2256+ node_a. sync_wallets ( ) . unwrap ( ) ;
2257+ node_b. sync_wallets ( ) . unwrap ( ) ;
2258+
2259+ let payments = splice_payments ( & node_a) ;
2260+ assert_eq ! ( payments. len( ) , 1 ) ;
2261+ let payment = & payments[ 0 ] ;
2262+ assert_eq ! ( payment. status, PaymentStatus :: Succeeded ) ;
2263+ assert_eq ! ( payment. amount_msat, recorded_amount_msat) ;
2264+ assert_eq ! ( payment. fee_paid_msat, recorded_fee_paid_msat) ;
2265+ assert ! ( matches!(
2266+ payment. kind,
2267+ PaymentKind :: Onchain {
2268+ status: ConfirmationStatus :: Confirmed { .. } ,
2269+ tx_type: Some ( TransactionType :: InteractiveFunding { .. } ) ,
2270+ ..
2271+ }
2272+ ) ) ;
2273+ assert ! ( splice_payments( & node_b) . is_empty( ) ) ;
2274+ }
2275+
21532276/// Canary for the upstream behavior the zero-activity skip in `classify_funding` works around:
21542277/// after a 0conf splice is promoted, LDK re-broadcasts the still-unconfirmed funding transaction
21552278/// through its generic funding path — re-typed as a plain funding transaction without its
0 commit comments