Skip to content

Commit 6e710be

Browse files
jkczyzclaude
andcommitted
Test splice resumption across restarts
Three restart tests cover the persisted-intent lifecycle end to end. A splice-out initiated while disconnected — so LDK drops it on restart having neither negotiated nor persisted anything — is resumed by the startup reconciler and completes; a further restart after the lock resubmits nothing. A fee bump initiated while disconnected is likewise resumed, but only while it still improves on the feerate LDK holds: once the negotiated splice carries the bump, restarts leave it alone. Without the reconciler, both scenarios time out waiting for the splice to be negotiated after the restart. The third test stops the node right after its splice is negotiated, confirms the transaction while the node is offline, and asserts that wallet sync and classification agree on the splice-time PaymentId across the restart — exactly one record, before and after the lock. It passes even without the reconciler and pins that the persisted intent record does not change that: a record now present at restart must fold into the same single payment rather than mint a duplicate. Generated with assistance from Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 85ca117 commit 6e710be

1 file changed

Lines changed: 303 additions & 0 deletions

File tree

tests/integration_tests_rust.rs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
28523155
async fn simple_bolt12_send_receive() {
28533156
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)