@@ -26,7 +26,6 @@ use lightning::chain::{chainmonitor, BestBlock};
2626use lightning:: ln:: channelmanager:: { self , ChainParameters , ChannelManagerReadArgs } ;
2727use lightning:: ln:: msgs:: { RoutingMessageHandler , SocketAddress } ;
2828use lightning:: ln:: peer_handler:: { IgnoringMessageHandler , MessageHandler } ;
29- use lightning:: log_trace;
3029use lightning:: onion_message:: dns_resolution:: DNSResolverMessageHandler ;
3130use lightning:: routing:: gossip:: NodeAlias ;
3231use lightning:: routing:: router:: DefaultRouter ;
@@ -42,6 +41,7 @@ use lightning::util::persist::{
4241} ;
4342use lightning:: util:: ser:: ReadableArgs ;
4443use lightning:: util:: sweep:: OutputSweeper ;
44+ use lightning:: { log_info, log_trace} ;
4545use lightning_dns_resolver:: OMDomainResolver ;
4646use lightning_persister:: fs_store:: v1:: FilesystemStore ;
4747use vss_client:: headers:: VssHeaderProvider ;
@@ -57,6 +57,7 @@ use crate::entropy::NodeEntropy;
5757use crate :: event:: EventQueue ;
5858use crate :: fee_estimator:: OnchainFeeEstimator ;
5959use crate :: gossip:: GossipSource ;
60+ use crate :: io:: recovery:: { list_existing_durable_keys, restore_from_backup} ;
6061use crate :: io:: sqlite_store:: SqliteStore ;
6162use crate :: io:: tier_store:: { BackupMode , BackupRetryQueue , TierStore } ;
6263use 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
224237impl 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
317339impl 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) ]
22642335mod 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}
0 commit comments