From 05cb3301347ab0ab339bd7b7a286e1045107ed5d Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Mon, 10 Nov 2025 13:16:20 -0500 Subject: [PATCH 1/2] feat: add configurable BIP39 mnemonic word counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support generating BIP39 mnemonics with configurable word counts (12, 15, 18, 21, 24). Defaults to 24 words (256-bit entropy) for backward compatibility. - Add MnemonicWordCount enum (12–24 variants) - Update generate_entropy_mnemonic to accept optional word_count - Map word counts to correct entropy sizes per BIP39 - Extend tests for all word count options and defaults - Expose enum and updated function in UDL bindings --- bindings/ldk_node.udl | 10 +++++++++- src/io/utils.rs | 43 ++++++++++++++++++++++++++++++++++++------- src/lib.rs | 3 ++- src/types.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index ab2f483a1..46bcffdf4 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -1,5 +1,5 @@ namespace ldk_node { - Mnemonic generate_entropy_mnemonic(); + Mnemonic generate_entropy_mnemonic(MnemonicWordCount? word_count); Config default_config(); }; @@ -46,6 +46,14 @@ dictionary LSPS2ServiceConfig { u64 max_payment_size_msat; }; +enum MnemonicWordCount { + "Words12", + "Words15", + "Words18", + "Words21", + "Words24", +}; + enum LogLevel { "Gossip", "Trace", diff --git a/src/io/utils.rs b/src/io/utils.rs index d92c9486b..7a8c6c576 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -47,13 +47,15 @@ use crate::io::{ }; use crate::logger::{log_error, LdkLogger, Logger}; use crate::peer_store::PeerStore; -use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; +use crate::types::{Broadcaster, DynStore, KeysManager, MnemonicWordCount, 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. +/// 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`]. @@ -61,9 +63,10 @@ pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_sc /// [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() -> Mnemonic { - // bip39::Mnemonic supports 256 bit entropy max - let mut entropy = [0; 32]; +pub fn generate_entropy_mnemonic(word_count: Option) -> Mnemonic { + let word_count = word_count.unwrap_or(MnemonicWordCount::Words24); + let entropy_bytes = word_count.entropy_bytes(); + let mut entropy = vec![0u8; entropy_bytes]; OsRng.try_fill_bytes(&mut entropy).expect("Failed to generate entropy"); Mnemonic::from_entropy(&entropy).unwrap() } @@ -627,9 +630,35 @@ mod tests { #[test] fn mnemonic_to_entropy_to_mnemonic() { - let mnemonic = generate_entropy_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 = [ + MnemonicWordCount::Words12, + MnemonicWordCount::Words15, + MnemonicWordCount::Words18, + MnemonicWordCount::Words21, + MnemonicWordCount::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 { + MnemonicWordCount::Words12 => 12, + MnemonicWordCount::Words15 => 15, + MnemonicWordCount::Words18 => 18, + MnemonicWordCount::Words21 => 21, + MnemonicWordCount::Words24 => 24, + }; + assert_eq!(mnemonic.word_count(), expected_words); + } } } diff --git a/src/lib.rs b/src/lib.rs index 701a14dde..05ac7e39e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,7 +155,8 @@ use types::{ OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ - ChannelDetails, CustomTlvRecord, DynStore, PeerDetails, SyncAndAsyncKVStore, UserChannelId, + ChannelDetails, CustomTlvRecord, DynStore, MnemonicWordCount, PeerDetails, SyncAndAsyncKVStore, + UserChannelId, }; pub use { bip39, bitcoin, lightning, lightning_invoice, lightning_liquidity, lightning_types, tokio, diff --git a/src/types.rs b/src/types.rs index b8dc10b18..75bde44fb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -36,6 +36,34 @@ 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 MnemonicWordCount { + /// 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 MnemonicWordCount { + /// Returns the entropy size in bytes for the word count. + pub fn entropy_bytes(&self) -> usize { + match self { + MnemonicWordCount::Words12 => 16, // 128 bits + MnemonicWordCount::Words15 => 20, // 160 bits + MnemonicWordCount::Words18 => 24, // 192 bits + MnemonicWordCount::Words21 => 28, // 224 bits + MnemonicWordCount::Words24 => 32, // 256 bits + } + } +} + /// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the /// same time. pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {} From 0bf87217e5ab4be59ca81388b36150e35ffc3e07 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Tue, 11 Nov 2025 13:03:54 -0500 Subject: [PATCH 2/2] refactor: replace entropy_bytes with word_count - Rename MnemonicWordCount to WordCount and update references as needed - Remove need for entropy_bytes in generate_entropy_mnemonic by passing WordCount enum directly to generate() instead - Add rand feature to bip39 dependency --- Cargo.toml | 2 +- bindings/ldk_node.udl | 4 ++-- src/io/utils.rs | 31 ++++++++++++++----------------- src/lib.rs | 4 ++-- src/types.rs | 18 +++++++++--------- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 544dfca08..2aa147a77 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus rustls = { version = "0.23", default-features = false } rusqlite = { version = "0.31.0", features = ["bundled"] } bitcoin = "0.32.7" -bip39 = "2.0.0" +bip39 = { version = "2.0.0", features = ["rand"] } bip21 = { version = "0.5", features = ["std"], default-features = false } base64 = { version = "0.22.1", default-features = false, features = ["std"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 46bcffdf4..009126feb 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -1,5 +1,5 @@ namespace ldk_node { - Mnemonic generate_entropy_mnemonic(MnemonicWordCount? word_count); + Mnemonic generate_entropy_mnemonic(WordCount? word_count); Config default_config(); }; @@ -46,7 +46,7 @@ dictionary LSPS2ServiceConfig { u64 max_payment_size_msat; }; -enum MnemonicWordCount { +enum WordCount { "Words12", "Words15", "Words18", diff --git a/src/io/utils.rs b/src/io/utils.rs index 7a8c6c576..1b4b02a82 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -47,7 +47,7 @@ use crate::io::{ }; use crate::logger::{log_error, LdkLogger, Logger}; use crate::peer_store::PeerStore; -use crate::types::{Broadcaster, DynStore, KeysManager, MnemonicWordCount, Sweeper}; +use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper, WordCount}; use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; @@ -63,12 +63,9 @@ pub const EXTERNAL_PATHFINDING_SCORES_CACHE_KEY: &str = "external_pathfinding_sc /// [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(MnemonicWordCount::Words24); - let entropy_bytes = word_count.entropy_bytes(); - let mut entropy = vec![0u8; entropy_bytes]; - OsRng.try_fill_bytes(&mut entropy).expect("Failed to generate entropy"); - Mnemonic::from_entropy(&entropy).unwrap() +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( @@ -638,11 +635,11 @@ mod tests { // Test with different word counts let word_counts = [ - MnemonicWordCount::Words12, - MnemonicWordCount::Words15, - MnemonicWordCount::Words18, - MnemonicWordCount::Words21, - MnemonicWordCount::Words24, + WordCount::Words12, + WordCount::Words15, + WordCount::Words18, + WordCount::Words21, + WordCount::Words24, ]; for word_count in word_counts { @@ -652,11 +649,11 @@ mod tests { // Verify expected word count let expected_words = match word_count { - MnemonicWordCount::Words12 => 12, - MnemonicWordCount::Words15 => 15, - MnemonicWordCount::Words18 => 18, - MnemonicWordCount::Words21 => 21, - MnemonicWordCount::Words24 => 24, + 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 05ac7e39e..21fc93fe8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -155,8 +155,8 @@ use types::{ OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ - ChannelDetails, CustomTlvRecord, DynStore, MnemonicWordCount, PeerDetails, SyncAndAsyncKVStore, - UserChannelId, + 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 75bde44fb..6d6bdcd20 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,7 +38,7 @@ use crate::payment::PaymentDetails; /// Supported BIP39 mnemonic word counts for entropy generation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MnemonicWordCount { +pub enum WordCount { /// 12-word mnemonic (128-bit entropy) Words12, /// 15-word mnemonic (160-bit entropy) @@ -51,15 +51,15 @@ pub enum MnemonicWordCount { Words24, } -impl MnemonicWordCount { - /// Returns the entropy size in bytes for the word count. - pub fn entropy_bytes(&self) -> usize { +impl WordCount { + /// Returns the word count as a usize value. + pub fn word_count(&self) -> usize { match self { - MnemonicWordCount::Words12 => 16, // 128 bits - MnemonicWordCount::Words15 => 20, // 160 bits - MnemonicWordCount::Words18 => 24, // 192 bits - MnemonicWordCount::Words21 => 28, // 224 bits - MnemonicWordCount::Words24 => 32, // 256 bits + WordCount::Words12 => 12, + WordCount::Words15 => 15, + WordCount::Words18 => 18, + WordCount::Words21 => 21, + WordCount::Words24 => 24, } } }