Skip to content

Commit cdb021e

Browse files
committed
Add filtered backup restoration
Introduce a restore path that copies durable state from a configured backup store into an empty primary store before normal node initialization. This commit: - Adds a recovery module to define the durable restore scope, filter known durable keys, restore them into the primary store, and detect when a primary already contains durable state. - Wires this into the builder by separating backup restore from wallet recovery, adding restore-specific build errors, and running restore before any normal startup reads. Also, covers the new logic with unit tests plus an integration test that exercises backup population, restore into a fresh primary, and successful node boot with preserved identity.
1 parent ca54394 commit cdb021e

8 files changed

Lines changed: 647 additions & 25 deletions

File tree

src/builder.rs

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ use lightning::chain::{chainmonitor, BestBlock};
2626
use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs};
2727
use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress};
2828
use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler};
29-
use lightning::log_trace;
3029
use lightning::onion_message::dns_resolution::DNSResolverMessageHandler;
3130
use lightning::routing::gossip::NodeAlias;
3231
use lightning::routing::router::DefaultRouter;
@@ -42,6 +41,7 @@ use lightning::util::persist::{
4241
};
4342
use lightning::util::ser::ReadableArgs;
4443
use lightning::util::sweep::OutputSweeper;
44+
use lightning::{log_info, log_trace};
4545
use lightning_dns_resolver::OMDomainResolver;
4646
use lightning_persister::fs_store::v1::FilesystemStore;
4747
use vss_client::headers::VssHeaderProvider;
@@ -57,6 +57,7 @@ use crate::entropy::NodeEntropy;
5757
use crate::event::EventQueue;
5858
use crate::fee_estimator::OnchainFeeEstimator;
5959
use crate::gossip::GossipSource;
60+
use crate::io::recovery::{list_existing_durable_keys, restore_from_backup};
6061
use crate::io::sqlite_store::SqliteStore;
6162
use crate::io::tier_store::{BackupMode, BackupRetryQueue, TierStore};
6263
use crate::io::utils::{
@@ -173,6 +174,12 @@ impl std::fmt::Debug for TierStoreConfig {
173174
}
174175
}
175176

177+
#[derive(Debug, Clone, Copy, Default)]
178+
struct RecoveryConfig {
179+
wallet_recovery: bool,
180+
restore_from_backup: bool,
181+
}
182+
176183
/// An error encountered during building a [`Node`].
177184
///
178185
/// [`Node`]: crate::Node
@@ -219,6 +226,12 @@ pub enum BuildError {
219226
AsyncPaymentsConfigMismatch,
220227
/// An attempt to setup a DNS Resolver failed.
221228
DNSResolverSetupFailed,
229+
/// Restore was requested but no backup store is configured.
230+
RestoreRequiresBackupStore,
231+
/// Restore was requested but the primary store already contains durable data.
232+
RestorePrimaryNotEmpty,
233+
/// Failed to restore state from the backup store.
234+
RestoreFailed,
222235
}
223236

224237
impl fmt::Display for BuildError {
@@ -256,6 +269,15 @@ impl fmt::Display for BuildError {
256269
Self::DNSResolverSetupFailed => {
257270
write!(f, "An attempt to setup a DNS resolver has failed.")
258271
},
272+
Self::RestoreRequiresBackupStore => {
273+
write!(f, "Restore from backup was requested but no backup store is configured.")
274+
},
275+
Self::RestorePrimaryNotEmpty => {
276+
write!(f, "Restore from backup was requested but the primary store already contains durable data.")
277+
},
278+
Self::RestoreFailed => {
279+
write!(f, "Failed to restore state from the backup store.")
280+
},
259281
}
260282
}
261283
}
@@ -311,7 +333,7 @@ pub struct NodeBuilder {
311333
tier_store_config: Option<TierStoreConfig>,
312334
runtime_handle: Option<tokio::runtime::Handle>,
313335
pathfinding_scores_sync_config: Option<PathfindingScoresSyncConfig>,
314-
recovery_mode: bool,
336+
recovery_config: RecoveryConfig,
315337
}
316338

