Skip to content

Commit 9f19768

Browse files
committed
Integrate TierStore into NodeBuilder
Add native builder support for tiered storage by introducing `TierStoreConfig` and builder methods for configuring ephemeral storage and a local SQLite backup mirror. During node construction, wrap the configured primary store in `TierStore` and attach secondary tiers for cache-like ephemeral data and mirrored durable backup writes. The builder constructs the backup and ephemeral stores internally using a dedicated SQLite database file. Add test coverage for full-cycle backup mirroring, same-path rejection, and UniFFI-backed builder configuration. Update `setup_builder!` so FFI-backed builder tests can use mutable configuration helpers.
1 parent 164edc8 commit 9f19768

6 files changed

Lines changed: 165 additions & 9 deletions

File tree

src/builder.rs

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ use crate::event::EventQueue;
5757
use crate::fee_estimator::OnchainFeeEstimator;
5858
use crate::gossip::GossipSource;
5959
use crate::io::sqlite_store::SqliteStore;
60+
use crate::io::tier_store::TierStore;
6061
use crate::io::utils::{
6162
open_or_migrate_fs_store, read_all_objects, read_event_queue,
6263
read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics,
@@ -150,6 +151,12 @@ impl std::fmt::Debug for LogWriterConfig {
150151
}
151152
}
152153

154+
#[derive(Default, Debug)]
155+
struct TierStoreConfig {
156+
ephemeral_storage_dir_path: Option<PathBuf>,
157+
backup_storage_dir_path: Option<PathBuf>,
158+
}
159+
153160
/// An error encountered during building a [`Node`].
154161
///
155162
/// [`Node`]: crate::Node
@@ -285,6 +292,7 @@ pub struct NodeBuilder {
285292
liquidity_source_config: Option<LiquiditySourceConfig>,
286293
log_writer_config: Option<LogWriterConfig>,
287294
async_payments_role: Option<AsyncPaymentsRole>,
295+
tier_store_config: Option<TierStoreConfig>,
288296
runtime_handle: Option<tokio::runtime::Handle>,
289297
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
290298
recovery_mode: bool,
@@ -303,6 +311,7 @@ impl NodeBuilder {
303311
let gossip_source_config = None;
304312
let liquidity_source_config = None;
305313
let log_writer_config = None;
314+
let tier_store_config = None;
306315
let runtime_handle = None;
307316
let pathfinding_scores_sync_config = None;
308317
let recovery_mode = false;
@@ -312,6 +321,7 @@ impl NodeBuilder {
312321
gossip_source_config,
313322
liquidity_source_config,
314323
log_writer_config,
324+
tier_store_config,
315325
runtime_handle,
316326
async_payments_role: None,
317327
pathfinding_scores_sync_config,
@@ -612,6 +622,39 @@ impl NodeBuilder {
612622
self
613623
}
614624

625+
/// Configures a local SQLite backup store for disaster recovery.
626+
///
627+
/// When building with tiered storage, a SQLite store will be created at the
628+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
629+
/// file name. It receives a second durable copy of data written to the
630+
/// primary store.
631+
///
632+
/// Writes and removals for primary-backed data only succeed once both the
633+
/// primary and backup SQLite stores complete successfully.
634+
///
635+
/// If not set, durable data will be stored only in the primary store.
636+
///
637+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
638+
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
639+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
640+
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
641+
self
642+
}
643+
644+
/// Configures the ephemeral storage directory path for non-critical, frequently-accessed data.
645+
///
646+
/// When set, a local SQLite store is created at this path for ephemeral data like
647+
/// the network graph and scorer. Data stored here can be rebuilt if lost.
648+
///
649+
/// If not set, non-critical data will be stored in the primary store.
650+
pub fn set_ephemeral_storage_dir_path(
651+
&mut self, ephemeral_storage_dir_path: String,
652+
) -> &mut Self {
653+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
654+
tier_store_config.ephemeral_storage_dir_path = Some(ephemeral_storage_dir_path.into());
655+
self
656+
}
657+
615658
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
616659
/// previously configured.
617660
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -813,11 +856,18 @@ impl NodeBuilder {
813856
}
814857

815858
/// Builds a [`Node`] instance according to the options previously configured.
859+
///
860+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
861+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
862+
/// and a local SQLite backup store for disaster recovery can be configured via
863+
/// [`set_ephemeral_storage_dir_path`] and [`set_backup_storage_dir_path`].
864+
///
865+
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
866+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
816867
pub fn build_with_store<S: KVStore + Send + Sync + 'static>(
817868
&self, node_entropy: NodeEntropy, kv_store: S,
818869
) -> Result<Node, BuildError> {
819870
let logger = setup_logger(&self.log_writer_config, &self.config)?;
820-
821871
self.build_with_store_and_logger(node_entropy, kv_store, logger)
822872
}
823873

@@ -842,6 +892,39 @@ impl NodeBuilder {
842892
fn build_with_store_runtime_and_logger<S: KVStore + Send + Sync + 'static>(
843893
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
844894
) -> Result<Node, BuildError> {
895+
let ts_config = self.tier_store_config.as_ref();
896+
let primary_store = Arc::new(DynStoreWrapper(kv_store));
897+
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
898+
if let Some(config) = ts_config {
899+
if let Some(ephemeral_storage_dir_path) = config.ephemeral_storage_dir_path.as_ref() {
900+
let ephemeral_store = SqliteStore::new(
901+
ephemeral_storage_dir_path.clone(),
902+
Some(io::sqlite_store::SQLITE_EPHEMERAL_DB_FILE_NAME.to_string()),
903+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
904+
)
905+
.map_err(|e| {
906+
log_error!(logger, "Failed to setup ephemeral SQLite store: {}", e);
907+
BuildError::KVStoreSetupFailed
908+
})?;
909+
let ephemeral_store: Arc<DynStore> = Arc::new(DynStoreWrapper(ephemeral_store));
910+
tier_store.set_ephemeral_store(ephemeral_store);
911+
}
912+
913+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
914+
let backup_store = SqliteStore::new(
915+
backup_storage_dir_path.clone(),
916+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
917+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
918+
)
919+
.map_err(|e| {
920+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
921+
BuildError::KVStoreSetupFailed
922+
})?;
923+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
924+
tier_store.set_backup_store(backup_store);
925+
}
926+
}
927+
845928
let seed_bytes = node_entropy.to_seed_bytes();
846929
let config = Arc::new(self.config.clone());
847930

@@ -856,7 +939,7 @@ impl NodeBuilder {
856939
seed_bytes,
857940
runtime,
858941
logger,
859-
Arc::new(DynStoreWrapper(kv_store)),
942+
Arc::new(DynStoreWrapper(tier_store)),
860943
)
861944
}
862945
}

