Skip to content

Commit 5889a10

Browse files
committed
feat: introduce and configure node with tiered KVStore
Introduces TierStore, a KVStore implementation that manages data across three storage layers: - Primary: Main/remote data store - Ephemeral: Secondary store for non-critical, easily-rebuildable data (e.g., network graph) with fast local access - Backup: Tertiary store for disaster recovery with async/lazy operations to avoid blocking primary store Adds four configuration methods to NodeBuilder: - set_tier_store_backup: Configure backup data store - set_tier_store_ephemeral: Configure ephemeral data store - set_tier_store_retry_config: Configure retry parameters with exponential backoff - build_with_tier_store: Build node with primary data store These methods are exposed to the foreign interface via additions in ffi/types.rs: - ForeignDynStoreTrait: An FFI-safe version of DynStoreTrait - FfiDynStore: A concrete wrapper over foreign language stores that implement ForeignDynStoreTrait.
1 parent d261724 commit 5889a10

9 files changed

Lines changed: 1891 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ bitcoin = "0.32.7"
6363
bip39 = { version = "2.0.0", features = ["rand"] }
6464
bip21 = { version = "0.5", features = ["std"], default-features = false }
6565

66+
async-trait = {version = "0.1.89"}
6667
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
6768
rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] }
6869
chrono = { version = "0.4", default-features = false, features = ["clock"] }

bindings/ldk_node.udl

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ enum WordCount {
7979
"Words24",
8080
};
8181

82+
dictionary RetryConfig {
83+
u16 initial_retry_delay_ms;
84+
u16 maximum_delay_secs;
85+
f32 backoff_multiplier;
86+
};
87+
8288
enum LogLevel {
8389
"Gossip",
8490
"Trace",
@@ -103,6 +109,53 @@ interface LogWriter {
103109
void log(LogRecord record);
104110
};
105111

112+
interface FfiDynStore {
113+
[Name=from_store]
114+
constructor(ForeignDynStoreTrait store);
115+
};
116+
117+
[Trait, WithForeign]
118+
interface ForeignDynStoreTrait {
119+
[Throws=IOError]
120+
sequence<u8> read(string primary_namespace, string secondary_namespace, string key);
121+
[Throws=IOError]
122+
void write(string primary_namespace, string secondary_namespace, string key, sequence<u8> buf);
123+
[Throws=IOError]
124+
void remove(string primary_namespace, string secondary_namespace, string key, boolean lazy);
125+
[Throws=IOError]
126+
sequence<string> list(string primary_namespace, string secondary_namespace);
127+
[Throws=IOError, Async]
128+
sequence<u8> read_async(string primary_namespace, string secondary_namespace, string key);
129+
[Throws=IOError, Async]
130+
void write_async(string primary_namespace, string secondary_namespace, string key, sequence<u8> buf);
131+
[Throws=IOError, Async]
132+
void remove_async(string primary_namespace, string secondary_namespace, string key, boolean lazy);
133+
[Throws=IOError, Async]
134+
sequence<string> list_async(string primary_namespace, string secondary_namespace);
135+
};
136+
137+
[Error]
138+
enum IOError {
139+
"NotFound",
140+
"PermissionDenied",
141+
"ConnectionRefused",
142+
"ConnectionReset",
143+
"ConnectionAborted",
144+
"NotConnected",
145+
"AddrInUse",
146+
"AddrNotAvailable",
147+
"BrokenPipe",
148+
"AlreadyExists",
149+
"WouldBlock",
150+
"InvalidInput",
151+
"InvalidData",
152+
"TimedOut",
153+
"WriteZero",
154+
"Interrupted",
155+
"UnexpectedEof",
156+
"Other",
157+
};
158+
106159
interface Builder {
107160
constructor();
108161
[Name=from_config]
@@ -127,6 +180,9 @@ interface Builder {
127180
void set_announcement_addresses(sequence<SocketAddress> announcement_addresses);
128181
[Throws=BuildError]
129182
void set_node_alias(string node_alias);
183+
void set_tier_store_retry_config(RetryConfig retry_config);
184+
void set_tier_store_backup(FfiDynStore backup_store);
185+
void set_tier_store_ephemeral(FfiDynStore ephemeral_store);
130186
[Throws=BuildError]
131187
void set_async_payments_role(AsyncPaymentsRole? role);
132188
[Throws=BuildError]
@@ -139,6 +195,8 @@ interface Builder {
139195
Node build_with_vss_store_and_fixed_headers(NodeEntropy node_entropy, string vss_url, string store_id, record<string, string> fixed_headers);
140196
[Throws=BuildError]
141197
Node build_with_vss_store_and_header_provider(NodeEntropy node_entropy, string vss_url, string store_id, VssHeaderProvider header_provider);
198+
[Throws=BuildError]
199+
Node build_with_tier_store(NodeEntropy node_entropy, FfiDynStore primary_store);
142200
};
143201

144202
interface Node {

src/builder.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ use crate::connection::ConnectionManager;
5252
use crate::entropy::NodeEntropy;
5353
use crate::event::EventQueue;
5454
use crate::fee_estimator::OnchainFeeEstimator;
55+
#[cfg(feature = "uniffi")]
56+
use crate::ffi::FfiDynStore;
5557
use crate::gossip::GossipSource;
5658
use crate::io::sqlite_store::SqliteStore;
59+
use crate::io::tier_store::{RetryConfig, TierStore};
5760
use 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

Comments
 (0)