From 10a8ce4b37d9b86019a36a70172043b3c5aa9eb9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 18 Nov 2025 14:39:25 +0100 Subject: [PATCH 1/4] Move entropy-related types to new `entropy.rs` module As we're about to expose more entropy-related things, we here introduce a new module and start moving related types there. --- src/entropy.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ src/ffi/types.rs | 1 + src/io/utils.rs | 57 +----------------------------- src/lib.rs | 3 +- src/types.rs | 28 --------------- 5 files changed, 95 insertions(+), 86 deletions(-) create mode 100644 src/entropy.rs diff --git a/src/entropy.rs b/src/entropy.rs new file mode 100644 index 000000000..6c75d6da4 --- /dev/null +++ b/src/entropy.rs @@ -0,0 +1,92 @@ +// 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. + +//! Contains utilities for configuring and generating entropy. + +use bip39::Mnemonic; + +/// Generates a random [BIP 39] mnemonic with the specified word count. +/// +/// If no word count is specified, defaults to 24 words (256-bit entropy). +/// +/// The result may be used to initialize the [`Node`] entropy, i.e., can be given to +/// [`Builder::set_entropy_bip39_mnemonic`]. +/// +/// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki +/// [`Node`]: crate::Node +/// [`Builder::set_entropy_bip39_mnemonic`]: crate::Builder::set_entropy_bip39_mnemonic +pub fn generate_entropy_mnemonic(word_count: Option) -> Mnemonic { + let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); + Mnemonic::generate(word_count).expect("Failed to generate mnemonic") +} + +/// Supported BIP39 mnemonic word counts for entropy generation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WordCount { + /// 12-word mnemonic (128-bit entropy) + Words12, + /// 15-word mnemonic (160-bit entropy) + Words15, + /// 18-word mnemonic (192-bit entropy) + Words18, + /// 21-word mnemonic (224-bit entropy) + Words21, + /// 24-word mnemonic (256-bit entropy) + Words24, +} + +impl WordCount { + /// Returns the word count as a usize value. + pub fn word_count(&self) -> usize { + match self { + WordCount::Words12 => 12, + WordCount::Words15 => 15, + WordCount::Words18 => 18, + WordCount::Words21 => 21, + WordCount::Words24 => 24, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mnemonic_to_entropy_to_mnemonic() { + // Test default (24 words) + let mnemonic = generate_entropy_mnemonic(None); + let entropy = mnemonic.to_entropy(); + assert_eq!(mnemonic, Mnemonic::from_entropy(&entropy).unwrap()); + assert_eq!(mnemonic.word_count(), 24); + + // Test with different word counts + let word_counts = [ + WordCount::Words12, + WordCount::Words15, + WordCount::Words18, + WordCount::Words21, + WordCount::Words24, + ]; + + for word_count in word_counts { + let mnemonic = generate_entropy_mnemonic(Some(word_count)); + let entropy = mnemonic.to_entropy(); + assert_eq!(mnemonic, Mnemonic::from_entropy(&entropy).unwrap()); + + // Verify expected word count + let expected_words = match word_count { + WordCount::Words12 => 12, + WordCount::Words15 => 15, + WordCount::Words18 => 18, + WordCount::Words21 => 21, + WordCount::Words24 => 24, + }; + assert_eq!(mnemonic.word_count(), expected_words); + } + } +} diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 3c88a665f..80be1fe79 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -47,6 +47,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; +pub use crate::entropy::{generate_entropy_mnemonic, WordCount}; use crate::error::Error; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; diff --git a/src/io/utils.rs b/src/io/utils.rs index 1b4b02a82..389767397 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -17,7 +17,6 @@ use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; use bdk_chain::tx_graph::ChangeSet as BdkTxGraphChangeSet; use bdk_chain::ConfirmationBlockTime; use bdk_wallet::ChangeSet as BdkWalletChangeSet; -use bip39::Mnemonic; use bitcoin::Network; use lightning::io::Cursor; use lightning::ln::msgs::DecodeError; @@ -47,27 +46,12 @@ use crate::io::{ }; use crate::logger::{log_error, LdkLogger, Logger}; use crate::peer_store::PeerStore; -use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper, WordCount}; +use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_scores_cache"; -/// Generates a random [BIP 39] mnemonic with the specified word count. -/// -/// If no word count is specified, defaults to 24 words (256-bit entropy). -/// -/// The result may be used to initialize the [`Node`] entropy, i.e., can be given to -/// [`Builder::set_entropy_bip39_mnemonic`]. -/// -/// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki -/// [`Node`]: crate::Node -/// [`Builder::set_entropy_bip39_mnemonic`]: crate::Builder::set_entropy_bip39_mnemonic -pub fn generate_entropy_mnemonic(word_count: Option) -> Mnemonic { - let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); - Mnemonic::generate(word_count).expect("Failed to generate mnemonic") -} - pub(crate) fn read_or_generate_seed_file( keys_seed_path: &str, logger: L, ) -> std::io::Result<[u8; WALLET_KEYS_SEED_LEN]> @@ -620,42 +604,3 @@ pub(crate) fn read_bdk_wallet_change_set( .map(|indexer| change_set.indexer = indexer); Ok(Some(change_set)) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn mnemonic_to_entropy_to_mnemonic() { - // Test default (24 words) - let mnemonic = generate_entropy_mnemonic(None); - let entropy = mnemonic.to_entropy(); - assert_eq!(mnemonic, Mnemonic::from_entropy(&entropy).unwrap()); - assert_eq!(mnemonic.word_count(), 24); - - // Test with different word counts - let word_counts = [ - WordCount::Words12, - WordCount::Words15, - WordCount::Words18, - WordCount::Words21, - WordCount::Words24, - ]; - - for word_count in word_counts { - let mnemonic = generate_entropy_mnemonic(Some(word_count)); - let entropy = mnemonic.to_entropy(); - assert_eq!(mnemonic, Mnemonic::from_entropy(&entropy).unwrap()); - - // Verify expected word count - let expected_words = match word_count { - WordCount::Words12 => 12, - WordCount::Words15 => 15, - WordCount::Words18 => 18, - WordCount::Words21 => 21, - WordCount::Words24 => 24, - }; - assert_eq!(mnemonic.word_count(), expected_words); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index c0b02ae2f..ccda53af9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ mod chain; pub mod config; mod connection; mod data_store; +pub mod entropy; mod error; mod event; mod fee_estimator; @@ -130,7 +131,6 @@ use fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; use ffi::*; use gossip::GossipSource; use graph::NetworkGraph; -pub use io::utils::generate_entropy_mnemonic; use io::utils::write_node_metrics; use lightning::chain::BestBlock; use lightning::events::bump_transaction::{Input, Wallet as LdkWallet}; @@ -160,7 +160,6 @@ use types::{ }; pub use types::{ ChannelDetails, CustomTlvRecord, DynStore, PeerDetails, SyncAndAsyncKVStore, UserChannelId, - WordCount, }; pub use { bip39, bitcoin, lightning, lightning_invoice, lightning_liquidity, lightning_types, tokio, diff --git a/src/types.rs b/src/types.rs index 6d6bdcd20..b8dc10b18 100644 --- a/src/types.rs +++ b/src/types.rs @@ -36,34 +36,6 @@ use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::PaymentDetails; -/// Supported BIP39 mnemonic word counts for entropy generation. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WordCount { - /// 12-word mnemonic (128-bit entropy) - Words12, - /// 15-word mnemonic (160-bit entropy) - Words15, - /// 18-word mnemonic (192-bit entropy) - Words18, - /// 21-word mnemonic (224-bit entropy) - Words21, - /// 24-word mnemonic (256-bit entropy) - Words24, -} - -impl WordCount { - /// Returns the word count as a usize value. - pub fn word_count(&self) -> usize { - match self { - WordCount::Words12 => 12, - WordCount::Words15 => 15, - WordCount::Words18 => 18, - WordCount::Words21 => 21, - WordCount::Words24 => 24, - } - } -} - /// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the /// same time. pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {} From 90400975d003a8eae0ee5e848d7982de654557ca Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 18 Nov 2025 15:42:16 +0100 Subject: [PATCH 2/4] Use `build_with_store` in `build_with_vss_store` Now that we don't use the `Runtime` in `VssStore` anymore, we can in fact revert to reuse the public interface. --- src/builder.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 63e84db37..5ab2c8b9b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -705,15 +705,6 @@ impl NodeBuilder { ) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; - let runtime = if let Some(handle) = self.runtime_handle.as_ref() { - Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(&logger))) - } else { - Arc::new(Runtime::new(Arc::clone(&logger)).map_err(|e| { - log_error!(logger, "Failed to setup tokio runtime: {}", e); - BuildError::RuntimeSetupFailed - })?) - }; - let seed_bytes = seed_bytes_from_config( &self.config, self.entropy_source_config.as_ref(), @@ -737,18 +728,7 @@ impl NodeBuilder { BuildError::KVStoreSetupFailed })?; - build_with_store_internal( - config, - self.chain_data_source_config.as_ref(), - self.gossip_source_config.as_ref(), - self.liquidity_source_config.as_ref(), - self.pathfinding_scores_sync_config.as_ref(), - self.async_payments_role, - seed_bytes, - runtime, - logger, - Arc::new(vss_store), - ) + self.build_with_store(Arc::new(vss_store)) } /// Builds a [`Node`] instance according to the options previously configured. From c06b118255fc14678f046358497484a3e569a9b0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 18 Nov 2025 16:41:10 +0100 Subject: [PATCH 3/4] Introduce new mandatory `NodeEntropy` object Previously, the `Builder` allowed setting different entropy sources via its `set_entropy...` methods, defaulting to sourcing from an auto-generated seed file in the storage path. While this allowed for really easy setup, it spared the user to actually think about where to store their node secret. Here, we therefore introduce a mandatory `NodeEntropy` object that, as before, allows the user to source entropy from BIP39 Mnemonic, seed bytes, or a seed file. However, it doesn't implement any default and hence intentionally requires manually setup by the user. Moreover, this API refactor also allows to reuse the same object outside of the `Node`'s `Builder` in a future commit. --- README.md | 16 +- .../lightningdevkit/ldknode/AndroidLibTest.kt | 9 +- .../lightningdevkit/ldknode/LibraryTest.kt | 9 +- bindings/ldk_node.udl | 30 ++- bindings/python/src/ldk_node/test_ldk_node.py | 4 +- src/builder.rs | 207 ++++++------------ src/entropy.rs | 105 ++++++++- src/ffi/types.rs | 2 +- src/io/utils.rs | 47 +--- src/lib.rs | 17 +- tests/common/mod.rs | 45 ++-- tests/integration_tests_cln.rs | 2 +- tests/integration_tests_lnd.rs | 2 +- tests/integration_tests_rust.rs | 55 +++-- tests/integration_tests_vss.rs | 23 +- tests/reorg_test.rs | 2 +- 16 files changed, 306 insertions(+), 269 deletions(-) diff --git a/README.md b/README.md index d11c5fc8e..4e60d3602 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,26 @@ LDK Node is a self-custodial Lightning node in library form. Its central goal is The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `open_channel`, `send`, etc. ```rust -use ldk_node::Builder; -use ldk_node::lightning_invoice::Bolt11Invoice; -use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Network; +use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_node::lightning_invoice::Bolt11Invoice; +use ldk_node::Builder; use std::str::FromStr; fn main() { let mut builder = Builder::new(); builder.set_network(Network::Testnet); builder.set_chain_source_esplora("https://blockstream.info/testnet/api".to_string(), None); - builder.set_gossip_source_rgs("https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string()); + builder.set_gossip_source_rgs( + "https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string(), + ); + - let node = builder.build().unwrap(); + let mnemonic = generate_entropy_mnemonic(None); + let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); + let node = builder.build(node_entropy).unwrap(); node.start().unwrap(); diff --git a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt index fb29d3219..dd550f71a 100644 --- a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt +++ b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt @@ -34,8 +34,13 @@ class AndroidLibTest { val builder1 = Builder.fromConfig(config1) val builder2 = Builder.fromConfig(config2) - val node1 = builder1.build() - val node2 = builder2.build() + val mnemonic1 = generateEntropyMnemonic(null) + val nodeEntropy1 = NodeEntropy.fromBip39Mnemonic(mnemonic1, null) + val node1 = builder1.build(nodeEntropy1) + + val mnemonic2 = generateEntropyMnemonic(null) + val nodeEntropy2 = NodeEntropy.fromBip39Mnemonic(mnemonic2, null) + val node2 = builder2.build(nodeEntropy2) node1.start() node2.start() diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt index c8c43c49c..006878a4c 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt @@ -193,8 +193,13 @@ class LibraryTest { builder2.setChainSourceEsplora(esploraEndpoint, null) builder2.setCustomLogger(logWriter2) - val node1 = builder1.build() - val node2 = builder2.build() + val mnemonic1 = generateEntropyMnemonic(null) + val nodeEntropy1 = NodeEntropy.fromBip39Mnemonic(mnemonic1, null) + val node1 = builder1.build(nodeEntropy1) + + val mnemonic2 = generateEntropyMnemonic(null) + val nodeEntropy2 = NodeEntropy.fromBip39Mnemonic(mnemonic2, null) + val node2 = builder2.build(nodeEntropy2) node1.start() node2.start() diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ff2469c7e..c4ebf56a6 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -47,6 +47,20 @@ dictionary LSPS2ServiceConfig { boolean client_trusts_lsp; }; +interface NodeEntropy { + [Name=from_bip39_mnemonic] + constructor(Mnemonic mnemonic, string? passphrase); + [Throws=EntropyError, Name=from_seed_bytes] + constructor(bytes seed_bytes); + [Throws=EntropyError, Name=from_seed_path] + constructor(string seed_path); +}; + +enum EntropyError { + "InvalidSeedBytes", + "InvalidSeedFile", +}; + enum WordCount { "Words12", "Words15", @@ -80,10 +94,6 @@ interface Builder { constructor(); [Name=from_config] constructor(Config config); - void set_entropy_seed_path(string seed_path); - [Throws=BuildError] - void set_entropy_seed_bytes(sequence seed_bytes); - void set_entropy_bip39_mnemonic(Mnemonic mnemonic, string? passphrase); 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); @@ -107,15 +117,15 @@ interface Builder { [Throws=BuildError] void set_async_payments_role(AsyncPaymentsRole? role); [Throws=BuildError] - Node build(); + Node build(NodeEntropy node_entropy); [Throws=BuildError] - Node build_with_fs_store(); + Node build_with_fs_store(NodeEntropy node_entropy); [Throws=BuildError] - Node build_with_vss_store(string vss_url, string store_id, string lnurl_auth_server_url, record fixed_headers); + Node build_with_vss_store(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(string vss_url, string store_id, record fixed_headers); + 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(string vss_url, string store_id, VssHeaderProvider header_provider); + Node build_with_vss_store_and_header_provider(NodeEntropy node_entropy, string vss_url, string store_id, VssHeaderProvider header_provider); }; interface Node { @@ -357,8 +367,6 @@ dictionary BestBlock { [Error] enum BuildError { - "InvalidSeedBytes", - "InvalidSeedFile", "InvalidSystemTime", "InvalidChannelMonitor", "InvalidListeningAddresses", diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index f71e89df8..0b73e6a47 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -97,13 +97,15 @@ def send_to_address(address, amount_sats): def setup_node(tmp_dir, esplora_endpoint, listening_addresses): + mnemonic = generate_entropy_mnemonic(None) + node_entropy = NodeEntropy.from_bip39_mnemonic(mnemonic, None) config = default_config() builder = Builder.from_config(config) builder.set_storage_dir_path(tmp_dir) builder.set_chain_source_esplora(esplora_endpoint, None) builder.set_network(DEFAULT_TEST_NETWORK) builder.set_listening_addresses(listening_addresses) - return builder.build() + return builder.build(node_entropy) def get_esplora_endpoint(): if os.environ.get('ESPLORA_ENDPOINT'): diff --git a/src/builder.rs b/src/builder.rs index 5ab2c8b9b..13a7567b7 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -15,7 +15,6 @@ use std::{fmt, fs}; use bdk_wallet::template::Bip84; use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; -use bip39::Mnemonic; use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Network}; @@ -45,9 +44,10 @@ use crate::chain::ChainSource; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, - DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, + DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, }; use crate::connection::ConnectionManager; +use crate::entropy::NodeEntropy; use crate::event::EventQueue; use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::GossipSource; @@ -101,13 +101,6 @@ enum ChainDataSourceConfig { }, } -#[derive(Debug, Clone)] -enum EntropySourceConfig { - SeedFile(String), - SeedBytes([u8; WALLET_KEYS_SEED_LEN]), - Bip39Mnemonic { mnemonic: Mnemonic, passphrase: Option }, -} - #[derive(Debug, Clone)] enum GossipSourceConfig { P2PNetwork, @@ -157,10 +150,6 @@ impl std::fmt::Debug for LogWriterConfig { /// [`Node`]: crate::Node #[derive(Debug, Clone, PartialEq)] pub enum BuildError { - /// The given seed bytes are invalid, e.g., have invalid length. - InvalidSeedBytes, - /// The given seed file is invalid, e.g., has invalid length, or could not be read. - InvalidSeedFile, /// The current system time is invalid, clocks might have gone backwards. InvalidSystemTime, /// The a read channel monitor is invalid. @@ -200,8 +189,6 @@ pub enum BuildError { impl fmt::Display for BuildError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Self::InvalidSeedBytes => write!(f, "Given seed bytes are invalid."), - Self::InvalidSeedFile => write!(f, "Given seed file is invalid or could not be read."), Self::InvalidSystemTime => { write!(f, "System time is invalid. Clocks might have gone back in time.") }, @@ -245,7 +232,6 @@ impl std::error::Error for BuildError {} #[derive(Debug)] pub struct NodeBuilder { config: Config, - entropy_source_config: Option, chain_data_source_config: Option, gossip_source_config: Option, liquidity_source_config: Option, @@ -264,7 +250,6 @@ impl NodeBuilder { /// Creates a new builder instance from an [`Config`]. pub fn from_config(config: Config) -> Self { - let entropy_source_config = None; let chain_data_source_config = None; let gossip_source_config = None; let liquidity_source_config = None; @@ -273,7 +258,6 @@ impl NodeBuilder { let pathfinding_scores_sync_config = None; Self { config, - entropy_source_config, chain_data_source_config, gossip_source_config, liquidity_source_config, @@ -294,33 +278,6 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its wallet entropy from a seed file on disk. - /// - /// If the given file does not exist a new random seed file will be generated and - /// stored at the given location. - pub fn set_entropy_seed_path(&mut self, seed_path: String) -> &mut Self { - self.entropy_source_config = Some(EntropySourceConfig::SeedFile(seed_path)); - self - } - - /// Configures the [`Node`] instance to source its wallet entropy from the given - /// [`WALLET_KEYS_SEED_LEN`] seed bytes. - pub fn set_entropy_seed_bytes(&mut self, seed_bytes: [u8; WALLET_KEYS_SEED_LEN]) -> &mut Self { - self.entropy_source_config = Some(EntropySourceConfig::SeedBytes(seed_bytes)); - self - } - - /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. - /// - /// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki - pub fn set_entropy_bip39_mnemonic( - &mut self, mnemonic: Mnemonic, passphrase: Option, - ) -> &mut Self { - self.entropy_source_config = - Some(EntropySourceConfig::Bip39Mnemonic { mnemonic, passphrase }); - self - } - /// 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 @@ -584,7 +541,7 @@ impl NodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. - pub fn build(&self) -> Result { + pub fn build(&self, node_entropy: NodeEntropy) -> Result { let storage_dir_path = self.config.storage_dir_path.clone(); fs::create_dir_all(storage_dir_path.clone()) .map_err(|_| BuildError::StoragePathAccessFailed)?; @@ -596,19 +553,19 @@ impl NodeBuilder { ) .map_err(|_| BuildError::KVStoreSetupFailed)?, ); - self.build_with_store(kv_store) + self.build_with_store(node_entropy, kv_store) } /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options /// previously configured. - pub fn build_with_fs_store(&self) -> Result { + pub fn build_with_fs_store(&self, node_entropy: NodeEntropy) -> Result { let mut storage_dir_path: PathBuf = self.config.storage_dir_path.clone().into(); storage_dir_path.push("fs_store"); fs::create_dir_all(storage_dir_path.clone()) .map_err(|_| BuildError::StoragePathAccessFailed)?; let kv_store = Arc::new(FilesystemStore::new(storage_dir_path)); - self.build_with_store(kv_store) + self.build_with_store(node_entropy, kv_store) } /// Builds a [`Node`] instance with a [VSS] backend and according to the options @@ -629,19 +586,14 @@ impl NodeBuilder { /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md pub fn build_with_vss_store( - &self, vss_url: String, store_id: String, lnurl_auth_server_url: String, - fixed_headers: HashMap, + &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, + lnurl_auth_server_url: String, fixed_headers: HashMap, ) -> Result { use bitcoin::key::Secp256k1; let logger = setup_logger(&self.log_writer_config, &self.config)?; - let seed_bytes = seed_bytes_from_config( - &self.config, - self.entropy_source_config.as_ref(), - Arc::clone(&logger), - )?; - + let seed_bytes = node_entropy.to_seed_bytes(); let config = Arc::new(self.config.clone()); let vss_xprv = @@ -666,7 +618,12 @@ impl NodeBuilder { let header_provider = Arc::new(lnurl_auth_jwt_provider); - self.build_with_vss_store_and_header_provider(vss_url, store_id, header_provider) + self.build_with_vss_store_and_header_provider( + node_entropy, + vss_url, + store_id, + header_provider, + ) } /// Builds a [`Node`] instance with a [VSS] backend and according to the options @@ -682,11 +639,17 @@ impl NodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md pub fn build_with_vss_store_and_fixed_headers( - &self, vss_url: String, store_id: String, fixed_headers: HashMap, + &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, + fixed_headers: HashMap, ) -> Result { let header_provider = Arc::new(FixedHeaders::new(fixed_headers)); - self.build_with_vss_store_and_header_provider(vss_url, store_id, header_provider) + self.build_with_vss_store_and_header_provider( + node_entropy, + vss_url, + store_id, + header_provider, + ) } /// Builds a [`Node`] instance with a [VSS] backend and according to the options @@ -701,16 +664,12 @@ impl NodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md pub fn build_with_vss_store_and_header_provider( - &self, vss_url: String, store_id: String, header_provider: Arc, + &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, + header_provider: Arc, ) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; - let seed_bytes = seed_bytes_from_config( - &self.config, - self.entropy_source_config.as_ref(), - Arc::clone(&logger), - )?; - + let seed_bytes = node_entropy.to_seed_bytes(); let config = Arc::new(self.config.clone()); let vss_xprv = derive_xprv( @@ -728,11 +687,13 @@ impl NodeBuilder { BuildError::KVStoreSetupFailed })?; - self.build_with_store(Arc::new(vss_store)) + self.build_with_store(node_entropy, Arc::new(vss_store)) } /// Builds a [`Node`] instance according to the options previously configured. - pub fn build_with_store(&self, kv_store: Arc) -> Result { + pub fn build_with_store( + &self, node_entropy: NodeEntropy, kv_store: Arc, + ) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; let runtime = if let Some(handle) = self.runtime_handle.as_ref() { @@ -744,11 +705,7 @@ impl NodeBuilder { })?) }; - let seed_bytes = seed_bytes_from_config( - &self.config, - self.entropy_source_config.as_ref(), - Arc::clone(&logger), - )?; + let seed_bytes = node_entropy.to_seed_bytes(); let config = Arc::new(self.config.clone()); build_with_store_internal( @@ -793,37 +750,6 @@ impl ArcedNodeBuilder { Self { inner } } - /// Configures the [`Node`] instance to source its wallet entropy from a seed file on disk. - /// - /// If the given file does not exist a new random seed file will be generated and - /// stored at the given location. - pub fn set_entropy_seed_path(&self, seed_path: String) { - self.inner.write().unwrap().set_entropy_seed_path(seed_path); - } - - /// Configures the [`Node`] instance to source its wallet entropy from the given - /// [`WALLET_KEYS_SEED_LEN`] seed bytes. - /// - /// **Note:** Will return an error if the length of the given `seed_bytes` differs from - /// [`WALLET_KEYS_SEED_LEN`]. - pub fn set_entropy_seed_bytes(&self, seed_bytes: Vec) -> Result<(), BuildError> { - if seed_bytes.len() != WALLET_KEYS_SEED_LEN { - return Err(BuildError::InvalidSeedBytes); - } - let mut bytes = [0u8; WALLET_KEYS_SEED_LEN]; - bytes.copy_from_slice(&seed_bytes); - - self.inner.write().unwrap().set_entropy_seed_bytes(bytes); - Ok(()) - } - - /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. - /// - /// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki - pub fn set_entropy_bip39_mnemonic(&self, mnemonic: Mnemonic, passphrase: Option) { - self.inner.write().unwrap().set_entropy_bip39_mnemonic(mnemonic, passphrase); - } - /// 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 @@ -1031,14 +957,16 @@ impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. - pub fn build(&self) -> Result, BuildError> { - self.inner.read().unwrap().build().map(Arc::new) + pub fn build(&self, node_entropy: Arc) -> Result, BuildError> { + self.inner.read().unwrap().build(*node_entropy).map(Arc::new) } /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options /// previously configured. - pub fn build_with_fs_store(&self) -> Result, BuildError> { - self.inner.read().unwrap().build_with_fs_store().map(Arc::new) + pub fn build_with_fs_store( + &self, node_entropy: Arc, + ) -> Result, BuildError> { + self.inner.read().unwrap().build_with_fs_store(*node_entropy).map(Arc::new) } /// Builds a [`Node`] instance with a [VSS] backend and according to the options @@ -1059,13 +987,19 @@ impl ArcedNodeBuilder { /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md pub fn build_with_vss_store( - &self, vss_url: String, store_id: String, lnurl_auth_server_url: String, - fixed_headers: HashMap, + &self, node_entropy: Arc, vss_url: String, store_id: String, + lnurl_auth_server_url: String, fixed_headers: HashMap, ) -> Result, BuildError> { self.inner .read() .unwrap() - .build_with_vss_store(vss_url, store_id, lnurl_auth_server_url, fixed_headers) + .build_with_vss_store( + *node_entropy, + vss_url, + store_id, + lnurl_auth_server_url, + fixed_headers, + ) .map(Arc::new) } @@ -1082,12 +1016,13 @@ impl ArcedNodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md pub fn build_with_vss_store_and_fixed_headers( - &self, vss_url: String, store_id: String, fixed_headers: HashMap, + &self, node_entropy: Arc, vss_url: String, store_id: String, + fixed_headers: HashMap, ) -> Result, BuildError> { self.inner .read() .unwrap() - .build_with_vss_store_and_fixed_headers(vss_url, store_id, fixed_headers) + .build_with_vss_store_and_fixed_headers(*node_entropy, vss_url, store_id, fixed_headers) .map(Arc::new) } @@ -1103,18 +1038,26 @@ impl ArcedNodeBuilder { /// /// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md pub fn build_with_vss_store_and_header_provider( - &self, vss_url: String, store_id: String, header_provider: Arc, + &self, node_entropy: Arc, vss_url: String, store_id: String, + header_provider: Arc, ) -> Result, BuildError> { self.inner .read() .unwrap() - .build_with_vss_store_and_header_provider(vss_url, store_id, header_provider) + .build_with_vss_store_and_header_provider( + *node_entropy, + vss_url, + store_id, + header_provider, + ) .map(Arc::new) } /// Builds a [`Node`] instance according to the options previously configured. - pub fn build_with_store(&self, kv_store: Arc) -> Result, BuildError> { - self.inner.read().unwrap().build_with_store(kv_store).map(Arc::new) + pub fn build_with_store( + &self, node_entropy: Arc, kv_store: Arc, + ) -> Result, BuildError> { + self.inner.read().unwrap().build_with_store(*node_entropy, kv_store).map(Arc::new) } } @@ -1265,7 +1208,7 @@ fn build_with_store_internal( // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::Xpriv::new_master(config.network, &seed_bytes).map_err(|e| { log_error!(logger, "Failed to derive master secret: {}", e); - BuildError::InvalidSeedBytes + BuildError::WalletSetupFailed })?; let descriptor = Bip84(xprv, KeychainKind::External); @@ -1851,28 +1794,6 @@ fn setup_logger( Ok(Arc::new(logger)) } -fn seed_bytes_from_config( - config: &Config, entropy_source_config: Option<&EntropySourceConfig>, logger: Arc, -) -> Result<[u8; 64], BuildError> { - match entropy_source_config { - Some(EntropySourceConfig::SeedBytes(bytes)) => Ok(bytes.clone()), - Some(EntropySourceConfig::SeedFile(seed_path)) => { - Ok(io::utils::read_or_generate_seed_file(seed_path, Arc::clone(&logger)) - .map_err(|_| BuildError::InvalidSeedFile)?) - }, - Some(EntropySourceConfig::Bip39Mnemonic { mnemonic, passphrase }) => match passphrase { - Some(passphrase) => Ok(mnemonic.to_seed(passphrase)), - None => Ok(mnemonic.to_seed("")), - }, - None => { - // Default to read or generate from the default location generate a seed file. - let seed_path = format!("{}/keys_seed", config.storage_dir_path); - Ok(io::utils::read_or_generate_seed_file(&seed_path, Arc::clone(&logger)) - .map_err(|_| BuildError::InvalidSeedFile)?) - }, - } -} - fn derive_xprv( config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc, ) -> Result { @@ -1880,13 +1801,13 @@ fn derive_xprv( let xprv = Xpriv::new_master(config.network, seed_bytes).map_err(|e| { log_error!(logger, "Failed to derive master secret: {}", e); - BuildError::InvalidSeedBytes + BuildError::WalletSetupFailed })?; xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }]) .map_err(|e| { log_error!(logger, "Failed to derive hardened child secret: {}", e); - BuildError::InvalidSeedBytes + BuildError::WalletSetupFailed }) } diff --git a/src/entropy.rs b/src/entropy.rs index 6c75d6da4..8bd338622 100644 --- a/src/entropy.rs +++ b/src/entropy.rs @@ -7,18 +7,117 @@ //! Contains utilities for configuring and generating entropy. +use std::fmt; + use bip39::Mnemonic; +use crate::config::WALLET_KEYS_SEED_LEN; +use crate::io; + +/// An error that could arise during [`NodeEntropy`] construction. +#[derive(Debug, Clone, PartialEq)] +pub enum EntropyError { + /// The given seed bytes are invalid, e.g., have invalid length. + InvalidSeedBytes, + /// The given seed file is invalid, e.g., has invalid length, or could not be read. + InvalidSeedFile, +} + +impl fmt::Display for EntropyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::InvalidSeedBytes => write!(f, "Given seed bytes are invalid."), + Self::InvalidSeedFile => write!(f, "Given seed file is invalid or could not be read."), + } + } +} + +impl std::error::Error for EntropyError {} + +/// The node entropy, i.e., the main secret from which all other secrets of the [`Node`] are +/// derived. +/// +/// [`Node`]: crate::Node +#[derive(Copy, Clone)] +pub struct NodeEntropy([u8; WALLET_KEYS_SEED_LEN]); + +impl NodeEntropy { + /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. + /// + /// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki + /// [`Node`]: crate::Node + pub fn from_bip39_mnemonic(mnemonic: Mnemonic, passphrase: Option) -> Self { + match passphrase { + Some(passphrase) => Self(mnemonic.to_seed(passphrase)), + None => Self(mnemonic.to_seed("")), + } + } + + /// Configures the [`Node`] instance to source its wallet entropy from the given + /// [`WALLET_KEYS_SEED_LEN`] seed bytes. + /// + /// [`Node`]: crate::Node + #[cfg(not(feature = "uniffi"))] + pub fn from_seed_bytes(seed_bytes: [u8; WALLET_KEYS_SEED_LEN]) -> Self { + Self(seed_bytes) + } + + /// Configures the [`Node`] instance to source its wallet entropy from the given + /// [`WALLET_KEYS_SEED_LEN`] seed bytes. + /// + /// Will return an error if the length of the given `Vec` is not exactly + /// [`WALLET_KEYS_SEED_LEN`]. + /// + /// [`Node`]: crate::Node + #[cfg(feature = "uniffi")] + pub fn from_seed_bytes(seed_bytes: Vec) -> Result { + if seed_bytes.len() != WALLET_KEYS_SEED_LEN { + return Err(EntropyError::InvalidSeedBytes); + } + let mut seed_bytes_inner = [0u8; WALLET_KEYS_SEED_LEN]; + seed_bytes_inner.copy_from_slice(&seed_bytes); + Ok(Self(seed_bytes_inner)) + } + + /// Configures the [`Node`] instance to source its wallet entropy from a seed file on disk. + /// + /// If the given file does not exist a new random seed file will be generated and + /// stored at the given location. + /// + /// [`Node`]: crate::Node + pub fn from_seed_path(seed_path: String) -> Result { + Ok(Self( + io::utils::read_or_generate_seed_file(&seed_path) + .map_err(|_| EntropyError::InvalidSeedFile)?, + )) + } + + pub(crate) fn to_seed_bytes(&self) -> [u8; WALLET_KEYS_SEED_LEN] { + self.0 + } +} + +impl fmt::Display for NodeEntropy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "NODE ENTROPY") + } +} + +impl fmt::Debug for NodeEntropy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "NODE ENTROPY") + } +} + /// Generates a random [BIP 39] mnemonic with the specified word count. /// /// If no word count is specified, defaults to 24 words (256-bit entropy). /// -/// The result may be used to initialize the [`Node`] entropy, i.e., can be given to -/// [`Builder::set_entropy_bip39_mnemonic`]. +/// The result may be used to initialize the [`NodeEntropy`], i.e., can be given to +/// [`NodeEntropy::from_bip39_mnemonic`]. /// /// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki /// [`Node`]: crate::Node -/// [`Builder::set_entropy_bip39_mnemonic`]: crate::Builder::set_entropy_bip39_mnemonic pub fn generate_entropy_mnemonic(word_count: Option) -> Mnemonic { let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); Mnemonic::generate(word_count).expect("Failed to generate mnemonic") diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 80be1fe79..c69987c96 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -47,7 +47,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; -pub use crate::entropy::{generate_entropy_mnemonic, WordCount}; +pub use crate::entropy::{generate_entropy_mnemonic, EntropyError, NodeEntropy, WordCount}; use crate::error::Error; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; diff --git a/src/io/utils.rs b/src/io/utils.rs index 389767397..4acc7dd41 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -52,24 +52,13 @@ use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_scores_cache"; -pub(crate) fn read_or_generate_seed_file( - keys_seed_path: &str, logger: L, -) -> std::io::Result<[u8; WALLET_KEYS_SEED_LEN]> -where - L::Target: LdkLogger, -{ +pub(crate) fn read_or_generate_seed_file( + keys_seed_path: &str, +) -> std::io::Result<[u8; WALLET_KEYS_SEED_LEN]> { if Path::new(&keys_seed_path).exists() { - let seed = fs::read(keys_seed_path).map_err(|e| { - log_error!(logger, "Failed to read keys seed file: {}", keys_seed_path); - e - })?; + let seed = fs::read(keys_seed_path)?; if seed.len() != WALLET_KEYS_SEED_LEN { - log_error!( - logger, - "Failed to read keys seed file due to invalid length: {}", - keys_seed_path - ); return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "Failed to read keys seed file due to invalid length", @@ -81,37 +70,19 @@ where Ok(key) } else { let mut key = [0; WALLET_KEYS_SEED_LEN]; - OsRng.try_fill_bytes(&mut key).map_err(|e| { - log_error!(logger, "Failed to generate entropy: {}", e); + OsRng.try_fill_bytes(&mut key).map_err(|_| { std::io::Error::new(std::io::ErrorKind::Other, "Failed to generate seed bytes") })?; if let Some(parent_dir) = Path::new(&keys_seed_path).parent() { - fs::create_dir_all(parent_dir).map_err(|e| { - log_error!( - logger, - "Failed to create parent directory for key seed file: {}.", - keys_seed_path - ); - e - })?; + fs::create_dir_all(parent_dir)?; } - let mut f = fs::File::create(keys_seed_path).map_err(|e| { - log_error!(logger, "Failed to create keys seed file: {}", keys_seed_path); - e - })?; - - f.write_all(&key).map_err(|e| { - log_error!(logger, "Failed to write node keys seed to disk: {}", keys_seed_path); - e - })?; + let mut f = fs::File::create(keys_seed_path)?; - f.sync_all().map_err(|e| { - log_error!(logger, "Failed to sync node keys seed to disk: {}", keys_seed_path); - e - })?; + f.write_all(&key)?; + f.sync_all()?; Ok(key) } } diff --git a/src/lib.rs b/src/lib.rs index ccda53af9..bbae8ac72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ //! //! use ldk_node::bitcoin::secp256k1::PublicKey; //! use ldk_node::bitcoin::Network; +//! use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; //! use ldk_node::lightning::ln::msgs::SocketAddress; //! use ldk_node::lightning_invoice::Bolt11Invoice; //! use ldk_node::Builder; @@ -41,7 +42,9 @@ //! "https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string(), //! ); //! -//! let node = builder.build().unwrap(); +//! let mnemonic = generate_entropy_mnemonic(None); +//! let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); +//! let node = builder.build(node_entropy).unwrap(); //! //! node.start().unwrap(); //! @@ -1625,11 +1628,19 @@ impl Node { /// # use ldk_node::config::Config; /// # use ldk_node::payment::PaymentDirection; /// # use ldk_node::bitcoin::Network; + /// # use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; + /// # use rand::distr::Alphanumeric; + /// # use rand::{rng, Rng}; /// # let mut config = Config::default(); /// # config.network = Network::Regtest; - /// # config.storage_dir_path = "/tmp/ldk_node_test/".to_string(); + /// # let mut temp_path = std::env::temp_dir(); + /// # let rand_dir: String = (0..7).map(|_| rng().sample(Alphanumeric) as char).collect(); + /// # temp_path.push(rand_dir); + /// # config.storage_dir_path = temp_path.display().to_string(); /// # let builder = Builder::from_config(config); - /// # let node = builder.build().unwrap(); + /// # let mnemonic = generate_entropy_mnemonic(None); + /// # let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); + /// # let node = builder.build(node_entropy.into()).unwrap(); /// node.list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound); /// ``` pub fn list_payments_with_filter bool>( diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b70d2d675..38ecb1fd3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -29,6 +29,7 @@ use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; use electrsd::{corepc_node, ElectrsD}; use electrum_client::ElectrumApi; use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; +use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ @@ -292,11 +293,24 @@ impl Default for TestStoreType { } } -#[derive(Clone, Default)] +#[derive(Clone)] pub(crate) struct TestConfig { pub node_config: Config, pub log_writer: TestLogWriter, pub store_type: TestStoreType, + pub node_entropy: NodeEntropy, +} + +impl Default for TestConfig { + fn default() -> Self { + let node_config = Default::default(); + let log_writer = Default::default(); + let store_type = Default::default(); + + let mnemonic = generate_entropy_mnemonic(None); + let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); + TestConfig { node_config, log_writer, store_type, node_entropy } + } } macro_rules! setup_builder { @@ -330,7 +344,7 @@ pub(crate) fn setup_two_nodes_with_store( println!("== Node A =="); let mut config_a = random_config(anchor_channels); config_a.store_type = store_type; - let node_a = setup_node(chain_source, config_a, None); + let node_a = setup_node(chain_source, config_a); println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); @@ -347,18 +361,16 @@ pub(crate) fn setup_two_nodes_with_store( .trusted_peers_no_reserve .push(node_a.node_id()); } - let node_b = setup_node(chain_source, config_b, None); + let node_b = setup_node(chain_source, config_b); (node_a, node_b) } -pub(crate) fn setup_node( - chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, -) -> TestNode { - setup_node_for_async_payments(chain_source, config, seed_bytes, None) +pub(crate) fn setup_node(chain_source: &TestChainSource, config: TestConfig) -> TestNode { + setup_node_for_async_payments(chain_source, config, None) } pub(crate) fn setup_node_for_async_payments( - chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, + chain_source: &TestChainSource, config: TestConfig, async_payments_role: Option, ) -> TestNode { setup_builder!(builder, config.node_config); @@ -412,27 +424,14 @@ pub(crate) fn setup_node_for_async_payments( }, } - if let Some(seed) = seed_bytes { - #[cfg(feature = "uniffi")] - { - builder.set_entropy_seed_bytes(seed).unwrap(); - } - #[cfg(not(feature = "uniffi"))] - { - let mut bytes = [0u8; 64]; - bytes.copy_from_slice(&seed); - builder.set_entropy_seed_bytes(bytes); - } - } - builder.set_async_payments_role(async_payments_role).unwrap(); let node = match config.store_type { TestStoreType::TestSyncStore => { let kv_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into())); - builder.build_with_store(kv_store).unwrap() + builder.build_with_store(config.node_entropy.into(), kv_store).unwrap() }, - TestStoreType::Sqlite => builder.build().unwrap(), + TestStoreType::Sqlite => builder.build(config.node_entropy.into()).unwrap(), }; node.start().unwrap(); diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index e8eb72a1d..0245f1fdf 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -43,7 +43,7 @@ async fn test_cln() { let mut builder = Builder::from_config(config.node_config); builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None); - let node = builder.build().unwrap(); + let node = builder.build(config.node_entropy).unwrap(); node.start().unwrap(); // Premine some funds and distribute diff --git a/tests/integration_tests_lnd.rs b/tests/integration_tests_lnd.rs index 311a11c3c..8f1d4c868 100755 --- a/tests/integration_tests_lnd.rs +++ b/tests/integration_tests_lnd.rs @@ -41,7 +41,7 @@ async fn test_lnd() { let mut builder = Builder::from_config(config.node_config); builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None); - let node = builder.build().unwrap(); + let node = builder.build(config.node_entropy).unwrap(); node.start().unwrap(); // Premine some funds and distribute diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index d6c7c9447..7c1ed8344 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -159,7 +159,7 @@ async fn multi_hop_sending() { let sync_config = EsploraSyncConfig { background_sync_config: None }; setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let node = builder.build().unwrap(); + let node = builder.build(config.node_entropy.into()).unwrap(); node.start().unwrap(); nodes.push(node); } @@ -259,7 +259,8 @@ async fn start_stop_reinit() { setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let node = builder.build_with_store(Arc::clone(&test_sync_store)).unwrap(); + let node = + builder.build_with_store(config.node_entropy.into(), Arc::clone(&test_sync_store)).unwrap(); node.start().unwrap(); let expected_node_id = node.node_id(); @@ -297,7 +298,8 @@ async fn start_stop_reinit() { setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let reinitialized_node = builder.build_with_store(Arc::clone(&test_sync_store)).unwrap(); + let reinitialized_node = + builder.build_with_store(config.node_entropy.into(), Arc::clone(&test_sync_store)).unwrap(); reinitialized_node.start().unwrap(); assert_eq!(reinitialized_node.node_id(), expected_node_id); @@ -606,10 +608,9 @@ async fn onchain_wallet_recovery() { let chain_source = TestChainSource::Esplora(&electrsd); - let seed_bytes = vec![42u8; 64]; - let original_config = random_config(true); - let original_node = setup_node(&chain_source, original_config, Some(seed_bytes.clone())); + let original_node_entropy = original_config.node_entropy; + let original_node = setup_node(&chain_source, original_config); let premine_amount_sat = 100_000; @@ -648,8 +649,9 @@ async fn onchain_wallet_recovery() { drop(original_node); // Now we start from scratch, only the seed remains the same. - let recovered_config = random_config(true); - let recovered_node = setup_node(&chain_source, recovered_config, Some(seed_bytes)); + let mut recovered_config = random_config(true); + recovered_config.node_entropy = original_node_entropy; + let recovered_node = setup_node(&chain_source, recovered_config); recovered_node.sync_wallets().unwrap(); assert_eq!( @@ -703,7 +705,7 @@ async fn run_rbf_test(is_insert_block: bool) { macro_rules! config_node { ($chain_source:expr, $anchor_channels:expr) => {{ let config_a = random_config($anchor_channels); - let node = setup_node(&$chain_source, config_a, None); + let node = setup_node(&$chain_source, config_a); node }}; } @@ -822,7 +824,7 @@ async fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let config = random_config(true); let chain_source = TestChainSource::Esplora(&electrsd); - let node = setup_node(&chain_source, config, None); + let node = setup_node(&chain_source, config); // Tests arbitrary message signing and later verification let msg = "OK computer".as_bytes(); @@ -1296,7 +1298,6 @@ async fn async_payment() { let node_sender = setup_node_for_async_payments( &chain_source, config_sender, - None, Some(AsyncPaymentsRole::Client), ); @@ -1306,7 +1307,6 @@ async fn async_payment() { let node_sender_lsp = setup_node_for_async_payments( &chain_source, config_sender_lsp, - None, Some(AsyncPaymentsRole::Server), ); @@ -1317,7 +1317,6 @@ async fn async_payment() { let node_receiver_lsp = setup_node_for_async_payments( &chain_source, config_receiver_lsp, - None, Some(AsyncPaymentsRole::Server), ); @@ -1326,7 +1325,7 @@ async fn async_payment() { config_receiver.node_config.node_alias = None; config_receiver.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); - let node_receiver = setup_node(&chain_source, config_receiver, None); + let node_receiver = setup_node(&chain_source, config_receiver); let address_sender = node_sender.onchain_payment().new_address().unwrap(); let address_sender_lsp = node_sender_lsp.onchain_payment().new_address().unwrap(); @@ -1450,8 +1449,8 @@ async fn test_node_announcement_propagation() { config_b.node_config.listening_addresses = Some(node_b_listening_addresses.clone()); config_b.node_config.announcement_addresses = None; - let node_a = setup_node(&chain_source, config_a, None); - let node_b = setup_node(&chain_source, config_b, None); + let node_a = setup_node(&chain_source, config_a); + let node_b = setup_node(&chain_source, config_b); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; @@ -1711,7 +1710,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.set_liquidity_provider_lsps2(lsps2_service_config); - let service_node = service_builder.build().unwrap(); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); let service_node_id = service_node.node_id(); @@ -1721,13 +1720,13 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); - let client_node = client_builder.build().unwrap(); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); client_node.start().unwrap(); let payer_config = random_config(true); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let payer_node = payer_builder.build().unwrap(); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); payer_node.start().unwrap(); let service_addr = service_node.onchain_payment().new_address().unwrap(); @@ -1916,7 +1915,7 @@ async fn facade_logging() { config.log_writer = TestLogWriter::LogFacade; println!("== Facade logging starts =="); - let _node = setup_node(&chain_source, config, None); + let _node = setup_node(&chain_source, config); assert!(!logger.retrieve_logs().is_empty()); for (_, entry) in logger.retrieve_logs().iter().enumerate() { @@ -1995,10 +1994,8 @@ async fn spontaneous_send_with_custom_preimage() { async fn drop_in_async_context() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let seed_bytes = vec![42u8; 64]; - let config = random_config(true); - let node = setup_node(&chain_source, config, Some(seed_bytes)); + let node = setup_node(&chain_source, config); node.stop().unwrap(); } @@ -2030,7 +2027,7 @@ async fn lsps2_client_trusts_lsp() { setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.set_liquidity_provider_lsps2(lsps2_service_config); - let service_node = service_builder.build().unwrap(); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); @@ -2039,14 +2036,14 @@ async fn lsps2_client_trusts_lsp() { setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); - let client_node = client_builder.build().unwrap(); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); client_node.start().unwrap(); let client_node_id = client_node.node_id(); let payer_config = random_config(true); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let payer_node = payer_builder.build().unwrap(); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); payer_node.start().unwrap(); let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); @@ -2203,7 +2200,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.set_liquidity_provider_lsps2(lsps2_service_config); - let service_node = service_builder.build().unwrap(); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); let service_node_id = service_node.node_id(); @@ -2213,7 +2210,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); - let client_node = client_builder.build().unwrap(); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); client_node.start().unwrap(); let client_node_id = client_node.node_id(); @@ -2221,7 +2218,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let payer_config = random_config(true); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - let payer_node = payer_builder.build().unwrap(); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); payer_node.start().unwrap(); let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 3b384ec45..54912b358 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -11,6 +11,7 @@ mod common; use std::collections::HashMap; +use ldk_node::entropy::NodeEntropy; use ldk_node::Builder; use rand::{rng, Rng}; @@ -25,6 +26,7 @@ async fn channel_full_cycle_with_vss_store() { let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let node_a = builder_a .build_with_vss_store_and_fixed_headers( + config_a.node_entropy, vss_base_url.clone(), "node_1_store".to_string(), HashMap::new(), @@ -38,6 +40,7 @@ async fn channel_full_cycle_with_vss_store() { builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b .build_with_vss_store_and_fixed_headers( + config_b.node_entropy, vss_base_url, "node_2_store".to_string(), HashMap::new(), @@ -68,6 +71,7 @@ async fn vss_v0_schema_backwards_compatibility() { let store_id = format!("v0_compat_test_{}", rand_suffix); let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); let seed_bytes = [42u8; 64]; + let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); // Setup a v0.6.2 `Node` persisted with the v0 scheme. let (old_balance, old_node_id) = { @@ -112,11 +116,15 @@ async fn vss_v0_schema_backwards_compatibility() { let mut builder_new = Builder::new(); builder_new.set_network(bitcoin::Network::Regtest); builder_new.set_storage_dir_path(storage_path); - builder_new.set_entropy_seed_bytes(seed_bytes); builder_new.set_chain_source_esplora(esplora_url, None); let node_new = builder_new - .build_with_vss_store_and_fixed_headers(vss_base_url, store_id, HashMap::new()) + .build_with_vss_store_and_fixed_headers( + node_entropy, + vss_base_url, + store_id, + HashMap::new(), + ) .unwrap(); node_new.start().unwrap(); @@ -142,16 +150,17 @@ async fn vss_node_restart() { let store_id = format!("restart_test_{}", rand_suffix); let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); let seed_bytes = [42u8; 64]; + let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); // 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_entropy_seed_bytes(seed_bytes); builder.set_chain_source_esplora(esplora_url.clone(), None); let node = builder .build_with_vss_store_and_fixed_headers( + node_entropy, vss_base_url.clone(), store_id.clone(), HashMap::new(), @@ -181,11 +190,15 @@ async fn vss_node_restart() { let mut builder = Builder::new(); builder.set_network(bitcoin::Network::Regtest); builder.set_storage_dir_path(storage_path); - builder.set_entropy_seed_bytes(seed_bytes); builder.set_chain_source_esplora(esplora_url, None); let node = builder - .build_with_vss_store_and_fixed_headers(vss_base_url, store_id, HashMap::new()) + .build_with_vss_store_and_fixed_headers( + node_entropy, + vss_base_url, + store_id, + HashMap::new(), + ) .unwrap(); node.start().unwrap(); diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 491a37fd4..89660a407 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -31,7 +31,7 @@ proptest! { macro_rules! config_node { ($chain_source: expr, $anchor_channels: expr) => {{ let config_a = random_config($anchor_channels); - let node = setup_node(&$chain_source, config_a, None); + let node = setup_node(&$chain_source, config_a); node }}; } From 2fb1df513b06249dc52799ab568295f68cd3b837 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 26 Nov 2025 08:38:09 +0100 Subject: [PATCH 4/4] Add test asserting generated seed bytes can be read back We add a simple test calling `read_or_generate_seed_file` twice, asserting it returns the same value in both cases. --- src/io/utils.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/io/utils.rs b/src/io/utils.rs index 4acc7dd41..928d4031b 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -575,3 +575,18 @@ pub(crate) fn read_bdk_wallet_change_set( .map(|indexer| change_set.indexer = indexer); Ok(Some(change_set)) } + +#[cfg(test)] +mod tests { + use super::read_or_generate_seed_file; + use super::test_utils::random_storage_path; + + #[test] + fn generated_seed_is_readable() { + let mut rand_path = random_storage_path(); + rand_path.push("test_keys_seed"); + let expected_seed_bytes = read_or_generate_seed_file(&rand_path.to_str().unwrap()).unwrap(); + let read_seed_bytes = read_or_generate_seed_file(&rand_path.to_str().unwrap()).unwrap(); + assert_eq!(expected_seed_bytes, read_seed_bytes); + } +}