317339
impl NodeBuilder {
@@ -330,7 +352,7 @@ impl NodeBuilder {
330352
let tier_store_config = None;
331353
let runtime_handle = None;
332354
let pathfinding_scores_sync_config = None;
333-
let recovery_mode = false;
355+
let recovery_config = RecoveryConfig::default();
334356
Self {
335357
config,
336358
chain_data_source_config,
@@ -341,7 +363,7 @@ impl NodeBuilder {
341363
runtime_handle,
342364
async_payments_role: None,
343365
pathfinding_scores_sync_config,
344-
recovery_mode,
366+
recovery_config,
345367
}
346368
}
347369

@@ -643,7 +665,28 @@ impl NodeBuilder {
643665
/// This should only be set on first startup when importing an older wallet from a previously
644666
/// used [`NodeEntropy`].
645667
pub fn set_wallet_recovery_mode(&mut self) -> &mut Self {
646-
self.recovery_mode = true;
668+
self.recovery_config.wallet_recovery = true;
669+
self
670+
}
671+
672+
/// Configures the [`Node`] to restore durable state from the configured backup
673+
/// store before normal startup reads occur.
674+
///
675+
/// This is intended for disaster recovery: when the primary store has been lost
676+
/// or corrupted, the node can be rebuilt from a backup store that was previously
677+
/// configured via [`set_backup_store`] and actively receiving writes during
678+
/// normal operation.
679+
///
680+
/// The primary store must be empty (contain no durable state) else the build will
681+
/// fail with [`BuildError::RestorePrimaryNotEmpty`] if existing durable data
682+
/// is detected.
683+
///
684+
/// Note: this is distinct from wallet recovery mode, which controls whether the
685+
/// on-chain wallet is rescanned from genesis on first startup.
686+
///
687+
/// [`set_backup_store`]: Self::set_backup_store
688+
pub fn restore_from_backup(&mut self) -> &mut Self {
689+
self.recovery_config.restore_from_backup = true;
647690
self
648691
}
649692

@@ -909,7 +952,7 @@ impl NodeBuilder {
909952
self.liquidity_source_config.as_ref(),
910953
self.pathfinding_scores_sync_config.as_ref(),
911954
self.async_payments_role,
912-
self.recovery_mode,
955+
self.recovery_config,
913956
seed_bytes,
914957
runtime,
915958
logger,
@@ -1352,10 +1395,38 @@ fn build_with_store_internal(
13521395
gossip_source_config: Option<&GossipSourceConfig>,
13531396
liquidity_source_config: Option<&LiquiditySourceConfig>,
13541397
pathfinding_scores_sync_config: Option<&PathfindingScoresSyncConfig>,
1355-
async_payments_role: Option<AsyncPaymentsRole>, recovery_mode: bool, seed_bytes: [u8; 64],
1356-
runtime: Arc<Runtime>, logger: Arc<Logger>, kv_store: Arc<DynStore>,
1398+
async_payments_role: Option<AsyncPaymentsRole>, recovery_config: RecoveryConfig,
1399+
seed_bytes: [u8; 64], runtime: Arc<Runtime>, logger: Arc<Logger>, kv_store: Arc<DynStore>,
13571400
tier_store: Arc<TierStore>,
13581401
) -> Result<Node, BuildError> {
1402+
if recovery_config.restore_from_backup {
1403+
let backup = Arc::clone(
1404+
&tier_store.backup_store().ok_or(BuildError::RestoreRequiresBackupStore)?.store,
1405+
);
1406+
1407+
let existing_durable_keys = list_existing_durable_keys(&*tier_store.primary_store())
1408+
.map_err(|e| {
1409+
log_error!(logger, "Failed to enumerate primary store during restore: {}", e);
1410+
BuildError::ReadFailed
1411+
})?;
1412+
1413+
if !existing_durable_keys.is_empty() {
1414+
log_error!(
1415+
logger,
1416+
"Restore refused: primary store already contains {} durable key(s)",
1417+
existing_durable_keys.len()
1418+
);
1419+
return Err(BuildError::RestorePrimaryNotEmpty);
1420+
}
1421+
1422+
restore_from_backup(&*tier_store.primary_store(), &*backup).map_err(|e| {
1423+
log_error!(logger, "Failed to restore from backup: {}", e);
1424+
BuildError::RestoreFailed
1425+
})?;
1426+
1427+
log_info!(logger, "Successfully restored durable state from backup store");
1428+
}
1429+
13591430
optionally_install_rustls_cryptoprovider();
13601431

13611432
if let Err(err) = may_announce_channel(&config) {
@@ -1561,7 +1632,7 @@ fn build_with_store_internal(
15611632
BuildError::WalletSetupFailed
15621633
})?;
15631634

1564-
if !recovery_mode {
1635+
if !recovery_config.wallet_recovery {
15651636
if let Some(best_block) = chain_tip_opt {
15661637
// Insert the first checkpoint if we have it, to avoid resyncing from genesis.
15671638
// TODO: Use a proper wallet birthday once BDK supports it.
@@ -2262,7 +2333,11 @@ pub(crate) fn sanitize_alias(alias_str: &str) -> Result<NodeAlias, BuildError> {
22622333

22632334
#[cfg(test)]
22642335
mod tests {
2265-
use super::{sanitize_alias, BuildError, NodeAlias};
2336+
use lightning::util::persist::KVStoreSync;
2337+
2338+
use crate::io::test_utils::InMemoryStore;
2339+
2340+
use super::*;
22662341

22672342
#[test]
22682343
fn sanitize_empty_node_alias() {
@@ -2299,4 +2374,39 @@ mod tests {
22992374
let node = sanitize_alias(alias);
23002375
assert_eq!(node.err().unwrap(), BuildError::InvalidNodeAlias);
23012376
}
2377+
2378+
#[test]
2379+
fn restore_requires_backup_store() {
2380+
let mut builder = NodeBuilder::new();
2381+
let entropy = NodeEntropy::from_seed_bytes([42; 64]);
2382+
let primary = InMemoryStore::new();
2383+
2384+
let res = builder.restore_from_backup().build_with_store(entropy, primary);
2385+
2386+
assert!(matches!(res, Err(BuildError::RestoreRequiresBackupStore)));
2387+
}
2388+
2389+
#[test]
2390+
fn restore_refuses_nonempty_primary() {
2391+
let mut builder = NodeBuilder::new();
2392+
let entropy = NodeEntropy::from_seed_bytes([43; 64]);
2393+
2394+
let primary = InMemoryStore::new();
2395+
let backup: Arc<DynStore> = Arc::new(DynStoreWrapper(InMemoryStore::new()));
2396+
2397+
KVStoreSync::write(
2398+
&primary,
2399+
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
2400+
CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE,
2401+
CHANNEL_MANAGER_PERSISTENCE_KEY,
2402+
b"existing".to_vec(),
2403+
)
2404+
.unwrap();
2405+
2406+
builder.set_backup_store(backup, BackupMode::BestEffortBackup);
2407+
builder.restore_from_backup();
2408+
2409+
let res = builder.build_with_store(entropy, primary);
2410+
assert!(matches!(res, Err(BuildError::RestorePrimaryNotEmpty)));
2411+
}
23022412
}

src/io/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
//! Objects and traits for data persistence.
99
10+
pub(crate) mod recovery;
1011
pub mod sqlite_store;
1112
#[cfg(test)]
1213
pub(crate) mod test_utils;

0 commit comments

Comments
 (0)