Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# 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.
- `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
Expand All @@ -23,6 +29,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
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 2 additions & 5 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
namespace ldk_node {
Mnemonic generate_entropy_mnemonic(WordCount? word_count);
Config default_config();
};

Expand All @@ -15,8 +14,6 @@ typedef interface NodeEntropy;

typedef interface ProbingConfig;

typedef enum WordCount;

[Remote]
enum LogLevel {
"Gossip",
Expand Down Expand Up @@ -220,6 +217,7 @@ enum NodeError {
"InvalidSocketAddress",
"InvalidPublicKey",
"InvalidSecretKey",
"InvalidMnemonic",
"InvalidOfferId",
"InvalidNodeId",
"InvalidPaymentId",
Expand Down Expand Up @@ -419,8 +417,7 @@ typedef string ChannelId;
[Custom]
typedef string UserChannelId;

[Custom]
typedef string Mnemonic;
typedef interface Mnemonic;

[Custom]
typedef string UntrustedString;
Expand Down
46 changes: 45 additions & 1 deletion bindings/python/src/ldk_node/test_ldk_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,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)
Expand Down Expand Up @@ -205,6 +205,50 @@ 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(Mnemonic, "from_str", Mnemonic)

with self.assertRaises(NodeError) as error:
mnemonic_constructor(invalid_mnemonic)

self.assertIsInstance(error.exception, NodeError.InvalidMnemonic)

def test_mnemonic_round_trip(self):
mnemonic = Mnemonic.generate(24)
parsed_mnemonic = Mnemonic.from_str(str(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 = 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(Mnemonic.generate(12).word_count(), 12)

with self.assertRaises(NodeError) as error:
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)


class TestLdkNode(unittest.TestCase):
def setUp(self):
Expand Down
93 changes: 7 additions & 86 deletions src/entropy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@

use std::fmt;

use bip39::rand::rngs::OsRng;
use bip39::{Language, Mnemonic};

use crate::config::WALLET_KEYS_SEED_LEN;
use crate::ffi::maybe_deref;
use crate::io;

#[cfg(not(feature = "uniffi"))]
type Mnemonic = bip39::Mnemonic;
#[cfg(feature = "uniffi")]
type Mnemonic = std::sync::Arc<crate::ffi::Mnemonic>;

/// An error that could arise during [`NodeEntropy`] construction.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
Expand Down Expand Up @@ -67,6 +70,7 @@ impl NodeEntropy {
/// [`Node`]: crate::Node
#[cfg_attr(feature = "uniffi", uniffi::constructor)]
pub fn from_bip39_mnemonic(mnemonic: Mnemonic, passphrase: Option<String>) -> Self {
let mnemonic = maybe_deref(&mnemonic);
match passphrase {
Some(passphrase) => Self(mnemonic.to_seed(passphrase)),
None => Self(mnemonic.to_seed("")),
Expand Down Expand Up @@ -117,86 +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<WordCount>) -> 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")
}

/// 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 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);
}
}
}
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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."),
Expand Down
Loading
Loading