src/io/sqlite_store/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ mod migrations;
2424

2525
/// LDK Node's database file name.
2626
pub const SQLITE_DB_FILE_NAME: &str = "ldk_node_data.sqlite";
27+
/// LDK Node's backup database file name.
28+
pub const SQLITE_BACKUP_DB_FILE_NAME: &str = "ldk_node_data_backup.sqlite";
29+
/// LDK Node's ephemeral database file name.
30+
pub const SQLITE_EPHEMERAL_DB_FILE_NAME: &str = "ldk_node_data_ephemeral.sqlite";
2731
/// LDK Node's table in which we store all data.
2832
pub const KV_TABLE_NAME: &str = "ldk_node_data";
2933

src/io/tier_store.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
7-
#![allow(dead_code)] // TODO: Temporal warning silencer. Will be removed in later commit.
87

98
use std::collections::HashMap;
109
use std::future::Future;

src/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ use crate::message_handler::NodeCustomMessageHandler;
4646
use crate::payment::{PaymentDetails, PendingPaymentDetails};
4747
use crate::runtime::RuntimeSpawner;
4848

49-
pub(crate) trait DynStoreTrait: Send + Sync {
49+
pub trait DynStoreTrait: Send + Sync {
5050
fn read_async(
5151
&self, primary_namespace: &str, secondary_namespace: &str, key: &str,
5252
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, bitcoin::io::Error>> + Send + 'static>>;
@@ -87,7 +87,7 @@ impl<'a> KVStore for dyn DynStoreTrait + 'a {
8787
}
8888
}
8989

90-
pub(crate) type DynStore = dyn DynStoreTrait;
90+
pub type DynStore = dyn DynStoreTrait;
9191

9292
// Newtype wrapper that implements `KVStore` for `Arc<DynStore>`. This is needed because `KVStore`
9393
// methods return `impl Future`, which is not object-safe. `DynStoreTrait` works around this by

tests/common/mod.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ pub(crate) fn setup_two_nodes_with_store(
532532

533533
pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestNode {
534534
setup_builder!(builder, config.node_config);
535+
535536
match chain_source {
536537
TestChainSource::Esplora(electrsd) => {
537538
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
@@ -601,10 +602,6 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
601602
},
602603
};
603604

604-
if config.recovery_mode {
605-
builder.set_wallet_recovery_mode();
606-
}
607-
608605
node.start().unwrap();
609606
assert!(node.status().is_running);
610607
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());

tests/integration_tests_rust.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use electrsd::corepc_node::Node as BitcoinD;
3030
use electrsd::ElectrsD;
3131
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
3232
use ldk_node::entropy::NodeEntropy;
33+
#[cfg(not(feature = "uniffi"))]
34+
use ldk_node::io::sqlite_store::SqliteStore;
3335
use ldk_node::liquidity::LSPS2ServiceConfig;
3436
use ldk_node::payment::{
3537
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
@@ -39,6 +41,8 @@ use ldk_node::{Builder, Event, NodeError};
3941
use lightning::ln::channelmanager::PaymentId;
4042
use lightning::routing::gossip::{NodeAlias, NodeId};
4143
use lightning::routing::router::RouteParametersConfig;
44+
#[cfg(not(feature = "uniffi"))]
45+
use lightning::util::persist::KVStore;
4246
use lightning_invoice::{Bolt11InvoiceDescription, Description};
4347
use lightning_types::payment::{PaymentHash, PaymentPreimage};
4448
use log::LevelFilter;
@@ -3046,6 +3050,7 @@ async fn splice_in_with_all_balance() {
30463050
node_b.stop().unwrap();
30473051
}
30483052

3053+
#[cfg(not(feature = "uniffi"))]
30493054
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
30503055
async fn lsps2_multi_lsp_picks_cheapest() {
30513056
do_lsps2_multi_lsp_picks_cheapest(false).await;
@@ -3140,3 +3145,71 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) {
31403145
cheap.stop().unwrap();
31413146
expensive.stop().unwrap();
31423147
}
3148+
3149+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
3150+
async fn builder_configures_sqlite_backup_store() {
3151+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
3152+
let chain_source = random_chain_source(&bitcoind, &electrsd);
3153+
3154+
let mut config_a = random_config(true);
3155+
config_a.store_type = TestStoreType::Sqlite;
3156+
let primary_dir = config_a.node_config.storage_dir_path.clone();
3157+
let backup_dir = common::random_storage_path();
3158+
3159+
// Build node_a with backup storage configured
3160+
setup_builder!(builder_a, config_a.node_config.clone());
3161+
builder_a.set_chain_source_esplora(
3162+
format!("http://{}", electrsd.esplora_url.as_ref().unwrap()),
3163+
None,
3164+
);
3165+
builder_a.set_filesystem_logger(None, None);
3166+
builder_a.set_backup_storage_dir_path(backup_dir.to_str().unwrap().to_owned());
3167+
3168+
let node_a = builder_a.build(config_a.node_entropy.into()).unwrap();
3169+
node_a.start().unwrap();
3170+
assert!(node_a.status().is_running);
3171+
assert!(node_a.status().latest_fee_rate_cache_update_timestamp.is_some());
3172+
3173+
let config_b = random_config(true);
3174+
let node_b = setup_node(&chain_source, config_b);
3175+
3176+
do_channel_full_cycle(
3177+
node_a,
3178+
node_b,
3179+
&bitcoind.client,
3180+
&electrsd.client,
3181+
false,
3182+
true,
3183+
true,
3184+
false,
3185+
)
3186+
.await;
3187+
3188+
let primary_store = SqliteStore::new(
3189+
primary_dir.into(),
3190+
Some(ldk_node::io::sqlite_store::SQLITE_DB_FILE_NAME.to_string()),
3191+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
3192+
)
3193+
.unwrap();
3194+
3195+
let backup_store = SqliteStore::new(
3196+
backup_dir,
3197+
Some(ldk_node::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
3198+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
3199+
)
3200+
.unwrap();
3201+
3202+
for (pn, sn, key) in [
3203+
("bdk_wallet", "", "descriptor"),
3204+
("bdk_wallet", "", "change_descriptor"),
3205+
("bdk_wallet", "", "network"),
3206+
("", "", "node_metrics"),
3207+
("", "", "events"),
3208+
("", "", "peers"),
3209+
] {
3210+
let primary = primary_store.read(pn, sn, key).await.unwrap();
3211+
let backup = backup_store.read(pn, sn, key).await.unwrap();
3212+
3213+
assert_eq!(backup, primary, "backup mismatch for {pn}/{sn}/{key}");
3214+
}
3215+
}

0 commit comments

Comments
 (0)