Skip to content

Commit 1508155

Browse files
committed
Add seeded startup operations benchmark
Add a startup benchmark that restarts a node whose store already contains channel and payment data, so startup cost reflects persisted node state. AI-assisted-by: OpenAI Codex
1 parent f23517b commit 1508155

3 files changed

Lines changed: 419 additions & 4 deletions

File tree

benches/operations.rs

Lines changed: 290 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,67 @@ mod common;
1111
use std::sync::Arc;
1212
use std::time::{Duration, Instant};
1313

14+
use bitcoin::secp256k1::PublicKey;
1415
use bitcoin::Amount;
1516
use common::{
1617
expect_channel_pending_event, expect_channel_ready_event, expect_event,
1718
generate_blocks_and_wait, premine_and_distribute_funds, random_chain_source, random_config,
18-
setup_bitcoind_and_electrsd, setup_node, setup_two_nodes_with_store,
19+
random_storage_path, setup_bitcoind_and_electrsd, setup_node, setup_two_nodes_with_store,
1920
};
2021
use criterion::{criterion_group, criterion_main, Criterion};
2122
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
23+
use ldk_node::io::sqlite_store::SqliteStore;
2224
use ldk_node::{Event, Node};
2325
use lightning::ln::channelmanager::PaymentId;
2426
use lightning::routing::router::RouteParametersConfig;
27+
use lightning::util::persist::migrate_kv_store_data_async;
2528
use lightning_invoice::{Bolt11InvoiceDescription, Description};
29+
use lightning_persister::fs_store::v2::FilesystemStoreV2;
30+
31+
use crate::common::{open_channel_push_amt, TestChainSource, TestConfig, TestStoreType};
32+
33+
#[cfg(feature = "postgres")]
34+
use ldk_node::io::postgres_store::{PostgresStore, POSTGRES_TEST_URL_ENV_VAR};
35+
36+
const STARTUP_SEED_SCENARIOS: [StartupSeedScenario; 6] = [
37+
StartupSeedScenario { channel_count: 1, payment_count: 2 },
38+
StartupSeedScenario { channel_count: 1, payment_count: 100 },
39+
StartupSeedScenario { channel_count: 1, payment_count: 1_000 },
40+
StartupSeedScenario { channel_count: 10, payment_count: 2 },
41+
StartupSeedScenario { channel_count: 100, payment_count: 2 },
42+
StartupSeedScenario { channel_count: 100, payment_count: 1_000 },
43+
];
44+
const STARTUP_SEED_PAYMENT_AMOUNT_MSAT: u64 = 1_000_000;
45+
const STARTUP_SEED_MIN_CHANNEL_FUNDING_SAT: u64 = 100_000;
46+
const STARTUP_SEED_CHANNEL_BUFFER_SAT: u64 = 1_000_000;
47+
const STARTUP_SEED_CHANNEL_BATCH_SIZE: u64 = 2;
2648

27-
use crate::common::{open_channel_push_amt, TestChainSource, TestStoreType};
49+
#[derive(Clone, Copy)]
50+
struct StartupSeedScenario {
51+
channel_count: u64,
52+
payment_count: u64,
53+
}
54+
55+
impl StartupSeedScenario {
56+
fn bench_name(self, store_name: &str) -> String {
57+
format!("{}/channels_{}_payments_{}", store_name, self.channel_count, self.payment_count)
58+
}
59+
60+
fn runs_in_ci(self) -> bool {
61+
self.channel_count == 1 && self.payment_count == 2
62+
}
63+
64+
fn channel_funding_sat(self) -> u64 {
65+
let payment_amount_sat = STARTUP_SEED_PAYMENT_AMOUNT_MSAT / 1_000;
66+
let payment_funding_sat =
67+
self.payment_count * payment_amount_sat + STARTUP_SEED_CHANNEL_BUFFER_SAT;
68+
payment_funding_sat.max(STARTUP_SEED_MIN_CHANNEL_FUNDING_SAT)
69+
}
70+
71+
fn premine_amount_sat(self) -> u64 {
72+
self.channel_count * self.channel_funding_sat() + STARTUP_SEED_CHANNEL_BUFFER_SAT
73+
}
74+
}
2875

