Skip to content

Commit 76055ae

Browse files
committed
Add probing test of state recovery after restart
1 parent 15edebc commit 76055ae

1 file changed

Lines changed: 105 additions & 1 deletion

File tree

tests/probing_tests.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
// exhausted_probe_budget_blocks_new_probes
1010
// Samples locked_msat across multiple probe cycles and asserts it never
1111
// exceeds the configured max_locked_msat budget cap.
12+
//
13+
// probing_budget_restored_after_node_restart
14+
// Dispatches a probe, then stops node_b before the failure can propagate
15+
// back so the pending probe HTLC is preserved. Restarts node_a and asserts
16+
// the prober's locked_msat is rebuilt non-zero from list_recent_payments().
1217

1318
mod common;
1419
use std::sync::atomic::{AtomicBool, Ordering};
1520

1621
use common::{
1722
expect_channel_ready_event, expect_event, generate_blocks_and_wait, open_channel,
1823
premine_and_distribute_funds, random_chain_source, random_config, setup_bitcoind_and_electrsd,
19-
setup_node, wait_for_channel_ready_to_send, TestNode,
24+
setup_node, wait_for_channel_ready_to_send, TestNode, TestStoreType,
2025
};
2126

2227
use ldk_node::bitcoin::Amount;
@@ -195,6 +200,105 @@ async fn probe_budget_increments_and_decrements() {
195200
node_c.stop().unwrap();
196201
}
197202

203+
/// Verifies that `locked_msat` is restored after the node is stopped and restarted
204+
/// while a probe is still in flight.
205+
///
206+
/// Race-sensitive: once a probe is dispatched, the failure round-trip
207+
/// (`A→B→C → C fails back → B → A`) resolves it within milliseconds. To keep the
208+
/// HTLC pending across the restart we observe `locked_msat > 0` and then *immediately*
209+
/// stop `node_b`, which prevents `B` from forwarding the failure back to `A`.
210+
/// The pending Probe entry persists in `node_a`'s channel manager and must be
211+
/// rebuilt by the prober's `locked_msat` on restart via `list_recent_payments()`.
212+
#[tokio::test(flavor = "multi_thread")]
213+
async fn probing_budget_restored_after_node_restart() {
214+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
215+
let chain_source = random_chain_source(&bitcoind, &electrsd);
216+
217+
let node_b = setup_node(&chain_source, random_config(false));
218+
let node_c = setup_node(&chain_source, random_config(false));
219+
220+
let mut config_a = random_config(false);
221+
// Use a pure on-disk store so state survives the restart.
222+
config_a.store_type = TestStoreType::Sqlite;
223+
let strategy = FixedPathStrategy::new();
224+
config_a.probing = Some(
225+
ProbingConfigBuilder::custom(strategy.clone())
226+
.interval(Duration::from_millis(PROBING_INTERVAL_MILLISECONDS))
227+
.max_locked_msat(10 * PROBE_AMOUNT_MSAT)
228+
.build(),
229+
);
230+
let restart_config = config_a.clone();
231+
let node_a = setup_node(&chain_source, config_a);
232+
233+
let addr_a = node_a.onchain_payment().new_address().unwrap();
234+
let addr_b = node_b.onchain_payment().new_address().unwrap();
235+
premine_and_distribute_funds(
236+
&bitcoind.client,
237+
&electrsd.client,
238+
vec![addr_a, addr_b],
239+
Amount::from_sat(2_000_000),
240+
)
241+
.await;
242+
node_a.sync_wallets().unwrap();
243+
node_b.sync_wallets().unwrap();
244+
245+
open_channel(&node_a, &node_b, 1_000_000, true, &electrsd).await;
246+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await;
247+
node_b.sync_wallets().unwrap();
248+
open_channel(&node_b, &node_c, 1_000_000, true, &electrsd).await;
249+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
250+
251+
node_a.sync_wallets().unwrap();
252+
node_b.sync_wallets().unwrap();
253+
node_c.sync_wallets().unwrap();
254+
255+
expect_channel_ready_event!(node_a, node_b.node_id());
256+
expect_event!(node_b, ChannelReady);
257+
expect_event!(node_b, ChannelReady);
258+
expect_event!(node_c, ChannelReady);
259+
260+
strategy.set_path(build_probe_path(&node_a, &node_b, &node_c, PROBE_AMOUNT_MSAT));
261+
wait_for_channel_ready_to_send(&node_a, &node_b, PROBE_AMOUNT_MSAT + 1000).await;
262+
wait_for_channel_ready_to_send(&node_b, &node_c, PROBE_AMOUNT_MSAT).await;
263+
264+
strategy.start_probing();
265+
let went_up = tokio::time::timeout(Duration::from_secs(30), async {
266+
loop {
267+
if node_a.prober().unwrap().locked_msat() > 0 {
268+
break;
269+
}
270+
tokio::time::sleep(Duration::from_millis(10)).await;
271+
}
272+
})
273+
.await
274+
.is_ok();
275+
assert!(went_up, "locked_msat never increased — no probe was dispatched");
276+
277+
node_b.stop().unwrap();
278+
strategy.stop_probing();
279+
280+
let locked_before = node_a.prober().unwrap().locked_msat();
281+
println!("Before restart: locked_msat = {}", locked_before);
282+
assert!(locked_before > 0, "probe resolved before we could stop node_b — flaky timing");
283+
284+
node_a.stop().unwrap();
285+
286+
// Restart node_a from the same persisted state.
287+
let node_a = setup_node(&chain_source, restart_config);
288+
289+
let locked_after = node_a.prober().unwrap().locked_msat();
290+
println!("After restart: locked_msat = {}", locked_after);
291+
assert!(
292+
locked_after > 0,
293+
"locked_msat was not restored after restart (before={} after={})",
294+
locked_before,
295+
locked_after
296+
);
297+
298+
node_a.stop().unwrap();
299+
node_c.stop().unwrap();
300+
}
301+
198302
/// Verifies that `locked_msat` never exceeds `max_locked_msat` across multiple probe cycles.
199303
#[tokio::test(flavor = "multi_thread")]
200304
async fn exhausted_probe_budget_blocks_new_probes() {

0 commit comments

Comments
 (0)