From f20122b7f4ed3820ed60f79300d29a92564ecff2 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:30:33 +0200 Subject: [PATCH 01/17] Make test chain selection configurable Allow integration runs to constrain randomized chain sources through the LDK_NODE_TEST_CHAIN_SOURCES environment variable. This makes backend-specific feature builds deterministic while preserving all-backend randomization by default. Co-Authored-By: HAL 9000 --- tests/common/mod.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 433fdd645..757ec78e2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -363,25 +363,38 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { pub(crate) fn random_chain_source<'a>( bitcoind: &'a BitcoinD, electrsd: &'a ElectrsD, ) -> TestChainSource<'a> { - let r = rand::random_range(0..4); - match r { - 0 => { + let configured_sources = env::var("LDK_NODE_TEST_CHAIN_SOURCES").ok().map(|value| { + value + .split(|c: char| c == ',' || c.is_ascii_whitespace()) + .filter(|source| !source.is_empty()) + .map(|source| source.to_ascii_uppercase()) + .collect::>() + }); + let sources = configured_sources.unwrap_or_else(|| { + ["ESPLORA", "ELECTRUM", "BITCOIND_RPC", "BITCOIND_REST"] + .into_iter() + .map(String::from) + .collect() + }); + let source = &sources[rand::random_range(0..sources.len())]; + match source.as_str() { + "ESPLORA" => { println!("Randomly setting up Esplora chain syncing..."); TestChainSource::Esplora(electrsd) }, - 1 => { + "ELECTRUM" => { println!("Randomly setting up Electrum chain syncing..."); TestChainSource::Electrum(electrsd) }, - 2 => { + "BITCOIND_RPC" => { println!("Randomly setting up Bitcoind RPC chain syncing..."); TestChainSource::BitcoindRpcSync(bitcoind) }, - 3 => { + "BITCOIND_REST" => { println!("Randomly setting up Bitcoind REST chain syncing..."); TestChainSource::BitcoindRestSync(bitcoind) }, - _ => unreachable!(), + _ => panic!("Unknown test chain source: {source}"), } } From 7df94cc5805d4592b21efaa9f8f0be58af1e81c1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 20 Aug 2026 12:45:05 +0200 Subject: [PATCH 02/17] Centralize test chain source setup Let tests with custom node construction reuse the common chain source configuration. Co-Authored-By: HAL 9000 --- tests/common/mod.rs | 116 +++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 757ec78e2..70d1a292b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -684,7 +684,7 @@ impl Default for TestConfig { macro_rules! setup_builder { ($builder:ident, $config:expr) => { #[cfg(feature = "uniffi")] - let $builder = Builder::from_config($config.clone()); + let mut $builder = Builder::from_config($config.clone()); #[cfg(not(feature = "uniffi"))] let mut $builder = Builder::from_config($config.clone()); }; @@ -692,6 +692,65 @@ macro_rules! setup_builder { pub(crate) use setup_builder; +pub(crate) fn configure_chain_source( + chain_source: &TestChainSource, builder: &mut Builder, config: &TestConfig, +) { + match chain_source { + TestChainSource::Esplora(electrsd) => { + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + sync_config.force_wallet_full_scan = config.force_wallet_full_scan; + if let Some(full_scan_stop_gap) = config.full_scan_stop_gap { + sync_config.full_scan_stop_gap = full_scan_stop_gap; + } + builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + }, + TestChainSource::Electrum(electrsd) => { + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let mut sync_config = ElectrumSyncConfig::default(); + sync_config.background_sync_config = None; + sync_config.force_wallet_full_scan = config.force_wallet_full_scan; + if let Some(full_scan_stop_gap) = config.full_scan_stop_gap { + sync_config.full_scan_stop_gap = full_scan_stop_gap; + } + builder.set_chain_source_electrum(electrum_url.clone(), Some(sync_config)); + }, + TestChainSource::BitcoindRpcSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + let rpc_user = values.user; + let rpc_password = values.password; + builder.set_chain_source_bitcoind_rpc( + rpc_host, + rpc_port, + rpc_user, + rpc_password, + config.wallet_rescan_from_height, + ); + }, + TestChainSource::BitcoindRestSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + let rpc_user = values.user; + let rpc_password = values.password; + let rest_host = bitcoind.params.rpc_socket.ip().to_string(); + let rest_port = bitcoind.params.rpc_socket.port(); + builder.set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + config.wallet_rescan_from_height, + ); + }, + } +} + #[cfg(any(cln_test, lnd_test, eclair_test))] pub(crate) mod scenarios; @@ -747,60 +806,7 @@ pub(crate) fn setup_two_nodes_with_store( pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestNode { setup_builder!(builder, config.node_config); - match chain_source { - TestChainSource::Esplora(electrsd) => { - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.background_sync_config = None; - sync_config.force_wallet_full_scan = config.force_wallet_full_scan; - if let Some(full_scan_stop_gap) = config.full_scan_stop_gap { - sync_config.full_scan_stop_gap = full_scan_stop_gap; - } - builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - }, - TestChainSource::Electrum(electrsd) => { - let electrum_url = format!("tcp://{}", electrsd.electrum_url); - let mut sync_config = ElectrumSyncConfig::default(); - sync_config.background_sync_config = None; - sync_config.force_wallet_full_scan = config.force_wallet_full_scan; - if let Some(full_scan_stop_gap) = config.full_scan_stop_gap { - sync_config.full_scan_stop_gap = full_scan_stop_gap; - } - builder.set_chain_source_electrum(electrum_url.clone(), Some(sync_config)); - }, - TestChainSource::BitcoindRpcSync(bitcoind) => { - let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); - let rpc_port = bitcoind.params.rpc_socket.port(); - let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); - let rpc_user = values.user; - let rpc_password = values.password; - builder.set_chain_source_bitcoind_rpc( - rpc_host, - rpc_port, - rpc_user, - rpc_password, - config.wallet_rescan_from_height, - ); - }, - TestChainSource::BitcoindRestSync(bitcoind) => { - let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); - let rpc_port = bitcoind.params.rpc_socket.port(); - let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); - let rpc_user = values.user; - let rpc_password = values.password; - let rest_host = bitcoind.params.rpc_socket.ip().to_string(); - let rest_port = bitcoind.params.rpc_socket.port(); - builder.set_chain_source_bitcoind_rest( - rest_host, - rest_port, - rpc_host, - rpc_port, - rpc_user, - rpc_password, - config.wallet_rescan_from_height, - ); - }, - } + configure_chain_source(chain_source, &mut builder, &config); match &config.log_writer { TestLogWriter::FileWriter => { From 8eaefe08c1f9c6b01526fab9e03d53875128288e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 20 Aug 2026 12:46:28 +0200 Subject: [PATCH 03/17] Vary PostgreSQL test chain sources Exercise PostgreSQL-backed nodes with the configured or randomly selected chain backend instead of always using Esplora. Co-Authored-By: HAL 9000 --- tests/integration_tests_postgres.rs | 37 +++++++++++------------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/tests/integration_tests_postgres.rs b/tests/integration_tests_postgres.rs index d972c6c7c..682255f21 100644 --- a/tests/integration_tests_postgres.rs +++ b/tests/integration_tests_postgres.rs @@ -9,10 +9,8 @@ mod common; -use common::{drop_table, test_connection_string}; -use ldk_node::entropy::NodeEntropy; +use common::{configure_chain_source, drop_table, random_chain_source, test_connection_string}; use ldk_node::Builder; -use rand::RngCore; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn channel_full_cycle_with_postgres_store() { @@ -20,11 +18,11 @@ async fn channel_full_cycle_with_postgres_store() { drop_table("channel_cycle_b").await; let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); println!("== Node A =="); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let config_a = common::random_config(); - let mut builder_a = Builder::from_config(config_a.node_config); - builder_a.set_chain_source_esplora(esplora_url.clone(), None); + let mut builder_a = Builder::from_config(config_a.node_config.clone()); + configure_chain_source(&chain_source, &mut builder_a, &config_a); let node_a = builder_a .build_with_postgres_store( config_a.node_entropy.into(), @@ -39,8 +37,8 @@ async fn channel_full_cycle_with_postgres_store() { println!("\n== Node B =="); let mut config_b = common::random_config(); config_b.node_config.manually_handle_unknown_bolt11_payments = true; - let mut builder_b = Builder::from_config(config_b.node_config); - builder_b.set_chain_source_esplora(esplora_url.clone(), None); + let mut builder_b = Builder::from_config(config_b.node_config.clone()); + configure_chain_source(&chain_source, &mut builder_b, &config_b); let node_b = builder_b .build_with_postgres_store( config_b.node_entropy.into(), @@ -73,23 +71,18 @@ async fn postgres_node_restart() { drop_table("restart_test").await; let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + let chain_source = random_chain_source(&bitcoind, &electrsd); let connection_string = test_connection_string(); let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); - let mut seed_bytes = [42u8; 64]; - rand::rng().fill_bytes(&mut seed_bytes); - #[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 mut config = common::random_config(); + config.node_config.storage_dir_path = storage_path; + let node_entropy = config.node_entropy; // Setup initial node and fund it. let (expected_balance_sats, expected_node_id) = { - let mut builder = Builder::new(); - builder.set_network(bitcoin::Network::Regtest); - builder.set_storage_dir_path(storage_path.clone()); - builder.set_chain_source_esplora(esplora_url.clone(), None); + let mut builder = Builder::from_config(config.node_config.clone()); + configure_chain_source(&chain_source, &mut builder, &config); let node = builder .build_with_postgres_store( node_entropy.into(), @@ -120,10 +113,8 @@ async fn postgres_node_restart() { }; // Verify node can be restarted from PostgreSQL backend. - let mut builder = Builder::new(); - builder.set_network(bitcoin::Network::Regtest); - builder.set_storage_dir_path(storage_path); - builder.set_chain_source_esplora(esplora_url, None); + let mut builder = Builder::from_config(config.node_config.clone()); + configure_chain_source(&chain_source, &mut builder, &config); let node = builder .build_with_postgres_store( From a4624167eb8a18291f15d43f78c64c894a533fd0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:35:41 +0200 Subject: [PATCH 04/17] Move filesystem migration to its own module Keep filesystem-specific code together so generic storage utilities can compile without that backend. This commit only relocates the existing migration behavior and tests. Co-Authored-By: HAL 9000 --- src/builder.rs | 7 +- src/io/fs_store.rs | 281 +++++++++++++++++++++++++++++++++++++++++++++ src/io/mod.rs | 1 + src/io/utils.rs | 272 +------------------------------------------ 4 files changed, 291 insertions(+), 270 deletions(-) create mode 100644 src/io/fs_store.rs diff --git a/src/builder.rs b/src/builder.rs index f0f38783f..95c08e706 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -59,11 +59,12 @@ use crate::entropy::NodeEntropy; use crate::event::EventQueue; use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::GossipSource; +use crate::io::fs_store::open_or_migrate_fs_store; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{ - open_or_migrate_fs_store, read_all_objects, read_event_queue, - read_external_pathfinding_scores_from_cache, read_n_objects, read_network_graph, - read_node_metrics, read_output_sweeper, read_peer_info, read_scorer, + read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache, + read_n_objects, read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info, + read_scorer, }; use crate::io::vss_store::VssStoreBuilder; use crate::io::{ diff --git a/src/io/fs_store.rs b/src/io/fs_store.rs new file mode 100644 index 000000000..855395d9d --- /dev/null +++ b/src/io/fs_store.rs @@ -0,0 +1,281 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use std::fs; +use std::path::{Path, PathBuf}; + +use lightning::util::persist::migrate_kv_store_data_async; +use lightning_persister::fs_store::v1::FilesystemStore; +use lightning_persister::fs_store::v2::{FilesystemStoreV2, FilesystemStoreV2Error}; + +use crate::BuildError; + +/// 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) async 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 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 v2_store = FilesystemStoreV2::new(v2_dir.clone()) + .map_err(|_| BuildError::KVStoreSetupFailed)?; + + migrate_kv_store_data_async(&v1_store, &v2_store) + .await + .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)?; + + // fsync the renames + fs::File::open(parent_dir) + .and_then(|f| f.sync_all()) + .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 std::fs; + use std::path::{Path, PathBuf}; + + use lightning::util::persist::{migrate_kv_store_data_async, KVStore}; + use lightning_persister::fs_store::v1::FilesystemStore; + use lightning_persister::fs_store::v2::FilesystemStoreV2; + + use super::open_or_migrate_fs_store; + use crate::io::test_utils::random_storage_path; + + 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"; + + #[tokio::test] + async fn fs_store_migration_recovers_before_v1_backup_rename() { + let fs_store_path = fs_store_path(); + let v1_store = write_v1_test_data(&fs_store_path).await; + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + migrate_kv_store_data_async(&v1_store, &v2_store).await.unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).await.unwrap(); + assert_eq!( + KVStore::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .await + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + #[tokio::test] + async fn fs_store_migration_recovers_after_v1_backup_rename() { + let fs_store_path = fs_store_path(); + let v1_store = write_v1_test_data(&fs_store_path).await; + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + migrate_kv_store_data_async(&v1_store, &v2_store).await.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()).await.unwrap(); + assert_eq!( + KVStore::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .await + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + #[tokio::test] + async fn fs_store_migration_recovers_after_v2_rename() { + let fs_store_path = fs_store_path(); + let v1_store = write_v1_test_data(&fs_store_path).await; + let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); + let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); + migrate_kv_store_data_async(&v1_store, &v2_store).await.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()).await.unwrap(); + assert_eq!( + KVStore::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .await + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(backup_path.exists()); + assert!(!v2_migrating_path.exists()); + } + + #[tokio::test] + async fn fs_store_migration_recovers_backup_without_migrating_dir() { + let fs_store_path = fs_store_path(); + write_v1_test_data(&fs_store_path).await; + + 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()).await.unwrap(); + assert_eq!( + KVStore::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .await + .unwrap(), + TEST_VALUE + ); + assert!(fs_store_path.exists()); + assert!(!sibling_path(&fs_store_path, "fs_store_v1_backup").exists()); + } + + #[tokio::test] + async 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(); + KVStore::write( + &v2_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY, + TEST_VALUE.to_vec(), + ) + .await + .unwrap(); + + let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).await.unwrap(); + assert_eq!( + KVStore::read( + &migrated_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY + ) + .await + .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 + } + + async fn write_v1_test_data(fs_store_path: &Path) -> FilesystemStore { + let v1_store = FilesystemStore::new(fs_store_path.to_path_buf()); + KVStore::write( + &v1_store, + TEST_PRIMARY_NAMESPACE, + TEST_SECONDARY_NAMESPACE, + TEST_KEY, + TEST_VALUE.to_vec(), + ) + .await + .unwrap(); + v1_store + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index c70c68d96..e01e8a5d9 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -7,6 +7,7 @@ //! Objects and traits for data persistence. +pub(crate) mod fs_store; #[cfg(feature = "postgres")] pub mod postgres_store; pub mod sqlite_store; diff --git a/src/io/utils.rs b/src/io/utils.rs index b9255120f..30fc0c62d 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -11,7 +11,7 @@ use std::num::NonZeroUsize; use std::ops::Deref; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; @@ -27,16 +27,14 @@ use lightning::routing::scoring::{ ChannelLiquidities, ProbabilisticScorer, ProbabilisticScoringDecayParameters, }; use lightning::util::persist::{ - migrate_kv_store_data_async, KVStore, PageToken, PaginatedKVStore, - KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY, + KVStore, PageToken, PaginatedKVStore, 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::*; @@ -50,7 +48,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::{BuildError, Error, EventQueue, NodeMetrics, PersistedNodeMetrics}; +use crate::{Error, EventQueue, NodeMetrics, PersistedNodeMetrics}; pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_scores_cache"; @@ -761,109 +759,10 @@ pub(crate) async 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) async 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 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 v2_store = FilesystemStoreV2::new(v2_dir.clone()) - .map_err(|_| BuildError::KVStoreSetupFailed)?; - - migrate_kv_store_data_async(&v1_store, &v2_store) - .await - .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)?; - - // fsync the renames - fs::File::open(parent_dir) - .and_then(|f| f.sync_all()) - .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 std::fs; - use std::path::{Path, PathBuf}; - - use lightning::util::persist::{migrate_kv_store_data_async, KVStore}; - use lightning_persister::fs_store::v1::FilesystemStore; - use lightning_persister::fs_store::v2::FilesystemStoreV2; - + use super::read_or_generate_seed_file; 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() { @@ -873,167 +772,6 @@ 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); } - - #[tokio::test] - async fn fs_store_migration_recovers_before_v1_backup_rename() { - let fs_store_path = fs_store_path(); - let v1_store = write_v1_test_data(&fs_store_path).await; - let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); - let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); - migrate_kv_store_data_async(&v1_store, &v2_store).await.unwrap(); - - let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).await.unwrap(); - assert_eq!( - KVStore::read( - &migrated_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY - ) - .await - .unwrap(), - TEST_VALUE - ); - assert!(fs_store_path.exists()); - assert!(!v2_migrating_path.exists()); - } - - #[tokio::test] - async fn fs_store_migration_recovers_after_v1_backup_rename() { - let fs_store_path = fs_store_path(); - let v1_store = write_v1_test_data(&fs_store_path).await; - let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); - let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); - migrate_kv_store_data_async(&v1_store, &v2_store).await.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()).await.unwrap(); - assert_eq!( - KVStore::read( - &migrated_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY - ) - .await - .unwrap(), - TEST_VALUE - ); - assert!(fs_store_path.exists()); - assert!(!v2_migrating_path.exists()); - } - - #[tokio::test] - async fn fs_store_migration_recovers_after_v2_rename() { - let fs_store_path = fs_store_path(); - let v1_store = write_v1_test_data(&fs_store_path).await; - let v2_migrating_path = sibling_path(&fs_store_path, "fs_store_v2_migrating"); - let v2_store = FilesystemStoreV2::new(v2_migrating_path.clone()).unwrap(); - migrate_kv_store_data_async(&v1_store, &v2_store).await.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()).await.unwrap(); - assert_eq!( - KVStore::read( - &migrated_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY - ) - .await - .unwrap(), - TEST_VALUE - ); - assert!(fs_store_path.exists()); - assert!(backup_path.exists()); - assert!(!v2_migrating_path.exists()); - } - - #[tokio::test] - async fn fs_store_migration_recovers_backup_without_migrating_dir() { - let fs_store_path = fs_store_path(); - write_v1_test_data(&fs_store_path).await; - - 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()).await.unwrap(); - assert_eq!( - KVStore::read( - &migrated_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY - ) - .await - .unwrap(), - TEST_VALUE - ); - assert!(fs_store_path.exists()); - assert!(!sibling_path(&fs_store_path, "fs_store_v1_backup").exists()); - } - - #[tokio::test] - async 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(); - KVStore::write( - &v2_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY, - TEST_VALUE.to_vec(), - ) - .await - .unwrap(); - - let migrated_store = open_or_migrate_fs_store(fs_store_path.clone()).await.unwrap(); - assert_eq!( - KVStore::read( - &migrated_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY - ) - .await - .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 - } - - async fn write_v1_test_data(fs_store_path: &Path) -> FilesystemStore { - let v1_store = FilesystemStore::new(fs_store_path.to_path_buf()); - KVStore::write( - &v1_store, - TEST_PRIMARY_NAMESPACE, - TEST_SECONDARY_NAMESPACE, - TEST_KEY, - TEST_VALUE.to_vec(), - ) - .await - .unwrap(); - v1_store - } } #[cfg(test)] From 24d94afd391558e688cc58e04a6abdb2eb339975 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:36:43 +0200 Subject: [PATCH 05/17] Decouple gossip from Bitcoin Core types Hide the concrete Bitcoin Core gossip verifier behind the LDK UTXO lookup trait. This lets common gossip types compile independently of the Bitcoin Core backend without changing verification behavior. Co-Authored-By: HAL 9000 --- src/gossip.rs | 3 ++- src/types.rs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/gossip.rs b/src/gossip.rs index 4ef280273..e50991478 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -14,7 +14,7 @@ use crate::chain::ChainSource; use crate::config::{RGS_SNAPSHOT_MAX_SIZE, RGS_SYNC_TIMEOUT_SECS}; use crate::logger::{log_error, log_trace, LdkLogger, Logger}; use crate::runtime::{Runtime, RuntimeSpawner}; -use crate::types::{GossipSync, Graph, P2PGossipSync, RapidGossipSync}; +use crate::types::{GossipSync, Graph, P2PGossipSync, RapidGossipSync, UtxoLookup}; use crate::Error; pub(crate) enum GossipSource { @@ -36,6 +36,7 @@ impl GossipSource { ) -> Self { let verifier = chain_source.as_utxo_source().map(|utxo_source| { Arc::new(GossipVerifier::new(Arc::new(utxo_source), RuntimeSpawner::new(runtime))) + as Arc }); let gossip_sync = Arc::new(P2PGossipSync::new(network_graph, verifier, logger)); diff --git a/src/types.rs b/src/types.rs index 65156982e..7dd70cc07 100644 --- a/src/types.rs +++ b/src/types.rs @@ -36,13 +36,11 @@ use lightning::util::persist::{ }; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; -use lightning_block_sync::gossip::GossipVerifier; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; #[cfg(not(feature = "uniffi"))] use lightning_types::features::ChannelTypeFeatures; -use crate::chain::bitcoind::UtxoSourceClient; use crate::chain::ChainSource; use crate::config::{AnchorChannelsConfig, ChannelConfig}; use crate::data_store::{DataStore, KeepAllEntries, KeepLeastRecentlyUsed}; @@ -289,7 +287,7 @@ pub(crate) type Scorer = CombinedScorer, Arc>; pub(crate) type Graph = gossip::NetworkGraph>; -pub(crate) type UtxoLookup = GossipVerifier>; +pub(crate) type UtxoLookup = dyn lightning::routing::utxo::UtxoLookup + Send + Sync; pub(crate) type P2PGossipSync = lightning::routing::gossip::P2PGossipSync, Arc, Arc>; From a6c8cb2e53665a48e91d57640e96808cba5b7168 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:38:41 +0200 Subject: [PATCH 06/17] Move HRN resolution into payment module Keep human-readable-name resolution beside the unified payment code that consumes it. This commit only relocates the existing resolver and updates its internal import paths. Co-Authored-By: HAL 9000 --- src/builder.rs | 5 ++-- src/lib.rs | 5 ++-- src/payment/hrn.rs | 53 ++++++++++++++++++++++++++++++++++++++++++ src/payment/mod.rs | 2 ++ src/payment/unified.rs | 2 +- src/types.rs | 41 -------------------------------- 6 files changed, 61 insertions(+), 47 deletions(-) create mode 100644 src/payment/hrn.rs diff --git a/src/builder.rs b/src/builder.rs index 95c08e706..99efa3d0b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -77,6 +77,7 @@ use crate::lnurl_auth::LnurlAuth; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::payment::HRNResolver; use crate::peer_store::PeerStore; use crate::probing::{ HighDegreeStrategy, Prober, ProbingConfig, ProbingStrategy, ProbingStrategyKind, @@ -86,8 +87,8 @@ use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper, - GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, - PeerManager, PendingPaymentStore, + GossipSync, Graph, KeysManager, MessageRouter, OnionMessenger, PaymentStore, PeerManager, + PendingPaymentStore, }; use crate::wallet::persist::{read_address_pool, KVStoreWalletPersister}; use crate::wallet::Wallet; diff --git a/src/lib.rs b/src/lib.rs index 18152b3a7..8b78f7218 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,7 +173,7 @@ use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ - Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, PaymentDetailsPage, + Bolt11Payment, Bolt12Payment, HRNResolver, OnchainPayment, PaymentDetails, PaymentDetailsPage, SpontaneousPayment, UnifiedPayment, }; use peer_store::{PeerInfo, PeerStore}; @@ -184,8 +184,7 @@ use runtime::Runtime; pub use tokio; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, - Wallet, + KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ ChannelCounterparty, ChannelDetails, CustomTlvRecord, PeerDetails, ReserveType, UserChannelId, diff --git a/src/payment/hrn.rs b/src/payment/hrn.rs new file mode 100644 index 000000000..91856e678 --- /dev/null +++ b/src/payment/hrn.rs @@ -0,0 +1,53 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use std::sync::Arc; + +use bitcoin_payment_instructions::amount::Amount as BPIAmount; +use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; +use bitcoin_payment_instructions::hrn_resolution::{ + HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, +}; +use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; + +use crate::logger::Logger; +use crate::types::Graph; + +#[derive(Clone)] +pub enum HRNResolver { + Onion(Arc, Arc>>), + Local(Arc), +} + +impl HrnResolver for HRNResolver { + fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> { + match self { + HRNResolver::Onion(inner) => inner.resolve_hrn(hrn), + HRNResolver::Local(inner) => inner.resolve_hrn(hrn), + } + } + + fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> { + match self { + HRNResolver::Onion(inner) => inner.resolve_lnurl(url), + HRNResolver::Local(inner) => inner.resolve_lnurl(url), + } + } + + fn resolve_lnurl_to_invoice<'a>( + &'a self, callback_url: String, amount: BPIAmount, expected_description_hash: [u8; 32], + ) -> LNURLResolutionFuture<'a> { + match self { + HRNResolver::Onion(inner) => { + inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash) + }, + HRNResolver::Local(inner) => { + inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash) + }, + } + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs index b0f4901a7..c483a3aef 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod asynchronous; mod bolt11; mod bolt12; +mod hrn; mod onchain; pub(crate) mod pending_payment_store; mod spontaneous; @@ -19,6 +20,7 @@ mod unified; pub use bolt11::Bolt11Payment; pub(crate) use bolt11::PaymentMetadata; pub use bolt12::{Bolt12Payment, PayerProofOptions}; +pub(crate) use hrn::HRNResolver; pub use onchain::OnchainPayment; pub(crate) use pending_payment_store::{FundingTxCandidate, PendingPaymentDetails}; pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/unified.rs b/src/payment/unified.rs index cb5117414..bb8f1ad0a 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -33,8 +33,8 @@ use crate::config::HRN_RESOLUTION_TIMEOUT_SECS; use crate::error::Error; use crate::ffi::maybe_wrap; use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::HRNResolver; use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; -use crate::types::HRNResolver; use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; diff --git a/src/types.rs b/src/types.rs index 7dd70cc07..1a61daa10 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,12 +12,6 @@ use std::sync::{Arc, Mutex}; use bitcoin::secp256k1::PublicKey; use bitcoin::{OutPoint, ScriptBuf}; -use bitcoin_payment_instructions::amount::Amount as BPIAmount; -use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; -use bitcoin_payment_instructions::hrn_resolution::{ - HrnResolutionFuture, HrnResolver, HumanReadableName, LNURLResolutionFuture, -}; -use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::{ @@ -314,41 +308,6 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse IgnoringMessageHandler, >; -#[derive(Clone)] -pub enum HRNResolver { - Onion(Arc, Arc>>), - Local(Arc), -} - -impl HrnResolver for HRNResolver { - fn resolve_hrn<'a>(&'a self, hrn: &'a HumanReadableName) -> HrnResolutionFuture<'a> { - match self { - HRNResolver::Onion(inner) => inner.resolve_hrn(hrn), - HRNResolver::Local(inner) => inner.resolve_hrn(hrn), - } - } - - fn resolve_lnurl<'a>(&'a self, url: &'a str) -> HrnResolutionFuture<'a> { - match self { - HRNResolver::Onion(inner) => inner.resolve_lnurl(url), - HRNResolver::Local(inner) => inner.resolve_lnurl(url), - } - } - - fn resolve_lnurl_to_invoice<'a>( - &'a self, callback_url: String, amount: BPIAmount, expected_description_hash: [u8; 32], - ) -> LNURLResolutionFuture<'a> { - match self { - HRNResolver::Onion(inner) => { - inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash) - }, - HRNResolver::Local(inner) => { - inner.resolve_lnurl_to_invoice(callback_url, amount, expected_description_hash) - }, - } - } -} - pub(crate) type MessageRouter = lightning::onion_message::messenger::DefaultMessageRouter< Arc, Arc, From 49f059bb59fca232eb9c7e33fdd86ad31ab618e0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:41:05 +0200 Subject: [PATCH 07/17] Keep builder aliases beside their types Define the cfg-selected Builder name next to the implementation it represents. Feature-specific binding exports can now live there without changing the native and lock-wrapped builder APIs. Co-Authored-By: HAL 9000 --- src/builder.rs | 6 ++++++ src/lib.rs | 6 +----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 99efa3d0b..92d3c2380 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -318,6 +318,9 @@ pub struct NodeBuilder { probing_config: Option, } +#[cfg(not(feature = "uniffi"))] +pub use self::NodeBuilder as Builder; + impl NodeBuilder { /// Creates a new builder instance with the default configuration. pub fn new() -> Self { @@ -967,6 +970,9 @@ pub struct ArcedNodeBuilder { inner: RwLock, } +#[cfg(feature = "uniffi")] +pub use self::ArcedNodeBuilder as Builder; + #[cfg(feature = "uniffi")] impl ArcedNodeBuilder { /// Creates a new builder instance with the default configuration. diff --git a/src/lib.rs b/src/lib.rs index 8b78f7218..b0a6efde4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,11 +123,7 @@ use bitcoin::secp256k1::PublicKey; #[cfg(feature = "uniffi")] pub use bitcoin::FeeRate; use bitcoin::{Address, Amount, BlockHash, Network}; -#[cfg(feature = "uniffi")] -pub use builder::ArcedNodeBuilder as Builder; -pub use builder::BuildError; -#[cfg(not(feature = "uniffi"))] -pub use builder::NodeBuilder as Builder; +pub use builder::{BuildError, Builder}; use chain::ChainSource; use config::{ default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, From bd853cac050f8fa18425f0db6bca53e81ce33789 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:49:49 +0200 Subject: [PATCH 08/17] Make node backends feature-selectable Keep the native defaults while letting applications select only required chain sources, storage backends, and unified payment support. At least one chain source remains mandatory. PostgreSQL now has a storage-prefixed feature name. Co-Authored-By: HAL 9000 --- .github/workflows/postgres-integration.yml | 4 +- Cargo.toml | 72 ++++++++++---- src/builder.rs | 105 +++++++++++++++++---- src/chain/mod.rs | 77 ++++++++++++--- src/error.rs | 1 + src/gossip.rs | 11 ++- src/io/mod.rs | 5 +- src/lib.rs | 19 +++- src/payment/mod.rs | 4 + src/payment/unified.rs | 5 + src/wallet/mod.rs | 3 + tests/common/mod.rs | 38 ++++++-- tests/integration_tests_hrn.rs | 2 +- tests/integration_tests_migration.rs | 2 +- tests/integration_tests_postgres.rs | 2 +- tests/integration_tests_rust.rs | 10 ++ tests/reorg_test.rs | 1 + 17 files changed, 294 insertions(+), 67 deletions(-) diff --git a/.github/workflows/postgres-integration.yml b/.github/workflows/postgres-integration.yml index 451bc7758..a1a6a10d9 100644 --- a/.github/workflows/postgres-integration.yml +++ b/.github/workflows/postgres-integration.yml @@ -58,9 +58,9 @@ jobs: - name: Run PostgreSQL store tests env: TEST_POSTGRES_URL: "host=localhost user=postgres password=postgres" - run: cargo test --features postgres io::postgres_store + run: cargo test --features storage-postgres io::postgres_store - name: Run PostgreSQL integration tests env: TEST_POSTGRES_URL: "host=localhost user=postgres password=postgres" run: | - RUSTFLAGS="--cfg no_download --cfg cycle_tests" cargo test --features postgres --test integration_tests_postgres + RUSTFLAGS="--cfg no_download --cfg cycle_tests" cargo test --features storage-postgres --test integration_tests_postgres diff --git a/Cargo.toml b/Cargo.toml index fc1fe6c36..6c9a2eba8 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,48 @@ codegen-units = 1 # Reduce number of codegen units to increase optimizations. panic = 'abort' # Abort on panic [features] -default = [] -postgres = ["dep:tokio-postgres", "dep:native-tls", "dep:postgres-native-tls"] +default = [ + "chain-esplora", + "chain-electrum", + "chain-bitcoind", + "storage-sqlite", + "storage-filesystem", + "storage-vss", + "unified-payments", +] +chain-esplora = [ + "dep:bdk_esplora", + "dep:esplora-client", + "dep:ldk-esplora-client", + "dep:lightning-transaction-sync", + "lightning-transaction-sync/esplora-async-https", + "lightning-transaction-sync/time", +] +chain-electrum = [ + "dep:bdk_electrum", + "dep:electrum-client", + "dep:lightning-transaction-sync", + "lightning-transaction-sync/electrum-rustls-ring", +] +chain-bitcoind = ["dep:lightning-block-sync"] +storage-sqlite = ["dep:rusqlite"] +storage-filesystem = ["dep:lightning-persister"] +storage-vss = ["dep:vss-client", "dep:prost"] +storage-postgres = ["dep:tokio-postgres", "dep:native-tls", "dep:postgres-native-tls"] +unified-payments = [ + "dep:bip21", + "dep:bitcoin-payment-instructions", + "dep:lightning-dns-resolver", +] +uniffi = ["dep:uniffi"] +uniffi-default = [ + "uniffi", + "chain-esplora", + "chain-electrum", + "storage-sqlite", + "storage-vss", + "unified-payments", +] [dependencies] #lightning = { version = "0.2.0", features = ["std"] } @@ -45,35 +85,35 @@ lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = " lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["std"] } lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["tokio"] } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["tokio"], optional = true } lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["rest-client", "rpc-client", "tokio"], optional = true } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", optional = true } lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", features = ["std"] } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } -lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb" } +lightning-dns-resolver = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "9174965af9437196c527a9aa0df36bbcf050c8bb", optional = true } bdk_chain = { version = "0.23.3", default-features = false, features = ["std"] } -bdk_esplora = { version = "0.22.2", default-features = false, features = ["async-https-rustls", "tokio"]} -bdk_electrum = { version = "0.24.0", default-features = false, features = ["use-rustls-ring"]} +bdk_esplora = { version = "0.22.2", default-features = false, features = ["async-https-rustls", "tokio"], optional = true } +bdk_electrum = { version = "0.24.0", default-features = false, features = ["use-rustls-ring"], optional = true } bdk_wallet = { version = "3.1.0", default-features = false, features = ["std", "keys-bip39"]} bitreq = { version = "0.3", default-features = false, features = ["async-https", "json-using-serde"] } rustls = { version = "0.23", default-features = false } -rusqlite = { version = "0.31.0", features = ["bundled"] } +rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } bitcoin = "0.32.7" bip39 = { version = "2.0.0", features = ["rand"] } -bip21 = { version = "0.5", features = ["std"], default-features = false } +bip21 = { version = "0.5", features = ["std"], default-features = false, optional = true } base64 = { version = "0.22.1", default-features = false, features = ["std"] } getrandom = { version = "0.3", default-features = false } chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.39", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros", "net" ] } tokio-util = { version = "0.7.10", default-features = false, features = ["rt"] } -esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] } -ldk-esplora-client = { package = "esplora-client", version = "0.13", default-features = false, features = ["tokio", "async-https-rustls"] } -electrum-client = { version = "0.25", default-features = false, features = ["proxy", "use-rustls-ring"] } +esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"], optional = true } +ldk-esplora-client = { package = "esplora-client", version = "0.13", default-features = false, features = ["tokio", "async-https-rustls"], optional = true } +electrum-client = { version = "0.25", default-features = false, features = ["proxy", "use-rustls-ring"], optional = true } libc = "0.2" uniffi = { version = "0.29.5", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } @@ -84,10 +124,10 @@ async-trait = { version = "0.1", default-features = false } tokio-postgres = { version = "0.7", default-features = false, features = ["runtime"], optional = true } native-tls = { version = "0.2", default-features = false, optional = true } postgres-native-tls = { version = "0.5", default-features = false, features = ["runtime"], optional = true } -vss-client = { package = "vss-client-ng", version = "0.6" } -prost = { version = "0.11.6", default-features = false} +vss-client = { package = "vss-client-ng", version = "0.6", optional = true } +prost = { version = "0.11.6", default-features = false, optional = true} #bitcoin-payment-instructions = { version = "0.6" } -bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "1d6ffaa8962391ddc84aeef98bd7439e55ccae9d" } +bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "1d6ffaa8962391ddc84aeef98bd7439e55ccae9d", optional = true } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } diff --git a/src/builder.rs b/src/builder.rs index 92d3c2380..747d0ae96 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,10 +5,13 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +#[cfg(any(feature = "chain-esplora", feature = "storage-vss"))] use std::collections::HashMap; use std::convert::TryInto; use std::default::Default; +#[cfg(feature = "unified-payments")] use std::net::ToSocketAddrs; +#[cfg(feature = "storage-filesystem")] use std::path::PathBuf; use std::sync::{Arc, Mutex, Once, RwLock}; use std::time::SystemTime; @@ -20,7 +23,9 @@ use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::key::Secp256k1; use bitcoin::secp256k1::PublicKey; use bitcoin::Network; +#[cfg(feature = "unified-payments")] use bitcoin_payment_instructions::dns_resolver::DNSHrnResolver; +#[cfg(feature = "unified-payments")] use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::{chainmonitor, BlockLocator}; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; @@ -42,14 +47,18 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +#[cfg(feature = "unified-payments")] use lightning_dns_resolver::OMDomainResolver; +#[cfg(feature = "storage-vss")] use vss_client::headers::VssHeaderProvider; use crate::chain::ChainSource; +#[cfg(feature = "chain-bitcoind")] +use crate::config::BitcoindRestClientConfig; use crate::config::{ - default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, - BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig, - TorConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, + default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, Config, + ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig, TorConfig, + DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, DEFAULT_MAX_PROBE_AMOUNT_MSAT, DEFAULT_MIN_PROBE_AMOUNT_MSAT, PAYMENT_CACHE_CAPACITY, PAYMENT_CACHE_WARMUP_COUNT, }; @@ -59,13 +68,16 @@ use crate::entropy::NodeEntropy; use crate::event::EventQueue; use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::GossipSource; +#[cfg(feature = "storage-filesystem")] use crate::io::fs_store::open_or_migrate_fs_store; +#[cfg(feature = "storage-sqlite")] use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{ read_all_objects, read_event_queue, read_external_pathfinding_scores_from_cache, read_n_objects, read_network_graph, read_node_metrics, read_output_sweeper, read_peer_info, read_scorer, }; +#[cfg(feature = "storage-vss")] use crate::io::vss_store::VssStoreBuilder; use crate::io::{ self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, @@ -77,6 +89,7 @@ use crate::lnurl_auth::LnurlAuth; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +#[cfg(feature = "unified-payments")] use crate::payment::HRNResolver; use crate::peer_store::PeerStore; use crate::probing::{ @@ -99,15 +112,15 @@ const PERSISTER_MAX_PENDING_UPDATES: u64 = 100; #[derive(Debug, Clone)] enum ChainDataSourceConfig { + #[cfg(feature = "chain-esplora")] Esplora { server_url: String, headers: HashMap, sync_config: Option, }, - Electrum { - server_url: String, - sync_config: Option, - }, + #[cfg(feature = "chain-electrum")] + Electrum { server_url: String, sync_config: Option }, + #[cfg(feature = "chain-bitcoind")] Bitcoind { rpc_host: String, rpc_port: u16, @@ -364,6 +377,7 @@ impl NodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more /// information. + #[cfg(feature = "chain-esplora")] pub fn set_chain_source_esplora( &mut self, server_url: String, sync_config: Option, ) -> &mut Self { @@ -382,6 +396,7 @@ impl NodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more /// information. + #[cfg(feature = "chain-esplora")] pub fn set_chain_source_esplora_with_headers( &mut self, server_url: String, headers: HashMap, sync_config: Option, @@ -395,6 +410,7 @@ impl NodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more /// information. + #[cfg(feature = "chain-electrum")] pub fn set_chain_source_electrum( &mut self, server_url: String, sync_config: Option, ) -> &mut Self { @@ -415,6 +431,7 @@ impl NodeBuilder { /// startup, before wallet state exists. Existing wallets are not rewound. The height must /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None` /// checkpoints at the current tip. + #[cfg(feature = "chain-bitcoind")] pub fn set_chain_source_bitcoind_rpc( &mut self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option, @@ -443,6 +460,7 @@ impl NodeBuilder { /// startup, before wallet state exists. Existing wallets are not rewound. The height must /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None` /// checkpoints at the current tip. + #[cfg(feature = "chain-bitcoind")] pub fn set_chain_source_bitcoind_rest( &mut self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option, @@ -670,6 +688,7 @@ impl NodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. + #[cfg(feature = "storage-sqlite")] pub fn build(&self, node_entropy: NodeEntropy) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; let storage_dir_path = self.config.storage_dir_path.clone(); @@ -717,7 +736,7 @@ impl NodeBuilder { /// will be unencrypted. /// /// [PostgreSQL]: https://www.postgresql.org - #[cfg(feature = "postgres")] + #[cfg(feature = "storage-postgres")] pub fn build_with_postgres_store( &self, node_entropy: NodeEntropy, connection_string: String, db_name: Option, kv_table_name: Option, certificate_pem: Option, @@ -746,6 +765,7 @@ impl NodeBuilder { /// automatically migrated to the v2 format. /// /// [`FilesystemStoreV2`]: lightning_persister::fs_store::v2::FilesystemStoreV2 + #[cfg(feature = "storage-filesystem")] pub fn build_with_fs_store(&self, node_entropy: NodeEntropy) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; let runtime = self.setup_runtime(&logger)?; @@ -773,6 +793,7 @@ impl NodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store( &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, fixed_headers: HashMap, @@ -809,6 +830,7 @@ impl NodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_lnurl_auth( &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, lnurl_auth_server_url: String, fixed_headers: HashMap, @@ -837,6 +859,7 @@ impl NodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md /// [`FixedHeaders`]: vss_client::headers::FixedHeaders + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_fixed_headers( &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, fixed_headers: HashMap, @@ -862,6 +885,7 @@ impl NodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_header_provider( &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, header_provider: Arc, @@ -991,6 +1015,7 @@ impl ArcedNodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more /// information. + #[cfg(feature = "chain-esplora")] pub fn set_chain_source_esplora( &self, server_url: String, sync_config: Option, ) { @@ -1004,6 +1029,7 @@ impl ArcedNodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more /// information. + #[cfg(feature = "chain-esplora")] pub fn set_chain_source_esplora_with_headers( &self, server_url: String, headers: HashMap, sync_config: Option, @@ -1019,6 +1045,7 @@ impl ArcedNodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more /// information. + #[cfg(feature = "chain-electrum")] pub fn set_chain_source_electrum( &self, server_url: String, sync_config: Option, ) { @@ -1037,6 +1064,7 @@ impl ArcedNodeBuilder { /// startup, before wallet state exists. Existing wallets are not rewound. The height must /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None` /// checkpoints at the current tip. + #[cfg(feature = "chain-bitcoind")] pub fn set_chain_source_bitcoind_rpc( &self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option, @@ -1063,6 +1091,7 @@ impl ArcedNodeBuilder { /// startup, before wallet state exists. Existing wallets are not rewound. The height must /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None` /// checkpoints at the current tip. + #[cfg(feature = "chain-bitcoind")] pub fn set_chain_source_bitcoind_rest( &self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option, @@ -1228,6 +1257,7 @@ impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. + #[cfg(feature = "storage-sqlite")] pub fn build(&self, node_entropy: Arc) -> Result, BuildError> { self.inner.read().expect("lock").build(*node_entropy).map(Arc::new) } @@ -1262,7 +1292,7 @@ impl ArcedNodeBuilder { /// will be unencrypted. /// /// [PostgreSQL]: https://www.postgresql.org - #[cfg(feature = "postgres")] + #[cfg(feature = "storage-postgres")] pub fn build_with_postgres_store( &self, node_entropy: Arc, connection_string: String, db_name: Option, kv_table_name: Option, certificate_pem: Option, @@ -1283,8 +1313,8 @@ impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options /// previously configured. /// - /// This requires the `postgres` crate feature. - #[cfg(not(feature = "postgres"))] + /// This requires the `storage-postgres` crate feature. + #[cfg(not(feature = "storage-postgres"))] pub fn build_with_postgres_store( &self, _node_entropy: Arc, _connection_string: String, _db_name: Option, _kv_table_name: Option, _certificate_pem: Option, @@ -1294,6 +1324,7 @@ impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options /// previously configured. + #[cfg(feature = "storage-filesystem")] pub fn build_with_fs_store( &self, node_entropy: Arc, ) -> Result, BuildError> { @@ -1317,6 +1348,7 @@ impl ArcedNodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store( &self, node_entropy: Arc, vss_url: String, store_id: String, fixed_headers: HashMap, @@ -1350,6 +1382,7 @@ impl ArcedNodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_lnurl_auth( &self, node_entropy: Arc, vss_url: String, store_id: String, lnurl_auth_server_url: String, fixed_headers: HashMap, @@ -1379,6 +1412,7 @@ impl ArcedNodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_fixed_headers( &self, node_entropy: Arc, vss_url: String, store_id: String, fixed_headers: HashMap, @@ -1401,6 +1435,7 @@ impl ArcedNodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md + #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_header_provider( &self, node_entropy: Arc, vss_url: String, store_id: String, header_provider: Arc, @@ -1518,6 +1553,7 @@ fn build_with_store_internal( }; let (chain_source, chain_tip_opt) = match chain_data_source_config { + #[cfg(feature = "chain-esplora")] Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default()); ChainSource::new_esplora( @@ -1533,6 +1569,7 @@ fn build_with_store_internal( ) .map_err(|()| BuildError::ChainSourceSetupFailed)? }, + #[cfg(feature = "chain-electrum")] Some(ChainDataSourceConfig::Electrum { server_url, sync_config }) => { let sync_config = sync_config.unwrap_or(ElectrumSyncConfig::default()); ChainSource::new_electrum( @@ -1546,6 +1583,7 @@ fn build_with_store_internal( Arc::clone(&node_metrics), ) }, + #[cfg(feature = "chain-bitcoind")] Some(ChainDataSourceConfig::Bitcoind { rpc_host, rpc_port, @@ -1587,6 +1625,7 @@ fn build_with_store_internal( }), }, + #[cfg(feature = "chain-esplora")] None => { // Default to Esplora client. let server_url = DEFAULT_ESPLORA_SERVER_URL.to_string(); @@ -1604,13 +1643,24 @@ fn build_with_store_internal( ) .map_err(|()| BuildError::ChainSourceSetupFailed)? }, + #[cfg(not(feature = "chain-esplora"))] + None => return Err(BuildError::ChainSourceSetupFailed), }; let chain_source = Arc::new(chain_source); - let wallet_rescan_from_height = match chain_data_source_config { - Some(ChainDataSourceConfig::Bitcoind { wallet_rescan_from_height, .. }) => { - *wallet_rescan_from_height - }, - _ => None, + let wallet_rescan_from_height = { + #[cfg(feature = "chain-bitcoind")] + { + match chain_data_source_config { + Some(ChainDataSourceConfig::Bitcoind { wallet_rescan_from_height, .. }) => { + *wallet_rescan_from_height + }, + _ => None, + } + } + #[cfg(not(feature = "chain-bitcoind"))] + { + None:: + } }; // Initialize the on-chain wallet and chain access @@ -1670,10 +1720,13 @@ fn build_with_store_internal( // Abort cleanly instead so the misconfiguration surfaces on the first startup. // Esplora/Electrum backends currently never return a tip at build time, so they // retain their existing behavior. - if wallet_rescan_from_height.is_none() - && chain_tip_opt.is_none() - && matches!(chain_data_source_config, Some(ChainDataSourceConfig::Bitcoind { .. })) - { + #[cfg(feature = "chain-bitcoind")] + let uses_bitcoind = + matches!(chain_data_source_config, Some(ChainDataSourceConfig::Bitcoind { .. })); + #[cfg(not(feature = "chain-bitcoind"))] + let uses_bitcoind = false; + + if wallet_rescan_from_height.is_none() && chain_tip_opt.is_none() && uses_bitcoind { log_error!( logger, "Failed to determine chain tip on first startup. Aborting to avoid pinning the wallet birthday to genesis." @@ -1698,6 +1751,7 @@ fn build_with_store_internal( // the checkpoint. Otherwise, use the current chain tip to avoid any rescan. let checkpoint_block = match wallet_rescan_from_height { None => chain_tip_opt, + #[cfg(feature = "chain-bitcoind")] Some(height) => { if let Some(chain_tip) = chain_tip_opt { if height > chain_tip.height { @@ -1738,6 +1792,8 @@ fn build_with_store_internal( }, } }, + #[cfg(not(feature = "chain-bitcoind"))] + Some(_) => unreachable!("wallet rescans require the chain-bitcoind feature"), }; if let Some(best_block) = checkpoint_block { @@ -2051,11 +2107,15 @@ fn build_with_store_internal( })?; } + #[cfg(feature = "unified-payments")] let hrn_resolver; + #[cfg(feature = "unified-payments")] let mut blip32_resolver = None; + #[cfg(feature = "unified-payments")] let runtime_handle = runtime.handle(); + #[cfg(feature = "unified-payments")] let om_resolver: Arc = match &config .hrn_config .resolution_config @@ -2101,6 +2161,9 @@ fn build_with_store_internal( } }, }; + #[cfg(not(feature = "unified-payments"))] + let om_resolver: Arc = + Arc::new(IgnoringMessageHandler {}); // Initialize the PeerManager let onion_messenger: Arc = @@ -2233,6 +2296,7 @@ fn build_with_store_internal( Arc::clone(&keys_manager), )); + #[cfg(feature = "unified-payments")] if let Some(res) = blip32_resolver { let pm_weak = Arc::downgrade(&peer_manager); res.register_post_queue_action(Box::new(move || { @@ -2399,6 +2463,7 @@ fn build_with_store_internal( node_metrics, om_mailbox, async_payments_role, + #[cfg(feature = "unified-payments")] hrn_resolver, prober, #[cfg(cycle_tests)] diff --git a/src/chain/mod.rs b/src/chain/mod.rs index ba7f798fa..f01c1c8cb 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -5,24 +5,35 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +#[cfg(feature = "chain-bitcoind")] pub(crate) mod bitcoind; +#[cfg(feature = "chain-electrum")] mod electrum; +#[cfg(feature = "chain-esplora")] mod esplora; -use std::collections::{HashMap, HashSet}; +#[cfg(feature = "chain-esplora")] +use std::collections::HashMap; +use std::collections::HashSet; use std::sync::{Arc, Mutex}; use std::time::Duration; use bitcoin::{Script, Txid}; use lightning::chain::{BlockLocator, Filter}; +#[cfg(feature = "chain-bitcoind")] use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient}; +#[cfg(feature = "chain-electrum")] use crate::chain::electrum::ElectrumChainSource; +#[cfg(feature = "chain-esplora")] use crate::chain::esplora::EsploraChainSource; -use crate::config::{ - BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, - WALLET_SYNC_INTERVAL_MINIMUM_SECS, -}; +#[cfg(feature = "chain-bitcoind")] +use crate::config::BitcoindRestClientConfig; +#[cfg(feature = "chain-electrum")] +use crate::config::ElectrumSyncConfig; +#[cfg(feature = "chain-esplora")] +use crate::config::EsploraSyncConfig; +use crate::config::{BackgroundSyncConfig, Config, WALLET_SYNC_INTERVAL_MINIMUM_SECS}; use crate::fee_estimator::OnchainFeeEstimator; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::runtime::Runtime; @@ -140,12 +151,16 @@ pub(crate) struct ChainSource { } enum ChainSourceKind { + #[cfg(feature = "chain-esplora")] Esplora(EsploraChainSource), + #[cfg(feature = "chain-electrum")] Electrum(ElectrumChainSource), + #[cfg(feature = "chain-bitcoind")] Bitcoind(BitcoindChainSource), } impl ChainSource { + #[cfg(feature = "chain-esplora")] pub(crate) fn new_esplora( server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, fee_estimator: Arc, tx_broadcaster: Arc, @@ -167,6 +182,7 @@ impl ChainSource { Ok((Self { kind, registered_txids, tx_broadcaster, logger }, None)) } + #[cfg(feature = "chain-electrum")] pub(crate) fn new_electrum( server_url: String, sync_config: ElectrumSyncConfig, fee_estimator: Arc, tx_broadcaster: Arc, @@ -187,6 +203,7 @@ impl ChainSource { (Self { kind, registered_txids, tx_broadcaster, logger }, None) } + #[cfg(feature = "chain-bitcoind")] pub(crate) async fn new_bitcoind_rpc( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, fee_estimator: Arc, tx_broadcaster: Arc, @@ -210,6 +227,7 @@ impl ChainSource { (Self { kind, registered_txids, tx_broadcaster, logger }, best_block) } + #[cfg(feature = "chain-bitcoind")] pub(crate) async fn new_bitcoind_rest( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, fee_estimator: Arc, tx_broadcaster: Arc, @@ -236,9 +254,8 @@ impl ChainSource { pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { match &self.kind { - ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.start(runtime)? - }, + #[cfg(feature = "chain-electrum")] + ChainSourceKind::Electrum(electrum_chain_source) => electrum_chain_source.start(runtime)?, _ => { // Nothing to do for other chain sources. }, @@ -248,6 +265,7 @@ impl ChainSource { pub(crate) fn stop(&self) { match &self.kind { + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => electrum_chain_source.stop(), _ => { // Nothing to do for other chain sources. @@ -257,9 +275,8 @@ impl ChainSource { pub(crate) fn begin_shutdown(&self) { match &self.kind { - ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.begin_shutdown() - }, + #[cfg(feature = "chain-electrum")] + ChainSourceKind::Electrum(electrum_chain_source) => electrum_chain_source.begin_shutdown(), _ => { // Other chain sources don't leave synchronous callbacks running after their // driving future is cancelled. @@ -267,6 +284,7 @@ impl ChainSource { } } + #[cfg(feature = "chain-bitcoind")] pub(crate) fn as_utxo_source(&self) -> Option { match &self.kind { ChainSourceKind::Bitcoind(bitcoind_chain_source) => { @@ -282,8 +300,11 @@ impl ChainSource { pub(crate) fn is_transaction_based(&self) -> bool { match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(_) => true, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum { .. } => true, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind { .. } => false, } } @@ -294,6 +315,7 @@ impl ChainSource { output_sweeper: Arc, ) { match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { if let Some(background_sync_config) = esplora_chain_source.sync_config.background_sync_config.as_ref() @@ -317,6 +339,7 @@ impl ChainSource { return; } }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { if let Some(background_sync_config) = electrum_chain_source.sync_config.background_sync_config.as_ref() @@ -340,6 +363,7 @@ impl ChainSource { return; } }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source .continuously_sync_wallets( @@ -419,12 +443,15 @@ impl ChainSource { &self, onchain_wallet: Arc, ) -> Result<(), Error> { match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.sync_onchain_wallet(onchain_wallet).await }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.sync_onchain_wallet(onchain_wallet).await }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. @@ -440,16 +467,19 @@ impl ChainSource { output_sweeper: Arc, ) -> Result<(), Error> { match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) .await }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) .await }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. @@ -462,17 +492,23 @@ impl ChainSource { &self, onchain_wallet: Arc, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { + #[cfg(not(feature = "chain-bitcoind"))] + let _ = (&onchain_wallet, &channel_manager, &chain_monitor, &output_sweeper); + match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora { .. } => { // In Esplora mode we sync lightning and onchain wallets via // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum { .. } => { // In Electrum mode we sync lightning and onchain wallets via // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source .poll_and_update_listeners( @@ -488,12 +524,15 @@ impl ChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.update_fee_rate_estimates().await }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.update_fee_rate_estimates().await }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source.update_fee_rate_estimates().await }, @@ -508,12 +547,15 @@ impl ChainSource { } match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.validate_zero_fee_commitments_support().await }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.validate_zero_fee_commitments_support().await }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source.validate_zero_fee_commitments_support().await }, @@ -551,12 +593,15 @@ impl ChainSource { }; let package = package.into_sorted_transactions(); match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.process_transaction_broadcast(package).await }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.process_transaction_broadcast(package).await }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind(bitcoind_chain_source) => { bitcoind_chain_source.process_transaction_broadcast(package).await }, @@ -571,23 +616,27 @@ impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { self.registered_txids.lock().expect("lock").insert(*txid); match &self.kind { + #[cfg(feature = "chain-esplora")] ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.register_tx(txid, script_pubkey) }, + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.register_tx(txid, script_pubkey) }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { match &self.kind { - ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.register_output(output) - }, + #[cfg(feature = "chain-esplora")] + ChainSourceKind::Esplora(esplora_chain_source) => esplora_chain_source.register_output(output), + #[cfg(feature = "chain-electrum")] ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.register_output(output) }, + #[cfg(feature = "chain-bitcoind")] ChainSourceKind::Bitcoind { .. } => (), } } diff --git a/src/error.rs b/src/error.rs index 107b8fe1b..485f944c2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -279,6 +279,7 @@ impl From for Error { } } +#[cfg(any(feature = "chain-esplora", feature = "chain-electrum"))] impl From for Error { fn from(_e: lightning_transaction_sync::TxSyncError) -> Self { Self::TxSyncFailed diff --git a/src/gossip.rs b/src/gossip.rs index e50991478..41206dfa8 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -8,12 +8,15 @@ use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; +#[cfg(feature = "chain-bitcoind")] use lightning_block_sync::gossip::GossipVerifier; use crate::chain::ChainSource; use crate::config::{RGS_SNAPSHOT_MAX_SIZE, RGS_SYNC_TIMEOUT_SECS}; use crate::logger::{log_error, log_trace, LdkLogger, Logger}; -use crate::runtime::{Runtime, RuntimeSpawner}; +use crate::runtime::Runtime; +#[cfg(feature = "chain-bitcoind")] +use crate::runtime::RuntimeSpawner; use crate::types::{GossipSync, Graph, P2PGossipSync, RapidGossipSync, UtxoLookup}; use crate::Error; @@ -34,10 +37,16 @@ impl GossipSource { network_graph: Arc, chain_source: Arc, runtime: Arc, logger: Arc, ) -> Self { + #[cfg(feature = "chain-bitcoind")] let verifier = chain_source.as_utxo_source().map(|utxo_source| { Arc::new(GossipVerifier::new(Arc::new(utxo_source), RuntimeSpawner::new(runtime))) as Arc }); + #[cfg(not(feature = "chain-bitcoind"))] + let verifier: Option> = { + let _ = (chain_source, runtime); + None + }; let gossip_sync = Arc::new(P2PGossipSync::new(network_graph, verifier, logger)); Self::P2PNetwork { gossip_sync } diff --git a/src/io/mod.rs b/src/io/mod.rs index e01e8a5d9..c11475c43 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -7,13 +7,16 @@ //! Objects and traits for data persistence. +#[cfg(feature = "storage-filesystem")] pub(crate) mod fs_store; -#[cfg(feature = "postgres")] +#[cfg(feature = "storage-postgres")] pub mod postgres_store; +#[cfg(feature = "storage-sqlite")] pub mod sqlite_store; #[cfg(test)] pub(crate) mod test_utils; pub(crate) mod utils; +#[cfg(feature = "storage-vss")] pub mod vss_store; /// The event queue will be persisted under this key. diff --git a/src/lib.rs b/src/lib.rs index b0a6efde4..7f70d8d2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,6 +81,13 @@ #![allow(ellipsis_inclusive_range_patterns)] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(not(any( + feature = "chain-esplora", + feature = "chain-electrum", + feature = "chain-bitcoind" +)))] +compile_error!("at least one chain source feature must be enabled"); + mod balance; mod builder; mod chain; @@ -169,9 +176,11 @@ use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ - Bolt11Payment, Bolt12Payment, HRNResolver, OnchainPayment, PaymentDetails, PaymentDetailsPage, - SpontaneousPayment, UnifiedPayment, + Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, PaymentDetailsPage, + SpontaneousPayment, }; +#[cfg(feature = "unified-payments")] +use payment::{HRNResolver, UnifiedPayment}; use peer_store::{PeerInfo, PeerStore}; #[cfg(feature = "uniffi")] pub use probing::ArcedProbingConfigBuilder as ProbingConfigBuilder; @@ -185,6 +194,7 @@ use types::{ pub use types::{ ChannelCounterparty, ChannelDetails, CustomTlvRecord, PeerDetails, ReserveType, UserChannelId, }; +#[cfg(feature = "storage-vss")] pub use vss_client; use crate::config::{LIQUIDITY_DISCOVERY_RETRY_INITIAL_DELAY, LIQUIDITY_DISCOVERY_RETRY_MAX_DELAY}; @@ -266,6 +276,7 @@ pub struct Node { node_metrics: Arc, om_mailbox: Option>, async_payments_role: Option, + #[cfg(feature = "unified-payments")] hrn_resolver: HRNResolver, prober: Option>, #[cfg(cycle_tests)] @@ -1151,7 +1162,7 @@ impl Node { /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki - #[cfg(not(feature = "uniffi"))] + #[cfg(all(feature = "unified-payments", not(feature = "uniffi")))] pub fn unified_payment(&self) -> UnifiedPayment { UnifiedPayment::new( self.onchain_payment().into(), @@ -1172,7 +1183,7 @@ impl Node { /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki - #[cfg(feature = "uniffi")] + #[cfg(all(feature = "unified-payments", feature = "uniffi"))] pub fn unified_payment(&self) -> Arc { Arc::new(UnifiedPayment::new( self.onchain_payment(), diff --git a/src/payment/mod.rs b/src/payment/mod.rs index c483a3aef..13dbe5106 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -10,16 +10,19 @@ pub(crate) mod asynchronous; mod bolt11; mod bolt12; +#[cfg(feature = "unified-payments")] mod hrn; mod onchain; pub(crate) mod pending_payment_store; mod spontaneous; pub(crate) mod store; +#[cfg(feature = "unified-payments")] mod unified; pub use bolt11::Bolt11Payment; pub(crate) use bolt11::PaymentMetadata; pub use bolt12::{Bolt12Payment, PayerProofOptions}; +#[cfg(feature = "unified-payments")] pub(crate) use hrn::HRNResolver; pub use onchain::OnchainPayment; pub(crate) use pending_payment_store::{FundingTxCandidate, PendingPaymentDetails}; @@ -28,4 +31,5 @@ pub use store::{ Channel, ConfirmationStatus, LSPS2Parameters, PageToken, PaymentDetails, PaymentDetailsPage, PaymentDirection, PaymentKind, PaymentStatus, TransactionType, }; +#[cfg(feature = "unified-payments")] pub use unified::{UnifiedPayment, UnifiedPaymentResult}; diff --git a/src/payment/unified.rs b/src/payment/unified.rs index bb8f1ad0a..185b2e2da 100644 --- a/src/payment/unified.rs +++ b/src/payment/unified.rs @@ -72,6 +72,7 @@ pub struct UnifiedPayment { onchain_payment: Arc, bolt11_invoice: Arc, bolt12_payment: Arc, + #[cfg(not(hrn_tests))] config: Arc, logger: Arc, hrn_resolver: HRNResolver, @@ -85,10 +86,14 @@ impl UnifiedPayment { bolt12_payment: Arc, config: Arc, logger: Arc, hrn_resolver: HRNResolver, ) -> Self { + #[cfg(hrn_tests)] + let _ = config; + Self { onchain_payment, bolt11_invoice, bolt12_payment, + #[cfg(not(hrn_tests))] config, logger, hrn_resolver, diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 892f40914..478efac47 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -213,6 +213,7 @@ impl Wallet { self.inner.lock().expect("lock").tx_graph().full_txs().map(|tx_node| tx_node.tx).collect() } + #[cfg(feature = "chain-bitcoind")] pub(crate) fn get_unconfirmed_txids(&self) -> Vec { self.inner .lock() @@ -223,6 +224,7 @@ impl Wallet { .collect() } + #[cfg(feature = "chain-bitcoind")] pub(crate) fn current_best_block(&self) -> BlockLocator { let checkpoint = self.inner.lock().expect("lock").latest_checkpoint(); let mut current_block = Some(checkpoint.clone()); @@ -261,6 +263,7 @@ impl Wallet { Ok(()) } + #[cfg(feature = "chain-bitcoind")] pub(crate) async fn apply_mempool_txs( &self, unconfirmed_txs: Vec<(Transaction, u64)>, evicted_txids: Vec<(Txid, u64)>, ) -> Result<(), Error> { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 70d1a292b..ef22a54ef 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -363,6 +363,9 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { pub(crate) fn random_chain_source<'a>( bitcoind: &'a BitcoinD, electrsd: &'a ElectrsD, ) -> TestChainSource<'a> { + #[cfg(not(feature = "chain-bitcoind"))] + let _ = bitcoind; + let configured_sources = env::var("LDK_NODE_TEST_CHAIN_SOURCES").ok().map(|value| { value .split(|c: char| c == ',' || c.is_ascii_whitespace()) @@ -371,25 +374,36 @@ pub(crate) fn random_chain_source<'a>( .collect::>() }); let sources = configured_sources.unwrap_or_else(|| { - ["ESPLORA", "ELECTRUM", "BITCOIND_RPC", "BITCOIND_REST"] - .into_iter() - .map(String::from) - .collect() + let mut sources = Vec::new(); + #[cfg(feature = "chain-esplora")] + sources.push("ESPLORA".to_string()); + #[cfg(feature = "chain-electrum")] + sources.push("ELECTRUM".to_string()); + #[cfg(feature = "chain-bitcoind")] + { + sources.push("BITCOIND_RPC".to_string()); + sources.push("BITCOIND_REST".to_string()); + } + sources }); let source = &sources[rand::random_range(0..sources.len())]; match source.as_str() { + #[cfg(feature = "chain-esplora")] "ESPLORA" => { println!("Randomly setting up Esplora chain syncing..."); TestChainSource::Esplora(electrsd) }, + #[cfg(feature = "chain-electrum")] "ELECTRUM" => { println!("Randomly setting up Electrum chain syncing..."); TestChainSource::Electrum(electrsd) }, + #[cfg(feature = "chain-bitcoind")] "BITCOIND_RPC" => { println!("Randomly setting up Bitcoind RPC chain syncing..."); TestChainSource::BitcoindRpcSync(bitcoind) }, + #[cfg(feature = "chain-bitcoind")] "BITCOIND_REST" => { println!("Randomly setting up Bitcoind REST chain syncing..."); TestChainSource::BitcoindRestSync(bitcoind) @@ -620,16 +634,22 @@ async fn settle_force_close_balance( #[derive(Clone)] pub(crate) enum TestChainSource<'a> { + #[cfg(feature = "chain-esplora")] Esplora(&'a ElectrsD), + #[cfg(feature = "chain-electrum")] Electrum(&'a ElectrsD), + #[cfg(feature = "chain-bitcoind")] BitcoindRpcSync(&'a BitcoinD), + #[cfg(feature = "chain-bitcoind")] BitcoindRestSync(&'a BitcoinD), } #[derive(Clone, Copy)] pub(crate) enum TestStoreType { TestSyncStore, + #[cfg(feature = "storage-sqlite")] Sqlite, + #[cfg(feature = "storage-filesystem")] FilesystemStore, } @@ -696,6 +716,7 @@ pub(crate) fn configure_chain_source( chain_source: &TestChainSource, builder: &mut Builder, config: &TestConfig, ) { match chain_source { + #[cfg(feature = "chain-esplora")] TestChainSource::Esplora(electrsd) => { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let mut sync_config = EsploraSyncConfig::default(); @@ -706,6 +727,7 @@ pub(crate) fn configure_chain_source( } builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); }, + #[cfg(feature = "chain-electrum")] TestChainSource::Electrum(electrsd) => { let electrum_url = format!("tcp://{}", electrsd.electrum_url); let mut sync_config = ElectrumSyncConfig::default(); @@ -716,6 +738,7 @@ pub(crate) fn configure_chain_source( } builder.set_chain_source_electrum(electrum_url.clone(), Some(sync_config)); }, + #[cfg(feature = "chain-bitcoind")] TestChainSource::BitcoindRpcSync(bitcoind) => { let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); let rpc_port = bitcoind.params.rpc_socket.port(); @@ -730,6 +753,7 @@ pub(crate) fn configure_chain_source( config.wallet_rescan_from_height, ); }, + #[cfg(feature = "chain-bitcoind")] TestChainSource::BitcoindRestSync(bitcoind) => { let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); let rpc_port = bitcoind.params.rpc_socket.port(); @@ -831,7 +855,9 @@ pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> let kv_store = TestSyncStore::new(config.node_config.storage_dir_path.into()); builder.build_with_store(config.node_entropy.into(), kv_store).unwrap() }, + #[cfg(feature = "storage-sqlite")] TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(), + #[cfg(feature = "storage-filesystem")] TestStoreType::FilesystemStore => { builder.build_with_fs_store(config.node_entropy.into()).unwrap() }, @@ -2129,7 +2155,7 @@ impl TestSyncStoreInner { /// The PostgreSQL connection string used by the Postgres-backed tests, overridable via the /// `TEST_POSTGRES_URL` environment variable. -#[cfg(feature = "postgres")] +#[cfg(feature = "storage-postgres")] pub(crate) fn test_connection_string() -> String { std::env::var("TEST_POSTGRES_URL") .unwrap_or_else(|_| "host=localhost user=postgres password=postgres".to_string()) @@ -2137,7 +2163,7 @@ pub(crate) fn test_connection_string() -> String { /// Drops the given table from the `ldk_db` database, ignoring the case where the database doesn't /// exist yet. Used to ensure a clean slate before and after Postgres-backed tests. -#[cfg(feature = "postgres")] +#[cfg(feature = "storage-postgres")] pub(crate) async fn drop_table(table_name: &str) { let connection_string = format!("{} dbname=ldk_db", test_connection_string()); let Ok((client, connection)) = diff --git a/tests/integration_tests_hrn.rs b/tests/integration_tests_hrn.rs index d61604798..ecf92dcc2 100644 --- a/tests/integration_tests_hrn.rs +++ b/tests/integration_tests_hrn.rs @@ -13,7 +13,7 @@ use bitcoin::Amount; use common::{ expect_channel_ready_event, expect_payment_successful_event, generate_blocks_and_wait, open_channel, premine_and_distribute_funds, random_chain_source, setup_bitcoind_and_electrsd, - setup_two_nodes, TestChainSource, + setup_two_nodes, }; use ldk_node::payment::UnifiedPaymentResult; use ldk_node::Event; diff --git a/tests/integration_tests_migration.rs b/tests/integration_tests_migration.rs index 26f7d7c78..84e332bce 100644 --- a/tests/integration_tests_migration.rs +++ b/tests/integration_tests_migration.rs @@ -7,7 +7,7 @@ // The migration test exercises the filesystem, SQLite, and Postgres stores. It is gated on the // `postgres` feature because Postgres is the only one of the three that needs an external service. -#![cfg(feature = "postgres")] +#![cfg(feature = "storage-postgres")] mod common; diff --git a/tests/integration_tests_postgres.rs b/tests/integration_tests_postgres.rs index 682255f21..280c11de5 100644 --- a/tests/integration_tests_postgres.rs +++ b/tests/integration_tests_postgres.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -#![cfg(feature = "postgres")] +#![cfg(feature = "storage-postgres")] mod common; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fd247f74c..b99d0781e 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -439,6 +439,7 @@ async fn address_pool_is_reloaded_on_restart() { expect_channel_ready_event!(node_b, node_a.node_id()); } +#[cfg(feature = "chain-bitcoind")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -1554,6 +1555,7 @@ async fn do_onchain_wallet_full_scan_stop_gap_recovers_far_funds( ); } +#[cfg(feature = "chain-bitcoind")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn onchain_wallet_recovery_rescans_from_birthday_height() { // End-to-end test for `wallet_rescan_from_height` against a bitcoind chain source. The @@ -1649,6 +1651,7 @@ async fn onchain_wallet_recovery_rescans_from_birthday_height() { ); } +#[cfg(feature = "chain-bitcoind")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn build_fails_when_wallet_rescan_height_is_above_tip() { let (bitcoind, _electrsd) = setup_bitcoind_and_electrsd(); @@ -1682,6 +1685,7 @@ async fn build_fails_when_wallet_rescan_height_is_above_tip() { } } +#[cfg(feature = "chain-bitcoind")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn build_aborts_on_first_startup_bitcoind_tip_fetch_failure() { // A fresh node pointed at an unreachable bitcoind RPC endpoint must not silently @@ -1713,11 +1717,13 @@ async fn build_aborts_on_first_startup_bitcoind_tip_fetch_failure() { } } +#[cfg(all(feature = "chain-esplora", feature = "chain-electrum", feature = "chain-bitcoind"))] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_rbf_via_mempool() { run_rbf_test(false).await; } +#[cfg(all(feature = "chain-esplora", feature = "chain-electrum", feature = "chain-bitcoind"))] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_rbf_via_direct_block_insertion() { run_rbf_test(true).await; @@ -1726,6 +1732,7 @@ async fn test_rbf_via_direct_block_insertion() { // `is_insert_block`: // - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling. // - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling. +#[cfg(all(feature = "chain-esplora", feature = "chain-electrum", feature = "chain-bitcoind"))] async fn run_rbf_test(is_insert_block: bool) { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); @@ -4227,7 +4234,9 @@ async fn build_0_7_0_node( builder_old.set_entropy_seed_bytes(seed_bytes); builder_old.set_chain_source_esplora(esplora_url, None); let node_old = match config.store_type { + #[cfg(feature = "storage-filesystem")] TestStoreType::FilesystemStore => builder_old.build_with_fs_store().unwrap(), + #[cfg(feature = "storage-sqlite")] TestStoreType::Sqlite => builder_old.build().unwrap(), TestStoreType::TestSyncStore => panic!("TestSyncStore not supported in v0.7.0 builder"), }; @@ -4311,6 +4320,7 @@ async fn persistence_backwards_compatibility() { do_persistence_backwards_compatibility(OldLdkVersion::V0_7_0).await; } +#[cfg(feature = "storage-filesystem")] #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn fs_store_persistence_backwards_compatibility() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index efab1480e..aa17f072c 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -16,6 +16,7 @@ use crate::common::{ TestChainSource, }; +#[cfg(feature = "chain-bitcoind")] #[test] fn bitcoind_rest_follows_valid_reorg() { let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap(); From b4127169767d4749882788fae9b94fbb0ad22bb1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 20 Aug 2026 12:50:59 +0200 Subject: [PATCH 09/17] f Gate optional binding APIs Keep custom UniFFI builds from requiring VSS and unified payment dependencies that they did not enable. Co-Authored-By: HAL 9000 --- bindings/ldk_node.udl | 3 --- src/ffi/types.rs | 5 +++++ src/lib.rs | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 3829ddfb2..38598f44f 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -108,7 +108,6 @@ interface Node { Bolt12Payment bolt12_payment(); SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); - UnifiedPayment unified_payment(); Liquidity liquidity(); [Throws=NodeError] void lnurl_auth(string lnurl); @@ -179,8 +178,6 @@ interface FeeRate { u64 to_sat_per_vb_ceil(); }; -typedef interface UnifiedPayment; - typedef interface Liquidity; [Error] diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 4972c636d..a8d297d94 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -53,6 +53,7 @@ use lightning_types::features::{ }; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_types::string::UntrustedString; +#[cfg(feature = "storage-vss")] use vss_client::headers::{ VssHeaderProvider as VssClientHeaderProvider, VssHeaderProviderError as VssClientHeaderProviderError, @@ -100,6 +101,7 @@ impl std::fmt::Display for VssHeaderProviderError { impl std::error::Error for VssHeaderProviderError {} +#[cfg(feature = "storage-vss")] impl From for VssClientHeaderProviderError { fn from(e: VssHeaderProviderError) -> Self { match e { @@ -130,16 +132,19 @@ pub trait VssHeaderProvider: Send + Sync { /// An adapter that wraps the local [`VssHeaderProvider`] and implements the upstream /// [`VssClientHeaderProvider`] trait. +#[cfg(feature = "storage-vss")] pub(crate) struct VssHeaderProviderAdapter { inner: Arc, } +#[cfg(feature = "storage-vss")] impl VssHeaderProviderAdapter { pub(crate) fn new(inner: Arc) -> Self { Self { inner } } } +#[cfg(feature = "storage-vss")] #[async_trait::async_trait] impl VssClientHeaderProvider for VssHeaderProviderAdapter { async fn get_headers( diff --git a/src/lib.rs b/src/lib.rs index 7f70d8d2e..b6f790767 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1173,7 +1173,11 @@ impl Node { self.hrn_resolver.clone(), ) } +} +#[cfg(all(feature = "unified-payments", feature = "uniffi"))] +#[uniffi::export] +impl Node { /// Returns a payment handler that supports creating and paying to [BIP 21] URIs with on-chain, /// [BOLT 11], and [BOLT 12] payment options. /// @@ -1183,7 +1187,6 @@ impl Node { /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki - #[cfg(all(feature = "unified-payments", feature = "uniffi"))] pub fn unified_payment(&self) -> Arc { Arc::new(UnifiedPayment::new( self.onchain_payment(), @@ -1194,7 +1197,9 @@ impl Node { self.hrn_resolver.clone(), )) } +} +impl Node { /// Authenticates the user via [LNURL-auth] for the given LNURL string. /// /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md From b971ce4ec55c3556de61fbdc30482e8df5605970 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 20 Aug 2026 12:53:08 +0200 Subject: [PATCH 10/17] f Gate backend-specific test helpers Let wallet and PostgreSQL test targets compile when Esplora or SQLite support is disabled. Co-Authored-By: HAL 9000 --- src/wallet/mod.rs | 20 ++++++++++++++++-- tests/common/mod.rs | 51 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 478efac47..b9c12b4a7 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -2685,7 +2685,7 @@ fn funding_reclassification_update( update } -#[cfg(test)] +#[cfg(all(test, any(feature = "chain-esplora", feature = "chain-electrum")))] mod tests { use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -2698,7 +2698,11 @@ mod tests { use lightning::util::persist::{KVStore, PageToken, PaginatedKVStore, PaginatedListResponse}; use super::*; - use crate::config::{EsploraSyncConfig, PAYMENT_CACHE_CAPACITY}; + #[cfg(all(not(feature = "chain-esplora"), feature = "chain-electrum"))] + use crate::config::ElectrumSyncConfig; + #[cfg(feature = "chain-esplora")] + use crate::config::EsploraSyncConfig; + use crate::config::PAYMENT_CACHE_CAPACITY; use crate::io::test_utils::InMemoryStore; use crate::io::{ BDK_WALLET_ADDRESS_POOL_KEY, BDK_WALLET_ADDRESS_POOL_PRIMARY_NAMESPACE, @@ -2810,6 +2814,7 @@ mod tests { let fee_estimator = Arc::new(OnchainFeeEstimator::new()); let broadcaster = Arc::new(Broadcaster::new(Arc::clone(&logger))); let node_metrics = Arc::new(PersistedNodeMetrics::new(NodeMetrics::default())); + #[cfg(feature = "chain-esplora")] let (chain_source, _) = ChainSource::new_esplora( "http://localhost:1".to_string(), HashMap::new(), @@ -2822,6 +2827,17 @@ mod tests { node_metrics, ) .unwrap(); + #[cfg(all(not(feature = "chain-esplora"), feature = "chain-electrum"))] + let (chain_source, _) = ChainSource::new_electrum( + "tcp://localhost:1".to_string(), + ElectrumSyncConfig::default(), + Arc::clone(&fee_estimator), + Arc::clone(&broadcaster), + Arc::clone(&store), + Arc::clone(&config), + Arc::clone(&logger), + node_metrics, + ); let payment_store = Arc::new(PaymentStore::new( Vec::new(), KeepLeastRecentlyUsed::new(PAYMENT_CACHE_CAPACITY), diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ef22a54ef..777bab700 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -43,6 +43,7 @@ use ldk_node::config::{ HumanReadableNamesConfig, }; use ldk_node::entropy::NodeEntropy; +#[cfg(feature = "storage-sqlite")] use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{ PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, TransactionType, @@ -1951,6 +1952,7 @@ struct TestSyncStoreInner { serializer: tokio::sync::RwLock<()>, test_store: InMemoryStore, fs_store: FilesystemStore, + #[cfg(feature = "storage-sqlite")] sqlite_store: SqliteStore, } @@ -1960,8 +1962,11 @@ impl TestSyncStoreInner { let mut fs_dir = dest_dir.clone(); fs_dir.push("fs_store"); let fs_store = FilesystemStore::new(fs_dir); + #[cfg(feature = "storage-sqlite")] let mut sql_dir = dest_dir.clone(); + #[cfg(feature = "storage-sqlite")] sql_dir.push("sqlite_store"); + #[cfg(feature = "storage-sqlite")] let sqlite_store = SqliteStore::new( sql_dir, Some("test_sync_db".to_string()), @@ -1969,15 +1974,21 @@ impl TestSyncStoreInner { ) .unwrap(); let test_store = InMemoryStore::new(); - Self { serializer, fs_store, sqlite_store, test_store } + Self { + serializer, + fs_store, + #[cfg(feature = "storage-sqlite")] + sqlite_store, + test_store, + } } async fn do_list_async( &self, primary_namespace: &str, secondary_namespace: &str, ) -> lightning::io::Result> { let fs_res = KVStore::list(&self.fs_store, primary_namespace, secondary_namespace).await; - let sqlite_res = - KVStore::list(&self.sqlite_store, primary_namespace, secondary_namespace).await; + #[cfg(feature = "storage-sqlite")] + let sqlite_res = KVStore::list(&self.sqlite_store, primary_namespace, secondary_namespace).await; let test_res = KVStore::list(&self.test_store, primary_namespace, secondary_namespace).await; @@ -1985,9 +1996,12 @@ impl TestSyncStoreInner { Ok(mut list) => { list.sort(); - let mut sqlite_list = sqlite_res.unwrap(); - sqlite_list.sort(); - assert_eq!(list, sqlite_list); + #[cfg(feature = "storage-sqlite")] + { + let mut sqlite_list = sqlite_res.unwrap(); + sqlite_list.sort(); + assert_eq!(list, sqlite_list); + } let mut test_list = test_res.unwrap(); test_list.sort(); @@ -1996,6 +2010,7 @@ impl TestSyncStoreInner { Ok(list) }, Err(e) => { + #[cfg(feature = "storage-sqlite")] assert!(sqlite_res.is_err()); assert!(test_res.is_err()); Err(e) @@ -2014,6 +2029,7 @@ impl TestSyncStoreInner { &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option, ) -> lightning::io::Result { let _guard = self.serializer.read().await; + #[cfg(feature = "storage-sqlite")] let sqlite_res = PaginatedKVStore::list_paginated( &self.sqlite_store, primary_namespace, @@ -2029,7 +2045,8 @@ impl TestSyncStoreInner { ) .await; - match sqlite_res { + #[cfg(feature = "storage-sqlite")] + return match sqlite_res { Ok(sqlite_response) => { assert_eq!(sqlite_response, test_res.unwrap()); Ok(sqlite_response) @@ -2038,7 +2055,10 @@ impl TestSyncStoreInner { assert!(test_res.is_err()); Err(e) }, - } + }; + + #[cfg(not(feature = "storage-sqlite"))] + test_res } async fn read_internal_async( @@ -2048,6 +2068,7 @@ impl TestSyncStoreInner { let fs_res = KVStore::read(&self.fs_store, primary_namespace, secondary_namespace, key).await; + #[cfg(feature = "storage-sqlite")] let sqlite_res = KVStore::read(&self.sqlite_store, primary_namespace, secondary_namespace, key).await; let test_res = @@ -2055,13 +2076,17 @@ impl TestSyncStoreInner { match fs_res { Ok(read) => { + #[cfg(feature = "storage-sqlite")] assert_eq!(read, sqlite_res.unwrap()); assert_eq!(read, test_res.unwrap()); Ok(read) }, Err(e) => { - assert!(sqlite_res.is_err()); - assert_eq!(e.kind(), unsafe { sqlite_res.unwrap_err_unchecked().kind() }); + #[cfg(feature = "storage-sqlite")] + { + assert!(sqlite_res.is_err()); + assert_eq!(e.kind(), unsafe { sqlite_res.unwrap_err_unchecked().kind() }); + } assert!(test_res.is_err()); assert_eq!(e.kind(), unsafe { test_res.unwrap_err_unchecked().kind() }); Err(e) @@ -2081,6 +2106,7 @@ impl TestSyncStoreInner { buf.clone(), ) .await; + #[cfg(feature = "storage-sqlite")] let sqlite_res = KVStore::write( &self.sqlite_store, primary_namespace, @@ -2106,11 +2132,13 @@ impl TestSyncStoreInner { match fs_res { Ok(()) => { + #[cfg(feature = "storage-sqlite")] assert!(sqlite_res.is_ok()); assert!(test_res.is_ok()); Ok(()) }, Err(e) => { + #[cfg(feature = "storage-sqlite")] assert!(sqlite_res.is_err()); assert!(test_res.is_err()); Err(e) @@ -2125,6 +2153,7 @@ impl TestSyncStoreInner { let fs_res = KVStore::remove(&self.fs_store, primary_namespace, secondary_namespace, key, lazy) .await; + #[cfg(feature = "storage-sqlite")] let sqlite_res = KVStore::remove(&self.sqlite_store, primary_namespace, secondary_namespace, key, lazy) .await; @@ -2140,11 +2169,13 @@ impl TestSyncStoreInner { match fs_res { Ok(()) => { + #[cfg(feature = "storage-sqlite")] assert!(sqlite_res.is_ok()); assert!(test_res.is_ok()); Ok(()) }, Err(e) => { + #[cfg(feature = "storage-sqlite")] assert!(sqlite_res.is_err()); assert!(test_res.is_err()); Err(e) From 0f85d78f2a1f5f8a9b7555767ae222dd75216f00 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:51:28 +0200 Subject: [PATCH 11/17] Group binding builder methods by feature Separate backend-specific binding methods into cfg-gated implementation blocks. Method bodies and availability remain unchanged, making later conditional exports easier to review. Co-Authored-By: HAL 9000 --- src/builder.rs | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 747d0ae96..97e5f4fe2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1010,12 +1010,14 @@ impl ArcedNodeBuilder { let inner = RwLock::new(NodeBuilder::from_config(config)); Self { inner } } +} +#[cfg(all(feature = "uniffi", feature = "chain-esplora"))] +impl ArcedNodeBuilder { /// Configures the [`Node`] instance to source its chain data from the given Esplora server. /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more /// information. - #[cfg(feature = "chain-esplora")] pub fn set_chain_source_esplora( &self, server_url: String, sync_config: Option, ) { @@ -1029,7 +1031,6 @@ impl ArcedNodeBuilder { /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more /// information. - #[cfg(feature = "chain-esplora")] pub fn set_chain_source_esplora_with_headers( &self, server_url: String, headers: HashMap, sync_config: Option, @@ -1040,18 +1041,23 @@ impl ArcedNodeBuilder { sync_config, ); } +} +#[cfg(all(feature = "uniffi", feature = "chain-electrum"))] +impl ArcedNodeBuilder { /// Configures the [`Node`] instance to source its chain data from the given Electrum server. /// /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more /// information. - #[cfg(feature = "chain-electrum")] pub fn set_chain_source_electrum( &self, server_url: String, sync_config: Option, ) { self.inner.write().expect("lock").set_chain_source_electrum(server_url, sync_config); } +} +#[cfg(all(feature = "uniffi", feature = "chain-bitcoind"))] +impl ArcedNodeBuilder { /// Configures the [`Node`] instance to connect to a Bitcoin Core node via RPC. /// /// This method establishes an RPC connection that enables all essential chain operations including @@ -1064,7 +1070,6 @@ impl ArcedNodeBuilder { /// startup, before wallet state exists. Existing wallets are not rewound. The height must /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None` /// checkpoints at the current tip. - #[cfg(feature = "chain-bitcoind")] pub fn set_chain_source_bitcoind_rpc( &self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option, @@ -1091,7 +1096,6 @@ impl ArcedNodeBuilder { /// startup, before wallet state exists. Existing wallets are not rewound. The height must /// be at or below the current tip. Passing `Some(0)` rescans from genesis; passing `None` /// checkpoints at the current tip. - #[cfg(feature = "chain-bitcoind")] pub fn set_chain_source_bitcoind_rest( &self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, wallet_rescan_from_height: Option, @@ -1106,7 +1110,10 @@ impl ArcedNodeBuilder { wallet_rescan_from_height, ); } +} +#[cfg(feature = "uniffi")] +impl ArcedNodeBuilder { /// Configures the [`Node`] instance to source its gossip data from the Lightning peer-to-peer /// network. pub fn set_gossip_source_p2p(&self) { @@ -1254,14 +1261,19 @@ impl ArcedNodeBuilder { pub fn set_probing_config(&self, config: Arc) { self.inner.write().expect("lock").set_probing_config((*config).clone()); } +} +#[cfg(all(feature = "uniffi", feature = "storage-sqlite"))] +impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. - #[cfg(feature = "storage-sqlite")] pub fn build(&self, node_entropy: Arc) -> Result, BuildError> { self.inner.read().expect("lock").build(*node_entropy).map(Arc::new) } +} +#[cfg(all(feature = "uniffi", feature = "storage-postgres"))] +impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options /// previously configured. /// @@ -1292,7 +1304,6 @@ impl ArcedNodeBuilder { /// will be unencrypted. /// /// [PostgreSQL]: https://www.postgresql.org - #[cfg(feature = "storage-postgres")] pub fn build_with_postgres_store( &self, node_entropy: Arc, connection_string: String, db_name: Option, kv_table_name: Option, certificate_pem: Option, @@ -1309,28 +1320,35 @@ impl ArcedNodeBuilder { ) .map(Arc::new) } +} +#[cfg(all(feature = "uniffi", not(feature = "storage-postgres")))] +impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options /// previously configured. /// /// This requires the `storage-postgres` crate feature. - #[cfg(not(feature = "storage-postgres"))] pub fn build_with_postgres_store( &self, _node_entropy: Arc, _connection_string: String, _db_name: Option, _kv_table_name: Option, _certificate_pem: Option, ) -> Result, BuildError> { Err(BuildError::KVStoreSetupFailed) } +} +#[cfg(all(feature = "uniffi", feature = "storage-filesystem"))] +impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options /// previously configured. - #[cfg(feature = "storage-filesystem")] pub fn build_with_fs_store( &self, node_entropy: Arc, ) -> Result, BuildError> { self.inner.read().expect("lock").build_with_fs_store(*node_entropy).map(Arc::new) } +} +#[cfg(all(feature = "uniffi", feature = "storage-vss"))] +impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [VSS] backend and according to the options /// previously configured. /// @@ -1348,7 +1366,6 @@ impl ArcedNodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md - #[cfg(feature = "storage-vss")] pub fn build_with_vss_store( &self, node_entropy: Arc, vss_url: String, store_id: String, fixed_headers: HashMap, @@ -1382,7 +1399,6 @@ impl ArcedNodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md - #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_lnurl_auth( &self, node_entropy: Arc, vss_url: String, store_id: String, lnurl_auth_server_url: String, fixed_headers: HashMap, @@ -1412,7 +1428,6 @@ impl ArcedNodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md - #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_fixed_headers( &self, node_entropy: Arc, vss_url: String, store_id: String, fixed_headers: HashMap, @@ -1435,7 +1450,6 @@ impl ArcedNodeBuilder { /// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted. /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md - #[cfg(feature = "storage-vss")] pub fn build_with_vss_store_and_header_provider( &self, node_entropy: Arc, vss_url: String, store_id: String, header_provider: Arc, @@ -1447,7 +1461,10 @@ impl ArcedNodeBuilder { .build_with_vss_store_and_header_provider(*node_entropy, vss_url, store_id, adapter) .map(Arc::new) } +} +#[cfg(feature = "uniffi")] +impl ArcedNodeBuilder { /// Builds a [`Node`] instance according to the options previously configured. // Note that the generics here don't actually work for Uniffi, but we don't currently expose // this so its not needed. From b85ad15600d7c3996cad17539720cc25bb924277 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 10:20:52 +0200 Subject: [PATCH 12/17] Define binding builder methods in Rust Keep binding signatures and documentation next to their Rust implementations. Leave only the UDL object declaration so backend features can add methods without failure stubs. Co-Authored-By: HAL 9000 --- bindings/ldk_node.udl | 41 --------------------------------- src/builder.rs | 53 ++++++++++++++++++++++++------------------- src/ffi/types.rs | 3 +-- 3 files changed, 31 insertions(+), 66 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 38598f44f..4c4c1a438 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -44,47 +44,6 @@ interface ProbingConfigBuilder { }; interface Builder { - constructor(); - [Name=from_config] - constructor(Config config); - void set_chain_source_esplora(string server_url, EsploraSyncConfig? config); - void set_chain_source_electrum(string server_url, ElectrumSyncConfig? config); - void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password, u32? wallet_rescan_from_height); - void set_chain_source_bitcoind_rest(string rest_host, u16 rest_port, string rpc_host, u16 rpc_port, string rpc_user, string rpc_password, u32? wallet_rescan_from_height); - void set_gossip_source_p2p(); - void set_gossip_source_rgs(string rgs_server_url); - void set_pathfinding_scores_source(string url); - void add_liquidity_source(PublicKey node_id, SocketAddress address, string? token, boolean trust_peer_0conf); - void set_storage_dir_path(string storage_dir_path); - void set_filesystem_logger(string? log_file_path, LogLevel? max_log_level); - void set_log_facade_logger(); - void set_custom_logger(LogWriter log_writer); - void set_network(Network network); - [Throws=BuildError] - void set_listening_addresses(sequence listening_addresses); - [Throws=BuildError] - void set_announcement_addresses(sequence announcement_addresses); - [Throws=BuildError] - void set_tor_config(TorConfig tor_config); - [Throws=BuildError] - void set_node_alias(string node_alias); - [Throws=BuildError] - void set_async_payments_role(AsyncPaymentsRole? role); - void set_probing_config(ProbingConfig config); - [Throws=BuildError] - Node build(NodeEntropy node_entropy); - [Throws=BuildError] - Node build_with_postgres_store(NodeEntropy node_entropy, string connection_string, string? db_name, string? kv_table_name, string? certificate_pem); - [Throws=BuildError] - Node build_with_fs_store(NodeEntropy node_entropy); - [Throws=BuildError] - Node build_with_vss_store(NodeEntropy node_entropy, string vss_url, string store_id, record fixed_headers); - [Throws=BuildError] - Node build_with_vss_store_and_lnurl_auth(NodeEntropy node_entropy, string vss_url, string store_id, string lnurl_auth_server_url, record fixed_headers); - [Throws=BuildError] - Node build_with_vss_store_and_fixed_headers(NodeEntropy node_entropy, string vss_url, string store_id, record fixed_headers); - [Throws=BuildError] - Node build_with_vss_store_and_header_provider(NodeEntropy node_entropy, string vss_url, string store_id, VssHeaderProvider header_provider); }; interface Node { diff --git a/src/builder.rs b/src/builder.rs index 97e5f4fe2..d5c9310e0 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -998,14 +998,17 @@ pub struct ArcedNodeBuilder { pub use self::ArcedNodeBuilder as Builder; #[cfg(feature = "uniffi")] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Creates a new builder instance with the default configuration. + #[uniffi::constructor] pub fn new() -> Self { let inner = RwLock::new(NodeBuilder::new()); Self { inner } } /// Creates a new builder instance from an [`Config`]. + #[uniffi::constructor] pub fn from_config(config: Config) -> Self { let inner = RwLock::new(NodeBuilder::from_config(config)); Self { inner } @@ -1013,7 +1016,8 @@ impl ArcedNodeBuilder { } #[cfg(all(feature = "uniffi", feature = "chain-esplora"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Configures the [`Node`] instance to source its chain data from the given Esplora server. /// /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more @@ -1023,7 +1027,10 @@ impl ArcedNodeBuilder { ) { self.inner.write().expect("lock").set_chain_source_esplora(server_url, sync_config); } +} +#[cfg(all(feature = "uniffi", feature = "chain-esplora"))] +impl ArcedNodeBuilder { /// Configures the [`Node`] instance to source its chain data from the given Esplora server. /// /// The given `headers` will be included in all requests to the Esplora server, typically used for @@ -1044,7 +1051,8 @@ impl ArcedNodeBuilder { } #[cfg(all(feature = "uniffi", feature = "chain-electrum"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Configures the [`Node`] instance to source its chain data from the given Electrum server. /// /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more @@ -1057,7 +1065,8 @@ impl ArcedNodeBuilder { } #[cfg(all(feature = "uniffi", feature = "chain-bitcoind"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Configures the [`Node`] instance to connect to a Bitcoin Core node via RPC. /// /// This method establishes an RPC connection that enables all essential chain operations including @@ -1113,7 +1122,8 @@ impl ArcedNodeBuilder { } #[cfg(feature = "uniffi")] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Configures the [`Node`] instance to source its gossip data from the Lightning peer-to-peer /// network. pub fn set_gossip_source_p2p(&self) { @@ -1157,7 +1167,10 @@ impl ArcedNodeBuilder { trust_peer_0conf, ); } +} +#[cfg(feature = "uniffi")] +impl ArcedNodeBuilder { /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time /// channels to clients. /// @@ -1167,7 +1180,11 @@ impl ArcedNodeBuilder { pub fn enable_liquidity_provider(&self, lsps2_service_config: LSPS2ServiceConfig) { self.inner.write().expect("lock").enable_liquidity_provider(lsps2_service_config); } +} +#[cfg(feature = "uniffi")] +#[uniffi::export] +impl Builder { /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().expect("lock").set_storage_dir_path(storage_dir_path); @@ -1264,7 +1281,8 @@ impl ArcedNodeBuilder { } #[cfg(all(feature = "uniffi", feature = "storage-sqlite"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self, node_entropy: Arc) -> Result, BuildError> { @@ -1273,7 +1291,8 @@ impl ArcedNodeBuilder { } #[cfg(all(feature = "uniffi", feature = "storage-postgres"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options /// previously configured. /// @@ -1322,22 +1341,9 @@ impl ArcedNodeBuilder { } } -#[cfg(all(feature = "uniffi", not(feature = "storage-postgres")))] -impl ArcedNodeBuilder { - /// Builds a [`Node`] instance with a [PostgreSQL] backend and according to the options - /// previously configured. - /// - /// This requires the `storage-postgres` crate feature. - pub fn build_with_postgres_store( - &self, _node_entropy: Arc, _connection_string: String, - _db_name: Option, _kv_table_name: Option, _certificate_pem: Option, - ) -> Result, BuildError> { - Err(BuildError::KVStoreSetupFailed) - } -} - #[cfg(all(feature = "uniffi", feature = "storage-filesystem"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Builds a [`Node`] instance with a [`FilesystemStoreV2`] backend and according to the options /// previously configured. pub fn build_with_fs_store( @@ -1348,7 +1354,8 @@ impl ArcedNodeBuilder { } #[cfg(all(feature = "uniffi", feature = "storage-vss"))] -impl ArcedNodeBuilder { +#[uniffi::export] +impl Builder { /// Builds a [`Node`] instance with a [VSS] backend and according to the options /// previously configured. /// diff --git a/src/ffi/types.rs b/src/ffi/types.rs index a8d297d94..d213f9d51 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -155,8 +155,7 @@ impl VssClientHeaderProvider for VssHeaderProviderAdapter { } use crate::builder::sanitize_alias; -pub use crate::config::{default_config, ElectrumSyncConfig, EsploraSyncConfig, TorConfig}; -pub use crate::entropy::NodeEntropy; +pub use crate::config::default_config; use crate::error::Error; pub use crate::liquidity::LSPS1OrderStatus; pub use crate::logger::{LogLevel, LogRecord, LogWriter}; From 06d7e6b191f707f444eea15367037a94d789850a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 10:22:50 +0200 Subject: [PATCH 13/17] Use lean defaults for binding builds Build published bindings with uniffi-default and without native default features. Let local builds add backend features through LDK_NODE_EXTRA_FEATURES. Co-Authored-By: HAL 9000 --- scripts/uniffi_bindgen_generate_kotlin.sh | 11 ++++++++--- .../uniffi_bindgen_generate_kotlin_android.sh | 11 ++++++++--- scripts/uniffi_bindgen_generate_python.sh | 8 +++++++- scripts/uniffi_bindgen_generate_swift.sh | 17 +++++++++++------ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/scripts/uniffi_bindgen_generate_kotlin.sh b/scripts/uniffi_bindgen_generate_kotlin.sh index f82d5c0d0..aca77a6e6 100755 --- a/scripts/uniffi_bindgen_generate_kotlin.sh +++ b/scripts/uniffi_bindgen_generate_kotlin.sh @@ -4,6 +4,11 @@ TARGET_DIR="target/bindings/kotlin" PROJECT_DIR="ldk-node-jvm" PACKAGE_DIR="org/lightningdevkit/ldknode" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" +UNIFFI_FEATURES="uniffi-default" + +if [[ -n "${LDK_NODE_EXTRA_FEATURES:-}" ]]; then + UNIFFI_FEATURES+=",$LDK_NODE_EXTRA_FEATURES" +fi case " ${RUSTFLAGS:-} " in *" --cfg tokio_unstable "*|*" --cfg=tokio_unstable "*) ;; @@ -12,21 +17,21 @@ esac if [[ "$OSTYPE" == "linux-gnu"* ]]; then rustup target add x86_64-unknown-linux-gnu || exit 1 - cargo build --release --target x86_64-unknown-linux-gnu --features uniffi || exit 1 + cargo build --release --target x86_64-unknown-linux-gnu --no-default-features --features "$UNIFFI_FEATURES" || exit 1 DYNAMIC_LIB_PATH="target/x86_64-unknown-linux-gnu/release/libldk_node.so" RES_DIR="$BINDINGS_DIR/$PROJECT_DIR/lib/src/main/resources/linux-x86-64/" mkdir -p $RES_DIR || exit 1 cp $DYNAMIC_LIB_PATH $RES_DIR || exit 1 else rustup target add x86_64-apple-darwin || exit 1 - cargo build --release --target x86_64-apple-darwin --features uniffi || exit 1 + cargo build --release --target x86_64-apple-darwin --no-default-features --features "$UNIFFI_FEATURES" || exit 1 DYNAMIC_LIB_PATH="target/x86_64-apple-darwin/release/libldk_node.dylib" RES_DIR="$BINDINGS_DIR/$PROJECT_DIR/lib/src/main/resources/darwin-x86-64/" mkdir -p $RES_DIR || exit 1 cp $DYNAMIC_LIB_PATH $RES_DIR || exit 1 rustup target add aarch64-apple-darwin || exit 1 - cargo build --release --target aarch64-apple-darwin --features uniffi || exit 1 + cargo build --release --target aarch64-apple-darwin --no-default-features --features "$UNIFFI_FEATURES" || exit 1 DYNAMIC_LIB_PATH="target/aarch64-apple-darwin/release/libldk_node.dylib" RES_DIR="$BINDINGS_DIR/$PROJECT_DIR/lib/src/main/resources/darwin-aarch64/" mkdir -p $RES_DIR || exit 1 diff --git a/scripts/uniffi_bindgen_generate_kotlin_android.sh b/scripts/uniffi_bindgen_generate_kotlin_android.sh index d0eb8654d..55de04bb6 100755 --- a/scripts/uniffi_bindgen_generate_kotlin_android.sh +++ b/scripts/uniffi_bindgen_generate_kotlin_android.sh @@ -4,6 +4,11 @@ BINDINGS_DIR="bindings/kotlin" TARGET_DIR="target" PROJECT_DIR="ldk-node-android" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" +UNIFFI_FEATURES="uniffi-default" + +if [[ -n "${LDK_NODE_EXTRA_FEATURES:-}" ]]; then + UNIFFI_FEATURES+=",$LDK_NODE_EXTRA_FEATURES" +fi case " ${RUSTFLAGS:-} " in *" --cfg tokio_unstable "*|*" --cfg=tokio_unstable "*) RUSTFLAGS_WITH_TOKIO_UNSTABLE="${RUSTFLAGS:-}" ;; @@ -40,9 +45,9 @@ case "$OSTYPE" in PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$LLVM_ARCH_PATH/bin:$PATH" rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi -RUSTFLAGS="$RUSTFLAGS_WITH_TOKIO_UNSTABLE -C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target x86_64-linux-android || exit 1 -RUSTFLAGS="$RUSTFLAGS_WITH_TOKIO_UNSTABLE -C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --profile release-smaller --features uniffi --target armv7-linux-androideabi || exit 1 -RUSTFLAGS="$RUSTFLAGS_WITH_TOKIO_UNSTABLE -C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target aarch64-linux-android || exit 1 +RUSTFLAGS="$RUSTFLAGS_WITH_TOKIO_UNSTABLE -C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target x86_64-linux-android || exit 1 +RUSTFLAGS="$RUSTFLAGS_WITH_TOKIO_UNSTABLE -C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target armv7-linux-androideabi || exit 1 +RUSTFLAGS="$RUSTFLAGS_WITH_TOKIO_UNSTABLE -C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target aarch64-linux-android || exit 1 $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --lib-file "$TARGET_DIR"/x86_64-linux-android/release-smaller/libldk_node.so --language kotlin --config uniffi-android.toml -o "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/kotlin || exit 1 JNI_LIB_DIR="$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/jniLibs/ diff --git a/scripts/uniffi_bindgen_generate_python.sh b/scripts/uniffi_bindgen_generate_python.sh index 8792d2bc2..21f910fac 100755 --- a/scripts/uniffi_bindgen_generate_python.sh +++ b/scripts/uniffi_bindgen_generate_python.sh @@ -7,6 +7,11 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" BINDINGS_DIR="$REPO_ROOT/bindings/python/src/ldk_node" TARGET_DIR="${CARGO_TARGET_DIR:-$REPO_ROOT/target}" CARGO_BUILD_ARGS=() +UNIFFI_FEATURES="uniffi-default" + +if [[ -n "${LDK_NODE_EXTRA_FEATURES:-}" ]]; then + UNIFFI_FEATURES+=",$LDK_NODE_EXTRA_FEATURES" +fi case " ${RUSTFLAGS:-} " in *" --cfg tokio_unstable "*|*" --cfg=tokio_unstable "*) ;; @@ -41,7 +46,8 @@ esac cd "$REPO_ROOT" mkdir -p "$BINDINGS_DIR" -cargo build "${CARGO_BUILD_ARGS[@]}" --profile release-smaller --features uniffi +cargo build "${CARGO_BUILD_ARGS[@]}" --profile release-smaller --no-default-features \ + --features "$UNIFFI_FEATURES" cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml -- \ generate bindings/ldk_node.udl \ --lib-file "$DYNAMIC_LIB_PATH" \ diff --git a/scripts/uniffi_bindgen_generate_swift.sh b/scripts/uniffi_bindgen_generate_swift.sh index d69ac1fbe..f2100ae09 100755 --- a/scripts/uniffi_bindgen_generate_swift.sh +++ b/scripts/uniffi_bindgen_generate_swift.sh @@ -3,6 +3,11 @@ set -eox pipefail BINDINGS_DIR="./bindings/swift" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" +UNIFFI_FEATURES="uniffi-default" + +if [[ -n "${LDK_NODE_EXTRA_FEATURES:-}" ]]; then + UNIFFI_FEATURES+=",$LDK_NODE_EXTRA_FEATURES" +fi case " ${RUSTFLAGS:-} " in *" --cfg tokio_unstable "*|*" --cfg=tokio_unstable "*) ;; @@ -19,12 +24,12 @@ rustup target add aarch64-apple-ios-sim --toolchain stable rustup target add aarch64-apple-darwin x86_64-apple-darwin --toolchain stable # Build rust target libs -cargo build --profile release-smaller --features uniffi || exit 1 -cargo build --profile release-smaller --features uniffi --target x86_64-apple-darwin || exit 1 -cargo build --profile release-smaller --features uniffi --target aarch64-apple-darwin || exit 1 -cargo build --profile release-smaller --features uniffi --target x86_64-apple-ios || exit 1 -cargo build --profile release-smaller --features uniffi --target aarch64-apple-ios || exit 1 -cargo +stable build --release --features uniffi --target aarch64-apple-ios-sim || exit 1 +cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" || exit 1 +cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target x86_64-apple-darwin || exit 1 +cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target aarch64-apple-darwin || exit 1 +cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target x86_64-apple-ios || exit 1 +cargo build --profile release-smaller --no-default-features --features "$UNIFFI_FEATURES" --target aarch64-apple-ios || exit 1 +cargo +stable build --release --no-default-features --features "$UNIFFI_FEATURES" --target aarch64-apple-ios-sim || exit 1 # Combine ios-sim and apple-darwin (macos) libs for x86_64 and aarch64 (m1) mkdir -p target/lipo-ios-sim/release-smaller || exit 1 From df1d410312864271326879945c68fc9f87869ac0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 10:29:46 +0200 Subject: [PATCH 14/17] Check lean and complete feature sets in CI Build and test UniFFI with its lean preset so excluded backends stay excluded. Check all features and test targets to catch incompatible optional dependencies without running the suite twice. Co-Authored-By: HAL 9000 --- .github/workflows/hrn-integration.yml | 2 +- .github/workflows/postgres-integration.yml | 2 ++ .github/workflows/rust.yml | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/hrn-integration.yml b/.github/workflows/hrn-integration.yml index 640da6fb2..732aad029 100644 --- a/.github/workflows/hrn-integration.yml +++ b/.github/workflows/hrn-integration.yml @@ -43,4 +43,4 @@ jobs: - name: Run HRN Integration Tests run: | RUSTFLAGS="--cfg no_download --cfg hrn_tests $RUSTFLAGS" cargo test --test integration_tests_hrn - RUSTFLAGS="--cfg no_download --cfg hrn_tests $RUSTFLAGS" cargo test --test integration_tests_hrn --features uniffi + RUSTFLAGS="--cfg no_download --cfg hrn_tests $RUSTFLAGS" cargo test --test integration_tests_hrn --no-default-features --features uniffi-default diff --git a/.github/workflows/postgres-integration.yml b/.github/workflows/postgres-integration.yml index a1a6a10d9..5e070c599 100644 --- a/.github/workflows/postgres-integration.yml +++ b/.github/workflows/postgres-integration.yml @@ -32,6 +32,8 @@ jobs: - name: Install Rust stable toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable + - name: Check all features + run: cargo check --all-features --tests --verbose --color always - name: Enable caching for bitcoind id: cache-bitcoind uses: actions/cache@v4 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6871a3927..ea3df44b2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -76,12 +76,12 @@ jobs: run: cargo build --verbose --color always - name: Build with UniFFI support on Rust ${{ matrix.toolchain }} if: matrix.build-uniffi - run: cargo build --features uniffi --verbose --color always + run: cargo build --no-default-features --features uniffi-default --verbose --color always - name: Check release build on Rust ${{ matrix.toolchain }} run: cargo check --release --verbose --color always - name: Check release build with UniFFI support on Rust ${{ matrix.toolchain }} if: matrix.build-uniffi - run: cargo check --release --features uniffi --verbose --color always + run: cargo check --release --no-default-features --features uniffi-default --verbose --color always - name: Test on Rust ${{ matrix.toolchain }} if: "matrix.platform != 'windows-latest'" run: | @@ -89,7 +89,7 @@ jobs: - name: Test with UniFFI support on Rust ${{ matrix.toolchain }} if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" run: | - RUSTFLAGS="--cfg no_download --cfg cycle_tests" cargo test --features uniffi + RUSTFLAGS="--cfg no_download --cfg cycle_tests" cargo test --no-default-features --features uniffi-default linting: name: Linting @@ -106,7 +106,7 @@ jobs: - name: Ban `unwrap` in library code run: | cargo clippy --lib --verbose --color always -- -A warnings -D clippy::unwrap_used -A clippy::tabs_in_doc_comments - cargo clippy --lib --features uniffi --verbose --color always -- -A warnings -D clippy::unwrap_used -A clippy::tabs_in_doc_comments + cargo clippy --lib --no-default-features --features uniffi-default --verbose --color always -- -A warnings -D clippy::unwrap_used -A clippy::tabs_in_doc_comments doc: name: Documentation From 889bf6c9cf74d5352320dd8cf8cb1fc1288bb71f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 20 Aug 2026 12:54:22 +0200 Subject: [PATCH 15/17] f Check sparse feature sets Cover custom UniFFI builds, non-Esplora wallet tests, and PostgreSQL tests without SQLite in CI. Co-Authored-By: HAL 9000 --- .github/workflows/postgres-integration.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/postgres-integration.yml b/.github/workflows/postgres-integration.yml index 5e070c599..d27e3123e 100644 --- a/.github/workflows/postgres-integration.yml +++ b/.github/workflows/postgres-integration.yml @@ -34,6 +34,11 @@ jobs: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable - name: Check all features run: cargo check --all-features --tests --verbose --color always + - name: Check sparse feature sets + run: | + cargo check --no-default-features --features uniffi,chain-bitcoind,storage-postgres --verbose --color always + cargo test --lib --no-run --no-default-features --features chain-electrum,storage-postgres --verbose --color always + cargo check --test integration_tests_postgres --no-default-features --features chain-electrum,storage-postgres --verbose --color always - name: Enable caching for bitcoind id: cache-bitcoind uses: actions/cache@v4 From 94b28b2d80038e9c8470685ef3135ef5d099526a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 10:30:52 +0200 Subject: [PATCH 16/17] Document selectable crate features List each chain, storage, payment, and binding feature and explain the unchanged native defaults. Show both lean and custom binding builds so optional backend dependencies can be selected deliberately. Co-Authored-By: HAL 9000 --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cd2e3643..55c0f56ee 100644 --- a/README.md +++ b/README.md @@ -64,10 +64,60 @@ LDK Node currently comes with a decidedly opinionated set of design choices: - On-chain data is handled by the integrated [BDK][bdk] wallet. - Chain data may currently be sourced from the Bitcoin Core RPC interface, or from an [Electrum][electrum] or [Esplora][esplora] server. -- Wallet and channel state may be persisted to an [SQLite][sqlite] or [PostgreSQL][postgresql] database, to file system, or to a custom back-end to be implemented by the user. +- Wallet and channel state may be persisted to an [SQLite][sqlite] or [PostgreSQL][postgresql] database, to the filesystem, to a VSS server, or to a custom back-end to be implemented by the user. - Gossip data may be sourced via Lightning's peer-to-peer network or the [Rapid Gossip Sync](https://docs.rs/lightning-rapid-gossip-sync/*/lightning_rapid_gossip_sync/) protocol. - Entropy for the Lightning and on-chain wallets may be sourced from raw bytes or a [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic. In addition, LDK Node offers the means to generate and persist the entropy bytes to disk. +### Cargo Features + +LDK Node's optional dependencies are grouped by the functionality they provide: + +| Feature | Functionality | +| --- | --- | +| `chain-esplora` | Esplora chain source | +| `chain-electrum` | Electrum chain source | +| `chain-bitcoind` | Bitcoin Core RPC and REST chain source | +| `storage-sqlite` | SQLite storage | +| `storage-filesystem` | Filesystem storage | +| `storage-vss` | Versioned Storage Service storage | +| `storage-postgres` | PostgreSQL storage | +| `unified-payments` | BIP 21 and human-readable-name payment support | +| `uniffi` | UniFFI language bindings | +| `uniffi-default` | The standard language-binding feature set | + +The `default` feature set preserves the native Rust API's previous behavior. It enables all three +chain sources, SQLite, filesystem and VSS storage, and unified payments. PostgreSQL and UniFFI +remain opt-in. Every build must enable at least one chain source feature. + +Disable the default features to select only the functionality and dependencies an application +needs. For example: + +```shell +cargo build --no-default-features --features chain-esplora,storage-sqlite +``` + +`uniffi-default` enables UniFFI, Esplora, Electrum, SQLite, VSS, and unified payments. It excludes +Bitcoin Core, filesystem storage, and PostgreSQL. Binding users can add any of those features: + +```shell +cargo build --no-default-features --features uniffi-default,chain-bitcoind +``` + +Use `uniffi` directly instead of `uniffi-default` to assemble a fully custom binding build. For +example, a Bitcoin Core and PostgreSQL-only binding build uses: + +```shell +cargo build --no-default-features --features uniffi,chain-bitcoind,storage-postgres +``` + +The binding generation scripts use `uniffi-default`. Set `LDK_NODE_EXTRA_FEATURES` to add features +to their builds: + +```shell +LDK_NODE_EXTRA_FEATURES=chain-bitcoind,storage-postgres \ + ./scripts/uniffi_bindgen_generate_python.sh +``` + ## Compatibility LDK Node does not provide a stable public API until v1.0. Persisted node state is backwards compatible: newer releases are guaranteed to load state written by older releases. Downgrades are not supported, so state written by a newer release may not load with an older release. From 3185f51068918b649ccea8eef4d6cbe3c9c0f64f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 20 Aug 2026 12:54:59 +0200 Subject: [PATCH 17/17] Remove redundant path clones Avoid allocating paths before filesystem operations that only need borrowed paths. Co-Authored-By: HAL 9000 --- src/io/fs_store.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/io/fs_store.rs b/src/io/fs_store.rs index 855395d9d..f0e28f878 100644 --- a/src/io/fs_store.rs +++ b/src/io/fs_store.rs @@ -26,8 +26,7 @@ pub(crate) async fn open_or_migrate_fs_store( 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)?; + fs::create_dir_all(&storage_dir_path).map_err(|_| BuildError::StoragePathAccessFailed)?; } match FilesystemStoreV2::new(storage_dir_path.clone()) { @@ -37,7 +36,7 @@ pub(crate) async fn open_or_migrate_fs_store( let 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)?; + fs::create_dir_all(&v2_dir).map_err(|_| BuildError::StoragePathAccessFailed)?; let v2_store = FilesystemStoreV2::new(v2_dir.clone()) .map_err(|_| BuildError::KVStoreSetupFailed)?;