Skip to content

Commit 666fedc

Browse files
committed
Integrate TierStore into NodeBuilder
TierStore remains internal until NodeBuilder provisions and installs its storage backends. Add builder options for local ephemeral and backup SQLite stores, wrap the configured primary store in TierStore, and pass the resulting store into node construction. When ephemeral storage is enabled, automatically create the persistent ordering index in the node's storage directory and require TierStore to own it exclusively. Update filesystem-backed tests and add integration coverage confirming that configured backup storage receives durable primary-backed data. Assisted-by: Amp (AI coding agent)
1 parent 1177faf commit 666fedc

5 files changed

Lines changed: 174 additions & 6 deletions

File tree

src/builder.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ use crate::event::EventQueue;
6060
use crate::fee_estimator::OnchainFeeEstimator;
6161
use crate::gossip::GossipSource;
6262
use crate::io::sqlite_store::SqliteStore;
63+
use crate::io::tier_store::{setup_index_store, TierStore};
6364
use crate::io::utils::{
6465
open_or_migrate_fs_store, read_all_objects, read_event_queue,
6566
read_external_pathfinding_scores_from_cache, read_n_objects, read_network_graph,
@@ -158,6 +159,12 @@ impl std::fmt::Debug for LogWriterConfig {
158159
}
159160
}
160161

162+
#[derive(Default, Debug)]
163+
struct TierStoreConfig {
164+
ephemeral_storage_dir_path: Option<PathBuf>,
165+
backup_storage_dir_path: Option<PathBuf>,
166+
}
167+
161168
/// An error encountered during building a [`Node`].
162169
///
163170
/// [`Node`]: crate::Node
@@ -311,6 +318,7 @@ pub struct NodeBuilder {
311318
liquidity_source_config: Option<LiquiditySourceConfig>,
312319
log_writer_config: Option<LogWriterConfig>,
313320
async_payments_role: Option<AsyncPaymentsRole>,
321+
tier_store_config: Option<TierStoreConfig>,
314322
runtime_handle: Option<tokio::runtime::Handle>,
315323
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
316324
probing_config: Option<ProbingConfig>,
@@ -329,6 +337,7 @@ impl NodeBuilder {
329337
let gossip_source_config = None;
330338
let liquidity_source_config = None;
331339
let log_writer_config = None;
340+
let tier_store_config = None;
332341
let runtime_handle = None;
333342
let pathfinding_scores_sync_config = None;
334343
let probing_config = None;
@@ -338,6 +347,7 @@ impl NodeBuilder {
338347
gossip_source_config,
339348
liquidity_source_config,
340349
log_writer_config,
350+
tier_store_config,
341351
runtime_handle,
342352
async_payments_role: None,
343353
pathfinding_scores_sync_config,
@@ -663,6 +673,41 @@ impl NodeBuilder {
663673
self
664674
}
665675

676+
/// Configures a local SQLite backup store for disaster recovery.
677+
///
678+
/// When building with tiered storage, a SQLite store will be created at the
679+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
680+
/// file name. It receives a second durable copy of data written to the
681+
/// primary store.
682+
///
683+
/// Writes and removals for primary-backed data only succeed once both the
684+
/// primary and backup SQLite stores complete successfully.
685+
///
686+
/// If not set, durable data will be stored only in the primary store.
687+
///
688+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
689+
#[cfg(not(feature = "uniffi"))]
690+
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
691+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
692+
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
693+
self
694+
}
695+
696+
/// Configures the ephemeral storage directory path for non-critical, frequently-accessed data.
697+
///
698+
/// When set, a local SQLite store is created at this path for ephemeral data like
699+
/// the network graph and scorer. Data stored here can be rebuilt if lost.
700+
///
701+
/// If not set, non-critical data will be stored in the primary store.
702+
#[cfg(not(feature = "uniffi"))]
703+
pub fn set_ephemeral_storage_dir_path(
704+
&mut self, ephemeral_storage_dir_path: String,
705+
) -> &mut Self {
706+
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
707+
tier_store_config.ephemeral_storage_dir_path = Some(ephemeral_storage_dir_path.into());
708+
self
709+
}
710+
666711
/// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options
667712
/// previously configured.
668713
pub fn build(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
@@ -872,11 +917,18 @@ impl NodeBuilder {
872917
}
873918

874919
/// Builds a [`Node`] instance according to the options previously configured.
920+
///
921+
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
922+
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
923+
/// and a local SQLite backup store for disaster recovery can be configured via
924+
/// [`set_ephemeral_storage_dir_path`] and [`set_backup_storage_dir_path`].
925+
///
926+
/// [`set_ephemeral_storage_dir_path`]: Self::set_ephemeral_storage_dir_path
927+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
875928
pub fn build_with_store<S: PaginatedKVStore + Send + Sync + 'static>(
876929
&self, node_entropy: NodeEntropy, kv_store: S,
877930
) -> Result<Node, BuildError> {
878931
let logger = setup_logger(&self.log_writer_config, &self.config)?;
879-
880932
self.build_with_store_and_logger(node_entropy, kv_store, logger)
881933
}
882934

@@ -901,6 +953,46 @@ impl NodeBuilder {
901953
fn build_with_store_runtime_and_logger<S: PaginatedKVStore + Send + Sync + 'static>(
902954
&self, node_entropy: NodeEntropy, kv_store: S, runtime: Arc<Runtime>, logger: Arc<Logger>,
903955
) -> Result<Node, BuildError> {
956+
let ts_config = self.tier_store_config.as_ref();
957+
let primary_store = Arc::new(DynStoreWrapper(kv_store));
958+
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
959+
if let Some(config) = ts_config {
960+
if let Some(ephemeral_storage_dir_path) = config.ephemeral_storage_dir_path.as_ref() {
961+
let index_store = runtime
962+
.block_on(setup_index_store(self.config.storage_dir_path.clone().into()))
963+
.map_err(|e| {
964+
log_error!(logger, "Failed to setup tier-store index: {}", e);
965+
BuildError::KVStoreSetupFailed
966+
})?;
967+
let ephemeral_store = SqliteStore::new(
968+
ephemeral_storage_dir_path.clone(),
969+
Some(io::sqlite_store::SQLITE_EPHEMERAL_DB_FILE_NAME.to_string()),
970+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
971+
)
972+
.map_err(|e| {
973+
log_error!(logger, "Failed to setup ephemeral SQLite store: {}", e);
974+
BuildError::KVStoreSetupFailed
975+
})?;
976+
let ephemeral_store: Arc<DynStore> = Arc::new(DynStoreWrapper(ephemeral_store));
977+
tier_store.set_index_store(index_store);
978+
tier_store.set_ephemeral_store(ephemeral_store);
979+
}
980+
981+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
982+
let backup_store = SqliteStore::new(
983+
backup_storage_dir_path.clone(),
984+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
985+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
986+
)
987+
.map_err(|e| {
988+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
989+
BuildError::KVStoreSetupFailed
990+
})?;
991+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
992+
tier_store.set_backup_store(backup_store);
993+
}
994+
}
995+
904996
let seed_bytes = node_entropy.to_seed_bytes();
905997
let config = Arc::new(self.config.clone());
906998

@@ -915,7 +1007,7 @@ impl NodeBuilder {
9151007
seed_bytes,
9161008
runtime,
9171009
logger,
918-
Arc::new(DynStoreWrapper(kv_store)),
1010+
Arc::new(DynStoreWrapper(tier_store)),
9191011
)
9201012
}
9211013
}

