From 75760133d7b3d720b37425a1432308c222ffd99c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 17 Sep 2026 15:17:36 +0200 Subject: [PATCH] Use upstream WordCount from bip39 3.0 Restore typed mnemonic generation so Rust and language bindings share bip39's supported word counts without a separate LDK Node enum. Require bip39 3.0 for the typed API while allowing legacy migration-test nodes to keep using bip39 2.x. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 3 +- Cargo.toml | 2 +- README.md | 4 +-- .../lightningdevkit/ldknode/AndroidLibTest.kt | 4 +-- .../lightningdevkit/ldknode/LibraryTest.kt | 4 +-- bindings/ldk_node.udl | 9 ++++++ bindings/python/src/ldk_node/test_ldk_node.py | 30 ++++++++++++++----- src/entropy.rs | 2 ++ src/ffi/types.rs | 10 +++---- src/lib.rs | 8 ++--- tests/common/mod.rs | 4 +-- 11 files changed, 52 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b6c6e97b1..8f8f563c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ `NodeError::InvalidMnemonic` for invalid input; generated mnemonics can be converted back to a string through their language's standard string conversion. - `generate_entropy_mnemonic` has been removed. Use `bip39::Mnemonic::generate` in Rust and - `Mnemonic::generate` in the language bindings instead. + `Mnemonic::generate` in the language bindings instead, passing a `WordCount` variant. + `WordCount` is now re-exported from bip39 rather than defined by LDK Node. - Migrating between storage backends does not preserve the relative creation order of pre-existing payments, as the generic KV store migration copies entries in an unspecified order. Expect the order in which `Node::list_payments` returns pre-existing payments to diff --git a/Cargo.toml b/Cargo.toml index 453212acd9..aafd7f5673 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,7 +108,7 @@ bitreq = { version = "0.3", default-features = false, features = ["async-https", rustls = { version = "0.23", default-features = false } rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } bitcoin = "0.32.7" -bip39 = { version = "2.0.0", features = ["rand"] } +bip39 = { version = "3.0.0", features = ["rand"] } bip21 = { version = "0.5", features = ["std"], default-features = false, optional = true } base64 = { version = "0.22.1", default-features = false, features = ["std"] } diff --git a/README.md b/README.md index ec9de516ff..b9d32c0a0c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The primary abstraction of the library is the [`Node`][api_docs_node], which can ```rust use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Network; -use ldk_node::bip39::Mnemonic; +use ldk_node::bip39::{Mnemonic, WordCount}; use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning_invoice::Bolt11Invoice; @@ -33,7 +33,7 @@ fn main() { ); - let mnemonic = Mnemonic::generate(24).unwrap(); + let mnemonic = Mnemonic::generate(WordCount::Words24).unwrap(); let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); let node = builder.build(node_entropy).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 71a7d20041..c7663329bc 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,11 +34,11 @@ class AndroidLibTest { val builder1 = Builder.fromConfig(config1) val builder2 = Builder.fromConfig(config2) - val mnemonic1 = Mnemonic.generate(24u) + val mnemonic1 = Mnemonic.generate(WordCount.WORDS24) val nodeEntropy1 = NodeEntropy.fromBip39Mnemonic(mnemonic1, null) val node1 = builder1.build(nodeEntropy1) - val mnemonic2 = Mnemonic.generate(24u) + val mnemonic2 = Mnemonic.generate(WordCount.WORDS24) val nodeEntropy2 = NodeEntropy.fromBip39Mnemonic(mnemonic2, null) val node2 = builder2.build(nodeEntropy2) 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 d41de589a0..96cccfa96e 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 @@ -207,11 +207,11 @@ class LibraryTest { builder2.setChainSourceEsplora(esploraEndpoint, null) builder2.setCustomLogger(logWriter2) - val mnemonic1 = Mnemonic.generate(24u) + val mnemonic1 = Mnemonic.generate(WordCount.WORDS24) val nodeEntropy1 = NodeEntropy.fromBip39Mnemonic(mnemonic1, null) val node1 = builder1.build(nodeEntropy1) - val mnemonic2 = Mnemonic.generate(24u) + val mnemonic2 = Mnemonic.generate(WordCount.WORDS24) val nodeEntropy2 = NodeEntropy.fromBip39Mnemonic(mnemonic2, null) val node2 = builder2.build(nodeEntropy2) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b2f3d74ac2..df6aa18f8d 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -388,6 +388,15 @@ typedef string UserChannelId; typedef interface Mnemonic; +[Remote] +enum WordCount { + "Words12", + "Words15", + "Words18", + "Words21", + "Words24", +}; + [Custom] typedef string UntrustedString; diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 08a866c3f0..11699da0af 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -98,7 +98,7 @@ def send_to_address(address, amount_sats): def setup_node(tmp_dir, esplora_endpoint, listening_addresses): - mnemonic = Mnemonic.generate(24) + mnemonic = Mnemonic.generate(WordCount.WORDS24) node_entropy = NodeEntropy.from_bip39_mnemonic(mnemonic, None) config = default_config() builder = Builder.from_config(config) @@ -206,6 +206,25 @@ def init_features_exposed(test_case, init_features): class TestMnemonic(unittest.TestCase): + def test_mnemonic_word_counts(self): + word_count_type = globals().get("WordCount") + self.assertIsNotNone(word_count_type, "Mnemonic generation must expose WordCount") + + for word_count, expected_words, expected_entropy_bytes in [ + (word_count_type.WORDS12, 12, 16), + (word_count_type.WORDS15, 15, 20), + (word_count_type.WORDS18, 18, 24), + (word_count_type.WORDS21, 21, 28), + (word_count_type.WORDS24, 24, 32), + ]: + with self.subTest(word_count=word_count): + mnemonic = Mnemonic.generate(word_count) + self.assertEqual(mnemonic.word_count(), expected_words) + self.assertEqual(len(mnemonic.words()), expected_words) + self.assertEqual(len(mnemonic.to_entropy()), expected_entropy_bytes) + self.assertEqual(Mnemonic.from_entropy(mnemonic.to_entropy()), mnemonic) + self.assertEqual(Mnemonic.from_str(str(mnemonic)), mnemonic) + def test_invalid_mnemonic_returns_node_error(self): invalid_mnemonic = "abandon " * 11 + "abandon" mnemonic_constructor = getattr(Mnemonic, "from_str", Mnemonic) @@ -216,7 +235,7 @@ def test_invalid_mnemonic_returns_node_error(self): self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) def test_mnemonic_round_trip(self): - mnemonic = Mnemonic.generate(24) + mnemonic = Mnemonic.generate(WordCount.WORDS24) parsed_mnemonic = Mnemonic.from_str(str(mnemonic)) self.assertIsInstance(mnemonic, Mnemonic) @@ -237,12 +256,7 @@ def test_mnemonic_functionality(self): "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553" "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", ) - self.assertEqual(Mnemonic.generate(12).word_count(), 12) - - with self.assertRaises(NodeError) as error: - Mnemonic.generate(13) - - self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) + self.assertEqual(Mnemonic.generate(WordCount.WORDS12).word_count(), 12) with self.assertRaises(NodeError) as error: Mnemonic.from_entropy(bytes(15)) diff --git a/src/entropy.rs b/src/entropy.rs index f372bc5f9d..1c918d6812 100644 --- a/src/entropy.rs +++ b/src/entropy.rs @@ -9,6 +9,8 @@ use std::fmt; +pub use bip39::WordCount; + use crate::config::WALLET_KEYS_SEED_LEN; use crate::ffi::maybe_deref; use crate::io; diff --git a/src/ffi/types.rs b/src/ffi/types.rs index a153a77ef5..e6ef62c539 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -18,6 +18,7 @@ use std::sync::Arc; use std::time::Duration; use bip39::Mnemonic as Bip39Mnemonic; +pub use bip39::WordCount; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; @@ -1201,10 +1202,8 @@ impl Mnemonic { /// Generates a random English mnemonic with the specified word count. #[uniffi::constructor] - pub fn generate(word_count: u8) -> Result { - Bip39Mnemonic::generate(word_count.into()) - .map(Self::from) - .map_err(|_| Error::InvalidMnemonic) + pub fn generate(word_count: WordCount) -> Result { + Bip39Mnemonic::generate(word_count).map(Self::from).map_err(|_| Error::InvalidMnemonic) } /// Returns the words in the mnemonic. @@ -3031,8 +3030,7 @@ mod tests { assert_eq!(mnemonic.to_entropy(), entropy); assert_eq!(mnemonic.checksum(), 3); assert_eq!(mnemonic.to_seed("TREZOR").len(), 64); - assert_eq!(Mnemonic::generate(12).unwrap().word_count(), 12); - assert_eq!(Mnemonic::generate(13), Err(Error::InvalidMnemonic)); + assert_eq!(Mnemonic::generate(WordCount::Words12).unwrap().word_count(), 12); assert_eq!(Mnemonic::from_entropy(&[0; 15]), Err(Error::InvalidMnemonic)); } } diff --git a/src/lib.rs b/src/lib.rs index 6d13141c6a..c2e0732e1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ //! # { //! use std::str::FromStr; //! -//! use ldk_node::bip39::Mnemonic; +//! use ldk_node::bip39::{Mnemonic, WordCount}; //! use ldk_node::bitcoin::secp256k1::PublicKey; //! use ldk_node::bitcoin::Network; //! use ldk_node::entropy::NodeEntropy; @@ -43,7 +43,7 @@ //! "https://rapidsync.lightningdevkit.org/testnet/v2/snapshot".to_string(), //! ); //! -//! let mnemonic = Mnemonic::generate(24).unwrap(); +//! let mnemonic = Mnemonic::generate(WordCount::Words24).unwrap(); //! let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); //! let node = builder.build(node_entropy).unwrap(); //! @@ -2267,7 +2267,7 @@ impl Node { /// # use ldk_node::config::Config; /// # use ldk_node::payment::{PaymentDetails, PaymentDirection}; /// # use ldk_node::bitcoin::Network; - /// # use ldk_node::bip39::Mnemonic; + /// # use ldk_node::bip39::{Mnemonic, WordCount}; /// # use ldk_node::entropy::NodeEntropy; /// # use rand::distr::Alphanumeric; /// # use rand::{rng, Rng}; @@ -2278,7 +2278,7 @@ impl Node { /// # temp_path.push(rand_dir); /// # config.storage_dir_path = temp_path.display().to_string(); /// # let builder = Builder::from_config(config); - /// # let mnemonic = Mnemonic::generate(24).unwrap(); + /// # let mnemonic = Mnemonic::generate(WordCount::Words24).unwrap(); /// # let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); /// # let node = builder.build(node_entropy.into()).unwrap(); /// let mut outbound = Vec::new(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index af6a49c69a..5a409b8104 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -38,7 +38,7 @@ use bitcoin::{ use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; use electrsd::electrum_client::ElectrumApi; use electrsd::{corepc_node, ElectrsD}; -use ldk_node::bip39::Mnemonic; +use ldk_node::bip39::{Mnemonic, WordCount}; #[cfg(feature = "chain-electrum")] use ldk_node::config::ElectrumSyncConfig; #[cfg(feature = "chain-esplora")] @@ -683,7 +683,7 @@ impl Default for TestConfig { let log_writer = Default::default(); let store_type = Default::default(); - let mnemonic = Mnemonic::generate(24).unwrap(); + let mnemonic = Mnemonic::generate(WordCount::Words24).unwrap(); #[cfg(not(feature = "uniffi"))] let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); #[cfg(feature = "uniffi")]