2976
#[derive(Clone, Copy)]
3077
struct StoreBenchConfig {
@@ -37,6 +84,7 @@ fn operations_benchmark(c: &mut Criterion) {
3784

3885
forwarding_benchmark(c);
3986
channel_open_benchmark(c);
87+
startup_benchmark(c);
4088
}
4189

4290
fn forwarding_benchmark(c: &mut Criterion) {
@@ -133,6 +181,214 @@ fn channel_open_benchmark(c: &mut Criterion) {
133181
}
134182
}
135183

184+
fn startup_benchmark(c: &mut Criterion) {
185+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
186+
let chain_source = random_chain_source(&bitcoind, &electrsd);
187+
let runtime =
188+
tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().unwrap();
189+
190+
let mut group = c.benchmark_group("startup");
191+
group.sample_size(10);
192+
193+
for startup_seed_scenario in STARTUP_SEED_SCENARIOS {
194+
if is_ci() && !startup_seed_scenario.runs_in_ci() {
195+
continue;
196+
}
197+
198+
let matching_store_configs: Vec<_> = store_bench_configs()
199+
.into_iter()
200+
.filter(|store_config| {
201+
let bench_name = startup_seed_scenario.bench_name(store_config.name);
202+
should_register_bench("startup", &bench_name)
203+
})
204+
.collect();
205+
if matching_store_configs.is_empty() {
206+
continue;
207+
}
208+
209+
let seeded_config = setup_startup_seed_node(
210+
&chain_source,
211+
&bitcoind,
212+
&electrsd,
213+
startup_seed_scenario,
214+
&runtime,
215+
);
216+
let startup_configs = migrate_startup_seed_configs(
217+
&seeded_config,
218+
startup_seed_scenario,
219+
matching_store_configs,
220+
&runtime,
221+
);
222+
223+
for (bench_name, config) in startup_configs {
224+
group.bench_function(bench_name, |b| {
225+
b.iter_custom(|iter| {
226+
let mut total = Duration::ZERO;
227+
for _ in 0..iter {
228+
let start = Instant::now();
229+
let node = setup_node(&chain_source, config.clone());
230+
total += start.elapsed();
231+
node.stop().unwrap();
232+
}
233+
total
234+
});
235+
});
236+
}
237+
}
238+
}
239+
240+
fn setup_startup_seed_node(
241+
chain_source: &TestChainSource, bitcoind: &BitcoinD, electrsd: &electrsd::ElectrsD,
242+
seed_scenario: StartupSeedScenario, runtime: &tokio::runtime::Runtime,
243+
) -> TestConfig {
244+
let mut config_a = random_config(true);
245+
config_a.store_type = TestStoreType::Sqlite;
246+
let node_a = Arc::new(setup_node(chain_source, config_a.clone()));
247+
248+
let mut config_b = random_config(true);
249+
config_b.store_type = TestStoreType::Sqlite;
250+
let node_b = Arc::new(setup_node(chain_source, config_b));
251+
252+
runtime.block_on(async {
253+
let address_a = node_a.onchain_payment().new_address().unwrap();
254+
premine_and_distribute_funds(
255+
&bitcoind.client,
256+
&electrsd.client,
257+
vec![address_a],
258+
Amount::from_sat(seed_scenario.premine_amount_sat()),
259+
)
260+
.await;
261+
node_a.sync_wallets().unwrap();
262+
node_b.sync_wallets().unwrap();
263+
264+
let funding_amount_sat = seed_scenario.channel_funding_sat();
265+
let mut remaining_channel_count = seed_scenario.channel_count;
266+
while remaining_channel_count > 0 {
267+
let channel_batch_size = remaining_channel_count.min(STARTUP_SEED_CHANNEL_BATCH_SIZE);
268+
for _ in 0..channel_batch_size {
269+
node_a
270+
.open_channel(
271+
node_b.node_id(),
272+
node_b.listening_addresses().unwrap().first().unwrap().clone(),
273+
funding_amount_sat,
274+
None,
275+
None,
276+
)
277+
.unwrap();
278+
assert!(node_a.list_peers().iter().any(|peer| peer.node_id == node_b.node_id()));
279+
280+
let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id());
281+
let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id());
282+
assert_eq!(funding_txo_a, funding_txo_b);
283+
node_a.sync_wallets().unwrap();
284+
}
285+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
286+
287+
for _ in 0..channel_batch_size {
288+
node_a.sync_wallets().unwrap();
289+
node_b.sync_wallets().unwrap();
290+
wait_for_channel_ready_events(&node_a, node_b.node_id(), 1).await;
291+
wait_for_channel_ready_events(&node_b, node_a.node_id(), 1).await;
292+
}
293+
remaining_channel_count -= channel_batch_size;
294+
}
295+
296+
for idx in 0..seed_scenario.payment_count {
297+
let invoice_description = Bolt11InvoiceDescription::Direct(
298+
Description::new(format!("startup seed {}", idx + 1)).unwrap(),
299+
);
300+
let invoice = node_b
301+
.bolt11_payment()
302+
.receive(STARTUP_SEED_PAYMENT_AMOUNT_MSAT, &invoice_description.into(), 9217)
303+
.unwrap();
304+
let payment_id = node_a.bolt11_payment().send(&invoice, None).unwrap();
305+
wait_for_payment_success(&node_a, payment_id).await;
306+
}
307+
308+
drain_events(&node_a);
309+
drain_events(&node_b);
310+
});
311+
312+
node_a.stop().unwrap();
313+
node_b.stop().unwrap();
314+
315+
config_a
316+
}
317+
318+
fn migrate_startup_seed_configs(
319+
source_config: &TestConfig, seed_scenario: StartupSeedScenario,
320+
store_configs: Vec<StoreBenchConfig>, runtime: &tokio::runtime::Runtime,
321+
) -> Vec<(String, TestConfig)> {
322+
let source_store =
323+
SqliteStore::new(source_config.node_config.storage_dir_path.clone().into(), None, None)
324+
.unwrap();
325+
326+
store_configs
327+
.into_iter()
328+
.map(|store_config| {
329+
let mut config = source_config.clone();
330+
config.store_type = store_config.store_type;
331+
if !matches!(store_config.store_type, TestStoreType::Sqlite) {
332+
config.node_config.storage_dir_path =
333+
random_storage_path().to_str().unwrap().to_owned();
334+
migrate_startup_seed_store(&source_store, &config, runtime);
335+
}
336+
337+
(seed_scenario.bench_name(store_config.name), config)
338+
})
339+
.collect()
340+
}
341+
342+
fn migrate_startup_seed_store(
343+
source_store: &SqliteStore, destination_config: &TestConfig, runtime: &tokio::runtime::Runtime,
344+
) {
345+
runtime.block_on(async {
346+
match destination_config.store_type {
347+
TestStoreType::Sqlite => {},
348+
TestStoreType::FilesystemStore => {
349+
let destination_store = FilesystemStoreV2::new(
350+
destination_config.node_config.storage_dir_path.clone().into(),
351+
)
352+
.unwrap();
353+
migrate_kv_store_data_async(source_store, &destination_store).await.unwrap();
354+
},
355+
#[cfg(feature = "postgres")]
356+
TestStoreType::Postgres => {
357+
let connection_string = postgres_connection_string();
358+
let table_name = postgres_table_name(destination_config);
359+
let destination_store =
360+
PostgresStore::new(connection_string, None, Some(table_name), None)
361+
.await
362+
.unwrap();
363+
migrate_kv_store_data_async(source_store, &destination_store).await.unwrap();
364+
},
365+
TestStoreType::TestSyncStore => {
366+
unreachable!("startup benches do not use TestSyncStore")
367+
},
368+
}
369+
});
370+
}
371+
372+
#[cfg(feature = "postgres")]
373+
fn postgres_connection_string() -> String {
374+
dotenvy::dotenv().ok();
375+
std::env::var(POSTGRES_TEST_URL_ENV_VAR)
376+
.unwrap_or_else(|_| "host=localhost user=postgres password=postgres".to_string())
377+
}
378+
379+
#[cfg(feature = "postgres")]
380+
fn postgres_table_name(config: &TestConfig) -> String {
381+
format!(
382+
"test_{}",
383+
config
384+
.node_config
385+
.storage_dir_path
386+
.chars()
387+
.filter(|c| c.is_ascii_alphanumeric())
388+
.collect::<String>()
389+
)
390+
}
391+
136392
fn should_register_bench(group: &str, name: &str) -> bool {
137393
let target = format!("{}/{}", group, name);
138394
let filters: Vec<String> =
@@ -143,6 +399,10 @@ fn should_register_bench(group: &str, name: &str) -> bool {
143399
})
144400
}
145401

