From 315f5688cd6ffba4048ce8d3b4d4cb7c317f967a Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 11 Feb 2026 17:21:00 +0100 Subject: [PATCH 1/4] glcli: add node init Add "node init" command to initialize the local data directory for the greenlight node. Including the generation of the hsm_secret file either from user provided mnemonic phrase or by generating a new one. The user is shown on screen the mnemonic phrase from which the secrets are derived. Signed-off-by: Lagrang3 --- libs/gl-cli/Cargo.toml | 1 + libs/gl-cli/src/node.rs | 55 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/libs/gl-cli/Cargo.toml b/libs/gl-cli/Cargo.toml index 493ec2333..4090f2749 100644 --- a/libs/gl-cli/Cargo.toml +++ b/libs/gl-cli/Cargo.toml @@ -18,6 +18,7 @@ test = true doc = true [dependencies] +bip39 = { version = "2.2", features = ["rand"] } clap = { version = "4.5", features = ["derive"] } dirs = "6.0" env_logger = "0.11" diff --git a/libs/gl-cli/src/node.rs b/libs/gl-cli/src/node.rs index 25c154f14..a1aa4bd8b 100644 --- a/libs/gl-cli/src/node.rs +++ b/libs/gl-cli/src/node.rs @@ -1,10 +1,13 @@ use crate::error::{Error, Result}; use crate::model; -use crate::util::{self, CREDENTIALS_FILE_NAME}; +use crate::util::{self, CREDENTIALS_FILE_NAME, SEED_FILE_NAME}; +use bip39::Mnemonic; use clap::Subcommand; use futures::stream::StreamExt; use gl_client::pb::StreamLogRequest; use gl_client::{bitcoin::Network, pb::cln}; +use std::fs::File; +use std::io::Write; use std::path::Path; pub struct Config> { @@ -14,6 +17,12 @@ pub struct Config> { #[derive(Subcommand, Debug)] pub enum Command { + /// Creates a new node + #[command(name = "init")] + Init { + #[arg(long)] + mnemonic: Option, + }, /// Stream logs to stdout Log, /// Returns some basic node info @@ -98,6 +107,7 @@ pub enum Command { pub async fn command_handler>(cmd: Command, config: Config

) -> Result<()> { match cmd { + Command::Init { mnemonic } => init_handler(config, mnemonic).await, Command::Log => log(config).await, Command::GetInfo => getinfo_handler(config).await, Command::Invoice { @@ -191,6 +201,49 @@ pub async fn command_handler>(cmd: Command, config: Config

) -> } } +async fn init_handler>(config: Config

, mnemonic: Option) -> Result<()> { + // Check if seed already exists in the configuration path + let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME); + if let Some(_) = util::read_seed(&seed_path) { + return Err(Error::custom(format!( + "Seed already exists at {}", + seed_path.to_string_lossy() + ))); + } else { + std::fs::create_dir_all(config.data_dir.as_ref()) + .map_err(|e| Error::custom(format!("Failed to create data directory: {e}")))?; + println!( + "Local greenlight directory created at {}", + config.data_dir.as_ref().to_string_lossy() + ); + } + + let message; + // generate mnemonic if not provided + let mnemonic = match mnemonic { + Some(sentence) => { + message = "Secret seed derived from user provided mnemonic"; + Mnemonic::parse(sentence).map_err(|e| Error::custom(format!("Bad mnemonic: {e}")))? + } + None => { + message = "Your recovery mnemonic is"; + Mnemonic::generate(12) + .map_err(|e| Error::custom(format!("Failed to generate mnemonic: {e}")))? + } + }; + + // create hsm_secret file + let seed = &mnemonic.to_seed("")[0..32]; + let mut file = File::create(seed_path) + .map_err(|e| Error::custom(format!("Failed to create seed: {e}")))?; + file.write_all(seed) + .map_err(|e| Error::custom(format!("Failed to write seed to file: {e}")))?; + + // report after success + println!("{message}: {mnemonic}"); + Ok(()) +} + async fn log>(config: Config

) -> Result<()> { let creds_path = config.data_dir.as_ref().join(CREDENTIALS_FILE_NAME); let creds = match util::read_credentials(&creds_path) { From 6d165392cbe6439b0a27e89b2212021a078219e4 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 11 Feb 2026 22:17:16 +0100 Subject: [PATCH 2/4] gl-cli: remove mnemonicless seed generation Remove footgun: a seed is always derived from a BIP39 mnemonic, in this way every wallet can be recovered from a known mnemonic. Let it be built with "glcli node init" or "glcli scheduler register". Signed-off-by: Lagrang3 --- libs/gl-cli/src/node.rs | 27 +++++---------------------- libs/gl-cli/src/scheduler.rs | 5 +++-- libs/gl-cli/src/util.rs | 17 +++++++++++------ 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/libs/gl-cli/src/node.rs b/libs/gl-cli/src/node.rs index a1aa4bd8b..219a017a0 100644 --- a/libs/gl-cli/src/node.rs +++ b/libs/gl-cli/src/node.rs @@ -1,13 +1,10 @@ use crate::error::{Error, Result}; use crate::model; use crate::util::{self, CREDENTIALS_FILE_NAME, SEED_FILE_NAME}; -use bip39::Mnemonic; use clap::Subcommand; use futures::stream::StreamExt; use gl_client::pb::StreamLogRequest; use gl_client::{bitcoin::Network, pb::cln}; -use std::fs::File; -use std::io::Write; use std::path::Path; pub struct Config> { @@ -218,26 +215,12 @@ async fn init_handler>(config: Config

, mnemonic: Option { - message = "Secret seed derived from user provided mnemonic"; - Mnemonic::parse(sentence).map_err(|e| Error::custom(format!("Bad mnemonic: {e}")))? - } - None => { - message = "Your recovery mnemonic is"; - Mnemonic::generate(12) - .map_err(|e| Error::custom(format!("Failed to generate mnemonic: {e}")))? - } + let message = match mnemonic { + Some(_) => "Secret seed derived from user provided mnemonic", + None => "Your recovery mnemonic is", }; - - // create hsm_secret file - let seed = &mnemonic.to_seed("")[0..32]; - let mut file = File::create(seed_path) - .map_err(|e| Error::custom(format!("Failed to create seed: {e}")))?; - file.write_all(seed) - .map_err(|e| Error::custom(format!("Failed to write seed to file: {e}")))?; + let (seed, mnemonic) = util::generate_seed(mnemonic)?; + util::write_seed(&seed_path, &seed)?; // report after success println!("{message}: {mnemonic}"); diff --git a/libs/gl-cli/src/scheduler.rs b/libs/gl-cli/src/scheduler.rs index da37def0e..ce11d90bc 100644 --- a/libs/gl-cli/src/scheduler.rs +++ b/libs/gl-cli/src/scheduler.rs @@ -90,12 +90,13 @@ async fn register_handler>( let seed_path = config.data_dir.as_ref().join(SEED_FILE_NAME); let seed = match util::read_seed(&seed_path) { Some(seed) => { - println!("Seed already exists at {}, usign it", seed_path.display()); + println!("Seed already exists at {}, using it", seed_path.display()); seed } None => { // Generate a new seed and save it. - let seed = util::generate_seed(); + let (seed, mnemonic) = util::generate_seed(None)?; + println!("New seed generated from mnemonic: {mnemonic}"); util::write_seed(&seed_path, &seed)?; println!("Seed saved to {}", seed_path.display()); seed.to_vec() diff --git a/libs/gl-cli/src/util.rs b/libs/gl-cli/src/util.rs index 6d11c24b8..bf152fe72 100644 --- a/libs/gl-cli/src/util.rs +++ b/libs/gl-cli/src/util.rs @@ -1,5 +1,4 @@ use dirs; -use gl_client::bitcoin::secp256k1::rand::{self, RngCore}; use gl_client::credentials; use std::path::PathBuf; use std::{ @@ -15,11 +14,13 @@ pub const DEFAULT_GREENLIGHT_DIR: &str = "greenlight"; // -- Seed section -pub fn generate_seed() -> [u8; 32] { - let mut seed = [0u8; 32]; - let mut rng = rand::thread_rng(); - rng.fill_bytes(&mut seed); - seed +pub fn generate_seed(words: Option) -> Result<([u8; 32], bip39::Mnemonic)> { + let mnemonic = match words { + Some(sentence) => bip39::Mnemonic::parse(sentence)?, + None => bip39::Mnemonic::generate(12)?, + }; + let seed: [u8; 32] = mnemonic.to_seed("")[0..32].try_into()?; + Ok((seed, mnemonic)) } pub fn read_seed(file_path: impl AsRef) -> Option> { @@ -81,6 +82,10 @@ impl AsRef for DataDir { pub enum UtilsError { #[error(transparent)] IoError(#[from] std::io::Error), + #[error(transparent)] + MnemonicError(#[from] bip39::Error), + #[error(transparent)] + DataError(#[from] std::array::TryFromSliceError), } type Result = core::result::Result; From 5e53ccafda99e551279104b99026cca32aeb2de5 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 11 Feb 2026 22:40:18 +0100 Subject: [PATCH 3/4] glcli: constraint mnemonic to 12 words Signed-off-by: Lagrang3 --- libs/gl-cli/src/util.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libs/gl-cli/src/util.rs b/libs/gl-cli/src/util.rs index bf152fe72..fb5b666fa 100644 --- a/libs/gl-cli/src/util.rs +++ b/libs/gl-cli/src/util.rs @@ -19,6 +19,12 @@ pub fn generate_seed(words: Option) -> Result<([u8; 32], bip39::Mnemonic Some(sentence) => bip39::Mnemonic::parse(sentence)?, None => bip39::Mnemonic::generate(12)?, }; + let n = mnemonic.word_count(); + if n != 12 { + return Err(UtilsError::custom(format!( + "Mnemonic contains {n} words, but 12 were expected." + ))); + } let seed: [u8; 32] = mnemonic.to_seed("")[0..32].try_into()?; Ok((seed, mnemonic)) } @@ -86,6 +92,14 @@ pub enum UtilsError { MnemonicError(#[from] bip39::Error), #[error(transparent)] DataError(#[from] std::array::TryFromSliceError), + #[error("{0}")] + Custom(String), +} + +impl UtilsError { + pub fn custom(e: impl std::fmt::Display) -> UtilsError { + UtilsError::Custom(e.to_string()) + } } type Result = core::result::Result; From 6dd66ff8e0d7936fc12fe69ec0cdf0bd74c36bf6 Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Wed, 11 Feb 2026 23:20:33 +0100 Subject: [PATCH 4/4] gl-cli: add unittest for seed generation Signed-off-by: Lagrang3 --- libs/gl-cli/src/util.rs | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/libs/gl-cli/src/util.rs b/libs/gl-cli/src/util.rs index fb5b666fa..1dc044841 100644 --- a/libs/gl-cli/src/util.rs +++ b/libs/gl-cli/src/util.rs @@ -103,3 +103,52 @@ impl UtilsError { } type Result = core::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gen_seed1() { + let (_, mnemonic) = generate_seed(None).unwrap(); + assert_eq!(mnemonic.word_count(), 12); + } + + #[test] + fn gen_seed2() { + let sentence = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(); + let expected_seed = [ + 0x5e, 0xb0, 0x0b, 0xbd, 0xdc, 0xf0, 0x69, 0x08, 0x48, 0x89, 0xa8, 0xab, 0x91, 0x55, + 0x56, 0x81, 0x65, 0xf5, 0xc4, 0x53, 0xcc, 0xb8, 0x5e, 0x70, 0x81, 0x1a, 0xae, 0xd6, + 0xf6, 0xda, 0x5f, 0xc1, + ]; + let (seed, mnemonic) = generate_seed(Some(sentence.clone())).unwrap(); + assert_eq!(seed, expected_seed); + assert_eq!(mnemonic.to_string(), sentence); + } + + #[test] + fn gen_seed3() { + // 0 words, invalid mnemonic + let result = generate_seed(Some("".to_string())); + assert!(result.is_err_and(|e| e.to_string().contains("invalid word count: 0"))); + + // 11 words, invalid mnemonic + let result = generate_seed(Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon".to_string())); + assert!(result.is_err_and(|e| e.to_string().contains("invalid word count: 11"))); + + // 15 words, valid mnemonic but we want 12 words + let result = generate_seed(Some("birth danger dismiss bounce ostrich museum model glory depth seed clip pitch skull carpet myself".to_string())); + assert!(result.is_err_and(|e| e + .to_string() + .contains("contains 15 words, but 12 were expected"))); + + // 12 words, but invalid word at the end + let result = generate_seed(Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon pizzzzza".to_string())); + assert!(result.is_err_and(|e| e.to_string().contains("unknown word (word 11)"))); + + // 12 words, but invalid checksum + let result = generate_seed(Some("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon pizza".to_string())); + assert!(result.is_err_and(|e| e.to_string().contains("invalid checksum"))); + } +}