@@ -52,8 +52,11 @@ use crate::connection::ConnectionManager;
5252use crate :: entropy:: NodeEntropy ;
5353use crate :: event:: EventQueue ;
5454use crate :: fee_estimator:: OnchainFeeEstimator ;
55+ #[ cfg( feature = "uniffi" ) ]
56+ use crate :: ffi:: FfiDynStore ;
5557use crate :: gossip:: GossipSource ;
5658use crate :: io:: sqlite_store:: SqliteStore ;
59+ use crate :: io:: tier_store:: { RetryConfig , TierStore } ;
5760use crate :: io:: utils:: {
5861 read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph,
5962 read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments,
@@ -150,6 +153,23 @@ impl std::fmt::Debug for LogWriterConfig {
150153 }
151154}
152155
156+ #[ derive( Default ) ]
157+ struct TierStoreConfig {
158+ ephemeral : Option < Arc < DynStore > > ,
159+ backup : Option < Arc < DynStore > > ,
160+ retry : Option < RetryConfig > ,
161+ }
162+
163+ impl std:: fmt:: Debug for TierStoreConfig {
164+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
165+ f. debug_struct ( "TierStoreConfig" )
166+ . field ( "ephemeral" , & self . ephemeral . as_ref ( ) . map ( |_| "Arc<DynStore>" ) )
167+ . field ( "backup" , & self . backup . as_ref ( ) . map ( |_| "Arc<DynStore>" ) )
168+ . field ( "retry" , & self . retry )
169+ . finish ( )
170+ }
171+ }
172+
153173/// An error encountered during building a [`Node`].
154174///
155175/// [`Node`]: crate::Node
@@ -242,6 +262,7 @@ pub struct NodeBuilder {
242262 liquidity_source_config : Option < LiquiditySourceConfig > ,
243263 log_writer_config : Option < LogWriterConfig > ,
244264 async_payments_role : Option < AsyncPaymentsRole > ,
265+ tier_store_config : Option < TierStoreConfig > ,
245266 runtime_handle : Option < tokio:: runtime:: Handle > ,
246267 pathfinding_scores_sync_config : Option < PathfindingScoresSyncConfig > ,
247268}
@@ -259,6 +280,7 @@ impl NodeBuilder {
259280 let gossip_source_config = None ;
260281 let liquidity_source_config = None ;
261282 let log_writer_config = None ;
283+ let tier_store_config = None ;
262284 let runtime_handle = None ;
263285 let pathfinding_scores_sync_config = None ;
264286 Self {
@@ -267,6 +289,7 @@ impl NodeBuilder {
267289 gossip_source_config,
268290 liquidity_source_config,
269291 log_writer_config,
292+ tier_store_config,
270293 runtime_handle,
271294 async_payments_role : None ,
272295 pathfinding_scores_sync_config,
@@ -544,6 +567,51 @@ impl NodeBuilder {
544567 Ok ( self )
545568 }
546569
570+ /// Configures retry behavior for transient errors when accessing the primary store.
571+ ///
572+ /// When building with [`build_with_tier_store`], controls the exponential backoff parameters
573+ /// used when retrying failed operations on the primary store due to transient errors
574+ /// (network issues, timeouts, etc.).
575+ ///
576+ /// If not set, default retry parameters are used. See [`RetryConfig`] for details.
577+ ///
578+ /// [`build_with_tier_store`]: Self::build_with_tier_store
579+ pub fn set_tier_store_retry_config ( & mut self , config : RetryConfig ) -> & mut Self {
580+ let tier_store_config = self . tier_store_config . get_or_insert ( TierStoreConfig :: default ( ) ) ;
581+ tier_store_config. retry = Some ( config) ;
582+ self
583+ }
584+
585+ /// Configures the backup store for local disaster recovery.
586+ ///
587+ /// When building with [`build_with_tier_store`], this store receives asynchronous copies
588+ /// of all critical data written to the primary store. If the primary store becomes
589+ /// unavailable, reads will fall back to this backup store.
590+ ///
591+ /// Backup writes are non-blocking and do not affect primary store operation performance.
592+ ///
593+ /// [`build_with_tier_store`]: Self::build_with_tier_store
594+ pub fn set_tier_store_backup ( & mut self , backup_store : Arc < DynStore > ) -> & mut Self {
595+ let tier_store_config = self . tier_store_config . get_or_insert ( TierStoreConfig :: default ( ) ) ;
596+ tier_store_config. backup = Some ( backup_store) ;
597+ self
598+ }
599+
600+ /// Configures the ephemeral store for non-critical, frequently-accessed data.
601+ ///
602+ /// When building with [`build_with_tier_store`], this store is used for data like
603+ /// the network graph and scorer data to reduce latency for reads. Data stored here
604+ /// can be rebuilt if lost.
605+ ///
606+ /// If not set, non-critical data will be stored in the primary store.
607+ ///
608+ /// [`build_with_tier_store`]: Self::build_with_tier_store
609+ pub fn set_tier_store_ephemeral ( & mut self , ephemeral_store : Arc < DynStore > ) -> & mut Self {
610+ let tier_store_config = self . tier_store_config . get_or_insert ( TierStoreConfig :: default ( ) ) ;
611+ tier_store_config. ephemeral = Some ( ephemeral_store) ;
612+ self
613+ }
614+
547615 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
548616 /// previously configured.
549617 pub fn build ( & self , node_entropy : NodeEntropy ) -> Result < Node , BuildError > {
@@ -556,6 +624,7 @@ impl NodeBuilder {
556624 Some ( io:: sqlite_store:: KV_TABLE_NAME . to_string ( ) ) ,
557625 )
558626 . map_err ( |_| BuildError :: KVStoreSetupFailed ) ?;
627+
559628 self . build_with_store ( node_entropy, kv_store)
560629 }
561630
@@ -568,6 +637,7 @@ impl NodeBuilder {
568637 fs:: create_dir_all ( storage_dir_path. clone ( ) )
569638 . map_err ( |_| BuildError :: StoragePathAccessFailed ) ?;
570639 let kv_store = FilesystemStore :: new ( storage_dir_path) ;
640+
571641 self . build_with_store ( node_entropy, kv_store)
572642 }
573643
@@ -654,6 +724,91 @@ impl NodeBuilder {
654724 self . build_with_store ( node_entropy, vss_store)
655725 }
656726
727+ /// Builds a [`Node`] instance with tiered storage for managing data across multiple storage layers.
728+ ///
729+ /// This build method enables a three-tier storage architecture optimized for different data types
730+ /// and access patterns:
731+ ///
732+ /// ### Storage Tiers
733+ ///
734+ /// - **Primary Store** (required): The authoritative store for critical channel state and payment data.
735+ /// Typically a remote/cloud storage service for durability and accessibility across devices.
736+ ///
737+ /// - **Ephemeral Store** (optional): Local storage for non-critical, frequently-accessed data like
738+ /// the network graph and scorer. Improves performance by reducing latency for data that can be
739+ /// rebuilt if lost. Configure with [`set_tier_store_ephemeral`].
740+ ///
741+ /// - **Backup Store** (optional): Local backup of critical data for disaster recovery scenarios.
742+ /// Provides a safety net if the primary store becomes temporarily unavailable. Writes are
743+ /// asynchronous to avoid blocking primary operations. Configure with [`set_tier_store_backup`].
744+ ///
745+ /// ## Configuration
746+ ///
747+ /// Use the setter methods to configure optional stores and retry behavior:
748+ /// - [`set_tier_store_ephemeral`] - Set local store for network graph and scorer
749+ /// - [`set_tier_store_backup`] - Set local backup store for disaster recovery
750+ /// - [`set_tier_store_retry_config`] - Configure retry delays and backoff for transient errors
751+ ///
752+ /// ## Example
753+ ///
754+ /// ```ignore
755+ /// # use ldk_node::{Builder, Config};
756+ /// # use ldk_node::io::tier_store::RetryConfig;
757+ /// # use std::sync::Arc;
758+ /// let config = Config::default();
759+ /// let mut builder = NodeBuilder::from_config(config);
760+ ///
761+ /// let primary = Arc::new(VssStore::new(...));
762+ /// let ephemeral = Arc::new(FilesystemStore::new(...));
763+ /// let backup = Arc::new(SqliteStore::new(...));
764+ /// let retry_config = RetryConfig::default();
765+ ///
766+ /// builder
767+ /// .set_tier_store_ephemeral(ephemeral)
768+ /// .set_tier_store_backup(backup)
769+ /// .set_tier_store_retry_config(retry_config);
770+ ///
771+ /// let node = builder.build_with_tier_store(primary)?;
772+ /// # Ok::<(), ldk_node::BuildError>(())
773+ /// ```
774+ ///
775+ /// [`set_tier_store_ephemeral`]: Self::set_tier_store_ephemeral
776+ /// [`set_tier_store_backup`]: Self::set_tier_store_backup
777+ /// [`set_tier_store_retry_config`]: Self::set_tier_store_retry_config
778+ #[ cfg( not( feature = "uniffi" ) ) ]
779+ pub fn build_with_tier_store (
780+ & self , node_entropy : NodeEntropy , primary_store : Arc < DynStore > ,
781+ ) -> Result < Node , BuildError > {
782+ self . build_with_tier_store_internal ( node_entropy, primary_store)
783+ }
784+
785+ fn build_with_tier_store_internal (
786+ & self , node_entropy : NodeEntropy , primary_store : Arc < DynStore > ,
787+ ) -> Result < Node , BuildError > {
788+ let logger = setup_logger ( & self . log_writer_config , & self . config ) ?;
789+ let runtime = if let Some ( handle) = self . runtime_handle . as_ref ( ) {
790+ Arc :: new ( Runtime :: with_handle ( handle. clone ( ) , Arc :: clone ( & logger) ) )
791+ } else {
792+ Arc :: new ( Runtime :: new ( Arc :: clone ( & logger) ) . map_err ( |e| {
793+ log_error ! ( logger, "Failed to setup tokio runtime: {}" , e) ;
794+ BuildError :: RuntimeSetupFailed
795+ } ) ?)
796+ } ;
797+
798+ let ts_config = self . tier_store_config . as_ref ( ) ;
799+ let retry_config = ts_config. and_then ( |c| c. retry ) . unwrap_or_default ( ) ;
800+
801+ let mut tier_store =
802+ TierStore :: new ( primary_store, Arc :: clone ( & runtime) , Arc :: clone ( & logger) , retry_config) ;
803+
804+ if let Some ( config) = ts_config {
805+ config. ephemeral . as_ref ( ) . map ( |s| tier_store. set_ephemeral_store ( Arc :: clone ( s) ) ) ;
806+ config. backup . as_ref ( ) . map ( |s| tier_store. set_backup_store ( Arc :: clone ( s) ) ) ;
807+ }
808+
809+ self . build_with_store ( node_entropy, tier_store)
810+ }
811+
657812 /// Builds a [`Node`] instance according to the options previously configured.
658813 pub fn build_with_store < S : SyncAndAsyncKVStore + Send + Sync + ' static > (
659814 & self , node_entropy : NodeEntropy , kv_store : S ,
@@ -919,6 +1074,49 @@ impl ArcedNodeBuilder {
9191074 self . inner . write ( ) . unwrap ( ) . set_async_payments_role ( role) . map ( |_| ( ) )
9201075 }
9211076
1077+ /// Configures retry behavior for transient errors when accessing the primary store.
1078+ ///
1079+ /// When building with [`build_with_tier_store`], controls the exponential backoff parameters
1080+ /// used when retrying failed operations on the primary store due to transient errors
1081+ /// (network issues, timeouts, etc.).
1082+ ///
1083+ /// If not set, default retry parameters are used. See [`RetryConfig`] for details.
1084+ ///
1085+ /// [`build_with_tier_store`]: Self::build_with_tier_store
1086+ pub fn set_tier_store_retry_config ( & self , config : RetryConfig ) {
1087+ self . inner . write ( ) . unwrap ( ) . set_tier_store_retry_config ( config) ;
1088+ }
1089+
1090+ /// Configures the backup store for local disaster recovery.
1091+ ///
1092+ /// When building with [`build_with_tier_store`], this store receives asynchronous copies
1093+ /// of all critical data written to the primary store. If the primary store becomes
1094+ /// unavailable, reads will fall back to this backup store.
1095+ ///
1096+ /// Backup writes are non-blocking and do not affect primary store operation performance.
1097+ ///
1098+ /// [`build_with_tier_store`]: Self::build_with_tier_store
1099+ pub fn set_tier_store_backup ( & self , backup_store : Arc < FfiDynStore > ) {
1100+ let wrapper = DynStoreWrapper ( ( * backup_store) . clone ( ) ) ;
1101+ let store: Arc < DynStore > = Arc :: new ( wrapper) ;
1102+ self . inner . write ( ) . unwrap ( ) . set_tier_store_backup ( store) ;
1103+ }
1104+
1105+ /// Configures the ephemeral store for non-critical, frequently-accessed data.
1106+ ///
1107+ /// When building with [`build_with_tier_store`], this store is used for data like
1108+ /// the network graph and scorer data to reduce latency for reads. Data stored here
1109+ /// can be rebuilt if lost.
1110+ ///
1111+ /// If not set, non-critical data will be stored in the primary store.
1112+ ///
1113+ /// [`build_with_tier_store`]: Self::build_with_tier_store
1114+ pub fn set_tier_store_ephemeral ( & self , ephemeral_store : Arc < FfiDynStore > ) {
1115+ let wrapper = DynStoreWrapper ( ( * ephemeral_store) . clone ( ) ) ;
1116+ let store: Arc < DynStore > = Arc :: new ( wrapper) ;
1117+ self . inner . write ( ) . unwrap ( ) . set_tier_store_ephemeral ( store) ;
1118+ }
1119+
9221120 /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
9231121 /// previously configured.
9241122 pub fn build ( & self , node_entropy : Arc < NodeEntropy > ) -> Result < Arc < Node > , BuildError > {
@@ -1017,6 +1215,24 @@ impl ArcedNodeBuilder {
10171215 . map ( Arc :: new)
10181216 }
10191217
1218+ // pub fn build_with_tier_store(
1219+ // &self, node_entropy: Arc<NodeEntropy>, primary_store: Arc<DynStore>,
1220+ // ) -> Result<Arc<Node>, BuildError> {
1221+ // self.inner.read().unwrap().build_with_tier_store(*node_entropy, primary_store).map(Arc::new)
1222+ // }
1223+
1224+ pub fn build_with_tier_store (
1225+ & self , node_entropy : Arc < NodeEntropy > , primary_store : Arc < FfiDynStore > ,
1226+ ) -> Result < Arc < Node > , BuildError > {
1227+ let wrapper = DynStoreWrapper ( ( * primary_store) . clone ( ) ) ;
1228+ let store: Arc < DynStore > = Arc :: new ( wrapper) ;
1229+ self . inner
1230+ . read ( )
1231+ . unwrap ( )
1232+ . build_with_tier_store_internal ( * node_entropy, store)
1233+ . map ( Arc :: new)
1234+ }
1235+
10201236 /// Builds a [`Node`] instance according to the options previously configured.
10211237 // Note that the generics here don't actually work for Uniffi, but we don't currently expose
10221238 // this so its not needed.
0 commit comments