diff --git a/src/builder.rs b/src/builder.rs index 5f616c4ce8..6e52781b9b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -43,7 +43,6 @@ use lightning::util::persist::{ use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; use lightning_dns_resolver::OMDomainResolver; -use lightning_persister::fs_store::v1::FilesystemStore; use vss_client::headers::VssHeaderProvider; use crate::chain::ChainSource; @@ -59,8 +58,9 @@ use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{ - read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache, - read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info, read_scorer, + open_or_migrate_fs_store, read_all_objects, read_event_queue, + read_external_pathfinding_scores_from_cache, read_network_graph, read_node_metrics, + read_output_sweeper, read_peer_info, read_scorer, }; use crate::io::vss_store::VssStoreBuilder; use crate::io::{ @@ -644,18 +644,19 @@ impl NodeBuilder { self.build_with_store_and_logger(node_entropy, kv_store, logger) } - /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options + /// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options /// previously configured. + /// + /// If the storage directory contains data from a v1 filesystem store, it will be + /// automatically migrated to the v2 format. + /// + /// [`FilesystemStoreV2`]: lightning_persister::fs_store::v2::FilesystemStoreV2 pub fn build_with_fs_store(&self, node_entropy: NodeEntropy) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; let mut storage_dir_path: PathBuf = self.config.storage_dir_path.clone().into(); storage_dir_path.push("fs_store"); - fs::create_dir_all(storage_dir_path.clone()).map_err(|e| { - log_error!(logger, "Failed to setup Filesystem store: {}", e); - BuildError::StoragePathAccessFailed - })?; - let kv_store = FilesystemStore::new(storage_dir_path); + let kv_store = open_or_migrate_fs_store(storage_dir_path)?; self.build_with_store_and_logger(node_entropy, kv_store, logger) } @@ -1115,7 +1116,7 @@ impl ArcedNodeBuilder { self.inner.read().expect("lock").build(*node_entropy).map(Arc::new) } - /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options + /// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options /// previously configured. pub fn build_with_fs_store( &self, node_entropy: Arc, diff --git a/src/io/utils.rs b/src/io/utils.rs index 5b51b88592..798ef4c531 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -10,7 +10,7 @@ use std::io::Write; use std::ops::Deref; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; @@ -26,14 +26,16 @@ use lightning::routing::scoring::{ ChannelLiquidities, ProbabilisticScorer, ProbabilisticScoringDecayParameters, }; use lightning::util::persist::{ - KVStore, KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, - NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, - NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY, - OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, - SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, - SCORER_PERSISTENCE_SECONDARY_NAMESPACE, + migrate_kv_store_data, KVStore, KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, + KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY, + NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, + OUTPUT_SWEEPER_PERSISTENCE_KEY, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, + OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, + SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; +use lightning_persister::fs_store::v1::FilesystemStore; +use lightning_persister::fs_store::v2::{FilesystemStoreV2, FilesystemStoreV2Error}; use lightning_types::string::PrintableString; use super::*; @@ -47,7 +49,7 @@ use crate::logger::{log_error, LdkLogger, Logger}; use crate::peer_store::PeerStore; use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; -use crate::{Error, EventQueue, NodeMetrics}; +use crate::{BuildError, Error, EventQueue, NodeMetrics}; pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_scores_cache"; @@ -619,10 +621,103 @@ pub(crate) fn read_bdk_wallet_change_set( Ok(Some(change_set)) } +/// Opens a [`FilesystemStoreV2`], automatically migrating from v1 format if necessary. +/// +/// If the directory contains v1 data (files at the top level), the data is migrated to v2 format +/// in a temporary directory, the original is renamed to `fs_store_v1_backup`, and the migrated +/// directory is moved into place. +pub(crate) fn open_or_migrate_fs_store( + storage_dir_path: PathBuf, +) -> Result { + let parent_dir = storage_dir_path.parent().ok_or(BuildError::StoragePathAccessFailed)?; + fs::create_dir_all(parent_dir).map_err(|_| BuildError::StoragePathAccessFailed)?; + recover_incomplete_fs_store_migration(&storage_dir_path)?; + if !storage_dir_path.exists() { + fs::create_dir_all(storage_dir_path.clone()) + .map_err(|_| BuildError::StoragePathAccessFailed)?; + } + + match FilesystemStoreV2::new(storage_dir_path.clone()) { + Ok(store) => Ok(store), + Err(FilesystemStoreV2Error::V1DataDetected(_)) => { + // The directory contains v1 data, migrate to v2. + let mut v1_store = FilesystemStore::new(storage_dir_path.clone()); + + let v2_dir = fs_store_sibling_path(&storage_dir_path, "fs_store_v2_migrating"); + fs::create_dir_all(v2_dir.clone()).map_err(|_| BuildError::StoragePathAccessFailed)?; + let mut v2_store = FilesystemStoreV2::new(v2_dir.clone()) + .map_err(|_| BuildError::KVStoreSetupFailed)?; + + migrate_kv_store_data(&mut v1_store, &mut v2_store) + .map_err(|_| BuildError::KVStoreSetupFailed)?; + + // Swap directories: rename v1 out of the way, move v2 into place. + let backup_dir = fs_store_sibling_path(&storage_dir_path, "fs_store_v1_backup"); + fs::rename(&storage_dir_path, &backup_dir) + .map_err(|_| BuildError::KVStoreSetupFailed)?; + fs::rename(&v2_dir, &storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?; + + FilesystemStoreV2::new(storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed) + }, + Err(_) => Err(BuildError::KVStoreSetupFailed), + } +} + +fn fs_store_sibling_path(storage_dir_path: &Path, file_name: &str) -> PathBuf { + let mut sibling_path = storage_dir_path.to_path_buf(); + sibling_path.set_file_name(file_name); + sibling_path +} + +fn recover_incomplete_fs_store_migration(storage_dir_path: &Path) -> Result<(), BuildError> { + let v2_dir = fs_store_sibling_path(storage_dir_path, "fs_store_v2_migrating"); + let backup_dir = fs_store_sibling_path(storage_dir_path, "fs_store_v1_backup"); + + if storage_dir_path.exists() { + if v2_dir.exists() { + // The original store is still in place, so a temp migration dir is from a crash before + // the rename step and can be discarded before retrying migration. + fs::remove_dir_all(&v2_dir).map_err(|_| BuildError::KVStoreSetupFailed)?; + } + return Ok(()); + } + + if backup_dir.exists() { + if v2_dir.exists() { + // Prefer retrying from the v1 backup instead of deciding here whether the temp v2 dir is + // usable. open_or_migrate_fs_store owns the actual v1-to-v2 migration. + fs::remove_dir_all(&v2_dir).map_err(|_| BuildError::KVStoreSetupFailed)?; + } + // The crash happened after moving v1 aside; restore it so normal startup can migrate it. + fs::rename(&backup_dir, storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?; + return Ok(()); + } + + if v2_dir.exists() { + // There is no v1 backup to retry from. Move the temp dir into place and let + // open_or_migrate_fs_store decide whether it is a valid v2 store. + fs::rename(&v2_dir, storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?; + } + + Ok(()) +} + #[cfg(test)] mod tests { - use super::read_or_generate_seed_file; + use std::fs; + use std::path::{Path, PathBuf}; + + use lightning::util::persist::{migrate_kv_store_data, KVStoreSync}; + use lightning_persister::fs_store::v1::FilesystemStore; + use lightning_persister::fs_store::v2::FilesystemStoreV2; + use super::test_utils::random_storage_path; + use super::{open_or_migrate_fs_store, read_or_generate_seed_file}; + + const TEST_PRIMARY_NAMESPACE: &str = "test_primary_namespace"; + const TEST_SECONDARY_NAMESPACE: &str = "test_secondary_namespace"; + const TEST_KEY: &str = "test_key"; + const TEST_VALUE: &[u8] = b"test_value"; #[test] fn generated_seed_is_readable() { @@ -632,4 +727,158 @@ mod tests { let read_seed_bytes = read_or_generate_seed_file(&rand_path.to_str().unwrap()).unwrap(); assert_eq!(expected_seed_bytes, read_seed_bytes); } + + #[test] + fn fs_store_migration_recovers_before_v1_backup_rename() { + let fs_store_path = fs_store_path(); + let mut v1_store = write_v1_test_data(&fs_store_path); + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let mut v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + migrate_kv_store_data(&mut v1_store, &mut v2_store).unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap(); + assert_eq!( + KVStoreSync::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + #[test] + fn fs_store_migration_recovers_after_v1_backup_rename() { + let fs_store_path = fs_store_path(); + let mut v1_store = write_v1_test_data(&fs_store_path); + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let mut v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + migrate_kv_store_data(&mut v1_store, &mut v2_store).unwrap(); + + let backup_path = sibling_path(&fs_store_path, "fs_store_v1_backup"); + fs::rename(&fs_store_path, backup_path).unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap(); + assert_eq!( + KVStoreSync::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + #[test] + fn fs_store_migration_recovers_after_v2_rename() { + let fs_store_path = fs_store_path(); + let mut v1_store = write_v1_test_data(&fs_store_path); + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let mut v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + migrate_kv_store_data(&mut v1_store, &mut v2_store).unwrap(); + + let backup_path = sibling_path(&fs_store_path, "fs_store_v1_backup"); + fs::rename(&fs_store_path, &backup_path).unwrap(); + fs::rename(&v2_migrating_path, &fs_store_path).unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap(); + assert_eq!( + KVStoreSync::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(backup_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + #[test] + fn fs_store_migration_recovers_backup_without_migrating_dir() { + let fs_store_path = fs_store_path(); + write_v1_test_data(&fs_store_path); + + let backup_path = sibling_path(&fs_store_path, "fs_store_v1_backup"); + fs::rename(&fs_store_path, backup_path).unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap(); + assert_eq!( + KVStoreSync::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!sibling_path(&fs_store_path, "fs_store_v1_backup").exists()); + } + + #[test] + fn fs_store_migration_recovers_unexpected_migrating_dir_without_backup() { + let fs_store_path = fs_store_path(); + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + KVStoreSync::write( + &v2_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY, + TEST_VALUE.to_vec(), + ) + .unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).unwrap(); + assert_eq!( + KVStoreSync::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + fn fs_store_path() -> PathBuf { + let mut fs_store_path = random_storage_path(); + fs_store_path.push("fs_store"); + fs_store_path + } + + fn sibling_path(path: &Path, file_name: &str) -> PathBuf { + let mut sibling_path = path.to_path_buf(); + sibling_path.set_file_name(file_name); + sibling_path + } + + fn write_v1_test_data(fs_store_path: &Path) -> FilesystemStore { + let v1_store = FilesystemStore::new(fs_store_path.to_path_buf()); + KVStoreSync::write( + &v1_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY, + TEST_VALUE.to_vec(), + ) + .unwrap(); + v1_store + } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b701028c54..30d9a4387e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -416,6 +416,7 @@ pub(crate) enum TestChainSource<'a> { pub(crate) enum TestStoreType { TestSyncStore, Sqlite, + FilesystemStore, } impl Default for TestStoreType { @@ -592,6 +593,9 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> builder.build_with_store(config.node_entropy.into(), kv_store).unwrap() }, TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(), + TestStoreType::FilesystemStore => { + builder.build_with_fs_store(config.node_entropy.into()).unwrap() + }, }; if config.recovery_mode { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5360661d32..1ea6c45845 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -24,7 +24,7 @@ use common::{ generate_listening_addresses, open_channel, open_channel_push_amt, open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all, - wait_for_tx, TestChainSource, TestStoreType, TestSyncStore, + wait_for_tx, TestChainSource, TestConfig, TestStoreType, TestSyncStore, }; use electrsd::corepc_node::Node as BitcoinD; use electrsd::ElectrsD; @@ -2556,15 +2556,19 @@ async fn build_0_6_2_node( } async fn build_0_7_0_node( - bitcoind: &BitcoinD, electrsd: &ElectrsD, storage_path: String, esplora_url: String, - seed_bytes: [u8; 64], + bitcoind: &BitcoinD, electrsd: &ElectrsD, esplora_url: String, seed_bytes: [u8; 64], + config: &TestConfig, ) -> (u64, bitcoin::secp256k1::PublicKey) { let mut builder_old = ldk_node_070::Builder::new(); builder_old.set_network(bitcoin::Network::Regtest); - builder_old.set_storage_dir_path(storage_path); + builder_old.set_storage_dir_path(config.node_config.storage_dir_path.clone()); builder_old.set_entropy_seed_bytes(seed_bytes); builder_old.set_chain_source_esplora(esplora_url, None); - let node_old = builder_old.build().unwrap(); + let node_old = match config.store_type { + TestStoreType::FilesystemStore => builder_old.build_with_fs_store().unwrap(), + TestStoreType::Sqlite => builder_old.build().unwrap(), + TestStoreType::TestSyncStore => panic!("TestSyncStore not supported in v0.7.0 builder"), + }; node_old.start().unwrap(); let addr_old = node_old.onchain_payment().new_address().unwrap(); @@ -2605,14 +2609,10 @@ async fn do_persistence_backwards_compatibility(version: OldLdkVersion) { .await }, OldLdkVersion::V0_7_0 => { - build_0_7_0_node( - &bitcoind, - &electrsd, - storage_path.clone(), - esplora_url.clone(), - seed_bytes, - ) - .await + let mut config = TestConfig::default(); + config.store_type = TestStoreType::Sqlite; + config.node_config.storage_dir_path = storage_path.clone(); + build_0_7_0_node(&bitcoind, &electrsd, esplora_url.clone(), seed_bytes, &config).await }, }; @@ -2649,6 +2649,49 @@ async fn persistence_backwards_compatibility() { do_persistence_backwards_compatibility(OldLdkVersion::V0_7_0).await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn fs_store_persistence_backwards_compatibility() { + let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); + let seed_bytes = [42u8; 64]; + + // Build a node using v0.7.0's build_with_fs_store (FilesystemStore v1). + let mut config = TestConfig::default(); + config.node_config.storage_dir_path = storage_path.clone(); + config.store_type = TestStoreType::FilesystemStore; + let (old_balance, old_node_id) = + build_0_7_0_node(&bitcoind, &electrsd, esplora_url.clone(), seed_bytes, &config).await; + + // Now reopen with current code's build_with_fs_store, which should + // auto-migrate from FilesystemStore v1 to FilesystemStoreV2. + #[cfg(feature = "uniffi")] + let builder_new = Builder::new(); + #[cfg(not(feature = "uniffi"))] + let mut builder_new = Builder::new(); + builder_new.set_network(bitcoin::Network::Regtest); + builder_new.set_storage_dir_path(storage_path); + builder_new.set_chain_source_esplora(esplora_url, None); + + #[cfg(feature = "uniffi")] + let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes.to_vec()).unwrap(); + #[cfg(not(feature = "uniffi"))] + let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); + let node_new = builder_new.build_with_fs_store(node_entropy.into()).unwrap(); + + node_new.start().unwrap(); + node_new.sync_wallets().unwrap(); + + let new_balance = node_new.list_balances().spendable_onchain_balance_sats; + let new_node_id = node_new.node_id(); + + assert_eq!(old_node_id, new_node_id); + assert_eq!(old_balance, new_balance); + + node_new.stop().unwrap(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_fee_bump_rbf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();