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..219a017a0 100644 --- a/libs/gl-cli/src/node.rs +++ b/libs/gl-cli/src/node.rs @@ -1,6 +1,6 @@ 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 clap::Subcommand; use futures::stream::StreamExt; use gl_client::pb::StreamLogRequest; @@ -14,6 +14,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 +104,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 +198,35 @@ 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 = match mnemonic { + Some(_) => "Secret seed derived from user provided mnemonic", + None => "Your recovery mnemonic is", + }; + let (seed, mnemonic) = util::generate_seed(mnemonic)?; + util::write_seed(&seed_path, &seed)?; + + // 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) { 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..1dc044841 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,19 @@ 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 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)) } pub fn read_seed(file_path: impl AsRef) -> Option> { @@ -81,6 +88,67 @@ 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), + #[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; + +#[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"))); + } +}