402+
fn is_ci() -> bool {
403+
std::env::var("CI").is_ok_and(|value| !value.is_empty() && value != "0" && value != "false")
404+
}
405+
146406
fn setup_forwarding_nodes(
147407
chain_source: &TestChainSource, bitcoind: &BitcoinD, electrsd: &electrsd::ElectrsD,
148408
store_type: TestStoreType, runtime: &tokio::runtime::Runtime,
@@ -408,6 +668,34 @@ async fn wait_for_payment_success(node: &Node, expected_payment_id: PaymentId) {
408668
}
409669
}
410670

671+
async fn wait_for_channel_ready_events(node: &Node, counterparty_node_id: PublicKey, count: u64) {
672+
let mut remaining_count = count;
673+
while remaining_count > 0 {
674+
let event = tokio::time::timeout(
675+
Duration::from_secs(common::INTEROP_TIMEOUT_SECS),
676+
node.next_event_async(),
677+
)
678+
.await
679+
.unwrap_or_else(|_| {
680+
panic!("{} timed out waiting for ChannelReady event after 60s", node.node_id())
681+
});
682+
683+
match event {
684+
ref e @ Event::ChannelReady { counterparty_node_id: Some(node_id), .. }
685+
if node_id == counterparty_node_id =>
686+
{
687+
println!("{} got event {:?}", node.node_id(), e);
688+
remaining_count -= 1;
689+
},
690+
ref e @ Event::ChannelReady { .. } => {
691+
panic!("{} got unexpected ChannelReady event: {:?}", node.node_id(), e);
692+
},
693+
_ => {},
694+
}
695+
node.event_handled().unwrap();
696+
}
697+
}
698+
411699
fn drain_events(node: &Node) {
412700
while node.next_event().is_some() {
413701
node.event_handled().unwrap();

0 commit comments

Comments
 (0)