Skip to content

Commit 18ed800

Browse files
ovitrifclaude
andcommitted
fix: derive_node_secret_from_mnemonic to match KeysManager
The function now correctly replicates LDK's KeysManager node_secret derivation: 1. BIP39 seed (64 bytes) → BIP32 master key (32 bytes) 2. Those 32 bytes as new seed → BIP32 master → derive m/0' → node_secret This ensures the derived key matches what a running Node instance would use, enabling proper backup authentication before the node starts. Added verification tests that compare against LDK's KeysManager output. Also bumps version to v0.7.0-rc.5 and updates bindings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a7a4726 commit 18ed800

7 files changed

Lines changed: 67 additions & 7 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ldk-node"
3-
version = "0.7.0-rc.4"
3+
version = "0.7.0-rc.5"
44
authors = ["Elias Rohrer <dev@tnull.de>"]
55
homepage = "https://lightningdevkit.org/"
66
license = "MIT OR Apache-2.0"

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import PackageDescription
55

6-
let tag = "v0.7.0-rc.4"
7-
let checksum = "6d3871ce5178f5b1f29721914820ae5f8161b6ed60464537b3aad490a3a560a7"
6+
let tag = "v0.7.0-rc.5"
7+
let checksum = "4ef5195192cdb079f58e87d2b8b80943b223d764b6e93a6c7146b9583cfcc064"
88
let url = "https://github.com/synonymdev/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip"
99

1010
let package = Package(

bindings/kotlin/ldk-node-android/gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m
22
android.useAndroidX=true
33
android.enableJetifier=true
44
kotlin.code.style=official
5-
libraryVersion=0.7.0-rc.4
5+
libraryVersion=0.7.0-rc.5
Binary file not shown.
Binary file not shown.
Binary file not shown.

src/io/utils.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,36 @@ pub fn generate_entropy_mnemonic(word_count: Option<WordCount>) -> Mnemonic {
7474
/// This is the same key that would be used by a [`Node`] built with this mnemonic via
7575
/// [`Builder::set_entropy_bip39_mnemonic`].
7676
///
77+
/// The derivation follows LDK's KeysManager behavior:
78+
/// 1. BIP39 seed (64 bytes) → BIP32 master key (32 bytes)
79+
/// 2. Those 32 bytes as new seed → BIP32 master → derive m/0' → node_secret
80+
///
7781
/// [`Node`]: crate::Node
7882
/// [`Builder::set_entropy_bip39_mnemonic`]: crate::Builder::set_entropy_bip39_mnemonic
7983
pub fn derive_node_secret_from_mnemonic(
8084
mnemonic: String, passphrase: Option<String>,
8185
) -> Result<Vec<u8>, Error> {
82-
let parsed_mnemonic = Mnemonic::parse(&mnemonic).map_err(|_| Error::InvalidMnemonic)?;
86+
use bitcoin::bip32::ChildNumber;
87+
use bitcoin::secp256k1::Secp256k1;
8388

89+
let parsed_mnemonic = Mnemonic::parse(&mnemonic).map_err(|_| Error::InvalidMnemonic)?;
8490
let seed = parsed_mnemonic.to_seed(passphrase.as_deref().unwrap_or(""));
8591

92+
// First BIP32 derivation: 64-byte BIP39 seed → 32-byte master private key
8693
let xpriv =
8794
Xpriv::new_master(Network::Bitcoin, &seed).map_err(|_| Error::InvalidMnemonic)?;
88-
89-
Ok(xpriv.private_key.secret_bytes().to_vec())
95+
let ldk_seed: [u8; 32] = xpriv.private_key.secret_bytes();
96+
97+
// Second BIP32 derivation: KeysManager treats the 32-byte key as a new seed
98+
// and derives node_secret at path m/0'
99+
let secp = Secp256k1::new();
100+
let keys_master =
101+
Xpriv::new_master(Network::Bitcoin, &ldk_seed).map_err(|_| Error::InvalidMnemonic)?;
102+
let node_secret_xpriv = keys_master
103+
.derive_priv(&secp, &[ChildNumber::from_hardened_idx(0).unwrap()])
104+
.map_err(|_| Error::InvalidMnemonic)?;
105+
106+
Ok(node_secret_xpriv.private_key.secret_bytes().to_vec())
90107
}
91108

92109
pub(crate) fn read_or_generate_seed_file<L: Deref>(
@@ -645,6 +662,7 @@ pub(crate) fn read_bdk_wallet_change_set(
645662
#[cfg(test)]
646663
mod tests {
647664
use super::*;
665+
use lightning::sign::KeysManager as LdkKeysManager;
648666

649667
#[test]
650668
fn mnemonic_to_entropy_to_mnemonic() {
@@ -679,4 +697,46 @@ mod tests {
679697
assert_eq!(mnemonic.word_count(), expected_words);
680698
}
681699
}
700+
701+
#[test]
702+
fn derive_node_secret_matches_keys_manager() {
703+
// Standard test mnemonic (BIP39 test vector)
704+
let mnemonic =
705+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
706+
707+
// Derive using our function
708+
let derived_secret =
709+
derive_node_secret_from_mnemonic(mnemonic.to_string(), None).unwrap();
710+
711+
// Derive using LDK's KeysManager (same flow as Builder)
712+
let parsed = Mnemonic::parse(mnemonic).unwrap();
713+
let seed = parsed.to_seed("");
714+
let xpriv = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
715+
let ldk_seed: [u8; 32] = xpriv.private_key.secret_bytes();
716+
717+
let keys_manager = LdkKeysManager::new(&ldk_seed, 0, 0, false);
718+
let expected_secret = keys_manager.get_node_secret_key();
719+
720+
assert_eq!(derived_secret, expected_secret.secret_bytes().to_vec());
721+
}
722+
723+
#[test]
724+
fn derive_node_secret_with_passphrase() {
725+
let mnemonic =
726+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
727+
let passphrase = Some("test_passphrase".to_string());
728+
729+
let derived_secret =
730+
derive_node_secret_from_mnemonic(mnemonic.to_string(), passphrase).unwrap();
731+
732+
let parsed = Mnemonic::parse(mnemonic).unwrap();
733+
let seed = parsed.to_seed("test_passphrase");
734+
let xpriv = Xpriv::new_master(Network::Bitcoin, &seed).unwrap();
735+
let ldk_seed: [u8; 32] = xpriv.private_key.secret_bytes();
736+
737+
let keys_manager = LdkKeysManager::new(&ldk_seed, 0, 0, false);
738+
let expected_secret = keys_manager.get_node_secret_key();
739+
740+
assert_eq!(derived_secret, expected_secret.secret_bytes().to_vec());
741+
}
682742
}

0 commit comments

Comments
 (0)