src/io/sqlite_store/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ mod migrations;
3030
pub const SQLITE_DB_FILE_NAME: &str = "ldk_node_data.sqlite";
3131
/// LDK Node's internal tier-store index database file name.
3232
pub(crate) const SQLITE_TIER_INDEX_DB_FILE_NAME: &str = "ldk_node_tier_index.sqlite";
33+
/// LDK Node's backup database file name.
34+
pub const SQLITE_BACKUP_DB_FILE_NAME: &str = "ldk_node_data_backup.sqlite";
35+
/// LDK Node's ephemeral database file name.
36+
pub const SQLITE_EPHEMERAL_DB_FILE_NAME: &str = "ldk_node_data_ephemeral.sqlite";
3337
/// LDK Node's table in which we store all data.
3438
pub const KV_TABLE_NAME: &str = "ldk_node_data";
3539

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;

tests/common/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ use lightning::ln::msgs::SocketAddress;
5656
use lightning::routing::gossip::NodeAlias;
5757
use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse};
5858
use lightning_invoice::{Bolt11InvoiceDescription, Description};
59-
use lightning_persister::fs_store::v1::FilesystemStore;
59+
use lightning_persister::fs_store::v2::FilesystemStoreV2;
6060
use lightning_types::payment::{PaymentHash, PaymentPreimage};
6161
use logging::TestLogWriter;
6262
use rand::distr::Alphanumeric;
@@ -1901,7 +1901,7 @@ impl PaginatedKVStore for TestSyncStore {
19011901
struct TestSyncStoreInner {
19021902
serializer: tokio::sync::RwLock<()>,
19031903
test_store: InMemoryStore,
1904-
fs_store: FilesystemStore,
1904+
fs_store: FilesystemStoreV2,
19051905
sqlite_store: SqliteStore,
19061906
}
19071907

@@ -1910,7 +1910,7 @@ impl TestSyncStoreInner {
19101910
let serializer = tokio::sync::RwLock::new(());
19111911
let mut fs_dir = dest_dir.clone();
19121912
fs_dir.push("fs_store");
1913-
let fs_store = FilesystemStore::new(fs_dir);
1913+
let fs_store = FilesystemStoreV2::new(fs_dir).unwrap();
19141914
let mut sql_dir = dest_dir.clone();
19151915
sql_dir.push("sqlite_store");
19161916
let sqlite_store = SqliteStore::new(

tests/integration_tests_rust.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ use ldk_node::config::{
3838
AsyncPaymentsRole, EsploraSyncConfig, ADDRESS_POOL_SIZE, DEFAULT_FULL_SCAN_STOP_GAP,
3939
};
4040
use ldk_node::entropy::NodeEntropy;
41+
#[cfg(not(feature = "uniffi"))]
42+
use ldk_node::io::sqlite_store::SqliteStore;
4143
use ldk_node::liquidity::LSPS2ServiceConfig;
4244
use ldk_node::payment::{
4345
ConfirmationStatus, PayerProofOptions, PaymentDetails, PaymentDirection, PaymentKind,
@@ -4925,3 +4927,74 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) {
49254927
cheap.stop().unwrap();
49264928
expensive.stop().unwrap();
49274929
}
4930+
4931+
// Builder backup-store configuration is not yet exposed via FFI (see #871)
4932+
#[cfg(not(feature = "uniffi"))]
4933+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4934+
async fn builder_configures_sqlite_backup_store() {
4935+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
4936+
let chain_source = random_chain_source(&bitcoind, &electrsd);
4937+
4938+
let mut config_a = random_config();
4939+
config_a.store_type = TestStoreType::Sqlite;
4940+
let primary_dir = config_a.node_config.storage_dir_path.clone();
4941+
let backup_dir = common::random_storage_path();
4942+
4943+
// Build node_a with backup storage configured
4944+
setup_builder!(builder_a, config_a.node_config.clone());
4945+
builder_a.set_chain_source_esplora(
4946+
format!("http://{}", electrsd.esplora_url.as_ref().unwrap()),
4947+
None,
4948+
);
4949+
builder_a.set_filesystem_logger(None, None);
4950+
builder_a.set_backup_storage_dir_path(backup_dir.to_str().unwrap().to_owned());
4951+
4952+
let node_a = builder_a.build(config_a.node_entropy.into()).unwrap();
4953+
node_a.start().unwrap();
4954+
assert!(node_a.status().is_running);
4955+
assert!(node_a.status().latest_fee_rate_cache_update_timestamp.is_some());
4956+
4957+
let mut config_b = random_config();
4958+
config_b.node_config.manually_handle_unknown_bolt11_payments = true;
4959+
let node_b = setup_node(&chain_source, config_b);
4960+
4961+
do_channel_full_cycle(
4962+
node_a,
4963+
node_b,
4964+
&bitcoind.client,
4965+
&electrsd.client,
4966+
false,
4967+
true,
4968+
true,
4969+
false,
4970+
)
4971+
.await;
4972+
4973+
let primary_store = SqliteStore::new(
4974+
primary_dir.into(),
4975+
Some(ldk_node::io::sqlite_store::SQLITE_DB_FILE_NAME.to_string()),
4976+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
4977+
)
4978+
.unwrap();
4979+
4980+
let backup_store = SqliteStore::new(
4981+
backup_dir,
4982+
Some(ldk_node::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
4983+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
4984+
)
4985+
.unwrap();
4986+
4987+
for (pn, sn, key) in [
4988+
("bdk_wallet", "", "descriptor"),
4989+
("bdk_wallet", "", "change_descriptor"),
4990+
("bdk_wallet", "", "network"),
4991+
("", "", "node_metrics"),
4992+
("", "", "events"),
4993+
("", "", "peers"),
4994+
] {
4995+
let primary = primary_store.read(pn, sn, key).await.unwrap();
4996+
let backup = backup_store.read(pn, sn, key).await.unwrap();
4997+
4998+
assert_eq!(backup, primary, "backup mismatch for {pn}/{sn}/{key}");
4999+
}
5000+
}

0 commit comments

Comments
 (0)