@@ -1734,7 +1734,9 @@ async fn run_rbf_splice_channel_test(confirm_original: bool) {
17341734 } ,
17351735 }
17361736 assert_eq ! ( payment. status, PaymentStatus :: Pending ) ;
1737- // Only one Onchain Pending payment for this splice attempt (not one per candidate).
1737+ // Only one Onchain Pending payment for this splice attempt (not one per candidate). This also
1738+ // guards the intent-clobber fix: had the sync above cleared this splice's live intent, the
1739+ // bump would not have found it and would have minted a second record under a fresh PaymentId.
17381740 let splice_payments = node_b. list_payments_with_filter ( |p| {
17391741 p. direction == PaymentDirection :: Outbound
17401742 && matches ! ( p. kind, PaymentKind :: Onchain { .. } )
@@ -2034,8 +2036,9 @@ async fn rbf_splice_payment_reverts_after_deep_reorg() {
20342036
20352037 let rbf_payment_id = PaymentId ( rbf_txo. txid . to_byte_array ( ) ) ;
20362038
2037- // Graduated: anchored to the original candidate's id, stamped with the confirmed RBF
2038- // candidate's txid, with no separate record under the RBF candidate's id.
2039+ // Graduated: keyed by the splice-time-generated PaymentId (located here via the confirmed RBF
2040+ // candidate's txid it is stamped with), with no separate record under the RBF candidate's
2041+ // txid-derived id.
20392042 let payment = funding_payment ( & node_b, rbf_txo. txid ) ;
20402043 assert_eq ! ( payment. status, PaymentStatus :: Succeeded ) ;
20412044 assert ! ( matches!(
@@ -2111,6 +2114,201 @@ async fn rbf_splice_payment_reverts_after_deep_reorg() {
21112114 node_b. stop ( ) . unwrap ( ) ;
21122115}
21132116
2117+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2118+ async fn splice_resumed_after_restart ( ) {
2119+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
2120+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2121+
2122+ // Set up node_a manually so it can be restarted with the same config.
2123+ let mut config_a = random_config ( true ) ;
2124+ config_a. store_type = TestStoreType :: Sqlite ;
2125+ let config_b = random_config ( true ) ;
2126+ let node_b = setup_node ( & chain_source, config_b) ;
2127+
2128+ let onchain_balance_before_sat = {
2129+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2130+
2131+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2132+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2133+ let premine_amount_sat = 5_000_000 ;
2134+ premine_and_distribute_funds (
2135+ & bitcoind. client ,
2136+ & electrsd. client ,
2137+ vec ! [ address_a, address_b] ,
2138+ Amount :: from_sat ( premine_amount_sat) ,
2139+ )
2140+ . await ;
2141+
2142+ node_a. sync_wallets ( ) . unwrap ( ) ;
2143+ node_b. sync_wallets ( ) . unwrap ( ) ;
2144+
2145+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
2146+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2147+ node_a. sync_wallets ( ) . unwrap ( ) ;
2148+ node_b. sync_wallets ( ) . unwrap ( ) ;
2149+
2150+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2151+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2152+
2153+ // Initiate a splice-out while disconnected: LDK accepts the contribution but cannot make
2154+ // progress before the restart below drops it, having neither negotiated nor persisted
2155+ // anything. Only the persisted splice intent allows resuming the splice.
2156+ node_a. disconnect ( node_b. node_id ( ) ) . unwrap ( ) ;
2157+ let address = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2158+ node_a. splice_out ( & user_channel_id_a, node_b. node_id ( ) , & address, 500_000 ) . unwrap ( ) ;
2159+
2160+ let onchain_balance_before_sat = node_a. list_balances ( ) . total_onchain_balance_sats ;
2161+ node_a. stop ( ) . unwrap ( ) ;
2162+ onchain_balance_before_sat
2163+ } ;
2164+
2165+ // On restart, the reconciler resubmits the splice, which proceeds once the peer connects.
2166+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2167+ node_a. sync_wallets ( ) . unwrap ( ) ;
2168+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
2169+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
2170+
2171+ let txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2172+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2173+
2174+ wait_for_tx ( & electrsd. client , txo. txid ) . await ;
2175+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2176+ node_a. sync_wallets ( ) . unwrap ( ) ;
2177+ node_b. sync_wallets ( ) . unwrap ( ) ;
2178+
2179+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2180+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2181+
2182+ assert ! (
2183+ node_a. list_balances( ) . total_onchain_balance_sats > onchain_balance_before_sat + 400_000 ,
2184+ "resumed splice-out should have moved ~500k sats to the on-chain balance" ,
2185+ ) ;
2186+
2187+ // The locked splice cleared the intent, so another restart must not resubmit it.
2188+ node_a. stop ( ) . unwrap ( ) ;
2189+ let node_a = setup_node ( & chain_source, config_a) ;
2190+ node_a. sync_wallets ( ) . unwrap ( ) ;
2191+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
2192+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2193+ assert ! ( node_a. next_event( ) . is_none( ) , "completed splice should not be resubmitted" ) ;
2194+
2195+ node_a. stop ( ) . unwrap ( ) ;
2196+ node_b. stop ( ) . unwrap ( ) ;
2197+ }
2198+
2199+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2200+ async fn splice_rbf_resumed_after_restart ( ) {
2201+ // Use a custom bitcoind config with a lower incrementalrelayfee so that the +25 sat/kwu
2202+ // (0.1 sat/vB) RBF feerate bump satisfies BIP125's absolute fee increase requirement.
2203+ let bitcoind_exe = std:: env:: var ( "BITCOIND_EXE" )
2204+ . ok ( )
2205+ . or_else ( || corepc_node:: downloaded_exe_path ( ) . ok ( ) )
2206+ . expect (
2207+ "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature" ,
2208+ ) ;
2209+ let mut bitcoind_conf = corepc_node:: Conf :: default ( ) ;
2210+ bitcoind_conf. network = "regtest" ;
2211+ bitcoind_conf. args . push ( "-rest" ) ;
2212+ bitcoind_conf. args . push ( "-incrementalrelayfee=0.00000100" ) ;
2213+ let bitcoind = BitcoinD :: with_conf ( bitcoind_exe, & bitcoind_conf) . unwrap ( ) ;
2214+
2215+ let electrs_exe = std:: env:: var ( "ELECTRS_EXE" )
2216+ . ok ( )
2217+ . or_else ( electrsd:: downloaded_exe_path)
2218+ . expect ( "you need to provide env var ELECTRS_EXE or specify an electrsd version feature" ) ;
2219+ let mut electrsd_conf = electrsd:: Conf :: default ( ) ;
2220+ electrsd_conf. http_enabled = true ;
2221+ electrsd_conf. network = "regtest" ;
2222+ let electrsd = ElectrsD :: with_conf ( electrs_exe, & bitcoind, & electrsd_conf) . unwrap ( ) ;
2223+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2224+
2225+ // Set up node_a manually so it can be restarted with the same config.
2226+ let mut config_a = random_config ( true ) ;
2227+ config_a. store_type = TestStoreType :: Sqlite ;
2228+ let config_b = random_config ( true ) ;
2229+ let node_b = setup_node ( & chain_source, config_b) ;
2230+
2231+ let original_txo = {
2232+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2233+
2234+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2235+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2236+ let premine_amount_sat = 5_000_000 ;
2237+ premine_and_distribute_funds (
2238+ & bitcoind. client ,
2239+ & electrsd. client ,
2240+ vec ! [ address_a, address_b] ,
2241+ Amount :: from_sat ( premine_amount_sat) ,
2242+ )
2243+ . await ;
2244+
2245+ node_a. sync_wallets ( ) . unwrap ( ) ;
2246+ node_b. sync_wallets ( ) . unwrap ( ) ;
2247+
2248+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
2249+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2250+ node_a. sync_wallets ( ) . unwrap ( ) ;
2251+ node_b. sync_wallets ( ) . unwrap ( ) ;
2252+
2253+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2254+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2255+
2256+ // Negotiate a splice but leave its transaction unconfirmed so it can be fee-bumped.
2257+ node_a. splice_in ( & user_channel_id_a, node_b. node_id ( ) , 500_000 ) . unwrap ( ) ;
2258+ let original_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2259+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2260+ wait_for_tx ( & electrsd. client , original_txo. txid ) . await ;
2261+ node_a. sync_wallets ( ) . unwrap ( ) ;
2262+ node_b. sync_wallets ( ) . unwrap ( ) ;
2263+
2264+ // Bump the fee while disconnected and restart before anything could be negotiated: only
2265+ // the persisted intent knows about the fee bump, while LDK still has the negotiated
2266+ // splice at the original feerate.
2267+ node_a. disconnect ( node_b. node_id ( ) ) . unwrap ( ) ;
2268+ node_a. bump_channel_funding_fee ( & user_channel_id_a, node_b. node_id ( ) ) . unwrap ( ) ;
2269+ node_a. stop ( ) . unwrap ( ) ;
2270+ original_txo
2271+ } ;
2272+
2273+ // On restart, the reconciler sees that the negotiated splice is still at a lower feerate
2274+ // than the persisted fee-bump intent and resubmits the bump.
2275+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2276+ node_a. sync_wallets ( ) . unwrap ( ) ;
2277+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
2278+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
2279+
2280+ let rbf_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2281+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2282+ assert_ne ! ( original_txo, rbf_txo, "resubmitted RBF should produce a different funding txo" ) ;
2283+
2284+ // Restarting again must not resubmit the bump: the negotiated splice now carries it.
2285+ node_a. stop ( ) . unwrap ( ) ;
2286+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2287+ node_a. sync_wallets ( ) . unwrap ( ) ;
2288+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
2289+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2290+ assert ! ( node_a. next_event( ) . is_none( ) , "carried-out fee bump should not be resubmitted" ) ;
2291+
2292+ wait_for_tx ( & electrsd. client , rbf_txo. txid ) . await ;
2293+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2294+ node_a. sync_wallets ( ) . unwrap ( ) ;
2295+ node_b. sync_wallets ( ) . unwrap ( ) ;
2296+
2297+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2298+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2299+
2300+ // The locked fee bump cleared its intent, so a further restart must not resubmit it.
2301+ node_a. stop ( ) . unwrap ( ) ;
2302+ let node_a = setup_node ( & chain_source, config_a) ;
2303+ node_a. sync_wallets ( ) . unwrap ( ) ;
2304+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
2305+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2306+ assert ! ( node_a. next_event( ) . is_none( ) , "locked fee bump should not be resubmitted" ) ;
2307+
2308+ node_a. stop ( ) . unwrap ( ) ;
2309+ node_b. stop ( ) . unwrap ( ) ;
2310+ }
2311+
21142312#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
21152313async fn splice_in_rbf_joins_counterparty_splice ( ) {
21162314 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments