@@ -2848,6 +2848,309 @@ async fn splice_retried_after_disconnect_mid_negotiation() {
28482848 node_b. stop ( ) . unwrap ( ) ;
28492849}
28502850
2851+ /// A splice LDK dropped without ever recording it — initiated while disconnected, then the node
2852+ /// restarts — is resumed from its persisted intent by the startup reconciler.
2853+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2854+ async fn splice_resumed_after_restart ( ) {
2855+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
2856+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2857+
2858+ // Set up node_a manually so it can be restarted with the same config.
2859+ let mut config_a = random_config ( ) ;
2860+ config_a. store_type = TestStoreType :: Sqlite ;
2861+ let config_b = random_config ( ) ;
2862+ let node_b = setup_node ( & chain_source, config_b) ;
2863+
2864+ let onchain_balance_before_sat = {
2865+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2866+
2867+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2868+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2869+ let premine_amount_sat = 5_000_000 ;
2870+ premine_and_distribute_funds (
2871+ & bitcoind. client ,
2872+ & electrsd. client ,
2873+ vec ! [ address_a, address_b] ,
2874+ Amount :: from_sat ( premine_amount_sat) ,
2875+ )
2876+ . await ;
2877+
2878+ node_a. sync_wallets ( ) . unwrap ( ) ;
2879+ node_b. sync_wallets ( ) . unwrap ( ) ;
2880+
2881+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
2882+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2883+ node_a. sync_wallets ( ) . unwrap ( ) ;
2884+ node_b. sync_wallets ( ) . unwrap ( ) ;
2885+
2886+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2887+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2888+
2889+ // Initiate a splice-out while disconnected: LDK accepts the contribution but cannot make
2890+ // progress before the restart below drops it, having neither negotiated nor persisted
2891+ // anything. Only the persisted splice intent allows resuming the splice.
2892+ node_a. disconnect ( node_b. node_id ( ) ) . unwrap ( ) ;
2893+ let address = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2894+ node_a. splice_out ( & user_channel_id_a, node_b. node_id ( ) , & address, 500_000 ) . unwrap ( ) ;
2895+
2896+ let onchain_balance_before_sat = node_a. list_balances ( ) . total_onchain_balance_sats ;
2897+ node_a. stop ( ) . unwrap ( ) ;
2898+ onchain_balance_before_sat
2899+ } ;
2900+
2901+ // On restart, the reconciler resubmits the splice, which proceeds once the peer connects.
2902+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2903+ node_a. sync_wallets ( ) . unwrap ( ) ;
2904+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
2905+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
2906+
2907+ let txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2908+
2909+ wait_for_tx ( & electrsd. client , txo. txid ) . await ;
2910+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2911+ node_a. sync_wallets ( ) . unwrap ( ) ;
2912+ node_b. sync_wallets ( ) . unwrap ( ) ;
2913+
2914+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2915+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2916+
2917+ assert ! (
2918+ node_a. list_balances( ) . total_onchain_balance_sats > onchain_balance_before_sat + 400_000 ,
2919+ "resumed splice-out should have moved ~500k sats to the on-chain balance" ,
2920+ ) ;
2921+
2922+ // The locked splice cleared the intent, so another restart must not resubmit it.
2923+ node_a. stop ( ) . unwrap ( ) ;
2924+ let node_a = setup_node ( & chain_source, config_a) ;
2925+ node_a. sync_wallets ( ) . unwrap ( ) ;
2926+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
2927+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
2928+ assert ! ( node_a. next_event( ) . is_none( ) , "completed splice should not be resubmitted" ) ;
2929+
2930+ node_a. stop ( ) . unwrap ( ) ;
2931+ node_b. stop ( ) . unwrap ( ) ;
2932+ }
2933+
2934+ /// A fee bump initiated while disconnected and dropped by a restart is resubmitted from its
2935+ /// persisted intent — but only while it still improves on the feerate LDK holds; once the
2936+ /// negotiated splice carries the bump, further restarts leave it alone.
2937+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
2938+ async fn splice_rbf_resumed_after_restart ( ) {
2939+ // Use a custom bitcoind config with a lower incrementalrelayfee so that the +25 sat/kwu
2940+ // (0.1 sat/vB) RBF feerate bump satisfies BIP125's absolute fee increase requirement.
2941+ let bitcoind_exe = std:: env:: var ( "BITCOIND_EXE" )
2942+ . ok ( )
2943+ . or_else ( || corepc_node:: downloaded_exe_path ( ) . ok ( ) )
2944+ . expect (
2945+ "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature" ,
2946+ ) ;
2947+ let mut bitcoind_conf = corepc_node:: Conf :: default ( ) ;
2948+ bitcoind_conf. network = "regtest" ;
2949+ bitcoind_conf. args . push ( "-rest" ) ;
2950+ bitcoind_conf. args . push ( "-incrementalrelayfee=0.00000100" ) ;
2951+ let bitcoind = BitcoinD :: with_conf ( bitcoind_exe, & bitcoind_conf) . unwrap ( ) ;
2952+
2953+ let electrs_exe = std:: env:: var ( "ELECTRS_EXE" )
2954+ . ok ( )
2955+ . or_else ( electrsd:: downloaded_exe_path)
2956+ . expect ( "you need to provide env var ELECTRS_EXE or specify an electrsd version feature" ) ;
2957+ let mut electrsd_conf = electrsd:: Conf :: default ( ) ;
2958+ electrsd_conf. http_enabled = true ;
2959+ electrsd_conf. network = "regtest" ;
2960+ let electrsd = ElectrsD :: with_conf ( electrs_exe, & bitcoind, & electrsd_conf) . unwrap ( ) ;
2961+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
2962+
2963+ // Set up node_a manually so it can be restarted with the same config.
2964+ let mut config_a = random_config ( ) ;
2965+ config_a. store_type = TestStoreType :: Sqlite ;
2966+ let config_b = random_config ( ) ;
2967+ let node_b = setup_node ( & chain_source, config_b) ;
2968+
2969+ let original_txo = {
2970+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
2971+
2972+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2973+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
2974+ let premine_amount_sat = 5_000_000 ;
2975+ premine_and_distribute_funds (
2976+ & bitcoind. client ,
2977+ & electrsd. client ,
2978+ vec ! [ address_a, address_b] ,
2979+ Amount :: from_sat ( premine_amount_sat) ,
2980+ )
2981+ . await ;
2982+
2983+ node_a. sync_wallets ( ) . unwrap ( ) ;
2984+ node_b. sync_wallets ( ) . unwrap ( ) ;
2985+
2986+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
2987+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
2988+ node_a. sync_wallets ( ) . unwrap ( ) ;
2989+ node_b. sync_wallets ( ) . unwrap ( ) ;
2990+
2991+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
2992+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
2993+
2994+ // Negotiate a splice but leave its transaction unconfirmed so it can be fee-bumped.
2995+ node_a. splice_in ( & user_channel_id_a, node_b. node_id ( ) , 500_000 ) . unwrap ( ) ;
2996+ let original_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
2997+ wait_for_tx ( & electrsd. client , original_txo. txid ) . await ;
2998+ node_a. sync_wallets ( ) . unwrap ( ) ;
2999+ node_b. sync_wallets ( ) . unwrap ( ) ;
3000+
3001+ // Bump the fee while disconnected and restart before anything could be negotiated: only
3002+ // the persisted intent knows about the fee bump, while LDK still has the negotiated
3003+ // splice at the original feerate.
3004+ node_a. disconnect ( node_b. node_id ( ) ) . unwrap ( ) ;
3005+ node_a. bump_channel_funding_fee ( & user_channel_id_a, node_b. node_id ( ) ) . unwrap ( ) ;
3006+ node_a. stop ( ) . unwrap ( ) ;
3007+ original_txo
3008+ } ;
3009+
3010+ // On restart, the reconciler sees that the negotiated splice is still at a lower feerate
3011+ // than the persisted fee-bump intent and resubmits the bump.
3012+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
3013+ node_a. sync_wallets ( ) . unwrap ( ) ;
3014+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
3015+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
3016+
3017+ let rbf_txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
3018+ assert_ne ! ( original_txo, rbf_txo, "resubmitted RBF should produce a different funding txo" ) ;
3019+
3020+ // Restarting again must not resubmit the bump: the negotiated splice now carries it.
3021+ node_a. stop ( ) . unwrap ( ) ;
3022+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
3023+ node_a. sync_wallets ( ) . unwrap ( ) ;
3024+ node_a. connect ( node_b. node_id ( ) , node_b_addr. clone ( ) , false ) . unwrap ( ) ;
3025+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
3026+ assert ! ( node_a. next_event( ) . is_none( ) , "carried-out fee bump should not be resubmitted" ) ;
3027+
3028+ wait_for_tx ( & electrsd. client , rbf_txo. txid ) . await ;
3029+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
3030+ node_a. sync_wallets ( ) . unwrap ( ) ;
3031+ node_b. sync_wallets ( ) . unwrap ( ) ;
3032+
3033+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
3034+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
3035+
3036+ // The locked fee bump cleared its intent, so a further restart must not resubmit it.
3037+ node_a. stop ( ) . unwrap ( ) ;
3038+ let node_a = setup_node ( & chain_source, config_a) ;
3039+ node_a. sync_wallets ( ) . unwrap ( ) ;
3040+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
3041+ tokio:: time:: sleep ( std:: time:: Duration :: from_secs ( 3 ) ) . await ;
3042+ assert ! ( node_a. next_event( ) . is_none( ) , "locked fee bump should not be resubmitted" ) ;
3043+
3044+ node_a. stop ( ) . unwrap ( ) ;
3045+ node_b. stop ( ) . unwrap ( ) ;
3046+ }
3047+
3048+ /// A splice confirmed while its node was offline keeps exactly one payment record under its
3049+ /// splice-time id across the restart, no matter whether wallet sync or classification sees the
3050+ /// confirmed transaction first.
3051+ #[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
3052+ async fn splice_payment_tracked_across_restart_before_lock ( ) {
3053+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
3054+ let chain_source = random_chain_source ( & bitcoind, & electrsd) ;
3055+
3056+ // Set up node_a manually so it can be restarted with the same config.
3057+ let mut config_a = random_config ( ) ;
3058+ config_a. store_type = TestStoreType :: Sqlite ;
3059+ let config_b = random_config ( ) ;
3060+ let node_b = setup_node ( & chain_source, config_b) ;
3061+
3062+ let splice_txid = {
3063+ let node_a = setup_node ( & chain_source, config_a. clone ( ) ) ;
3064+
3065+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
3066+ let address_b = node_b. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
3067+ let premine_amount_sat = 5_000_000 ;
3068+ premine_and_distribute_funds (
3069+ & bitcoind. client ,
3070+ & electrsd. client ,
3071+ vec ! [ address_a, address_b] ,
3072+ Amount :: from_sat ( premine_amount_sat) ,
3073+ )
3074+ . await ;
3075+
3076+ node_a. sync_wallets ( ) . unwrap ( ) ;
3077+ node_b. sync_wallets ( ) . unwrap ( ) ;
3078+
3079+ open_channel ( & node_a, & node_b, 4_000_000 , false , & electrsd) . await ;
3080+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
3081+ node_a. sync_wallets ( ) . unwrap ( ) ;
3082+ node_b. sync_wallets ( ) . unwrap ( ) ;
3083+
3084+ let user_channel_id_a = expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
3085+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
3086+
3087+ node_a. splice_in ( & user_channel_id_a, node_b. node_id ( ) , 500_000 ) . unwrap ( ) ;
3088+ let txo = expect_splice_negotiated_event ! ( node_a, node_b. node_id( ) ) ;
3089+
3090+ // Stop node_a as soon as the splice is negotiated. node_b broadcasts the transaction
3091+ // either way, so it reaches the chain while node_a is offline. Depending on timing,
3092+ // node_a may or may not have classified its own broadcast into a payment record before
3093+ // stopping; the assertions below must hold in both cases.
3094+ node_a. stop ( ) . unwrap ( ) ;
3095+ txo. txid
3096+ } ;
3097+
3098+ // Confirm the splice while node_a is offline, but keep it short of the depth at which it
3099+ // locks, so node_a restarts with its splice intent still live.
3100+ wait_for_tx ( & electrsd. client , splice_txid) . await ;
3101+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 1 ) . await ;
3102+
3103+ // After the restart, wallet sync and classification must agree on the splice-time
3104+ // `PaymentId` no matter which of them sees the confirmed transaction first: exactly one
3105+ // payment record, and not one keyed by a txid-derived id.
3106+ let node_a = setup_node ( & chain_source, config_a) ;
3107+ node_a. sync_wallets ( ) . unwrap ( ) ;
3108+
3109+ let splice_payments = |node : & Node | {
3110+ node. list_payments_with_filter (
3111+ |p| matches ! ( p. kind, PaymentKind :: Onchain { txid, .. } if txid == splice_txid) ,
3112+ )
3113+ } ;
3114+ let payments = splice_payments ( & node_a) ;
3115+ assert_eq ! (
3116+ payments. len( ) ,
3117+ 1 ,
3118+ "expected exactly one payment record for the splice, got {}: {:#?}" ,
3119+ payments. len( ) ,
3120+ payments,
3121+ ) ;
3122+ assert_ne ! (
3123+ payments[ 0 ] . id,
3124+ PaymentId ( splice_txid. to_byte_array( ) ) ,
3125+ "the splice payment must keep its splice-time id, not a txid-derived fallback" ,
3126+ ) ;
3127+ assert_eq ! ( payments[ 0 ] . status, PaymentStatus :: Pending ) ;
3128+
3129+ // Reconnect and let the splice lock: the single record graduates instead of gaining a
3130+ // duplicate.
3131+ let node_b_addr = node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ;
3132+ node_a. connect ( node_b. node_id ( ) , node_b_addr, false ) . unwrap ( ) ;
3133+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 5 ) . await ;
3134+ node_a. sync_wallets ( ) . unwrap ( ) ;
3135+ node_b. sync_wallets ( ) . unwrap ( ) ;
3136+
3137+ expect_channel_ready_event ! ( node_a, node_b. node_id( ) ) ;
3138+ expect_channel_ready_event ! ( node_b, node_a. node_id( ) ) ;
3139+
3140+ let payments = splice_payments ( & node_a) ;
3141+ assert_eq ! (
3142+ payments. len( ) ,
3143+ 1 ,
3144+ "expected exactly one payment record after the splice locked, got {}: {:#?}" ,
3145+ payments. len( ) ,
3146+ payments,
3147+ ) ;
3148+ assert_eq ! ( payments[ 0 ] . status, PaymentStatus :: Succeeded ) ;
3149+
3150+ node_a. stop ( ) . unwrap ( ) ;
3151+ node_b. stop ( ) . unwrap ( ) ;
3152+ }
3153+
28513154#[ tokio:: test( flavor = "multi_thread" , worker_threads = 1 ) ]
28523155async fn simple_bolt12_send_receive ( ) {
28533156 let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
0 commit comments