@@ -2003,7 +2003,9 @@ async fn run_rbf_splice_channel_test(confirm_original: bool) {
20032003 } ,
20042004 }
20052005 assert_eq ! ( payment. status, PaymentStatus :: Pending ) ;
2006- // Only one Onchain Pending payment for this splice attempt (not one per candidate).
2006+ // Only one Onchain Pending payment for this splice attempt (not one per candidate). This also
2007+ // guards the intent-clobber fix: had the sync above cleared this splice's live intent, the
2008+ // bump would not have found it and would have minted a second record under a fresh PaymentId.
20072009 let splice_payments = node_b. list_payments_with_filter ( |p| {
20082010 p. direction == PaymentDirection :: Outbound
20092011 && matches ! ( p. kind, PaymentKind :: Onchain { .. } )
@@ -2221,6 +2223,305 @@ async fn splice_payment_reorged_to_unconfirmed() {
22212223 node_b. stop ( ) . unwrap ( ) ;
22222224}
22232225
2226+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2227+ async fn splice_resumed_after_restart ( ) {
2228+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
2229+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2230+
2231+ // Set up node_a manually so it can be restarted with the same config.
2232+ let mut config_a = random_config ( ) ;
2233+ config_a. store_type = TestStoreType :: Sqlite ;
2234+ let config_b = random_config ( ) ;
2235+ let node_b = setup_node ( & chain_source, config_b) ;
2236+
2237+ let onchain_balance_before_sat = {
2238+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2239+
2240+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2241+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2242+ let premine_amount_sat = 5_000_000 ;
2243+ premine_and_distribute_funds (
2244+ & bitcoind. client ,
2245+ & electrsd. client ,
2246+ vec ! [ address_a, address_b] ,
2247+ Amount :: from_sat ( premine_amount_sat) ,
2248+ )
2249+ . await ;
2250+
2251+ node_a. sync_wallets ( ) . unwrap ( ) ;
2252+ node_b. sync_wallets ( ) . unwrap ( ) ;
2253+
2254+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
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 user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2260+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2261+
2262+ // Initiate a splice-out while disconnected: LDK accepts the contribution but cannot make
2263+ // progress before the restart below drops it, having neither negotiated nor persisted
2264+ // anything. Only the persisted splice intent allows resuming the splice.
2265+ node_a. disconnect ( node_b. node_id ( ) ) . unwrap ( ) ;
2266+ let address = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2267+ node_a. splice_out ( & user_channel_id_a, node_b. node_id ( ) , & address, 500_000 ) . unwrap ( ) ;
2268+
2269+ let onchain_balance_before_sat = node_a. list_balances ( ) . total_onchain_balance_sats ;
2270+ node_a. stop ( ) . unwrap ( ) ;
2271+ onchain_balance_before_sat
2272+ } ;
2273+
2274+ // On restart, the reconciler resubmits the splice, which proceeds once the peer connects.
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 txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2281+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2282+
2283+ wait_for_tx ( & electrsd. client , txo. txid ) . await ;
2284+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2285+ node_a. sync_wallets ( ) . unwrap ( ) ;
2286+ node_b. sync_wallets ( ) . unwrap ( ) ;
2287+
2288+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2289+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2290+
2291+ assert ! (
2292+ node_a. list_balances( ) . total_onchain_balance_sats > onchain_balance_before_sat + 400_000 ,
2293+ "resumed splice-out should have moved ~500k sats to the on-chain balance" ,
2294+ ) ;
2295+
2296+ // The locked splice cleared the intent, so another restart must not resubmit it.
2297+ node_a. stop ( ) . unwrap ( ) ;
2298+ let node_a = setup_node ( & chain_source, config_a) ;
2299+ node_a. sync_wallets ( ) . unwrap ( ) ;
2300+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
2301+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2302+ assert ! ( node_a. next_event( ) . is_none( ) , "completed splice should not be resubmitted" ) ;
2303+
2304+ node_a. stop ( ) . unwrap ( ) ;
2305+ node_b. stop ( ) . unwrap ( ) ;
2306+ }
2307+
2308+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2309+ async fn splice_rbf_resumed_after_restart ( ) {
2310+ // Use a custom bitcoind config with a lower incrementalrelayfee so that the +25 sat/kwu
2311+ // (0.1 sat/vB) RBF feerate bump satisfies BIP125's absolute fee increase requirement.
2312+ let bitcoind_exe = std:: env:: var ( "BITCOIND_EXE" )
2313+ . ok ( )
2314+ . or_else ( || corepc_node:: downloaded_exe_path ( ) . ok ( ) )
2315+ . expect (
2316+ "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature" ,
2317+ ) ;
2318+ let mut bitcoind_conf = corepc_node:: Conf :: default ( ) ;
2319+ bitcoind_conf. network = "regtest" ;
2320+ bitcoind_conf. args . push ( "-rest" ) ;
2321+ bitcoind_conf. args . push ( "-incrementalrelayfee=0.00000100" ) ;
2322+ let bitcoind = BitcoinD :: with_conf ( bitcoind_exe, & bitcoind_conf) . unwrap ( ) ;
2323+
2324+ let electrs_exe = std:: env:: var ( "ELECTRS_EXE" )
2325+ . ok ( )
2326+ . or_else ( electrsd:: downloaded_exe_path)
2327+ . expect ( "you need to provide env var ELECTRS_EXE or specify an electrsd version feature" ) ;
2328+ let mut electrsd_conf = electrsd:: Conf :: default ( ) ;
2329+ electrsd_conf. http_enabled = true ;
2330+ electrsd_conf. network = "regtest" ;
2331+ let electrsd = ElectrsD :: with_conf ( electrs_exe, & bitcoind, & electrsd_conf) . unwrap ( ) ;
2332+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2333+
2334+ // Set up node_a manually so it can be restarted with the same config.
2335+ let mut config_a = random_config ( ) ;
2336+ config_a. store_type = TestStoreType :: Sqlite ;
2337+ let config_b = random_config ( ) ;
2338+ let node_b = setup_node ( & chain_source, config_b) ;
2339+
2340+ let original_txo = {
2341+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2342+
2343+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2344+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2345+ let premine_amount_sat = 5_000_000 ;
2346+ premine_and_distribute_funds (
2347+ & bitcoind. client ,
2348+ & electrsd. client ,
2349+ vec ! [ address_a, address_b] ,
2350+ Amount :: from_sat ( premine_amount_sat) ,
2351+ )
2352+ . await ;
2353+
2354+ node_a. sync_wallets ( ) . unwrap ( ) ;
2355+ node_b. sync_wallets ( ) . unwrap ( ) ;
2356+
2357+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
2358+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2359+ node_a. sync_wallets ( ) . unwrap ( ) ;
2360+ node_b. sync_wallets ( ) . unwrap ( ) ;
2361+
2362+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2363+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2364+
2365+ // Negotiate a splice but leave its transaction unconfirmed so it can be fee-bumped.
2366+ node_a. splice_in ( & user_channel_id_a, node_b. node_id ( ) , 500_000 ) . unwrap ( ) ;
2367+ let original_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2368+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2369+ wait_for_tx ( & electrsd. client , original_txo. txid ) . await ;
2370+ node_a. sync_wallets ( ) . unwrap ( ) ;
2371+ node_b. sync_wallets ( ) . unwrap ( ) ;
2372+
2373+ // Bump the fee while disconnected and restart before anything could be negotiated: only
2374+ // the persisted intent knows about the fee bump, while LDK still has the negotiated
2375+ // splice at the original feerate.
2376+ node_a. disconnect ( node_b. node_id ( ) ) . unwrap ( ) ;
2377+ node_a. bump_channel_funding_fee ( & user_channel_id_a, node_b. node_id ( ) ) . unwrap ( ) ;
2378+ node_a. stop ( ) . unwrap ( ) ;
2379+ original_txo
2380+ } ;
2381+
2382+ // On restart, the reconciler sees that the negotiated splice is still at a lower feerate
2383+ // than the persisted fee-bump intent and resubmits the bump.
2384+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2385+ node_a. sync_wallets ( ) . unwrap ( ) ;
2386+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
2387+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
2388+
2389+ let rbf_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2390+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2391+ assert_ne ! ( original_txo, rbf_txo, "resubmitted RBF should produce a different funding txo" ) ;
2392+
2393+ // Restarting again must not resubmit the bump: the negotiated splice now carries it.
2394+ node_a. stop ( ) . unwrap ( ) ;
2395+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2396+ node_a. sync_wallets ( ) . unwrap ( ) ;
2397+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
2398+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2399+ assert ! ( node_a. next_event( ) . is_none( ) , "carried-out fee bump should not be resubmitted" ) ;
2400+
2401+ wait_for_tx ( & electrsd. client , rbf_txo. txid ) . await ;
2402+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2403+ node_a. sync_wallets ( ) . unwrap ( ) ;
2404+ node_b. sync_wallets ( ) . unwrap ( ) ;
2405+
2406+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2407+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2408+
2409+ // The locked fee bump cleared its intent, so a further restart must not resubmit it.
2410+ node_a. stop ( ) . unwrap ( ) ;
2411+ let node_a = setup_node ( & chain_source, config_a) ;
2412+ node_a. sync_wallets ( ) . unwrap ( ) ;
2413+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
2414+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2415+ assert ! ( node_a. next_event( ) . is_none( ) , "locked fee bump should not be resubmitted" ) ;
2416+
2417+ node_a. stop ( ) . unwrap ( ) ;
2418+ node_b. stop ( ) . unwrap ( ) ;
2419+ }
2420+
2421+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2422+ async fn splice_payment_tracked_across_restart_before_lock ( ) {
2423+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
2424+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2425+
2426+ // Set up node_a manually so it can be restarted with the same config.
2427+ let mut config_a = random_config ( ) ;
2428+ config_a. store_type = TestStoreType :: Sqlite ;
2429+ let config_b = random_config ( ) ;
2430+ let node_b = setup_node ( & chain_source, config_b) ;
2431+
2432+ let splice_txid = {
2433+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2434+
2435+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2436+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2437+ let premine_amount_sat = 5_000_000 ;
2438+ premine_and_distribute_funds (
2439+ & bitcoind. client ,
2440+ & electrsd. client ,
2441+ vec ! [ address_a, address_b] ,
2442+ Amount :: from_sat ( premine_amount_sat) ,
2443+ )
2444+ . await ;
2445+
2446+ node_a. sync_wallets ( ) . unwrap ( ) ;
2447+ node_b. sync_wallets ( ) . unwrap ( ) ;
2448+
2449+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
2450+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2451+ node_a. sync_wallets ( ) . unwrap ( ) ;
2452+ node_b. sync_wallets ( ) . unwrap ( ) ;
2453+
2454+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2455+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2456+
2457+ node_a. splice_in ( & user_channel_id_a, node_b. node_id ( ) , 500_000 ) . unwrap ( ) ;
2458+ let txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2459+ expect_splice_negotiated_event ! ( node_b, node_a. node_id( ) ) ;
2460+
2461+ // Stop node_a as soon as the splice is negotiated. node_b broadcasts the transaction
2462+ // either way, so it reaches the chain while node_a is offline. Depending on timing,
2463+ // node_a may or may not have classified its own broadcast into a payment record before
2464+ // stopping; the assertions below must hold in both cases.
2465+ node_a. stop ( ) . unwrap ( ) ;
2466+ txo. txid
2467+ } ;
2468+
2469+ // Confirm the splice while node_a is offline, but keep it short of the depth at which it
2470+ // locks, so node_a restarts with its splice intent still live.
2471+ wait_for_tx ( & electrsd. client , splice_txid) . await ;
2472+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 1 ) . await ;
2473+
2474+ // After the restart, wallet sync and classification must agree on the splice-time
2475+ // `PaymentId` no matter which of them sees the confirmed transaction first: exactly one
2476+ // payment record, and not one keyed by a txid-derived id.
2477+ let node_a = setup_node ( & chain_source, config_a) ;
2478+ node_a. sync_wallets ( ) . unwrap ( ) ;
2479+
2480+ let splice_payments = |node : & Node | {
2481+ node. list_payments_with_filter (
2482+ |p| matches ! ( p. kind, PaymentKind :: Onchain { txid, .. } if txid == splice_txid) ,
2483+ )
2484+ } ;
2485+ let payments = splice_payments ( & node_a) ;
2486+ assert_eq ! (
2487+ payments. len( ) ,
2488+ 1 ,
2489+ "expected exactly one payment record for the splice, got {}: {:#?}" ,
2490+ payments. len( ) ,
2491+ payments,
2492+ ) ;
2493+ assert_ne ! (
2494+ payments[ 0 ] . id,
2495+ PaymentId ( splice_txid. to_byte_array( ) ) ,
2496+ "the splice payment must keep its splice-time id, not a txid-derived fallback" ,
2497+ ) ;
2498+ assert_eq ! ( payments[ 0 ] . status, PaymentStatus :: Pending ) ;
2499+
2500+ // Reconnect and let the splice lock: the single record graduates instead of gaining a
2501+ // duplicate.
2502+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
2503+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
2504+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 5 ) . await ;
2505+ node_a. sync_wallets ( ) . unwrap ( ) ;
2506+ node_b. sync_wallets ( ) . unwrap ( ) ;
2507+
2508+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2509+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2510+
2511+ let payments = splice_payments ( & node_a) ;
2512+ assert_eq ! (
2513+ payments. len( ) ,
2514+ 1 ,
2515+ "expected exactly one payment record after the splice locked, got {}: {:#?}" ,
2516+ payments. len( ) ,
2517+ payments,
2518+ ) ;
2519+ assert_eq ! ( payments[ 0 ] . status, PaymentStatus :: Succeeded ) ;
2520+
2521+ node_a. stop ( ) . unwrap ( ) ;
2522+ node_b. stop ( ) . unwrap ( ) ;
2523+ }
2524+
22242525#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
22252526async fn splice_in_rbf_joins_counterparty_splice ( ) {
22262527 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments