Skip to content

Commit 75ca8d7

Browse files
committed
Safely migrate to FileSystemStoreV2
Before moving to PaginatedKVStore everywhere we need to use FileSystemStoreV2 instead of FileSystemStoreV1. This will safely migrate over to it on first start up. Also adds a test to make sure we handle it properly.
1 parent 3fd0194 commit 75ca8d7

3 files changed

Lines changed: 102 additions & 15 deletions

File tree

src/builder.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ use lightning::util::persist::{
4343
use lightning::util::ser::ReadableArgs;
4444
use lightning::util::sweep::OutputSweeper;
4545
use lightning_dns_resolver::OMDomainResolver;
46-
use lightning_persister::fs_store::v1::FilesystemStore;
4746
use vss_client::headers::VssHeaderProvider;
4847

4948
use crate::chain::ChainSource;
@@ -59,9 +58,9 @@ use crate::fee_estimator::OnchainFeeEstimator;
5958
use crate::gossip::GossipSource;
6059
use crate::io::sqlite_store::SqliteStore;
6160
use crate::io::utils::{
62-
read_event_queue, read_external_pathfinding_scores_from_cache, read_network_graph,
63-
read_node_metrics, read_output_sweeper, read_payments, read_peer_info, read_pending_payments,
64-
read_scorer,
61+
open_or_migrate_fs_store, read_event_queue, read_external_pathfinding_scores_from_cache,
62+
read_network_graph, read_node_metrics, read_output_sweeper, read_payments, read_peer_info,
63+
read_pending_payments, read_scorer,
6564
};
6665
use crate::io::vss_store::VssStoreBuilder;
6766
use crate::io::{
@@ -641,15 +640,22 @@ impl NodeBuilder {
641640
self.build_with_store(node_entropy, kv_store)
642641
}
643642

644-
/// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options
643+
/// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options
645644
/// previously configured.
645+
///
646+
/// If the storage directory contains data from a v1 filesystem store, it will be
647+
/// automatically migrated to the v2 format.
648+
///
649+
/// [`FilesystemStoreV2`]: lightning_persister::fs_store::v2::FilesystemStoreV2
646650
pub fn build_with_fs_store(&self, node_entropy: NodeEntropy) -> Result<Node, BuildError> {
647651
let mut storage_dir_path: PathBuf = self.config.storage_dir_path.clone().into();
648652
storage_dir_path.push("fs_store");
649653

650654
fs::create_dir_all(storage_dir_path.clone())
651655
.map_err(|_| BuildError::StoragePathAccessFailed)?;
652-
let kv_store = FilesystemStore::new(storage_dir_path);
656+
657+
let kv_store = open_or_migrate_fs_store(storage_dir_path)?;
658+
653659
self.build_with_store(node_entropy, kv_store)
654660
}
655661

@@ -1103,7 +1109,7 @@ impl ArcedNodeBuilder {
11031109
self.inner.read().expect("lock").build(*node_entropy).map(Arc::new)
11041110
}
11051111

1106-
/// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options
1112+
/// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options
11071113
/// previously configured.
11081114
pub fn build_with_fs_store(
11091115
&self, node_entropy: Arc<NodeEntropy>,

src/io/utils.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::io::Write;
1010
use std::ops::Deref;
1111
#[cfg(unix)]
1212
use std::os::unix::fs::OpenOptionsExt;
13-
use std::path::Path;
13+
use std::path::{Path, PathBuf};
1414
use std::sync::{Arc, RwLock};
1515

1616
use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet;
@@ -26,14 +26,16 @@ use lightning::routing::scoring::{
2626
ChannelLiquidities, ProbabilisticScorer, ProbabilisticScoringDecayParameters,
2727
};
2828
use lightning::util::persist::{
29-
KVStore, KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN,
30-
NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE,
31-
NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY,
32-
OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE,
33-
SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE,
34-
SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
29+
migrate_kv_store_data, KVStore, KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET,
30+
KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY,
31+
NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE,
32+
OUTPUT_SWEEPER_PERSISTENCE_KEY, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE,
33+
OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY,
34+
SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE,
3535
};
3636
use lightning::util::ser::{Readable, ReadableArgs, Writeable};
37+
use lightning_persister::fs_store::v1::FilesystemStore;
38+
use lightning_persister::fs_store::v2::{FilesystemStoreV2, FilesystemStoreV2Error};
3739
use lightning_types::string::PrintableString;
3840

3941
use super::*;
@@ -48,7 +50,7 @@ use crate::payment::PendingPaymentDetails;
4850
use crate::peer_store::PeerStore;
4951
use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper};
5052
use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper};
51-
use crate::{Error, EventQueue, NodeMetrics, PaymentDetails};
53+
use crate::{BuildError, Error, EventQueue, NodeMetrics, PaymentDetails};
5254

5355
pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_scores_cache";
5456

@@ -709,6 +711,42 @@ where
709711
Ok(res)
710712
}
711713

