Skip to content

Commit 51ff7a4

Browse files
committed
feat(wip): configure node builder with tier storage
1 parent 0f5f022 commit 51ff7a4

6 files changed

Lines changed: 491 additions & 21 deletions

File tree

bindings/ldk_node.udl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ dictionary LSPS2ServiceConfig {
4646
u64 max_payment_size_msat;
4747
};
4848

49+
dictionary RetryConfig {
50+
u16 initial_retry_delay_ms;
51+
u16 maximum_delay_secs;
52+
f32 backoff_multiplier;
53+
};
54+
4955
enum LogLevel {
5056
"Gossip",
5157
"Trace",
@@ -67,6 +73,45 @@ interface LogWriter {
6773
void log(LogRecord record);
6874
};
6975

76+
interface DynStore {
77+
[Name=from_store]
78+
constructor(KVStore store);
79+
};
80+
81+
[Trait, WithForeign]
82+
interface KVStore {
83+
[Throws=IOError]
84+
sequence<u8> read(string primary_namespace, string secondary_namespace, string key);
85+
[Throws=IOError]
86+
void write(string primary_namespace, string secondary_namespace, string key, sequence<u8> buf);
87+
[Throws=IOError]
88+
void remove(string primary_namespace, string secondary_namespace, string key, boolean lazy);
89+
[Throws=IOError]
90+
sequence<string> list(string primary_namespace, string secondary_namespace);
91+
};
92+
93+
[Error]
94+
enum IOError {
95+
"NotFound",
96+
"PermissionDenied",
97+
"ConnectionRefused",
98+
"ConnectionReset",
99+
"ConnectionAborted",
100+
"NotConnected",
101+
"AddrInUse",
102+
"AddrNotAvailable",
103+
"BrokenPipe",
104+
"AlreadyExists",
105+
"WouldBlock",
106+
"InvalidInput",
107+
"InvalidData",
108+
"TimedOut",
109+
"WriteZero",
110+
"Interrupted",
111+
"UnexpectedEof",
112+
"Other",
113+
};
114+
70115
interface Builder {
71116
constructor();
72117
[Name=from_config]
@@ -94,6 +139,9 @@ interface Builder {
94139
void set_announcement_addresses(sequence<SocketAddress> announcement_addresses);
95140
[Throws=BuildError]
96141
void set_node_alias(string node_alias);
142+
void set_tier_store_retry_config(RetryConfig retry_config);
143+
void set_tier_store_backup(KVStore backup_store);
144+
void set_tier_store_ephemeral(KVStore ephemeral_store);
97145
[Throws=BuildError]
98146
Node build();
99147
[Throws=BuildError]

src/builder.rs

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ use crate::config::{
1515
use crate::connection::ConnectionManager;
1616
use crate::event::EventQueue;
1717
use crate::fee_estimator::OnchainFeeEstimator;
18+
use crate::ffi::maybe_extract_inner;
1819
use crate::gossip::GossipSource;
1920
use crate::io::sqlite_store::SqliteStore;
21+
use crate::io::tier_store::{RetryConfig, TierStore};
2022
use crate::io::utils::{read_node_metrics, write_node_metrics};
2123
use crate::io::vss_store::VssStore;
2224
use crate::io::{
@@ -30,14 +32,20 @@ use crate::message_handler::NodeCustomMessageHandler;
3032
use crate::peer_store::PeerStore;
3133
use crate::runtime::Runtime;
3234
use crate::tx_broadcaster::TransactionBroadcaster;
35+
3336
use crate::types::{
34-
ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter,
35-
OnionMessenger, PaymentStore, PeerManager,
37+
ChainMonitor, ChannelManager, GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger,
38+
PaymentStore, PeerManager,
3639
};
3740
use crate::wallet::persist::KVStoreWalletPersister;
3841
use crate::wallet::Wallet;
3942
use crate::{Node, NodeMetrics};
4043

44+
#[cfg(feature = "uniffi")]
45+
use crate::ffi::DynStore;
46+
#[cfg(not(feature = "uniffi"))]
47+
use crate::types::DynStore;
48+
4149
use lightning::chain::{chainmonitor, BestBlock, Watch};
4250
use lightning::io::Cursor;
4351
use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs};
@@ -150,6 +158,23 @@ impl std::fmt::Debug for LogWriterConfig {
150158
}
151159
}
152160

161+
#[derive(Default)]
162+
struct TierStoreConfig {
163+
ephemeral: Option<Arc<DynStore>>,
164+
backup: Option<Arc<DynStore>>,
165+
retry: Option<RetryConfig>,
166+
}
167+
168+
impl std::fmt::Debug for TierStoreConfig {
169+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170+
f.debug_struct("TierStoreConfig")
171+
.field("ephemeral", &self.ephemeral.as_ref().map(|_| "Arc<DynStore>"))
172+
.field("backup", &self.backup.as_ref().map(|_| "Arc<DynStore>"))
173+
.field("retry", &self.retry)
174+
.finish()
175+
}
176+
}
177+
153178
/// An error encountered during building a [`Node`].
154179
///
155180
/// [`Node`]: crate::Node
@@ -240,6 +265,7 @@ pub struct NodeBuilder {
240265
gossip_source_config: Option<GossipSourceConfig>,
241266
liquidity_source_config: Option<LiquiditySourceConfig>,
242267
log_writer_config: Option<LogWriterConfig>,
268+
tier_store_config: Option<TierStoreConfig>,
243269
runtime_handle: Option<tokio::runtime::Handle>,
244270
}
245271

@@ -257,6 +283,7 @@ impl NodeBuilder {
257283
let gossip_source_config = None;
258284
let liquidity_source_config = None;
259285
let log_writer_config = None;
286+
let tier_store_config = None;
260287
let runtime_handle = None;
261288
Self {
262289
config,
@@ -265,6 +292,7 @@ impl NodeBuilder {
265292
gossip_source_config,
266293
liquidity_source_config,
267294
log_writer_config,
295+
tier_store_config,
268296
runtime_handle,
269297
}
270298
}
@@ -544,6 +572,53 @@ impl NodeBuilder {
544572
Ok(self)
545573
}
546574

575+
/// Configures retry behavior for transient errors when accessing the primary store.
576+
///
577+
/// When building with [`build_with_tier_store`], controls the exponential backoff parameters
578+
/// used when retrying failed operations on the primary store due to transient errors
579+
/// (network issues, timeouts, etc.).
580+
///
581+
/// If not set, default retry parameters are used. See [`RetryConfig`] for details.
582+
///
583+
/// [`build_with_tier_store`]: Self::build_with_tier_store
584+
pub fn set_tier_store_retry_config(&mut self, config: RetryConfig) -> &mut Self {
585+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
586+
tier_store_config.retry = Some(config);
587+
self
588+
}
589+
590+
/// Configures the backup store for local disaster recovery.
591+
///
592+
/// When building with [`build_with_tier_store`], this store receives asynchronous copies
593+
/// of all critical data written to the primary store. If the primary store becomes
594+
/// unavailable, reads will fall back to this backup store.
595+
///
596+
/// Backup writes are non-blocking and do not affect primary store operation performance.
597+
///
598+
/// [`build_with_tier_store`]: Self::build_with_tier_store
599+
pub fn set_tier_store_backup(&mut self, backup_store: Arc<DynStore>) -> &mut Self {
600+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
601+
let store = maybe_extract_inner(backup_store);
602+
tier_store_config.backup = Some(store);
603+
self
604+
}
605+
606+
/// Configures the ephemeral store for non-critical, frequently-accessed data.
607+
///
608+
/// When building with [`build_with_tier_store`], this store is used for data like
609+
/// the network graph and scorer data to reduce latency for reads. Data stored here
610+
/// can be rebuilt if lost.
611+
///
612+
/// If not set, non-critical data will be stored in the primary store.
613+
///
614+
/// [`build_with_tier_store`]: Self::build_with_tier_store
615+
pub fn set_tier_store_ephemeral(&mut self, ephemeral_store: Arc<DynStore>) -> &mut Self {
616+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
617+
let store = maybe_extract_inner(ephemeral_store);
618+
tier_store_config.ephemeral = Some(store);
619+
self
620+
}
621+
547622
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
548623
/// previously configured.
549624
pub fn build(&self) -> Result<Node, BuildError> {
@@ -707,6 +782,98 @@ impl NodeBuilder {
707782
)
708783
}
709784

785+
/// Builds a [`Node`] instance with tiered storage for managing data across multiple storage layers.
786+
///
787+
/// This build method enables a three-tier storage architecture optimized for different data types
788+
/// and access patterns:
789+
///
790+
/// ### Storage Tiers
791+
///
792+
/// - **Primary Store** (required): The authoritative store for critical channel state and payment data.
793+
/// Typically a remote/cloud storage service for durability and accessibility across devices.
794+
///
795+
/// - **Ephemeral Store** (optional): Local storage for non-critical, frequently-accessed data like
796+
/// the network graph and scorer. Improves performance by reducing latency for data that can be
797+
/// rebuilt if lost. Configure with [`set_tier_store_ephemeral`].
798+
///
799+
/// - **Backup Store** (optional): Local backup of critical data for disaster recovery scenarios.
800+
/// Provides a safety net if the primary store becomes temporarily unavailable. Writes are
801+
/// asynchronous to avoid blocking primary operations. Configure with [`set_tier_store_backup`].
802+
///
803+
/// ## Configuration
804+
///
805+
/// Use the setter methods to configure optional stores and retry behavior:
806+
/// - [`set_tier_store_ephemeral`] - Set local store for network graph and scorer
807+
/// - [`set_tier_store_backup`] - Set local backup store for disaster recovery
808+
/// - [`set_tier_store_retry_config`] - Configure retry delays and backoff for transient errors
809+
///
810+
/// ## Example
811+
///
812+
/// ```ignore
813+
/// # use ldk_node::{Builder, Config};
814+
/// # use ldk_node::io::tier_store::RetryConfig;
815+
/// # use std::sync::Arc;
816+
/// let config = Config::default();
817+
/// let mut builder = NodeBuilder::from_config(config);
818+
///
819+
/// let primary = Arc::new(VssStore::new(...));
820+
/// let ephemeral = Arc::new(FilesystemStore::new(...));
821+
/// let backup = Arc::new(SqliteStore::new(...));
822+
/// let retry_config = RetryConfig::default();
823+
///
824+
/// builder
825+
/// .set_tier_store_ephemeral(ephemeral)
826+
/// .set_tier_store_backup(backup)
827+
/// .set_tier_store_retry_config(retry_config);
828+
///
829+
/// let node = builder.build_with_tier_store(primary)?;
830+
/// # Ok::<(), ldk_node::BuildError>(())
831+
/// ```
832+
///
833+
/// [`set_tier_store_ephemeral`]: Self::set_tier_store_ephemeral
834+
/// [`set_tier_store_backup`]: Self::set_tier_store_backup
835+
/// [`set_tier_store_retry_config`]: Self::set_tier_store_retry_config
836+
pub fn build_with_tier_store(&self, primary_store: Arc<DynStore>) -> Result<Node, BuildError> {
837+
let logger = setup_logger(&self.log_writer_config, &self.config)?;
838+
let runtime = if let Some(handle) = self.runtime_handle.as_ref() {
839+
Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(&logger)))
840+
} else {
841+
Arc::new(Runtime::new(Arc::clone(&logger)).map_err(|e| {
842+
log_error!(logger, "Failed to setup tokio runtime: {}", e);
843+
BuildError::RuntimeSetupFailed
844+
})?)
845+
};
846+
let seed_bytes = seed_bytes_from_config(
847+
&self.config,
848+
self.entropy_source_config.as_ref(),
849+
Arc::clone(&logger),
850+
)?;
851+
let config = Arc::new(self.config.clone());
852+
853+
let ts_config = self.tier_store_config.as_ref();
854+
let retry_config = ts_config.and_then(|c| c.retry).unwrap_or_default();
855+
856+
let primary = maybe_extract_inner(primary_store);
857+
let mut tier_store =
858+
TierStore::new(primary, Arc::clone(&runtime), Arc::clone(&logger), retry_config);
859+
860+
if let Some(config) = ts_config {
861+
config.ephemeral.as_ref().map(|s| tier_store.set_ephemeral_store(Arc::clone(s)));
862+
config.backup.as_ref().map(|s| tier_store.set_backup_store(Arc::clone(s)));
863+
}
864+
865+
build_with_store_internal(
866+
config,
867+
self.chain_data_source_config.as_ref(),
868+
self.gossip_source_config.as_ref(),
869+
self.liquidity_source_config.as_ref(),
870+
seed_bytes,
871+
runtime,
872+
logger,
873+
Arc::new(tier_store),
874+
)
875+
}
876+
710877
/// Builds a [`Node`] instance according to the options previously configured.
711878
pub fn build_with_store(&self, kv_store: Arc<DynStore>) -> Result<Node, BuildError> {
712879
let logger = setup_logger(&self.log_writer_config, &self.config)?;
@@ -989,6 +1156,45 @@ impl ArcedNodeBuilder {
9891156
self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ())
9901157
}
9911158

1159+
/// Configures retry behavior for transient errors when accessing the primary store.
1160+
///
1161+
/// When building with [`build_with_tier_store`], controls the exponential backoff parameters
1162+
/// used when retrying failed operations on the primary store due to transient errors
1163+
/// (network issues, timeouts, etc.).
1164+
///
1165+
/// If not set, default retry parameters are used. See [`RetryConfig`] for details.
1166+
///
1167+
/// [`build_with_tier_store`]: Self::build_with_tier_store
1168+
pub fn set_tier_store_retry_config(&self, config: RetryConfig) {
1169+
self.inner.write().unwrap().set_tier_store_retry_config(config);
1170+
}
1171+
1172+
/// Configures the backup store for local disaster recovery.
1173+
///
1174+
/// When building with [`build_with_tier_store`], this store receives asynchronous copies
1175+
/// of all critical data written to the primary store. If the primary store becomes
1176+
/// unavailable, reads will fall back to this backup store.
1177+
///
1178+
/// Backup writes are non-blocking and do not affect primary store operation performance.
1179+
///
1180+
/// [`build_with_tier_store`]: Self::build_with_tier_store
1181+
pub fn set_tier_store_backup(&mut self, backup_store: Arc<DynStore>) {
1182+
self.inner.write().unwrap().set_tier_store_backup(backup_store);
1183+
}
1184+
1185+
/// Configures the ephemeral store for non-critical, frequently-accessed data.
1186+
///
1187+
/// When building with [`build_with_tier_store`], this store is used for data like
1188+
/// the network graph and scorer data to reduce latency for reads. Data stored here
1189+
/// can be rebuilt if lost.
1190+
///
1191+
/// If not set, non-critical data will be stored in the primary store.
1192+
///
1193+
/// [`build_with_tier_store`]: Self::build_with_tier_store
1194+
pub fn set_tier_store_ephemeral(&mut self, ephemeral_store: Arc<DynStore>) {
1195+
self.inner.write().unwrap().set_tier_store_ephemeral(ephemeral_store);
1196+
}
1197+
9921198
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
9931199
/// previously configured.
9941200
pub fn build(&self) -> Result<Arc<Node>, BuildError> {

src/ffi/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ pub fn maybe_wrap<T>(ldk_type: impl Into<T>) -> std::sync::Arc<T> {
3131
std::sync::Arc::new(ldk_type.into())
3232
}
3333

34+
#[cfg(feature = "uniffi")]
35+
pub fn maybe_extract_inner<T, R>(wrapped: std::sync::Arc<T>) -> std::sync::Arc<R>
36+
where
37+
T: AsRef<std::sync::Arc<R>>,
38+
R: ?Sized,
39+
{
40+
std::sync::Arc::clone(wrapped.as_ref().as_ref())
41+
}
42+
3443
#[cfg(not(feature = "uniffi"))]
3544
pub fn maybe_deref<T>(value: &T) -> &T {
3645
value
@@ -45,3 +54,11 @@ pub fn maybe_try_convert_enum<T>(value: &T) -> Result<&T, crate::error::Error> {
4554
pub fn maybe_wrap<T>(value: T) -> T {
4655
value
4756
}
57+
58+
#[cfg(not(feature = "uniffi"))]
59+
pub fn maybe_extract_inner<R>(value: std::sync::Arc<R>) -> std::sync::Arc<R>
60+
where
61+
R: ?Sized,
62+
{
63+
value
64+
}

0 commit comments

Comments
 (0)