@@ -11,19 +11,66 @@ mod common;
1111use std:: sync:: Arc ;
1212use std:: time:: { Duration , Instant } ;
1313
14+ use bitcoin:: secp256k1:: PublicKey ;
1415use bitcoin:: Amount ;
1516use common:: {
1617 expect_channel_pending_event, expect_channel_ready_event, expect_event,
17- generate_blocks_and_wait, premine_and_distribute_funds, random_config,
18+ generate_blocks_and_wait, premine_and_distribute_funds, random_config, random_storage_path ,
1819 setup_bitcoind_and_electrsd, setup_node, setup_two_nodes_with_store,
1920} ;
2021use criterion:: { criterion_group, criterion_main, Criterion } ;
2122use electrsd:: corepc_node:: { Client as BitcoindClient , Node as BitcoinD } ;
23+ use ldk_node:: io:: sqlite_store:: { SqliteStore , KV_TABLE_NAME , SQLITE_DB_FILE_NAME } ;
2224use ldk_node:: { Event , Node } ;
2325use lightning:: ln:: channelmanager:: PaymentId ;
26+ use lightning:: util:: persist:: migrate_kv_store_data_async;
2427use lightning_invoice:: { Bolt11InvoiceDescription , Description } ;
28+ use lightning_persister:: fs_store:: v2:: FilesystemStoreV2 ;
29+
30+ use crate :: common:: { open_channel_push_amt, TestChainSource , TestConfig , TestStoreType } ;
31+
32+ #[ cfg( feature = "postgres" ) ]
33+ use ldk_node:: io:: postgres_store:: { PostgresStore , POSTGRES_TEST_URL_ENV_VAR } ;
34+
35+ const STARTUP_SEED_SCENARIOS : [ StartupSeedScenario ; 6 ] = [
36+ StartupSeedScenario { channel_count : 1 , payment_count : 2 } ,
37+ StartupSeedScenario { channel_count : 1 , payment_count : 100 } ,
38+ StartupSeedScenario { channel_count : 1 , payment_count : 1_000 } ,
39+ StartupSeedScenario { channel_count : 10 , payment_count : 2 } ,
40+ StartupSeedScenario { channel_count : 100 , payment_count : 2 } ,
41+ StartupSeedScenario { channel_count : 100 , payment_count : 1_000 } ,
42+ ] ;
43+ const STARTUP_SEED_PAYMENT_AMOUNT_MSAT : u64 = 1_000_000 ;
44+ const STARTUP_SEED_MIN_CHANNEL_FUNDING_SAT : u64 = 100_000 ;
45+ const STARTUP_SEED_CHANNEL_BUFFER_SAT : u64 = 1_000_000 ;
46+ const STARTUP_SEED_CHANNEL_BATCH_SIZE : u64 = 2 ;
2547
26- use crate :: common:: { open_channel_push_amt, TestChainSource , TestStoreType } ;
48+ #[ derive( Clone , Copy ) ]
49+ struct StartupSeedScenario {
50+ channel_count : u64 ,
51+ payment_count : u64 ,
52+ }
53+
54+ impl StartupSeedScenario {
55+ fn bench_name ( self , store_name : & str ) -> String {
56+ format ! ( "{}/channels_{}_payments_{}" , store_name, self . channel_count, self . payment_count)
57+ }
58+
59+ fn runs_in_ci ( self ) -> bool {
60+ self . channel_count == 1 && self . payment_count == 2
61+ }
62+
63+ fn channel_funding_sat ( self ) -> u64 {
64+ let payment_amount_sat = STARTUP_SEED_PAYMENT_AMOUNT_MSAT / 1_000 ;
65+ let payment_funding_sat =
66+ self . payment_count * payment_amount_sat + STARTUP_SEED_CHANNEL_BUFFER_SAT ;
67+ payment_funding_sat. max ( STARTUP_SEED_MIN_CHANNEL_FUNDING_SAT )
68+ }
69+
70+ fn premine_amount_sat ( self ) -> u64 {
71+ self . channel_count * self . channel_funding_sat ( ) + STARTUP_SEED_CHANNEL_BUFFER_SAT
72+ }
73+ }
2774
2875#[ derive( Clone , Copy ) ]
2976struct StoreBenchConfig {
@@ -34,6 +81,7 @@ struct StoreBenchConfig {
3481fn operations_benchmark ( c : & mut Criterion ) {
3582 forwarding_benchmark ( c) ;
3683 channel_open_benchmark ( c) ;
84+ startup_benchmark ( c) ;
3785}
3886
3987fn forwarding_benchmark ( c : & mut Criterion ) {
@@ -147,6 +195,230 @@ fn channel_open_benchmark(c: &mut Criterion) {
147195 }
148196}
149197
198+ fn startup_benchmark ( c : & mut Criterion ) {
199+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
200+ let chain_source = TestChainSource :: BitcoindRpcSync ( & bitcoind) ;
201+ let runtime = benchmark_runtime ( ) ;
202+
203+ let mut group = c. benchmark_group ( "startup" ) ;
204+ group. sample_size ( 10 ) ;
205+
206+ for startup_seed_scenario in STARTUP_SEED_SCENARIOS {
207+ // Larger seeded startup scenarios are useful locally, but take too long to run in CI.
208+ if is_ci ( ) && !startup_seed_scenario. runs_in_ci ( ) {
209+ continue ;
210+ }
211+
212+ let matching_store_configs: Vec < _ > = store_bench_configs ( )
213+ . into_iter ( )
214+ . filter ( |store_config| {
215+ let bench_name = startup_seed_scenario. bench_name ( store_config. name ) ;
216+ should_register_bench ( "startup" , & bench_name)
217+ } )
218+ . collect ( ) ;
219+ if matching_store_configs. is_empty ( ) {
220+ continue ;
221+ }
222+
223+ // Seed a canonical sqlite node once, then copy its store into each backend under test. This
224+ // keeps the channel/payment history identical across stores while avoiding repeated expensive
225+ // channel and payment setup for every store backend.
226+ let seeded_config = setup_startup_seed_node (
227+ & chain_source,
228+ & bitcoind,
229+ & electrsd,
230+ startup_seed_scenario,
231+ & runtime,
232+ ) ;
233+ let startup_configs = migrate_startup_seed_configs (
234+ & seeded_config,
235+ startup_seed_scenario,
236+ matching_store_configs,
237+ & runtime,
238+ ) ;
239+
240+ for ( bench_name, config) in startup_configs {
241+ group. bench_function ( bench_name, |b| {
242+ b. iter_custom ( |iter| {
243+ let mut total = Duration :: ZERO ;
244+ for _ in 0 ..iter {
245+ let start = Instant :: now ( ) ;
246+ let node = setup_node ( & chain_source, config. clone ( ) ) ;
247+ total += start. elapsed ( ) ;
248+ node. stop ( ) . unwrap ( ) ;
249+ }
250+ total
251+ } ) ;
252+ } ) ;
253+ }
254+ }
255+ }
256+
257+ /// Builds a canonical sqlite node store with the requested channel and payment history.
258+ ///
259+ /// Startup benchmarks use this store as the source fixture for every backend so differences in
260+ /// measured startup time come from loading equivalent persisted state, not from different setup
261+ /// runs.
262+ fn setup_startup_seed_node (
263+ chain_source : & TestChainSource , bitcoind : & BitcoinD , electrsd : & electrsd:: ElectrsD ,
264+ seed_scenario : StartupSeedScenario , runtime : & tokio:: runtime:: Runtime ,
265+ ) -> TestConfig {
266+ let mut config_a = random_config ( true ) ;
267+ config_a. store_type = TestStoreType :: Sqlite ;
268+ let node_a = Arc :: new ( setup_node ( chain_source, config_a. clone ( ) ) ) ;
269+
270+ let mut config_b = random_config ( true ) ;
271+ config_b. store_type = TestStoreType :: Sqlite ;
272+ let node_b = Arc :: new ( setup_node ( chain_source, config_b) ) ;
273+
274+ runtime. block_on ( async {
275+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
276+ premine_and_distribute_funds (
277+ & bitcoind. client ,
278+ & electrsd. client ,
279+ vec ! [ address_a] ,
280+ Amount :: from_sat ( seed_scenario. premine_amount_sat ( ) ) ,
281+ )
282+ . await ;
283+ node_a. sync_wallets ( ) . unwrap ( ) ;
284+ node_b. sync_wallets ( ) . unwrap ( ) ;
285+
286+ let funding_amount_sat = seed_scenario. channel_funding_sat ( ) ;
287+ let mut remaining_channel_count = seed_scenario. channel_count ;
288+ while remaining_channel_count > 0 {
289+ let channel_batch_size = remaining_channel_count. min ( STARTUP_SEED_CHANNEL_BATCH_SIZE ) ;
290+ for _ in 0 ..channel_batch_size {
291+ node_a
292+ . open_channel (
293+ node_b. node_id ( ) ,
294+ node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ,
295+ funding_amount_sat,
296+ None ,
297+ None ,
298+ )
299+ . unwrap ( ) ;
300+ assert ! ( node_a. list_peers( ) . iter( ) . any( |peer| peer. node_id == node_b. node_id( ) ) ) ;
301+
302+ let funding_txo_a = expect_channel_pending_event ! ( node_a, node_b. node_id( ) ) ;
303+ let funding_txo_b = expect_channel_pending_event ! ( node_b, node_a. node_id( ) ) ;
304+ assert_eq ! ( funding_txo_a, funding_txo_b) ;
305+ node_a. sync_wallets ( ) . unwrap ( ) ;
306+ }
307+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
308+
309+ for _ in 0 ..channel_batch_size {
310+ node_a. sync_wallets ( ) . unwrap ( ) ;
311+ node_b. sync_wallets ( ) . unwrap ( ) ;
312+ wait_for_channel_ready_events ( & node_a, node_b. node_id ( ) , 1 ) . await ;
313+ wait_for_channel_ready_events ( & node_b, node_a. node_id ( ) , 1 ) . await ;
314+ }
315+ remaining_channel_count -= channel_batch_size;
316+ }
317+
318+ for idx in 0 ..seed_scenario. payment_count {
319+ let invoice_description = Bolt11InvoiceDescription :: Direct (
320+ Description :: new ( format ! ( "startup seed {}" , idx + 1 ) ) . unwrap ( ) ,
321+ ) ;
322+ let invoice = node_b
323+ . bolt11_payment ( )
324+ . receive ( STARTUP_SEED_PAYMENT_AMOUNT_MSAT , & invoice_description. into ( ) , 9217 )
325+ . unwrap ( ) ;
326+ let payment_id = node_a. bolt11_payment ( ) . send ( & invoice, None ) . unwrap ( ) ;
327+ wait_for_payment_success ( & node_a, payment_id) . await ;
328+ }
329+
330+ drain_events ( & node_a) ;
331+ drain_events ( & node_b) ;
332+ } ) ;
333+
334+ node_a. stop ( ) . unwrap ( ) ;
335+ node_b. stop ( ) . unwrap ( ) ;
336+
337+ config_a
338+ }
339+
340+ /// Produces benchmark configs backed by copies of the canonical seeded store.
341+ ///
342+ /// Sqlite can reuse the source store directly. Other store backends get a fresh storage path and a
343+ /// migrated copy of the same key-value data.
344+ fn migrate_startup_seed_configs (
345+ source_config : & TestConfig , seed_scenario : StartupSeedScenario ,
346+ store_configs : Vec < StoreBenchConfig > , runtime : & tokio:: runtime:: Runtime ,
347+ ) -> Vec < ( String , TestConfig ) > {
348+ // Open the seeded source store with the same db file and table the node itself uses, otherwise
349+ // we'd read from an empty default-named store and migrate nothing into the other backends.
350+ let source_store = SqliteStore :: new (
351+ source_config. node_config . storage_dir_path . clone ( ) . into ( ) ,
352+ Some ( SQLITE_DB_FILE_NAME . to_string ( ) ) ,
353+ Some ( KV_TABLE_NAME . to_string ( ) ) ,
354+ )
355+ . unwrap ( ) ;
356+
357+ store_configs
358+ . into_iter ( )
359+ . map ( |store_config| {
360+ let mut config = source_config. clone ( ) ;
361+ config. store_type = store_config. store_type ;
362+ if !matches ! ( store_config. store_type, TestStoreType :: Sqlite ) {
363+ config. node_config . storage_dir_path =
364+ random_storage_path ( ) . to_str ( ) . unwrap ( ) . to_owned ( ) ;
365+ migrate_startup_seed_store ( & source_store, & config, runtime) ;
366+ }
367+
368+ ( seed_scenario. bench_name ( store_config. name ) , config)
369+ } )
370+ . collect ( )
371+ }
372+
373+ fn migrate_startup_seed_store (
374+ source_store : & SqliteStore , destination_config : & TestConfig , runtime : & tokio:: runtime:: Runtime ,
375+ ) {
376+ runtime. block_on ( async {
377+ match destination_config. store_type {
378+ TestStoreType :: Sqlite => { } ,
379+ TestStoreType :: FilesystemStore => {
380+ let destination_store = FilesystemStoreV2 :: new (
381+ destination_config. node_config . storage_dir_path . clone ( ) . into ( ) ,
382+ )
383+ . unwrap ( ) ;
384+ migrate_kv_store_data_async ( source_store, & destination_store) . await . unwrap ( ) ;
385+ } ,
386+ #[ cfg( feature = "postgres" ) ]
387+ TestStoreType :: Postgres => {
388+ let connection_string = postgres_connection_string ( ) ;
389+ let table_name = postgres_table_name ( destination_config) ;
390+ let destination_store =
391+ PostgresStore :: new ( connection_string, None , Some ( table_name) , None )
392+ . await
393+ . unwrap ( ) ;
394+ migrate_kv_store_data_async ( source_store, & destination_store) . await . unwrap ( ) ;
395+ } ,
396+ TestStoreType :: TestSyncStore => {
397+ unreachable ! ( "startup benches do not use TestSyncStore" )
398+ } ,
399+ }
400+ } ) ;
401+ }
402+
403+ #[ cfg( feature = "postgres" ) ]
404+ fn postgres_connection_string ( ) -> String {
405+ std:: env:: var ( POSTGRES_TEST_URL_ENV_VAR )
406+ . unwrap_or_else ( |_| "host=localhost user=postgres password=postgres" . to_string ( ) )
407+ }
408+
409+ #[ cfg( feature = "postgres" ) ]
410+ fn postgres_table_name ( config : & TestConfig ) -> String {
411+ format ! (
412+ "test_{}" ,
413+ config
414+ . node_config
415+ . storage_dir_path
416+ . chars( )
417+ . filter( |c| c. is_ascii_alphanumeric( ) )
418+ . collect:: <String >( )
419+ )
420+ }
421+
150422/// Returns whether the benchmark identified by `group/name` matches the CLI filters.
151423///
152424/// Criterion applies its own filters after benchmark registration, but these benches do expensive
@@ -163,6 +435,10 @@ fn should_register_bench(group: &str, name: &str) -> bool {
163435 } )
164436}
165437
438+ fn is_ci ( ) -> bool {
439+ std:: env:: var ( "CI" ) . is_ok_and ( |value| !value. is_empty ( ) && value != "0" && value != "false" )
440+ }
441+
166442fn setup_forwarding_nodes (
167443 chain_source : & TestChainSource , bitcoind : & BitcoinD , electrsd : & electrsd:: ElectrsD ,
168444 store_type : TestStoreType , runtime : & tokio:: runtime:: Runtime ,
@@ -373,6 +649,34 @@ async fn wait_for_payment_success(node: &Node, expected_payment_id: PaymentId) {
373649 }
374650}
375651
652+ async fn wait_for_channel_ready_events ( node : & Node , counterparty_node_id : PublicKey , count : u64 ) {
653+ let mut remaining_count = count;
654+ while remaining_count > 0 {
655+ let event = tokio:: time:: timeout (
656+ Duration :: from_secs ( common:: INTEROP_TIMEOUT_SECS ) ,
657+ node. next_event_async ( ) ,
658+ )
659+ . await
660+ . unwrap_or_else ( |_| {
661+ panic ! ( "{} timed out waiting for ChannelReady event after 60s" , node. node_id( ) )
662+ } ) ;
663+
664+ match event {
665+ ref e @ Event :: ChannelReady { counterparty_node_id : Some ( node_id) , .. }
666+ if node_id == counterparty_node_id =>
667+ {
668+ println ! ( "{} got event {:?}" , node. node_id( ) , e) ;
669+ remaining_count -= 1 ;
670+ } ,
671+ ref e @ Event :: ChannelReady { .. } => {
672+ panic ! ( "{} got unexpected ChannelReady event: {:?}" , node. node_id( ) , e) ;
673+ } ,
674+ _ => { } ,
675+ }
676+ node. event_handled ( ) . unwrap ( ) ;
677+ }
678+ }
679+
376680fn drain_events ( node : & Node ) {
377681 while node. next_event ( ) . is_some ( ) {
378682 node. event_handled ( ) . unwrap ( ) ;
0 commit comments