@@ -41,6 +41,8 @@ use lightning::routing::scoring::{
4141} ;
4242use lightning:: sign:: { EntropySource , NodeSigner } ;
4343use lightning:: util:: config:: HTLCInterceptionFlags ;
44+ #[ cfg( feature = "storage-tier" ) ]
45+ use lightning:: util:: persist:: MigratableKVStore ;
4446use lightning:: util:: persist:: {
4547 KVStore , PaginatedKVStore , CHANNEL_MANAGER_PERSISTENCE_KEY ,
4648 CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE , CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE ,
@@ -73,7 +75,7 @@ use crate::io::fs_store::open_or_migrate_fs_store;
7375#[ cfg( feature = "storage-sqlite" ) ]
7476use crate :: io:: sqlite_store:: SqliteStore ;
7577#[ cfg( feature = "storage-tier" ) ]
76- use crate :: io:: tier_store:: { setup_index_store, TierStore } ;
78+ use crate :: io:: tier_store:: { setup_index_store, BackupSyncStatus , TierStore } ;
7779use crate :: io:: utils:: {
7880 read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache,
7981 read_n_objects, read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info,
@@ -959,13 +961,34 @@ impl NodeBuilder {
959961 ///
960962 /// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
961963 /// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
964+ #[ cfg( not( feature = "storage-tier" ) ) ]
962965 pub fn build_with_store < S : PaginatedKVStore + Send + Sync + ' static > (
963966 & self , node_entropy : NodeEntropy , kv_store : S ,
964967 ) -> Result < Node , BuildError > {
965968 let logger = setup_logger ( & self . log_writer_config , & self . config ) ?;
966969 self . build_with_store_and_logger ( node_entropy, kv_store, logger)
967970 }
968971
972+ /// Builds a [`Node`] instance according to the options previously configured.
973+ ///
974+ /// The provided `kv_store` will be used as the primary storage backend. Optionally,
975+ /// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
976+ /// and a local SQLite backup store for disaster recovery can be configured via
977+ /// [`set_ephemeral_storage_dir_path`] and [`set_backup_storage_dir_path`].
978+ ///
979+ /// The store must implement [`MigratableKVStore`] so a configured backup can be backfilled or
980+ /// resilvered from the complete set of primary-store keys before the node starts.
981+ ///
982+ /// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
983+ /// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
984+ #[ cfg( feature = "storage-tier" ) ]
985+ pub fn build_with_store < S : PaginatedKVStore + MigratableKVStore + Send + Sync + ' static > (
986+ & self , node_entropy : NodeEntropy , kv_store : S ,
987+ ) -> Result < Node , BuildError > {
988+ let logger = setup_logger ( & self . log_writer_config , & self . config ) ?;
989+ self . build_with_store_and_logger ( node_entropy, kv_store, logger)
990+ }
991+
969992 fn setup_runtime ( & self , logger : & Arc < Logger > ) -> Result < Arc < Runtime > , BuildError > {
970993 if let Some ( handle) = self . runtime_handle . as_ref ( ) {
971994 Ok ( Arc :: new ( Runtime :: with_handle ( handle. clone ( ) , Arc :: clone ( logger) ) ) )
@@ -977,17 +1000,38 @@ impl NodeBuilder {
9771000 }
9781001 }
9791002
1003+ #[ cfg( not( feature = "storage-tier" ) ) ]
9801004 fn build_with_store_and_logger < S : PaginatedKVStore + Send + Sync + ' static > (
9811005 & self , node_entropy : NodeEntropy , kv_store : S , logger : Arc < Logger > ,
9821006 ) -> Result < Node , BuildError > {
9831007 let runtime = self . setup_runtime ( & logger) ?;
9841008 self . build_with_store_runtime_and_logger ( node_entropy, kv_store, runtime, logger)
9851009 }
9861010
1011+ #[ cfg( feature = "storage-tier" ) ]
1012+ fn build_with_store_and_logger <
1013+ S : PaginatedKVStore + MigratableKVStore + Send + Sync + ' static ,
1014+ > (
1015+ & self , node_entropy : NodeEntropy , kv_store : S , logger : Arc < Logger > ,
1016+ ) -> Result < Node , BuildError > {
1017+ let runtime = self . setup_runtime ( & logger) ?;
1018+ self . build_with_store_runtime_and_logger ( node_entropy, kv_store, runtime, logger)
1019+ }
1020+
1021+ #[ cfg( not( feature = "storage-tier" ) ) ]
9871022 fn build_with_store_runtime_and_logger < S : PaginatedKVStore + Send + Sync + ' static > (
9881023 & self , node_entropy : NodeEntropy , kv_store : S , runtime : Arc < Runtime > , logger : Arc < Logger > ,
9891024 ) -> Result < Node , BuildError > {
990- #[ cfg( feature = "storage-tier" ) ]
1025+ let store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( kv_store) ) ;
1026+ self . build_with_dyn_store ( node_entropy, store, runtime, logger)
1027+ }
1028+
1029+ #[ cfg( feature = "storage-tier" ) ]
1030+ fn build_with_store_runtime_and_logger <
1031+ S : PaginatedKVStore + MigratableKVStore + Send + Sync + ' static ,
1032+ > (
1033+ & self , node_entropy : NodeEntropy , kv_store : S , runtime : Arc < Runtime > , logger : Arc < Logger > ,
1034+ ) -> Result < Node , BuildError > {
9911035 let store: Arc < DynStore > = {
9921036 let ts_config = self . tier_store_config . as_ref ( ) ;
9931037 let primary_store = Arc :: new ( DynStoreWrapper ( kv_store) ) ;
@@ -1040,15 +1084,27 @@ impl NodeBuilder {
10401084 tier_store. set_backup_store ( backup_store) ;
10411085 }
10421086 }
1043- runtime. block_on ( tier_store. initialize_backup_synchronization ( ) ) . map_err ( |e| {
1044- log_error ! ( logger, "Failed to initialize tier-store backup synchronization: {}" , e) ;
1045- BuildError :: KVStoreSetupFailed
1046- } ) ?;
1087+ runtime
1088+ . block_on ( async {
1089+ let status = tier_store. initialize_backup_synchronization ( ) . await ?;
1090+ if status == BackupSyncStatus :: Required {
1091+ tier_store. synchronize_backup ( ) . await ?;
1092+ }
1093+ Ok :: < ( ) , bitcoin:: io:: Error > ( ( ) )
1094+ } )
1095+ . map_err ( |e| {
1096+ log_error ! ( logger, "Failed to prepare or synchronize tier-store backup: {}" , e) ;
1097+ BuildError :: KVStoreSetupFailed
1098+ } ) ?;
10471099 Arc :: new ( DynStoreWrapper ( tier_store) )
10481100 } ;
1049- # [ cfg ( not ( feature = "storage-tier" ) ) ]
1050- let store : Arc < DynStore > = Arc :: new ( DynStoreWrapper ( kv_store ) ) ;
1101+ self . build_with_dyn_store ( node_entropy , store , runtime , logger )
1102+ }
10511103
1104+ fn build_with_dyn_store (
1105+ & self , node_entropy : NodeEntropy , store : Arc < DynStore > , runtime : Arc < Runtime > ,
1106+ logger : Arc < Logger > ,
1107+ ) -> Result < Node , BuildError > {
10521108 let seed_bytes = node_entropy. to_seed_bytes ( ) ;
10531109 let config = Arc :: new ( self . config . clone ( ) ) ;
10541110
@@ -1594,11 +1650,19 @@ impl ArcedNodeBuilder {
15941650 /// Builds a [`Node`] instance according to the options previously configured.
15951651 // Note that the generics here don't actually work for Uniffi, but we don't currently expose
15961652 // this so its not needed.
1653+ #[ cfg( not( feature = "storage-tier" ) ) ]
15971654 pub fn build_with_store < S : PaginatedKVStore + Send + Sync + ' static > (
15981655 & self , node_entropy : Arc < NodeEntropy > , kv_store : S ,
15991656 ) -> Result < Arc < Node > , BuildError > {
16001657 self . inner . read ( ) . expect ( "lock" ) . build_with_store ( * node_entropy, kv_store) . map ( Arc :: new)
16011658 }
1659+
1660+ #[ cfg( feature = "storage-tier" ) ]
1661+ pub fn build_with_store < S : PaginatedKVStore + MigratableKVStore + Send + Sync + ' static > (
1662+ & self , node_entropy : Arc < NodeEntropy > , kv_store : S ,
1663+ ) -> Result < Arc < Node > , BuildError > {
1664+ self . inner . read ( ) . expect ( "lock" ) . build_with_store ( * node_entropy, kv_store) . map ( Arc :: new)
1665+ }
16021666}
16031667
16041668/// Builds a [`Node`] instance according to the options previously configured.
@@ -2698,30 +2762,66 @@ pub(crate) fn sanitize_alias(alias_str: &str) -> Result<NodeAlias, BuildError> {
26982762#[ cfg( test) ]
26992763mod tests {
27002764 use std:: future:: Future ;
2765+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2766+ use std:: path:: PathBuf ;
27012767 use std:: sync:: Arc ;
27022768
27032769 use lightning:: io;
27042770 use lightning:: util:: persist:: {
2705- KVStore , PageToken , PaginatedKVStore , PaginatedListResponse ,
2771+ KVStore , MigratableKVStore , PageToken , PaginatedKVStore , PaginatedListResponse ,
27062772 CHANNEL_MANAGER_PERSISTENCE_KEY , CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE ,
27072773 CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE ,
27082774 } ;
27092775
27102776 use super :: { sanitize_alias, BuildError , NodeAlias , NodeBuilder } ;
27112777 use crate :: entropy:: NodeEntropy ;
27122778 use crate :: io:: test_utils:: InMemoryStore ;
2779+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2780+ use crate :: io:: {
2781+ sqlite_store:: { SqliteStore , KV_TABLE_NAME , SQLITE_BACKUP_DB_FILE_NAME } ,
2782+ test_utils:: random_storage_path,
2783+ } ;
27132784 use crate :: logger:: Logger ;
27142785
2715- struct ChannelManagerReadFailingStore ( InMemoryStore ) ;
2786+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2787+ struct CleanupDir ( PathBuf ) ;
2788+
2789+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2790+ impl Drop for CleanupDir {
2791+ fn drop ( & mut self ) {
2792+ let _ = std:: fs:: remove_dir_all ( & self . 0 ) ;
2793+ }
2794+ }
2795+
2796+ #[ derive( Clone , Copy ) ]
2797+ enum BuilderStoreBehavior {
2798+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2799+ Normal ,
2800+ FailChannelManagerRead ,
2801+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2802+ FailListAllKeys ,
2803+ }
2804+
2805+ struct BuilderTestStore {
2806+ inner : InMemoryStore ,
2807+ behavior : BuilderStoreBehavior ,
2808+ }
2809+
2810+ impl BuilderTestStore {
2811+ fn new ( behavior : BuilderStoreBehavior ) -> Self {
2812+ Self { inner : InMemoryStore :: new ( ) , behavior }
2813+ }
2814+ }
27162815
2717- impl KVStore for ChannelManagerReadFailingStore {
2816+ impl KVStore for BuilderTestStore {
27182817 fn read (
27192818 & self , primary_namespace : & str , secondary_namespace : & str , key : & str ,
27202819 ) -> impl Future < Output = Result < Vec < u8 > , io:: Error > > + ' static + Send {
2721- let fail_read = primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE
2820+ let fail_read = matches ! ( self . behavior, BuilderStoreBehavior :: FailChannelManagerRead )
2821+ && primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE
27222822 && secondary_namespace == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE
27232823 && key == CHANNEL_MANAGER_PERSISTENCE_KEY ;
2724- let read = KVStore :: read ( & self . 0 , primary_namespace, secondary_namespace, key) ;
2824+ let read = KVStore :: read ( & self . inner , primary_namespace, secondary_namespace, key) ;
27252825 async move {
27262826 if fail_read {
27272827 Err ( io:: Error :: new ( io:: ErrorKind :: Other , "channel manager read failed" ) )
@@ -2734,36 +2834,56 @@ mod tests {
27342834 fn write (
27352835 & self , primary_namespace : & str , secondary_namespace : & str , key : & str , buf : Vec < u8 > ,
27362836 ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + Send {
2737- KVStore :: write ( & self . 0 , primary_namespace, secondary_namespace, key, buf)
2837+ KVStore :: write ( & self . inner , primary_namespace, secondary_namespace, key, buf)
27382838 }
27392839
27402840 fn remove (
27412841 & self , primary_namespace : & str , secondary_namespace : & str , key : & str , lazy : bool ,
27422842 ) -> impl Future < Output = Result < ( ) , io:: Error > > + ' static + Send {
2743- KVStore :: remove ( & self . 0 , primary_namespace, secondary_namespace, key, lazy)
2843+ KVStore :: remove ( & self . inner , primary_namespace, secondary_namespace, key, lazy)
27442844 }
27452845
27462846 fn list (
27472847 & self , primary_namespace : & str , secondary_namespace : & str ,
27482848 ) -> impl Future < Output = Result < Vec < String > , io:: Error > > + ' static + Send {
2749- KVStore :: list ( & self . 0 , primary_namespace, secondary_namespace)
2849+ KVStore :: list ( & self . inner , primary_namespace, secondary_namespace)
27502850 }
27512851 }
27522852
2753- impl PaginatedKVStore for ChannelManagerReadFailingStore {
2853+ impl PaginatedKVStore for BuilderTestStore {
27542854 fn list_paginated (
27552855 & self , primary_namespace : & str , secondary_namespace : & str ,
27562856 page_token : Option < PageToken > ,
27572857 ) -> impl Future < Output = Result < PaginatedListResponse , io:: Error > > + ' static + Send {
27582858 PaginatedKVStore :: list_paginated (
2759- & self . 0 ,
2859+ & self . inner ,
27602860 primary_namespace,
27612861 secondary_namespace,
27622862 page_token,
27632863 )
27642864 }
27652865 }
27662866
2867+ impl MigratableKVStore for BuilderTestStore {
2868+ fn list_all_keys (
2869+ & self ,
2870+ ) -> impl Future < Output = Result < Vec < ( String , String , String ) > , io:: Error > > + ' static + Send
2871+ {
2872+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2873+ let fail = matches ! ( self . behavior, BuilderStoreBehavior :: FailListAllKeys ) ;
2874+ #[ cfg( any( not( feature = "storage-tier" ) , feature = "uniffi" ) ) ]
2875+ let fail = false ;
2876+ let list = MigratableKVStore :: list_all_keys ( & self . inner ) ;
2877+ async move {
2878+ if fail {
2879+ Err ( io:: Error :: new ( io:: ErrorKind :: Other , "list_all_keys failed" ) )
2880+ } else {
2881+ list. await
2882+ }
2883+ }
2884+ }
2885+ }
2886+
27672887 #[ test]
27682888 fn channel_manager_read_failure_fails_build ( ) {
27692889 let builder = NodeBuilder :: new ( ) ;
@@ -2775,13 +2895,80 @@ mod tests {
27752895
27762896 let result = builder. build_with_store_and_logger (
27772897 node_entropy,
2778- ChannelManagerReadFailingStore ( InMemoryStore :: new ( ) ) ,
2898+ BuilderTestStore :: new ( BuilderStoreBehavior :: FailChannelManagerRead ) ,
27792899 logger,
27802900 ) ;
27812901
27822902 assert ! ( matches!( result, Err ( BuildError :: ReadFailed ) ) ) ;
27832903 }
27842904
2905+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2906+ #[ test]
2907+ fn builder_synchronizes_a_configured_backup_before_returning ( ) {
2908+ let base_dir = random_storage_path ( ) ;
2909+ let _cleanup = CleanupDir ( base_dir. clone ( ) ) ;
2910+ let storage_dir = base_dir. join ( "node" ) ;
2911+ let backup_dir = base_dir. join ( "backup" ) ;
2912+ let runtime = tokio:: runtime:: Runtime :: new ( ) . unwrap ( ) ;
2913+ let primary_store = BuilderTestStore :: new ( BuilderStoreBehavior :: Normal ) ;
2914+ runtime. block_on ( primary_store. write ( "namespace" , "" , "current" , vec ! [ 1 ] ) ) . unwrap ( ) ;
2915+ let backup_store = SqliteStore :: new (
2916+ backup_dir. clone ( ) ,
2917+ Some ( SQLITE_BACKUP_DB_FILE_NAME . to_string ( ) ) ,
2918+ Some ( KV_TABLE_NAME . to_string ( ) ) ,
2919+ )
2920+ . unwrap ( ) ;
2921+ runtime
2922+ . block_on ( async { backup_store. write ( "namespace" , "" , "stale" , vec ! [ 2 ] ) . await } )
2923+ . unwrap ( ) ;
2924+ drop ( backup_store) ;
2925+ drop ( runtime) ;
2926+
2927+ let mut builder = NodeBuilder :: new ( ) ;
2928+ builder
2929+ . set_storage_dir_path ( storage_dir. to_string_lossy ( ) . into_owned ( ) )
2930+ . set_backup_storage_dir_path ( backup_dir. to_string_lossy ( ) . into_owned ( ) ) ;
2931+ let node = builder
2932+ . build_with_store ( NodeEntropy :: from_seed_bytes ( [ 42 ; 64 ] ) , primary_store)
2933+ . unwrap ( ) ;
2934+
2935+ let backup_store = SqliteStore :: new (
2936+ backup_dir,
2937+ Some ( SQLITE_BACKUP_DB_FILE_NAME . to_string ( ) ) ,
2938+ Some ( KV_TABLE_NAME . to_string ( ) ) ,
2939+ )
2940+ . unwrap ( ) ;
2941+ let runtime = tokio:: runtime:: Runtime :: new ( ) . unwrap ( ) ;
2942+ assert_eq ! (
2943+ runtime
2944+ . block_on( async { backup_store. read( "namespace" , "" , "current" ) . await } )
2945+ . unwrap( ) ,
2946+ vec![ 1 ]
2947+ ) ;
2948+ assert ! ( runtime
2949+ . block_on( async { backup_store. read( "namespace" , "" , "stale" ) . await } )
2950+ . is_err( ) ) ;
2951+ drop ( node) ;
2952+ }
2953+
2954+ #[ cfg( all( feature = "storage-tier" , not( feature = "uniffi" ) ) ) ]
2955+ #[ test]
2956+ fn builder_fails_when_backup_synchronization_cannot_list_primary_keys ( ) {
2957+ let base_dir = random_storage_path ( ) ;
2958+ let _cleanup = CleanupDir ( base_dir. clone ( ) ) ;
2959+ let mut builder = NodeBuilder :: new ( ) ;
2960+ builder
2961+ . set_storage_dir_path ( base_dir. join ( "node" ) . to_string_lossy ( ) . into_owned ( ) )
2962+ . set_backup_storage_dir_path ( base_dir. join ( "backup" ) . to_string_lossy ( ) . into_owned ( ) ) ;
2963+
2964+ let result = builder. build_with_store (
2965+ NodeEntropy :: from_seed_bytes ( [ 42 ; 64 ] ) ,
2966+ BuilderTestStore :: new ( BuilderStoreBehavior :: FailListAllKeys ) ,
2967+ ) ;
2968+
2969+ assert ! ( matches!( result, Err ( BuildError :: KVStoreSetupFailed ) ) ) ;
2970+ }
2971+
27852972 #[ test]
27862973 fn sanitize_empty_node_alias ( ) {
27872974 // Empty node alias
0 commit comments