@@ -11,20 +11,67 @@ 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,
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} ;
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 ;
2426use lightning:: routing:: router:: RouteParametersConfig ;
27+ use lightning:: util:: persist:: migrate_kv_store_data_async;
2528use 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 ) ]
3077struct 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
4290fn 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+
136392fn 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+
146406fn 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+
411699fn drain_events ( node : & Node ) {
412700 while node. next_event ( ) . is_some ( ) {
413701 node. event_handled ( ) . unwrap ( ) ;
0 commit comments