From 6d9828ffdfd2951b310c8efb4b71e825546db52d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:00:07 +0200 Subject: [PATCH 1/2] Expose mnemonics as UniFFI objects Invalid mnemonic strings currently fail during implicit custom-type lifting, which leaves generated bindings without a catchable validation error. Parse mnemonic phrases through a fallible object constructor while keeping bip39::Mnemonic in the native Rust API. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 6 + bindings/ldk_node.udl | 4 +- bindings/python/src/ldk_node/test_ldk_node.py | 40 +++++ src/entropy.rs | 28 ++-- src/error.rs | 3 + src/ffi/types.rs | 145 ++++++++++++++++-- 6 files changed, 205 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46112d79be..5e17c3ad50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Pending ## Compatibility Notes +- The language bindings now expose `Mnemonic` as an object instead of a string alias. Existing + mnemonic phrases must be passed through its fallible constructor, which returns + `NodeError::InvalidMnemonic` for invalid input; generated mnemonics can be converted back to a + string through their language's standard string conversion. - 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 @@ -23,6 +27,8 @@ `Event::PaymentClaimable`. ## Feature and API updates +- Language-binding `Mnemonic` objects can be generated or constructed from entropy and expose + their words, word indices, word count, entropy, checksum, and passphrase-derived seed. - `Node::list_payments` is now paginated: it takes an optional `PageToken` and returns a `PaymentDetailsPage` holding one page of payments, ordered from most recently created to least recently created, plus the token for the next page. Ordering and page tokens come diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index fddf5940ca..6763172f79 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -220,6 +220,7 @@ enum NodeError { "InvalidSocketAddress", "InvalidPublicKey", "InvalidSecretKey", + "InvalidMnemonic", "InvalidOfferId", "InvalidNodeId", "InvalidPaymentId", @@ -419,8 +420,7 @@ typedef string ChannelId; [Custom] typedef string UserChannelId; -[Custom] -typedef string Mnemonic; +typedef interface Mnemonic; [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 063c95f700..0c50f05a3a 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -8,6 +8,7 @@ import socket from ldk_node import * +from ldk_node import ldk_node as bindings DEFAULT_ESPLORA_SERVER_URL = "http://127.0.0.1:3002" DEFAULT_TEST_NETWORK = Network.REGTEST @@ -205,6 +206,45 @@ def init_features_exposed(test_case, init_features): test_case.assertIsInstance(init_features.initial_routing_sync(), bool) +class TestMnemonic(unittest.TestCase): + def test_invalid_mnemonic_returns_node_error(self): + invalid_mnemonic = "abandon " * 11 + "abandon" + mnemonic_constructor = getattr(bindings.Mnemonic, "from_str", bindings.Mnemonic) + + with self.assertRaises(NodeError) as error: + mnemonic_constructor(invalid_mnemonic) + + self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) + + def test_mnemonic_round_trip(self): + mnemonic = generate_entropy_mnemonic(None) + parsed_mnemonic = bindings.Mnemonic.from_str(str(mnemonic)) + + self.assertIsInstance(mnemonic, bindings.Mnemonic) + self.assertEqual(parsed_mnemonic, mnemonic) + self.assertIsInstance(NodeEntropy.from_bip39_mnemonic(parsed_mnemonic, None), NodeEntropy) + + def test_mnemonic_functionality(self): + entropy = bytes(16) + mnemonic = bindings.Mnemonic.from_entropy(entropy) + + self.assertEqual(mnemonic.words(), ["abandon"] * 11 + ["about"]) + self.assertEqual(mnemonic.word_indices(), [0] * 11 + [3]) + self.assertEqual(mnemonic.word_count(), 12) + self.assertEqual(mnemonic.to_entropy(), entropy) + self.assertEqual(mnemonic.checksum(), 3) + self.assertEqual( + mnemonic.to_seed("TREZOR").hex(), + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553" + "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + ) + self.assertEqual(bindings.Mnemonic.generate(WordCount.WORDS12).word_count(), 12) + + with self.assertRaises(NodeError) as error: + bindings.Mnemonic.from_entropy(bytes(15)) + + self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) + class TestLdkNode(unittest.TestCase): def setUp(self): diff --git a/src/entropy.rs b/src/entropy.rs index 2f7faa1b4d..3bcab832e8 100644 --- a/src/entropy.rs +++ b/src/entropy.rs @@ -10,11 +10,17 @@ use std::fmt; use bip39::rand::rngs::OsRng; -use bip39::{Language, Mnemonic}; +use bip39::{Language, Mnemonic as Bip39Mnemonic}; use crate::config::WALLET_KEYS_SEED_LEN; +use crate::ffi::{maybe_deref, maybe_wrap}; use crate::io; +#[cfg(not(feature = "uniffi"))] +type Mnemonic = Bip39Mnemonic; +#[cfg(feature = "uniffi")] +type Mnemonic = std::sync::Arc; + /// An error that could arise during [`NodeEntropy`] construction. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "uniffi", derive(uniffi::Error))] @@ -67,6 +73,7 @@ impl NodeEntropy { /// [`Node`]: crate::Node #[cfg_attr(feature = "uniffi", uniffi::constructor)] pub fn from_bip39_mnemonic(mnemonic: Mnemonic, passphrase: Option) -> Self { + let mnemonic = maybe_deref(&mnemonic); match passphrase { Some(passphrase) => Self(mnemonic.to_seed(passphrase)), None => Self(mnemonic.to_seed("")), @@ -129,8 +136,9 @@ impl fmt::Debug for NodeEntropy { /// [`Node`]: crate::Node pub fn generate_entropy_mnemonic(word_count: Option) -> Mnemonic { let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); - Mnemonic::generate_in_with(&mut OsRng, Language::English, word_count) - .expect("Failed to generate mnemonic") + let mnemonic = Bip39Mnemonic::generate_in_with(&mut OsRng, Language::English, word_count) + .expect("Failed to generate mnemonic"); + maybe_wrap(mnemonic) } /// Supported BIP39 mnemonic word counts for entropy generation. @@ -170,9 +178,10 @@ mod tests { 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); + let mnemonic_inner = maybe_deref(&mnemonic); + let entropy = mnemonic_inner.to_entropy(); + assert_eq!(mnemonic_inner, &Bip39Mnemonic::from_entropy(&entropy).unwrap()); + assert_eq!(mnemonic_inner.word_count(), 24); // Test with different word counts let word_counts = [ @@ -185,8 +194,9 @@ mod tests { 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()); + let mnemonic_inner = maybe_deref(&mnemonic); + let entropy = mnemonic_inner.to_entropy(); + assert_eq!(mnemonic_inner, &Bip39Mnemonic::from_entropy(&entropy).unwrap()); // Verify expected word count let expected_words = match word_count { @@ -196,7 +206,7 @@ mod tests { WordCount::Words21 => 21, WordCount::Words24 => 24, }; - assert_eq!(mnemonic.word_count(), expected_words); + assert_eq!(mnemonic_inner.word_count(), expected_words); } } } diff --git a/src/error.rs b/src/error.rs index 9a03c446fa..107b8fe1b1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -81,6 +81,8 @@ pub enum Error { InvalidPublicKey, /// The given secret key is invalid. InvalidSecretKey, + /// The given BIP 39 mnemonic is invalid. + InvalidMnemonic, /// The given offer id is invalid. InvalidOfferId, /// The given node id is invalid. @@ -188,6 +190,7 @@ impl fmt::Display for Error { Self::InvalidSocketAddress => write!(f, "The given network address is invalid."), Self::InvalidPublicKey => write!(f, "The given public key is invalid."), Self::InvalidSecretKey => write!(f, "The given secret key is invalid."), + Self::InvalidMnemonic => write!(f, "The given BIP 39 mnemonic is invalid."), Self::InvalidOfferId => write!(f, "The given offer id is invalid."), Self::InvalidNodeId => write!(f, "The given node id is invalid."), Self::InvalidPaymentId => write!(f, "The given payment id is invalid."), diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 34c3ae6715..5e765ce83a 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -17,7 +17,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -pub use bip39::Mnemonic; +use bip39::Mnemonic as Bip39Mnemonic; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; @@ -1151,15 +1151,107 @@ uniffi::custom_type!(BlockHash, String, { }, }); -uniffi::custom_type!(Mnemonic, String, { - remote, - try_lift: |val| { - Ok(Mnemonic::from_str(&val).map_err(|_| Error::InvalidSecretKey)?) - }, - lower: |obj| { - obj.to_string() - }, -}); +/// A syntactically and semantically valid BIP 39 mnemonic. +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Object)] +#[uniffi::export(Debug, Display, Eq)] +pub struct Mnemonic { + pub(crate) inner: Bip39Mnemonic, +} + +#[uniffi::export] +impl Mnemonic { + /// Constructs a mnemonic from its BIP 39 phrase. + #[uniffi::constructor] + pub fn from_str(mnemonic_str: &str) -> Result { + mnemonic_str.parse() + } + + /// Constructs an English mnemonic from 128-256 bits of entropy. + /// + /// The entropy must be a multiple of 32 bits. + #[uniffi::constructor] + pub fn from_entropy(entropy: &[u8]) -> Result { + Bip39Mnemonic::from_entropy(entropy).map(Self::from).map_err(|_| Error::InvalidMnemonic) + } + + /// Generates a random English mnemonic with the specified word count. + /// + /// Defaults to 24 words when no word count is specified. + #[uniffi::constructor] + pub fn generate(word_count: Option) -> Self { + let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); + let inner = Bip39Mnemonic::generate(word_count) + .expect("WordCount always maps to a valid BIP 39 word count"); + Self { inner } + } + + /// Returns the words in the mnemonic. + pub fn words(&self) -> Vec { + self.inner.words().map(String::from).collect() + } + + /// Returns the indices of the mnemonic's words in the English BIP 39 word list. + pub fn word_indices(&self) -> Vec { + self.inner.word_indices().map(|index| index as u16).collect() + } + + /// Returns the number of words in the mnemonic. + pub fn word_count(&self) -> u8 { + self.inner.word_count() as u8 + } + + /// Returns the entropy used to construct the mnemonic. + pub fn to_entropy(&self) -> Vec { + self.inner.to_entropy() + } + + /// Derives the 64-byte BIP 39 seed using the given passphrase. + pub fn to_seed(&self, passphrase: &str) -> Vec { + self.inner.to_seed(passphrase).to_vec() + } + + /// Returns the checksum encoded in the mnemonic's last word. + pub fn checksum(&self) -> u8 { + self.inner.checksum() + } +} + +impl FromStr for Mnemonic { + type Err = Error; + + fn from_str(mnemonic_str: &str) -> Result { + mnemonic_str + .parse::() + .map(|inner| Self { inner }) + .map_err(|_| Error::InvalidMnemonic) + } +} + +impl From for Mnemonic { + fn from(inner: Bip39Mnemonic) -> Self { + Self { inner } + } +} + +impl Deref for Mnemonic { + type Target = Bip39Mnemonic; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Mnemonic { + fn as_ref(&self) -> &Bip39Mnemonic { + self.deref() + } +} + +impl std::fmt::Display for Mnemonic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} uniffi::custom_type!(SocketAddress, String, { remote, @@ -2887,6 +2979,39 @@ mod tests { let hrn3 = hrn1; assert_eq!(hrn1, hrn3); } + + #[test] + fn test_mnemonic_traits() { + let mnemonic_str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let mnemonic = Mnemonic::from_str(mnemonic_str).unwrap(); + let bip39_mnemonic = Bip39Mnemonic::from_str(mnemonic_str).unwrap(); + + assert_eq!(mnemonic.as_ref(), &bip39_mnemonic); + assert_eq!(mnemonic.to_string(), mnemonic_str); + assert_eq!(mnemonic, Mnemonic::from(bip39_mnemonic)); + assert!(format!("{:?}", mnemonic).contains("Mnemonic")); + + let invalid_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"; + assert_eq!(Mnemonic::from_str(invalid_mnemonic), Err(Error::InvalidMnemonic)); + } + + #[test] + fn test_mnemonic_functionality() { + let entropy = [0; 16]; + let mnemonic = Mnemonic::from_entropy(&entropy).unwrap(); + + assert_eq!( + mnemonic.words(), + [vec!["abandon".to_string(); 11], vec!["about".to_string()]].concat() + ); + assert_eq!(mnemonic.word_indices(), [vec![0; 11], vec![3]].concat()); + assert_eq!(mnemonic.word_count(), 12); + assert_eq!(mnemonic.to_entropy(), entropy); + assert_eq!(mnemonic.checksum(), 3); + assert_eq!(mnemonic.to_seed("TREZOR").len(), 64); + assert_eq!(Mnemonic::generate(Some(WordCount::Words12)).word_count(), 12); + assert_eq!(Mnemonic::from_entropy(&[0; 15]), Err(Error::InvalidMnemonic)); + } } /// An opaque token used to continue a paginated listing. From ec9567f5c0417a056ce846f9a13d8eced9d3745a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Aug 2026 09:37:30 +0200 Subject: [PATCH 2/2] Use associated mnemonic generation Mnemonic generation no longer needs a separate global entry point now that bindings expose a real mnemonic object. Use numeric word counts for both native and binding constructors so the APIs stay aligned without a binding-specific enum. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 2 + README.md | 5 +- .../lightningdevkit/ldknode/AndroidLibTest.kt | 4 +- .../lightningdevkit/ldknode/LibraryTest.kt | 4 +- bindings/ldk_node.udl | 3 - bindings/python/src/ldk_node/test_ldk_node.py | 22 +++-- src/entropy.rs | 93 +------------------ src/ffi/types.rs | 16 ++-- src/lib.rs | 17 +++- tests/common/mod.rs | 8 +- 10 files changed, 49 insertions(+), 125 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e17c3ad50..3b6c6e97b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ mnemonic phrases must be passed through its fallible constructor, which returns `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. - 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/README.md b/README.md index e59dba569b..1cd2e36431 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ 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::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use ldk_node::bip39::Mnemonic; +use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning_invoice::Bolt11Invoice; use ldk_node::Builder; @@ -32,7 +33,7 @@ fn main() { ); - let mnemonic = generate_entropy_mnemonic(None); + let mnemonic = Mnemonic::generate(24).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 dd550f71a2..71a7d20041 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 = generateEntropyMnemonic(null) + val mnemonic1 = Mnemonic.generate(24u) val nodeEntropy1 = NodeEntropy.fromBip39Mnemonic(mnemonic1, null) val node1 = builder1.build(nodeEntropy1) - val mnemonic2 = generateEntropyMnemonic(null) + val mnemonic2 = Mnemonic.generate(24u) 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 90788c59e3..d41de589a0 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 = generateEntropyMnemonic(null) + val mnemonic1 = Mnemonic.generate(24u) val nodeEntropy1 = NodeEntropy.fromBip39Mnemonic(mnemonic1, null) val node1 = builder1.build(nodeEntropy1) - val mnemonic2 = generateEntropyMnemonic(null) + val mnemonic2 = Mnemonic.generate(24u) val nodeEntropy2 = NodeEntropy.fromBip39Mnemonic(mnemonic2, null) val node2 = builder2.build(nodeEntropy2) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 6763172f79..3829ddfb2c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -1,5 +1,4 @@ namespace ldk_node { - Mnemonic generate_entropy_mnemonic(WordCount? word_count); Config default_config(); }; @@ -15,8 +14,6 @@ typedef interface NodeEntropy; typedef interface ProbingConfig; -typedef enum WordCount; - [Remote] enum LogLevel { "Gossip", diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 0c50f05a3a..08a866c3f0 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -8,7 +8,6 @@ import socket from ldk_node import * -from ldk_node import ldk_node as bindings DEFAULT_ESPLORA_SERVER_URL = "http://127.0.0.1:3002" DEFAULT_TEST_NETWORK = Network.REGTEST @@ -99,7 +98,7 @@ def send_to_address(address, amount_sats): def setup_node(tmp_dir, esplora_endpoint, listening_addresses): - mnemonic = generate_entropy_mnemonic(None) + mnemonic = Mnemonic.generate(24) node_entropy = NodeEntropy.from_bip39_mnemonic(mnemonic, None) config = default_config() builder = Builder.from_config(config) @@ -209,7 +208,7 @@ def init_features_exposed(test_case, init_features): class TestMnemonic(unittest.TestCase): def test_invalid_mnemonic_returns_node_error(self): invalid_mnemonic = "abandon " * 11 + "abandon" - mnemonic_constructor = getattr(bindings.Mnemonic, "from_str", bindings.Mnemonic) + mnemonic_constructor = getattr(Mnemonic, "from_str", Mnemonic) with self.assertRaises(NodeError) as error: mnemonic_constructor(invalid_mnemonic) @@ -217,16 +216,16 @@ def test_invalid_mnemonic_returns_node_error(self): self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) def test_mnemonic_round_trip(self): - mnemonic = generate_entropy_mnemonic(None) - parsed_mnemonic = bindings.Mnemonic.from_str(str(mnemonic)) + mnemonic = Mnemonic.generate(24) + parsed_mnemonic = Mnemonic.from_str(str(mnemonic)) - self.assertIsInstance(mnemonic, bindings.Mnemonic) + self.assertIsInstance(mnemonic, Mnemonic) self.assertEqual(parsed_mnemonic, mnemonic) self.assertIsInstance(NodeEntropy.from_bip39_mnemonic(parsed_mnemonic, None), NodeEntropy) def test_mnemonic_functionality(self): entropy = bytes(16) - mnemonic = bindings.Mnemonic.from_entropy(entropy) + mnemonic = Mnemonic.from_entropy(entropy) self.assertEqual(mnemonic.words(), ["abandon"] * 11 + ["about"]) self.assertEqual(mnemonic.word_indices(), [0] * 11 + [3]) @@ -238,10 +237,15 @@ def test_mnemonic_functionality(self): "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553" "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", ) - self.assertEqual(bindings.Mnemonic.generate(WordCount.WORDS12).word_count(), 12) + self.assertEqual(Mnemonic.generate(12).word_count(), 12) with self.assertRaises(NodeError) as error: - bindings.Mnemonic.from_entropy(bytes(15)) + Mnemonic.generate(13) + + self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) + + with self.assertRaises(NodeError) as error: + Mnemonic.from_entropy(bytes(15)) self.assertIsInstance(error.exception, NodeError.InvalidMnemonic) diff --git a/src/entropy.rs b/src/entropy.rs index 3bcab832e8..f372bc5f9d 100644 --- a/src/entropy.rs +++ b/src/entropy.rs @@ -9,15 +9,12 @@ use std::fmt; -use bip39::rand::rngs::OsRng; -use bip39::{Language, Mnemonic as Bip39Mnemonic}; - use crate::config::WALLET_KEYS_SEED_LEN; -use crate::ffi::{maybe_deref, maybe_wrap}; +use crate::ffi::maybe_deref; use crate::io; #[cfg(not(feature = "uniffi"))] -type Mnemonic = Bip39Mnemonic; +type Mnemonic = bip39::Mnemonic; #[cfg(feature = "uniffi")] type Mnemonic = std::sync::Arc; @@ -124,89 +121,3 @@ impl fmt::Debug for NodeEntropy { 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 [`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 -pub fn generate_entropy_mnemonic(word_count: Option) -> Mnemonic { - let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); - let mnemonic = Bip39Mnemonic::generate_in_with(&mut OsRng, Language::English, word_count) - .expect("Failed to generate mnemonic"); - maybe_wrap(mnemonic) -} - -/// Supported BIP39 mnemonic word counts for entropy generation. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] -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 mnemonic_inner = maybe_deref(&mnemonic); - let entropy = mnemonic_inner.to_entropy(); - assert_eq!(mnemonic_inner, &Bip39Mnemonic::from_entropy(&entropy).unwrap()); - assert_eq!(mnemonic_inner.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 mnemonic_inner = maybe_deref(&mnemonic); - let entropy = mnemonic_inner.to_entropy(); - assert_eq!(mnemonic_inner, &Bip39Mnemonic::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_inner.word_count(), expected_words); - } - } -} diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 5e765ce83a..4972c636d5 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -151,7 +151,7 @@ impl VssClientHeaderProvider for VssHeaderProviderAdapter { use crate::builder::sanitize_alias; pub use crate::config::{default_config, ElectrumSyncConfig, EsploraSyncConfig, TorConfig}; -pub use crate::entropy::{generate_entropy_mnemonic, NodeEntropy, WordCount}; +pub use crate::entropy::NodeEntropy; use crate::error::Error; pub use crate::liquidity::LSPS1OrderStatus; pub use crate::logger::{LogLevel, LogRecord, LogWriter}; @@ -1175,14 +1175,11 @@ impl Mnemonic { } /// Generates a random English mnemonic with the specified word count. - /// - /// Defaults to 24 words when no word count is specified. #[uniffi::constructor] - pub fn generate(word_count: Option) -> Self { - let word_count = word_count.unwrap_or(WordCount::Words24).word_count(); - let inner = Bip39Mnemonic::generate(word_count) - .expect("WordCount always maps to a valid BIP 39 word count"); - Self { inner } + pub fn generate(word_count: u8) -> Result { + Bip39Mnemonic::generate(word_count.into()) + .map(Self::from) + .map_err(|_| Error::InvalidMnemonic) } /// Returns the words in the mnemonic. @@ -3009,7 +3006,8 @@ 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(Some(WordCount::Words12)).word_count(), 12); + assert_eq!(Mnemonic::generate(12).unwrap().word_count(), 12); + assert_eq!(Mnemonic::generate(13), Err(Error::InvalidMnemonic)); assert_eq!(Mnemonic::from_entropy(&[0; 15]), Err(Error::InvalidMnemonic)); } } diff --git a/src/lib.rs b/src/lib.rs index 2bee539f73..722fcbf5c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,8 @@ //! //! use ldk_node::bitcoin::secp256k1::PublicKey; //! use ldk_node::bitcoin::Network; -//! use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +//! use ldk_node::bip39::Mnemonic; +//! use ldk_node::entropy::NodeEntropy; //! use ldk_node::lightning::ln::msgs::SocketAddress; //! use ldk_node::lightning_invoice::Bolt11Invoice; //! use ldk_node::Builder; @@ -42,7 +43,7 @@ //! "https://rapidsync.lightningdevkit.org/testnet/v2/snapshot".to_string(), //! ); //! -//! let mnemonic = generate_entropy_mnemonic(None); +//! let mnemonic = Mnemonic::generate(24).unwrap(); //! let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); //! let node = builder.build(node_entropy).unwrap(); //! @@ -2212,11 +2213,14 @@ impl Node { /// /// For example, you could retrieve all stored outbound payments as follows: /// ``` + /// # #[cfg(not(feature = "uniffi"))] + /// # fn main() -> Result<(), ldk_node::NodeError> { /// # use ldk_node::Builder; /// # use ldk_node::config::Config; /// # use ldk_node::payment::{PaymentDetails, PaymentDirection}; /// # use ldk_node::bitcoin::Network; - /// # use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; + /// # use ldk_node::bip39::Mnemonic; + /// # use ldk_node::entropy::NodeEntropy; /// # use rand::distr::Alphanumeric; /// # use rand::{rng, Rng}; /// # let mut config = Config::default(); @@ -2226,7 +2230,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 = generate_entropy_mnemonic(None); + /// # let mnemonic = Mnemonic::generate(24).unwrap(); /// # let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); /// # let node = builder.build(node_entropy.into()).unwrap(); /// let mut outbound = Vec::new(); @@ -2241,7 +2245,10 @@ impl Node { /// None => break, /// } /// } - /// # Ok::<(), ldk_node::NodeError>(()) + /// # Ok(()) + /// # } + /// # #[cfg(feature = "uniffi")] + /// # fn main() {} /// ``` pub fn list_payments( &self, page_token: Option, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 85f618c958..7bbf408605 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -37,11 +37,12 @@ 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::config::{ AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig, HRNResolverConfig, HumanReadableNamesConfig, }; -use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy}; +use ldk_node::entropy::NodeEntropy; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{ PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, TransactionType, @@ -644,8 +645,11 @@ impl Default for TestConfig { let log_writer = Default::default(); let store_type = Default::default(); - let mnemonic = generate_entropy_mnemonic(None); + let mnemonic = Mnemonic::generate(24).unwrap(); + #[cfg(not(feature = "uniffi"))] let node_entropy = NodeEntropy::from_bip39_mnemonic(mnemonic, None); + #[cfg(feature = "uniffi")] + let node_entropy = NodeEntropy::from_seed_bytes(mnemonic.to_seed("").to_vec()).unwrap(); let async_payments_role = None; let wallet_rescan_from_height = None; let force_wallet_full_scan = false;