Skip to content

Commit a2458e4

Browse files
committed
fixup! Integrate TierStore into NodeBuilder
Refactor backup storage to local SQLite Replaces the builder's BYO backup-store configuration with a path-based local SQLite backup mirror. The builder now constructs the backup store internally using a dedicated backup database file name and rejects configurations where the backup path conflicts with the primary storage path. Also adds test coverage for full-cycle backup mirroring and same-path rejection, as well as a `setup_node_with_builder` test helper to allow builder customization in integration tests.
1 parent 60c46b2 commit a2458e4

4 files changed

Lines changed: 141 additions & 16 deletions

File tree

src/builder.rs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,14 @@ impl std::fmt::Debug for LogWriterConfig {
158158
#[derive(Default)]
159159
struct TierStoreConfig {
160160
ephemeral: Option<Arc<DynStore>>,
161-
backup: Option<Arc<DynStore>>,
161+
backup_storage_dir_path: Option<PathBuf>,
162162
}
163163

164164
impl std::fmt::Debug for TierStoreConfig {
165165
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166166
f.debug_struct("TierStoreConfig")
167167
.field("ephemeral", &self.ephemeral.as_ref().map(|_| "Arc<DynStore>"))
168-
.field("backup", &self.backup.as_ref().map(|_| "Arc<DynStore>"))
168+
.field("backup_storage_dir_path", &self.backup_storage_dir_path)
169169
.finish()
170170
}
171171
}
@@ -216,6 +216,11 @@ pub enum BuildError {
216216
AsyncPaymentsConfigMismatch,
217217
/// An attempt to setup a DNS Resolver failed.
218218
DNSResolverSetupFailed,
219+
/// The configured backup storage path conflicts with the primary storage path.
220+
///
221+
/// Backup storage must use a distinct local directory so that the primary and
222+
/// backup stores do not point to the same SQLite database.
223+
BackupStorePathConflict,
219224
}
220225

221226
impl fmt::Display for BuildError {
@@ -253,6 +258,12 @@ impl fmt::Display for BuildError {
253258
Self::DNSResolverSetupFailed => {
254259
write!(f, "An attempt to setup a DNS resolver has failed.")
255260
},
261+
Self::BackupStorePathConflict => {
262+
write!(
263+
f,
264+
"The configured backup storage path conflicts with the primary storage path."
265+
)
266+
},
256267
}
257268
}
258269
}
@@ -644,18 +655,26 @@ impl NodeBuilder {
644655
self
645656
}
646657

