From a4bdb8b9942632b0a6fc8d89a4744c10f0736322 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 4 May 2026 15:31:09 -0500 Subject: [PATCH 1/3] Bump rust-lightning, fix splice fee accounting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update rust-lightning to commit 2313bd5 along with the matching bitcoin-payment-instructions fork. The new splice API computes its fee estimate independently of the BDK coin selection it drives, so any surplus from BDK's higher reservation flows into the new funding output instead of staying in change. Bridge the gap in `select_confirmed_utxos`: * Strip 5 WU per foreign input when calling `add_foreign_utxo` so BDK doesn't double-count the empty script_sig byte and witness-count varint that LDK's `satisfaction_weight` already includes. * Bump the change output up by the residual rounding surplus from BDK's per-component fee ceilings — goes away once https://github.com/bitcoindevkit/bdk_wallet/pull/479 ships. Generated with the help of Claude Opus 4.7. --- Cargo.toml | 28 +++++++-------- src/lib.rs | 11 ++---- src/payment/unified.rs | 5 +++ src/wallet/mod.rs | 62 ++++++++++++++++++++++++++++++--- tests/integration_tests_rust.rs | 2 +- 5 files changed, 79 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d34710a6e..ce2bc3742 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,18 +40,18 @@ default = [] #lightning-macros = { version = "0.2.0" } #lightning-dns-resolver = { version = "0.3.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } -lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["std"] } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -81,13 +81,13 @@ async-trait = { version = "0.1", default-features = false } vss-client = { package = "vss-client-ng", version = "0.5" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "679dac50cc0d81ec4d31da94b93d467e5308f16a" } +bitcoin-payment-instructions = { git = "https://github.com/benthecarman/bitcoin-payment-instructions", rev = "23bb47b2d568571c3191d59881ff048d21537ebd" } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "369a2cf9c8ef810deea0cd2b4cf6ed0691b78144", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "2313bd584d2c46a50d67b8266f488c07516e0b3c", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/lib.rs b/src/lib.rs index 6d877ae10..025c6d3bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1699,15 +1699,8 @@ impl Node { value: Amount::from_sat(splice_amount_sats), script_pubkey: address.script_pubkey(), }]; - let contribution = self - .runtime - .block_on(funding_template.splice_out( - outputs, - min_feerate, - max_feerate, - Arc::clone(&self.wallet), - )) - .map_err(|e| { + let contribution = + funding_template.splice_out(outputs, min_feerate, max_feerate).map_err(|e| { log_error!(self.logger, "Failed to splice channel: {}", e); Error::ChannelSplicingFailed })?; diff --git a/src/payment/unified.rs b/src/payment/unified.rs index 3708afe8e..d27911185 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -256,6 +256,7 @@ impl UnifiedPayment { PaymentMethod::LightningBolt12(_) => 0, PaymentMethod::LightningBolt11(_) => 1, PaymentMethod::OnChain(_) => 2, + PaymentMethod::Cashu(_) => 3, }); for method in sorted_payment_methods { @@ -331,6 +332,10 @@ impl UnifiedPayment { let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?; return Ok(UnifiedPaymentResult::Onchain { txid }); }, + PaymentMethod::Cashu(_) => { + log_error!(self.logger, "Cashu payment methods are not supported. Skipping."); + continue; + }, } } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index daeb7becb..412db6e8e 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -574,7 +574,7 @@ impl Wallet { witness_utxo: Some(input.previous_utxo.clone()), ..Default::default() }; - let weight = Weight::from_wu(input.satisfaction_weight); + let weight = ldk_to_bdk_satisfaction_weight(input.satisfaction_weight); tx_builder.only_witness_utxo().exclude_unconfirmed(); tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|e| { log_error!(self.logger, "Failed to add shared input for fee estimation: {e}"); @@ -916,7 +916,7 @@ impl Wallet { witness_utxo: Some(input.previous_utxo.clone()), ..Default::default() }; - let weight = Weight::from_wu(input.satisfaction_weight); + let weight = ldk_to_bdk_satisfaction_weight(input.satisfaction_weight); tx_builder.add_foreign_utxo(input.outpoint, psbt_input, weight).map_err(|_| ())?; } @@ -955,11 +955,41 @@ impl Wallet { return Err(()); } - let change_output = unsigned_tx + let mut change_output = unsigned_tx .output .into_iter() - .filter(|txout| must_pay_to.iter().all(|output| output != txout)) - .next(); + .find(|txout| must_pay_to.iter().all(|output| output != txout)); + + // BDK's `create_tx` applies `ceil(fee_rate * weight)` separately for each + // fee component instead of once over the total weight, and LDK's + // `estimate_transaction_fee` does a single ceiling. Because LDK consumes + // our `change_output.value` verbatim and recomputes its own fee to size + // the new funding output, every sat BDK over-reserves gets funneled into + // the channel instead of staying in change. Absorb the worst-case + // over-reservation by bumping the change output up here. + // + // Two sources of rounding contribute: + // + // * Per-input ceilings — `bdk_wallet`'s `select_sorted_utxos` ceilings + // `fee_rate * (TxIn::default().segwit_weight() + satisfaction_weight)` + // once per input. With `N` inputs, the worst-case over-reservation is + // `N - 1` sats. TODO: drop once + // https://github.com/bitcoindevkit/bdk_wallet/pull/479 lands in a + // release. + // + // * Initial-tx and change ceilings — `create_tx` ceilings + // `fee_rate * tx.weight()` on the recipient-only tx, and `decide_change` + // ceilings `fee_rate * Weight::from_vb(drain_output_len)`. Two more + // independent ceilings, contributing up to 2 sats. + if let Some(ref mut change) = change_output { + let num_inputs = (confirmed_utxos.len() + must_spend.len()) as u64; + let per_input_rounding_surplus_sat = num_inputs.saturating_sub(1); + let initial_and_change_rounding_surplus_sat = 2; + let bdk_rounding_surplus_sat = + per_input_rounding_surplus_sat + initial_and_change_rounding_surplus_sat; + change.value = + Amount::from_sat(change.value.to_sat().saturating_add(bdk_rounding_surplus_sat)); + } if change_output.is_some() { locked_wallet.persist(&mut locked_persister).map_err(|e| { @@ -1717,6 +1747,28 @@ impl ChangeDestinationSource for WalletKeysManager { } } +/// Convert LDK's `Input::satisfaction_weight` to the value BDK's +/// [`bdk_wallet::TxBuilder::add_foreign_utxo`] expects. +/// +/// LDK and BDK disagree on what `satisfaction_weight` includes for a SegWit input. LDK +/// treats it as the full weight of the spent input's `script_sig` and `witness` *each +/// with their lengths included* — i.e., the empty `script_sig` length byte (4 WU) and +/// the witness-elements-count varint (1 WU) are part of the value. BDK adds +/// `TxIn::default().segwit_weight()` internally, which already accounts for those same +/// 5 WU (an empty TxIn has a 1-byte empty `script_sig` length and a 1-byte empty +/// witness-count varint). Passing LDK's value directly to BDK therefore double-counts +/// 5 WU per foreign input, which inflates BDK's fee estimate and ultimately funnels the +/// surplus into the new funding output during splice negotiation. +fn ldk_to_bdk_satisfaction_weight(ldk_satisfaction_weight: u64) -> Weight { + const EMPTY_SCRIPT_SIG_WEIGHT: u64 = + 1 /* empty script_sig length byte */ * WITNESS_SCALE_FACTOR as u64; + const EMPTY_WITNESS_COUNT_WEIGHT: u64 = 1 /* witness elements count varint */; + Weight::from_wu( + ldk_satisfaction_weight + .saturating_sub(EMPTY_SCRIPT_SIG_WEIGHT + EMPTY_WITNESS_COUNT_WEIGHT), + ) +} + // FIXME/TODO: This is copied-over from bdk_wallet and only used to generate `WalletEvent`s after // applying mempool transactions. We should drop this when BDK offers to generate events for // mempool transactions natively. diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index d2c057a16..3df954765 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1067,7 +1067,7 @@ async fn splice_channel() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); - let expected_splice_in_fee_sat = 255; + let expected_splice_in_fee_sat = 251; let payments = node_b.list_payments(); let payment = From 02f461589bcf300c6ec72cf020a20a97f72bd31c Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 13 Apr 2026 12:36:05 -0500 Subject: [PATCH 2/3] Allow building 0.7.0 node with different store --- tests/common/mod.rs | 4 ++++ tests/integration_tests_rust.rs | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 00c8808a7..5ae7a638d 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 3df954765..404961776 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; @@ -2541,15 +2541,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(); @@ -2590,14 +2594,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 }, }; From 1bce66d864b8b520e48d0b3bb265a6847e3b261c Mon Sep 17 00:00:00 2001 From: benthecarman Date: Mon, 13 Apr 2026 12:36:29 -0500 Subject: [PATCH 3/3] 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. --- src/builder.rs | 19 +++++++----- src/io/utils.rs | 54 ++++++++++++++++++++++++++++----- tests/integration_tests_rust.rs | 43 ++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 15 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 54a2f51ab..69d4bc1eb 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,23 @@ 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); + log_error!(logger, "Failed to set up 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 +1120,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 5b51b8859..4ef51d4ae 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,6 +621,42 @@ 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 { + 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 mut v2_dir = storage_dir_path.clone(); + v2_dir.set_file_name("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 mut backup_dir = storage_dir_path.clone(); + backup_dir.set_file_name("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), + } +} + #[cfg(test)] mod tests { use super::read_or_generate_seed_file; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 404961776..8917db5f9 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2634,6 +2634,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();