714+
/// Opens a [`FilesystemStoreV2`], automatically migrating from v1 format if necessary.
715+
///
716+
/// If the directory contains v1 data (files at the top level), the data is migrated to v2 format
717+
/// in a temporary directory, the original is renamed to `fs_store_v1_backup`, and the migrated
718+
/// directory is moved into place.
719+
pub(crate) fn open_or_migrate_fs_store(
720+
storage_dir_path: PathBuf,
721+
) -> Result<FilesystemStoreV2, BuildError> {
722+
match FilesystemStoreV2::new(storage_dir_path.clone()) {
723+
Ok(store) => Ok(store),
724+
Err(FilesystemStoreV2Error::V1DataDetected(_)) => {
725+
// The directory contains v1 data, migrate to v2.
726+
let mut v1_store = FilesystemStore::new(storage_dir_path.clone());
727+
728+
let mut v2_dir = storage_dir_path.clone();
729+
v2_dir.set_file_name("fs_store_v2_migrating");
730+
fs::create_dir_all(v2_dir.clone()).map_err(|_| BuildError::StoragePathAccessFailed)?;
731+
let mut v2_store = FilesystemStoreV2::new(v2_dir.clone())
732+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
733+
734+
migrate_kv_store_data(&mut v1_store, &mut v2_store)
735+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
736+
737+
// Swap directories: rename v1 out of the way, move v2 into place.
738+
let mut backup_dir = storage_dir_path.clone();
739+
backup_dir.set_file_name("fs_store_v1_backup");
740+
fs::rename(&storage_dir_path, &backup_dir)
741+
.map_err(|_| BuildError::KVStoreSetupFailed)?;
742+
fs::rename(&v2_dir, &storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)?;
743+
744+
FilesystemStoreV2::new(storage_dir_path).map_err(|_| BuildError::KVStoreSetupFailed)
745+
},
746+
Err(_) => Err(BuildError::KVStoreSetupFailed),
747+
}
748+
}
749+
712750
#[cfg(test)]
713751
mod tests {
714752
use super::read_or_generate_seed_file;

tests/integration_tests_rust.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2634,6 +2634,49 @@ async fn persistence_backwards_compatibility() {
26342634
do_persistence_backwards_compatibility(OldLdkVersion::V0_7_0).await;
26352635
}
26362636

2637+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2638+
async fn fs_store_persistence_backwards_compatibility() {
2639+
let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd();
2640+
let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
2641+
2642+
let storage_path = common::random_storage_path().to_str().unwrap().to_owned();
2643+
let seed_bytes = [42u8; 64];
2644+
2645+
// Build a node using v0.7.0's build_with_fs_store (FilesystemStore v1).
2646+
let mut config = TestConfig::default();
2647+
config.node_config.storage_dir_path = storage_path.clone();
2648+
config.store_type = TestStoreType::FilesystemStore;
2649+
let (old_balance, old_node_id) =
2650+
build_0_7_0_node(&bitcoind, &electrsd, esplora_url.clone(), seed_bytes, &config).await;
2651+
2652+
// Now reopen with current code's build_with_fs_store, which should
2653+
// auto-migrate from FilesystemStore v1 to FilesystemStoreV2.
2654+
#[cfg(feature = "uniffi")]
2655+
let builder_new = Builder::new();
2656+
#[cfg(not(feature = "uniffi"))]
2657+
let mut builder_new = Builder::new();
2658+
builder_new.set_network(bitcoin::Network::Regtest);
2659+
builder_new.set_storage_dir_path(storage_path);
2660+
builder_new.set_chain_source_esplora(esplora_url, None);
2661+
2662+
#[cfg(feature = "uniffi")]
2663+
let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes.to_vec()).unwrap();
2664+
#[cfg(not(feature = "uniffi"))]
2665+
let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes);
2666+
let node_new = builder_new.build_with_fs_store(node_entropy.into()).unwrap();
2667+
2668+
node_new.start().unwrap();
2669+
node_new.sync_wallets().unwrap();
2670+
2671+
let new_balance = node_new.list_balances().spendable_onchain_balance_sats;
2672+
let new_node_id = node_new.node_id();
2673+
2674+
assert_eq!(old_node_id, new_node_id);
2675+
assert_eq!(old_balance, new_balance);
2676+
2677+
node_new.stop().unwrap();
2678+
}
2679+
26372680
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
26382681
async fn onchain_fee_bump_rbf() {
26392682
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)