647-
/// Configures the backup store for local disaster recovery.
658+
/// Configures a local SQLite backup store for disaster recovery.
648659
///
649-
/// When building with tiered storage, this store receives a second durable
650-
/// copy of data written to the primary store.
660+
/// When building with tiered storage, a SQLite store will be created at the
661+
/// given directory path using [`SQLITE_BACKUP_DB_FILE_NAME`] as its database
662+
/// file name. It receives a second durable copy of data written to the
663+
/// primary store.
651664
///
652665
/// Writes and removals for primary-backed data only succeed once both the
653-
/// primary and backup stores complete successfully.
666+
/// primary and backup SQLite stores complete successfully.
667+
///
668+
/// The configured path must point to a distinct local directory from the
669+
/// primary storage path. If the backup path equals the primary storage path,
670+
/// building will fail with [`BuildError::BackupStorePathConflict`].
654671
///
655672
/// If not set, durable data will be stored only in the primary store.
656-
pub fn set_backup_store(&mut self, backup_store: Arc<DynStore>) -> &mut Self {
673+
///
674+
/// [`SQLITE_BACKUP_DB_FILE_NAME`]: crate::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME
675+
pub fn set_backup_storage_dir_path(&mut self, backup_storage_dir_path: String) -> &mut Self {
657676
let tier_store_config = self.tier_store_config.get_or_insert(TierStoreConfig::default());
658-
tier_store_config.backup = Some(backup_store);
677+
tier_store_config.backup_storage_dir_path = Some(backup_storage_dir_path.into());
659678
self
660679
}
661680

@@ -830,11 +849,11 @@ impl NodeBuilder {
830849
///
831850
/// The provided `kv_store` will be used as the primary storage backend. Optionally,
832851
/// an ephemeral store for frequently-accessed non-critical data (e.g., network graph, scorer)
833-
/// and a backup store for local disaster recovery can be configured via
834-
/// [`set_ephemeral_store`] and [`set_backup_store`].
852+
/// and a local SQLite backup store for disaster recovery can be configured via
853+
/// [`set_ephemeral_store`] and [`set_backup_storage_dir_path`].
835854
///
836855
/// [`set_ephemeral_store`]: Self::set_ephemeral_store
837-
/// [`set_backup_store`]: Self::set_backup_store
856+
/// [`set_backup_storage_dir_path`]: Self::set_backup_storage_dir_path
838857
pub fn build_with_store<S: SyncAndAsyncKVStore + Send + Sync + 'static>(
839858
&self, node_entropy: NodeEntropy, kv_store: S,
840859
) -> Result<Node, BuildError> {
@@ -859,7 +878,29 @@ impl NodeBuilder {
859878
let mut tier_store = TierStore::new(primary_store, Arc::clone(&logger));
860879
if let Some(config) = ts_config {
861880
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)));
881+
if let Some(backup_storage_dir_path) = config.backup_storage_dir_path.as_ref() {
882+
let primary_storage_dir_path = PathBuf::from(&self.config.storage_dir_path);
883+
if primary_storage_dir_path == *backup_storage_dir_path {
884+
log_error!(
885+
logger,
886+
"Backup storage path must differ from primary storage path: {}",
887+
backup_storage_dir_path.display()
888+
);
889+
return Err(BuildError::BackupStorePathConflict);
890+
}
891+
892+
let backup_store = SqliteStore::new(
893+
backup_storage_dir_path.clone(),
894+
Some(io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
895+
Some(io::sqlite_store::KV_TABLE_NAME.to_string()),
896+
)
897+
.map_err(|e| {
898+
log_error!(logger, "Failed to setup backup SQLite store: {}", e);
899+
BuildError::KVStoreSetupFailed
900+
})?;
901+
let backup_store: Arc<DynStore> = Arc::new(DynStoreWrapper(backup_store));
902+
tier_store.set_backup_store(backup_store);
903+
}
863904
}
864905

865906
let seed_bytes = node_entropy.to_seed_bytes();

src/io/sqlite_store/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ mod migrations;
2626

2727
/// LDK Node's database file name.
2828
pub const SQLITE_DB_FILE_NAME: &str = "ldk_node_data.sqlite";
29+
/// LDK Node's backup database file name.
30+
pub const SQLITE_BACKUP_DB_FILE_NAME: &str = "ldk_node_data_backup.sqlite";
2931
/// LDK Node's table in which we store all data.
3032
pub const KV_TABLE_NAME: &str = "ldk_node_data";
3133

tests/common/mod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,17 @@ pub(crate) fn setup_two_nodes_with_store(
527527
}
528528

529529
pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestNode {
530+
setup_node_with_builder(chain_source, config, |_| {})
531+
}
532+
533+
pub(crate) fn setup_node_with_builder<F>(
534+
chain_source: &TestChainSource, config: TestConfig, configure_builder: F,
535+
) -> TestNode
536+
where
537+
F: FnOnce(&mut Builder),
538+
{
530539
setup_builder!(builder, config.node_config);
540+
531541
match chain_source {
532542
TestChainSource::Esplora(electrsd) => {
533543
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
@@ -586,6 +596,8 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
586596
builder.set_wallet_recovery_mode();
587597
}
588598

599+
configure_builder(&mut builder);
600+
589601
let node = match config.store_type {
590602
TestStoreType::TestSyncStore => {
591603
let kv_store = TestSyncStore::new(config.node_config.storage_dir_path.into());
@@ -594,10 +606,6 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) ->
594606
TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(),
595607
};
596608

597-
if config.recovery_mode {
598-
builder.set_wallet_recovery_mode();
599-
}
600-
601609
node.start().unwrap();
602610
assert!(node.status().is_running);
603611
assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());

tests/integration_tests_rust.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ 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+
use ldk_node::io::sqlite_store::SqliteStore;
3334
use ldk_node::liquidity::LSPS2ServiceConfig;
3435
use ldk_node::payment::{
3536
ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus,
@@ -39,6 +40,7 @@ use ldk_node::{Builder, Event, NodeError};
3940
use lightning::ln::channelmanager::PaymentId;
4041
use lightning::routing::gossip::{NodeAlias, NodeId};
4142
use lightning::routing::router::RouteParametersConfig;
43+
use lightning::util::persist::KVStoreSync;
4244
use lightning_invoice::{Bolt11InvoiceDescription, Description};
4345
use lightning_types::payment::{PaymentHash, PaymentPreimage};
4446
use log::LevelFilter;
@@ -2957,3 +2959,75 @@ async fn splice_in_with_all_balance() {
29572959
node_a.stop().unwrap();
29582960
node_b.stop().unwrap();
29592961
}
2962+
2963+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2964+
async fn builder_configures_sqlite_backup_store() {
2965+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2966+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2967+
2968+
let mut config_a = random_config(true);
2969+
config_a.store_type = TestStoreType::Sqlite;
2970+
let primary_dir = config_a.node_config.storage_dir_path.clone();
2971+
let backup_dir = common::random_storage_path();
2972+
let node_a = common::setup_node_with_builder(&chain_source, config_a.clone(), |builder| {
2973+
builder.set_backup_storage_dir_path(backup_dir.to_str().unwrap().to_owned());
2974+
});
2975+
2976+
let config_b = random_config(true);
2977+
let node_b = setup_node(&chain_source, config_b);
2978+
2979+
do_channel_full_cycle(
2980+
node_a,
2981+
node_b,
2982+
&bitcoind.client,
2983+
&electrsd.client,
2984+
false,
2985+
true,
2986+
true,
2987+
false,
2988+
)
2989+
.await;
2990+
2991+
let primary_store = SqliteStore::new(
2992+
primary_dir.into(),
2993+
Some(ldk_node::io::sqlite_store::SQLITE_DB_FILE_NAME.to_string()),
2994+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
2995+
)
2996+
.unwrap();
2997+
2998+
let backup_store = SqliteStore::new(
2999+
backup_dir,
3000+
Some(ldk_node::io::sqlite_store::SQLITE_BACKUP_DB_FILE_NAME.to_string()),
3001+
Some(ldk_node::io::sqlite_store::KV_TABLE_NAME.to_string()),
3002+
)
3003+
.unwrap();
3004+
3005+
for (pn, sn, key) in [
3006+
("bdk_wallet", "", "descriptor"),
3007+
("bdk_wallet", "", "change_descriptor"),
3008+
("bdk_wallet", "", "network"),
3009+
("", "", "node_metrics"),
3010+
("", "", "events"),
3011+
("", "", "peers"),
3012+
] {
3013+
let primary = KVStoreSync::read(&primary_store, pn, sn, key).unwrap();
3014+
let backup = KVStoreSync::read(&backup_store, pn, sn, key).unwrap();
3015+
3016+
assert_eq!(backup, primary, "backup mismatch for {pn}/{sn}/{key}");
3017+
}
3018+
}
3019+
3020+
#[test]
3021+
fn sqlite_backup_rejects_primary_storage_path() {
3022+
let mut config = random_config(false);
3023+
config.store_type = TestStoreType::Sqlite;
3024+
3025+
let primary_dir = config.node_config.storage_dir_path.clone();
3026+
3027+
setup_builder!(builder, config.node_config);
3028+
builder.set_backup_storage_dir_path(primary_dir);
3029+
3030+
let res = builder.build(config.node_entropy.into());
3031+
3032+
assert!(matches!(res, Err(ldk_node::BuildError::BackupStorePathConflict)));
3033+
}

0 commit comments

Comments
 (0)