diff --git a/Cargo.toml b/Cargo.toml index d34710a6e0..ce2bc37426 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/builder.rs b/src/builder.rs index 54a2f51abc..b719f40a73 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/src/lib.rs b/src/lib.rs index 6d877ae10d..025c6d3bb1 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 3708afe8e6..d279111853 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 daeb7becb3..f4e4df6800 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(|_| ())?; } @@ -958,8 +958,7 @@ impl Wallet { let 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)); if change_output.is_some() { locked_wallet.persist(&mut locked_persister).map_err(|e| { @@ -1717,6 +1716,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/common/mod.rs b/tests/common/mod.rs index 00c8808a7b..5ae7a638d0 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 d2c057a164..6f845bcf34 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; @@ -1067,7 +1067,9 @@ 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 expected_splice_in_onchain_cost_sat = 254; + let expected_splice_in_lightning_balance_sat = 4_000_003; let payments = node_b.list_payments(); let payment = @@ -1076,9 +1078,12 @@ async fn splice_channel() { assert_eq!( node_b.list_balances().total_onchain_balance_sats, - premine_amount_sat - 4_000_000 - expected_splice_in_fee_sat + premine_amount_sat - 4_000_000 - expected_splice_in_onchain_cost_sat + ); + assert_eq!( + node_b.list_balances().total_lightning_balance_sats, + expected_splice_in_lightning_balance_sat ); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000); let payment_id = node_b.spontaneous_payment().send(amount_msat, node_a.node_id(), None).unwrap(); @@ -1093,7 +1098,10 @@ async fn splice_channel() { node_a.list_balances().total_lightning_balance_sats, 4_000_000 - closing_transaction_fee_sat - anchor_output_sat + amount_msat / 1000 ); - assert_eq!(node_b.list_balances().total_lightning_balance_sats, 4_000_000 - amount_msat / 1000); + assert_eq!( + node_b.list_balances().total_lightning_balance_sats, + expected_splice_in_lightning_balance_sat - amount_msat / 1000 + ); // Splice-out funds for Node A from the payment sent by Node B let address = node_a.onchain_payment().new_address().unwrap(); @@ -2541,15 +2549,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 +2602,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 }, }; @@ -2634,6 +2642,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();