@@ -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 ;
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 {
@@ -36,6 +83,7 @@ fn operations_benchmark(c: &mut Criterion) {
3683
3784 forwarding_benchmark ( c) ;
3885 channel_open_benchmark ( c) ;
86+ startup_benchmark ( c) ;
3987}
4088
4189fn forwarding_benchmark ( c : & mut Criterion ) {
@@ -149,6 +197,226 @@ fn channel_open_benchmark(c: &mut Criterion) {
149197 }
150198}
151199
200+ fn startup_benchmark ( c : & mut Criterion ) {
201+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
202+ let chain_source = TestChainSource :: BitcoindRpcSync ( & bitcoind) ;
203+ let runtime = benchmark_runtime ( ) ;
204+
205+ let mut group = c. benchmark_group ( "startup" ) ;
206+ group. sample_size ( 10 ) ;
207+
208+ for startup_seed_scenario in STARTUP_SEED_SCENARIOS {
209+ // Larger seeded startup scenarios are useful locally, but take too long to run in CI.
210+ if is_ci ( ) && !startup_seed_scenario. runs_in_ci ( ) {
211+ continue ;
212+ }
213+
214+ let matching_store_configs: Vec < _ > = store_bench_configs ( )
215+ . into_iter ( )
216+ . filter ( |store_config| {
217+ let bench_name = startup_seed_scenario. bench_name ( store_config. name ) ;
218+ should_register_bench ( "startup" , & bench_name)
219+ } )
220+ . collect ( ) ;
221+ if matching_store_configs. is_empty ( ) {
222+ continue ;
223+ }
224+
225+ // Seed a canonical sqlite node once, then copy its store into each backend under test. This
226+ // keeps the channel/payment history identical across stores while avoiding repeated expensive
227+ // channel and payment setup for every store backend.
228+ let seeded_config = setup_startup_seed_node (
229+ & chain_source,
230+ & bitcoind,
231+ & electrsd,
232+ startup_seed_scenario,
233+ & runtime,
234+ ) ;
235+ let startup_configs = migrate_startup_seed_configs (
236+ & seeded_config,
237+ startup_seed_scenario,
238+ matching_store_configs,
239+ & runtime,
240+ ) ;
241+
242+ for ( bench_name, config) in startup_configs {
243+ group. bench_function ( bench_name, |b| {
244+ b. iter_custom ( |iter| {
245+ let mut total = Duration :: ZERO ;
246+ for _ in 0 ..iter {
247+ let start = Instant :: now ( ) ;
248+ let node = setup_node ( & chain_source, config. clone ( ) ) ;
249+ total += start. elapsed ( ) ;
250+ node. stop ( ) . unwrap ( ) ;
251+ }
252+ total
253+ } ) ;
254+ } ) ;
255+ }
256+ }
257+ }
258+
259+ /// Builds a canonical sqlite node store with the requested channel and payment history.
260+ ///
261+ /// Startup benchmarks use this store as the source fixture for every backend so differences in
262+ /// measured startup time come from loading equivalent persisted state, not from different setup
263+ /// runs.
264+ fn setup_startup_seed_node (
265+ chain_source : & TestChainSource , bitcoind : & BitcoinD , electrsd : & electrsd:: ElectrsD ,
266+ seed_scenario : StartupSeedScenario , runtime : & tokio:: runtime:: Runtime ,
267+ ) -> TestConfig {
268+ let mut config_a = random_config ( true ) ;
269+ config_a. store_type = TestStoreType :: Sqlite ;
270+ let node_a = Arc :: new ( setup_node ( chain_source, config_a. clone ( ) ) ) ;
271+
272+ let mut config_b = random_config ( true ) ;
273+ config_b. store_type = TestStoreType :: Sqlite ;
274+ let node_b = Arc :: new ( setup_node ( chain_source, config_b) ) ;
275+
276+ runtime. block_on ( async {
277+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
278+ premine_and_distribute_funds (
279+ & bitcoind. client ,
280+ & electrsd. client ,
281+ vec ! [ address_a] ,
282+ Amount :: from_sat ( seed_scenario. premine_amount_sat ( ) ) ,
283+ )
284+ . await ;
285+ node_a. sync_wallets ( ) . unwrap ( ) ;
286+ node_b. sync_wallets ( ) . unwrap ( ) ;
287+
288+ let funding_amount_sat = seed_scenario. channel_funding_sat ( ) ;
289+ let mut remaining_channel_count = seed_scenario. channel_count ;
290+ while remaining_channel_count > 0 {
291+ let channel_batch_size = remaining_channel_count. min ( STARTUP_SEED_CHANNEL_BATCH_SIZE ) ;
292+ for _ in 0 ..channel_batch_size {
293+ node_a
294+ . open_channel (
295+ node_b. node_id ( ) ,
296+ node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ,
297+ funding_amount_sat,
298+ None ,
299+ None ,
300+ )
301+ . unwrap ( ) ;
302+ assert ! ( node_a. list_peers( ) . iter( ) . any( |peer| peer. node_id == node_b. node_id( ) ) ) ;
303+
304+ let funding_txo_a = expect_channel_pending_event ! ( node_a, node_b. node_id( ) ) ;
305+ let funding_txo_b = expect_channel_pending_event ! ( node_b, node_a. node_id( ) ) ;
306+ assert_eq ! ( funding_txo_a, funding_txo_b) ;
307+ node_a. sync_wallets ( ) . unwrap ( ) ;
308+ }
309+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
310+
311+ for _ in 0 ..channel_batch_size {
312+ node_a. sync_wallets ( ) . unwrap ( ) ;
313+ node_b. sync_wallets ( ) . unwrap ( ) ;
314+ wait_for_channel_ready_events ( & node_a, node_b. node_id ( ) , 1 ) . await ;
315+ wait_for_channel_ready_events ( & node_b, node_a. node_id ( ) , 1 ) . await ;
316+ }
317+ remaining_channel_count -= channel_batch_size;
318+ }
319+
320+ for idx in 0 ..seed_scenario. payment_count {
321+ let invoice_description = Bolt11InvoiceDescription :: Direct (
322+ Description :: new ( format ! ( "startup seed {}" , idx + 1 ) ) . unwrap ( ) ,
323+ ) ;
324+ let invoice = node_b
325+ . bolt11_payment ( )
326+ . receive ( STARTUP_SEED_PAYMENT_AMOUNT_MSAT , & invoice_description. into ( ) , 9217 )
327+ . unwrap ( ) ;
328+ let payment_id = node_a. bolt11_payment ( ) . send ( & invoice, None ) . unwrap ( ) ;
329+ wait_for_payment_success ( & node_a, payment_id) . await ;
330+ }
331+
332+ drain_events ( & node_a) ;
333+ drain_events ( & node_b) ;
334+ } ) ;
335+
336+ node_a. stop ( ) . unwrap ( ) ;
337+ node_b. stop ( ) . unwrap ( ) ;
338+
339+ config_a
340+ }
341+
342+ /// Produces benchmark configs backed by copies of the canonical seeded store.
343+ ///
344+ /// Sqlite can reuse the source store directly. Other store backends get a fresh storage path and a
345+ /// migrated copy of the same key-value data.
346+ fn migrate_startup_seed_configs (
347+ source_config : & TestConfig , seed_scenario : StartupSeedScenario ,
348+ store_configs : Vec < StoreBenchConfig > , runtime : & tokio:: runtime:: Runtime ,
349+ ) -> Vec < ( String , TestConfig ) > {
350+ let source_store =
351+ SqliteStore :: new ( source_config. node_config . storage_dir_path . clone ( ) . into ( ) , None , None )
352+ . unwrap ( ) ;
353+
354+ store_configs
355+ . into_iter ( )
356+ . map ( |store_config| {
357+ let mut config = source_config. clone ( ) ;
358+ config. store_type = store_config. store_type ;
359+ if !matches ! ( store_config. store_type, TestStoreType :: Sqlite ) {
360+ config. node_config . storage_dir_path =
361+ random_storage_path ( ) . to_str ( ) . unwrap ( ) . to_owned ( ) ;
362+ migrate_startup_seed_store ( & source_store, & config, runtime) ;
363+ }
364+
365+ ( seed_scenario. bench_name ( store_config. name ) , config)
366+ } )
367+ . collect ( )
368+ }
369+
370+ fn migrate_startup_seed_store (
371+ source_store : & SqliteStore , destination_config : & TestConfig , runtime : & tokio:: runtime:: Runtime ,
372+ ) {
373+ runtime. block_on ( async {
374+ match destination_config. store_type {
375+ TestStoreType :: Sqlite => { } ,
376+ TestStoreType :: FilesystemStore => {
377+ let destination_store = FilesystemStoreV2 :: new (
378+ destination_config. node_config . storage_dir_path . clone ( ) . into ( ) ,
379+ )
380+ . unwrap ( ) ;
381+ migrate_kv_store_data_async ( source_store, & destination_store) . await . unwrap ( ) ;
382+ } ,
383+ #[ cfg( feature = "postgres" ) ]
384+ TestStoreType :: Postgres => {
385+ let connection_string = postgres_connection_string ( ) ;
386+ let table_name = postgres_table_name ( destination_config) ;
387+ let destination_store =
388+ PostgresStore :: new ( connection_string, None , Some ( table_name) , None )
389+ . await
390+ . unwrap ( ) ;
391+ migrate_kv_store_data_async ( source_store, & destination_store) . await . unwrap ( ) ;
392+ } ,
393+ TestStoreType :: TestSyncStore => {
394+ unreachable ! ( "startup benches do not use TestSyncStore" )
395+ } ,
396+ }
397+ } ) ;
398+ }
399+
400+ #[ cfg( feature = "postgres" ) ]
401+ fn postgres_connection_string ( ) -> String {
402+ dotenvy:: dotenv ( ) . ok ( ) ;
403+ std:: env:: var ( POSTGRES_TEST_URL_ENV_VAR )
404+ . unwrap_or_else ( |_| "host=localhost user=postgres password=postgres" . to_string ( ) )
405+ }
406+
407+ #[ cfg( feature = "postgres" ) ]
408+ fn postgres_table_name ( config : & TestConfig ) -> String {
409+ format ! (
410+ "test_{}" ,
411+ config
412+ . node_config
413+ . storage_dir_path
414+ . chars( )
415+ . filter( |c| c. is_ascii_alphanumeric( ) )
416+ . collect:: <String >( )
417+ )
418+ }
419+
152420/// Returns whether the benchmark identified by `group/name` matches the CLI filters.
153421///
154422/// Criterion applies its own filters after benchmark registration, but these benches do expensive
@@ -165,6 +433,10 @@ fn should_register_bench(group: &str, name: &str) -> bool {
165433 } )
166434}
167435
436+ fn is_ci ( ) -> bool {
437+ std:: env:: var ( "CI" ) . is_ok_and ( |value| !value. is_empty ( ) && value != "0" && value != "false" )
438+ }
439+
168440fn setup_forwarding_nodes (
169441 chain_source : & TestChainSource , bitcoind : & BitcoinD , electrsd : & electrsd:: ElectrsD ,
170442 store_type : TestStoreType , runtime : & tokio:: runtime:: Runtime ,
@@ -375,6 +647,34 @@ async fn wait_for_payment_success(node: &Node, expected_payment_id: PaymentId) {
375647 }
376648}
377649
650+ async fn wait_for_channel_ready_events ( node : & Node , counterparty_node_id : PublicKey , count : u64 ) {
651+ let mut remaining_count = count;
652+ while remaining_count > 0 {
653+ let event = tokio:: time:: timeout (
654+ Duration :: from_secs ( common:: INTEROP_TIMEOUT_SECS ) ,
655+ node. next_event_async ( ) ,
656+ )
657+ . await
658+ . unwrap_or_else ( |_| {
659+ panic ! ( "{} timed out waiting for ChannelReady event after 60s" , node. node_id( ) )
660+ } ) ;
661+
662+ match event {
663+ ref e @ Event :: ChannelReady { counterparty_node_id : Some ( node_id) , .. }
664+ if node_id == counterparty_node_id =>
665+ {
666+ println ! ( "{} got event {:?}" , node. node_id( ) , e) ;
667+ remaining_count -= 1 ;
668+ } ,
669+ ref e @ Event :: ChannelReady { .. } => {
670+ panic ! ( "{} got unexpected ChannelReady event: {:?}" , node. node_id( ) , e) ;
671+ } ,
672+ _ => { } ,
673+ }
674+ node. event_handled ( ) . unwrap ( ) ;
675+ }
676+ }
677+
378678fn drain_events ( node : & Node ) {
379679 while node. next_event ( ) . is_some ( ) {
380680 node. event_handled ( ) . unwrap ( ) ;
0 commit comments