diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e7126db2b..8d0047d6561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Description of the upcoming release here. ### Added +- [#1465](https://github.com/FuelLabs/fuel-core/pull/1465): Improvements for keygen cli and crates - [#1457](https://github.com/FuelLabs/fuel-core/pull/1457): Fixing incorrect measurement for fast(µs) opcodes. - [#1456](https://github.com/FuelLabs/fuel-core/pull/1456): Added flushing of the RocksDB during a graceful shutdown. - [#1456](https://github.com/FuelLabs/fuel-core/pull/1456): Added more logs to track the service lifecycle. diff --git a/Cargo.lock b/Cargo.lock index a533b9aa077..6c706106751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2951,12 +2951,10 @@ name = "fuel-core-keygen" version = "0.20.8" dependencies = [ "anyhow", - "atty", "clap 4.4.7", "fuel-core-types", "libp2p-identity 0.2.7", - "serde_json", - "termion", + "serde", ] [[package]] @@ -2964,8 +2962,11 @@ name = "fuel-core-keygen-bin" version = "0.20.8" dependencies = [ "anyhow", + "atty", "clap 4.4.7", "fuel-core-keygen", + "serde_json", + "termion", ] [[package]] diff --git a/bin/keygen/Cargo.toml b/bin/keygen/Cargo.toml index 71735bcc491..80419c9e09f 100644 --- a/bin/keygen/Cargo.toml +++ b/bin/keygen/Cargo.toml @@ -11,5 +11,8 @@ description = "Command line utilities for fuel-core key management" [dependencies] anyhow = { workspace = true } +atty = "0.2.14" clap = { workspace = true, features = ["derive", "env"] } fuel-core-keygen = { workspace = true } +serde_json = { workspace = true, features = ["raw_value"] } +termion = "2.0.1" diff --git a/bin/keygen/src/main.rs b/bin/keygen/src/main.rs index c05cef36f68..deee9c0f639 100644 --- a/bin/keygen/src/main.rs +++ b/bin/keygen/src/main.rs @@ -1,25 +1,114 @@ //! A simple keygen cli utility tool for configuring fuel-core - +use atty::Stream; use clap::Parser; -use fuel_core_keygen::keygen; +use fuel_core_keygen::{ + new_key, + parse_secret, + KeyType, +}; +use std::io::{ + stdin, + stdout, + Read, + Write, +}; +use termion::screen::IntoAlternateScreen; + +/// Parse a secret key to view the associated public key +#[derive(Debug, clap::Args)] +#[clap(author, version, about)] +pub struct ParseSecret { + /// A private key in hex format + secret: String, + /// Print the JSON in pretty format + #[clap(long = "pretty", short = 'p')] + pub pretty: bool, + /// Key type to generate. It can either be `block-production` or `peering`. + #[clap( + long = "key-type", + short = 'k', + value_enum, + default_value = >::into(KeyType::BlockProduction), + )] + pub key_type: KeyType, +} + +/// Generate a random new secret & public key in the format expected by fuel-core +#[derive(Debug, clap::Args)] +#[clap(author, version, about)] +pub struct NewKey { + /// Print the JSON in pretty format + #[clap(long = "pretty", short = 'p')] + pub pretty: bool, + /// Key type to generate. It can either be `block-production` or `peering`. + #[clap( + long = "key-type", + short = 'k', + value_enum, + default_value = >::into(KeyType::BlockProduction), + )] + pub key_type: KeyType, +} /// Key management utilities for configuring fuel-core #[derive(Debug, Parser)] pub(crate) enum Command { - New(keygen::NewKey), - Parse(keygen::ParseSecret), + New(NewKey), + Parse(ParseSecret), } impl Command { - pub(crate) fn exec(&self) -> anyhow::Result<()> { + pub(crate) fn exec(&self) -> anyhow::Result<(serde_json::Value, bool)> { match self { - Command::New(cmd) => cmd.exec(), - Command::Parse(cmd) => cmd.exec(), + Command::New(cmd) => { + Ok((serde_json::to_value(new_key(cmd.key_type)?)?, cmd.pretty)) + } + Command::Parse(cmd) => Ok(( + serde_json::to_value(parse_secret(cmd.key_type, &cmd.secret)?)?, + cmd.pretty, + )), } } } fn main() -> anyhow::Result<()> { let cmd = Command::parse(); - cmd.exec() + let (result, is_pretty) = cmd.exec()?; + print_value(result, is_pretty) +} + +fn wait_for_keypress() { + let mut single_key = [0u8]; + stdin().read_exact(&mut single_key).unwrap(); +} + +fn display_string_discreetly( + discreet_string: &str, + continue_message: &str, +) -> anyhow::Result<()> { + if atty::is(Stream::Stdout) { + let mut screen = stdout().into_alternate_screen()?; + writeln!(screen, "{discreet_string}")?; + screen.flush()?; + println!("{continue_message}"); + wait_for_keypress(); + } else { + println!("{discreet_string}"); + } + Ok(()) +} + +fn print_value(output: serde_json::Value, pretty: bool) -> anyhow::Result<()> { + let output = if pretty { + serde_json::to_string_pretty(&output) + } else { + serde_json::to_string(&output) + } + .map_err(anyhow::Error::msg); + + let _ = display_string_discreetly( + &output?, + "### Do not share or lose this private key! Press any key to complete. ###", + ); + Ok(()) } diff --git a/crates/keygen/Cargo.toml b/crates/keygen/Cargo.toml index 53569f0123e..2d47e837797 100644 --- a/crates/keygen/Cargo.toml +++ b/crates/keygen/Cargo.toml @@ -12,9 +12,7 @@ description = "Create to create command line utilities for fuel-core key managem [dependencies] anyhow = { workspace = true } -atty = "0.2.14" clap = { workspace = true, features = ["derive", "env"] } fuel-core-types = { workspace = true, features = ["serde", "random"] } libp2p-identity = { version = "0.2.4", features = ["secp256k1", "peerid"] } -serde_json = { workspace = true, features = ["raw_value"] } -termion = "2.0.1" +serde = { workspace = true, features = ["derive"] } diff --git a/crates/keygen/src/keygen.rs b/crates/keygen/src/keygen.rs deleted file mode 100644 index ba817f7040a..00000000000 --- a/crates/keygen/src/keygen.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::{ - BLOCK_PRODUCTION, - P2P, -}; -use atty::Stream; -use clap::ValueEnum; -use fuel_core_types::{ - fuel_crypto::{ - rand::{ - prelude::StdRng, - SeedableRng, - }, - SecretKey, - }, - fuel_tx::Input, -}; -use libp2p_identity::{ - secp256k1, - Keypair, - PeerId, -}; -use serde_json::json; -use std::{ - io::{ - stdin, - stdout, - Read, - Write, - }, - ops::Deref, - str::FromStr, -}; -use termion::screen::IntoAlternateScreen; - -/// Generate a random new secret & public key in the format expected by fuel-core -#[derive(Debug, clap::Args)] -#[clap(author, version, about)] -pub struct NewKey { - #[clap(long = "pretty", short = 'p')] - pretty: bool, - #[clap( - long = "key-type", - short = 'k', - value_enum, - default_value = BLOCK_PRODUCTION, - )] - key_type: KeyType, -} - -#[derive(Clone, Debug, Default, ValueEnum)] -pub enum KeyType { - #[default] - BlockProduction, - Peering, -} - -impl NewKey { - pub fn exec(&self) -> anyhow::Result<()> { - let mut rng = StdRng::from_entropy(); - let secret = SecretKey::random(&mut rng); - let public_key = secret.public_key(); - let secret_str = secret.to_string(); - - let output = match self.key_type { - KeyType::BlockProduction => { - let address = Input::owner(&public_key); - json!({ - "secret": secret_str, - "address": address, - "type": BLOCK_PRODUCTION, - }) - } - KeyType::Peering => { - let mut bytes = *secret.deref(); - let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes) - .expect("Should be a valid private key"); - let p2p_keypair = secp256k1::Keypair::from(p2p_secret); - let libp2p_keypair = Keypair::from(p2p_keypair); - let peer_id = PeerId::from_public_key(&libp2p_keypair.public()); - json!({ - "secret": secret_str, - "peer_id": peer_id.to_string(), - "type": P2P - }) - } - }; - print_value(output, self.pretty) - } -} - -/// Parse a secret key to view the associated public key -#[derive(Debug, clap::Args)] -#[clap(author, version, about)] -pub struct ParseSecret { - secret: String, - #[clap(long = "pretty", short = 'p')] - pretty: bool, - #[clap( - long = "key-type", - short = 'k', - value_enum, - default_value = BLOCK_PRODUCTION, - )] - key_type: KeyType, -} - -impl ParseSecret { - pub fn exec(&self) -> anyhow::Result<()> { - let secret = SecretKey::from_str(&self.secret) - .map_err(|_| anyhow::anyhow!("invalid secret key"))?; - match self.key_type { - KeyType::BlockProduction => { - let address = Input::owner(&secret.public_key()); - let output = json!({ - "address": address.to_string(), - "type": "block-production", - }); - print_value(output, self.pretty) - } - KeyType::Peering => { - let mut bytes = *secret.deref(); - let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes) - .expect("Should be a valid private key"); - let p2p_keypair = secp256k1::Keypair::from(p2p_secret); - let libp2p_keypair = Keypair::from(p2p_keypair); - let peer_id = PeerId::from_public_key(&libp2p_keypair.public()); - let output = json!({ - "peer_id": peer_id.to_string(), - "type": P2P - }); - print_value(output, self.pretty) - } - } - } -} - -fn wait_for_keypress() { - let mut single_key = [0u8]; - stdin().read_exact(&mut single_key).unwrap(); -} - -fn display_string_discreetly( - discreet_string: &str, - continue_message: &str, -) -> anyhow::Result<()> { - if atty::is(Stream::Stdout) { - let mut screen = stdout().into_alternate_screen()?; - writeln!(screen, "{discreet_string}")?; - screen.flush()?; - println!("{continue_message}"); - wait_for_keypress(); - } else { - println!("{discreet_string}"); - } - Ok(()) -} - -fn print_value(output: serde_json::Value, pretty: bool) -> anyhow::Result<()> { - let output = if pretty { - serde_json::to_string_pretty(&output) - } else { - serde_json::to_string(&output) - } - .map_err(anyhow::Error::msg); - - let _ = display_string_discreetly( - &output?, - "### Do not share or lose this private key! Press any key to complete. ###", - ); - Ok(()) -} diff --git a/crates/keygen/src/lib.rs b/crates/keygen/src/lib.rs index 8dfe2700546..7abe6a77972 100644 --- a/crates/keygen/src/lib.rs +++ b/crates/keygen/src/lib.rs @@ -1,6 +1,144 @@ -//! Keygen crate +use clap::ValueEnum; +use fuel_core_types::{ + fuel_crypto::{ + rand::{ + prelude::StdRng, + SeedableRng, + }, + SecretKey, + }, + fuel_tx::Input, + fuel_types::Address, +}; +use libp2p_identity::{ + secp256k1, + Keypair, + PeerId, +}; +use serde::Serialize; +use std::{ + ops::Deref, + str::FromStr, +}; -pub const BLOCK_PRODUCTION: &str = "block-production"; -pub const P2P: &str = "p2p"; +#[derive(Clone, Copy, Debug, Default, Serialize, ValueEnum)] +#[serde(rename_all = "kebab-case")] +pub enum KeyType { + #[default] + BlockProduction, + Peering, +} -pub mod keygen; +impl From for &'static str { + fn from(key_type: KeyType) -> Self { + match key_type { + KeyType::BlockProduction => "block-production", + KeyType::Peering => "p2p", + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct ParseSecretResponse { + #[serde(skip_serializing_if = "Option::is_none")] + address: Option
, + #[serde( + serialize_with = "serialize_option_to_string", + skip_serializing_if = "Option::is_none" + )] + peer_id: Option, + #[serde(rename = "type")] + typ: KeyType, +} + +#[derive(Clone, Debug, Serialize)] +pub struct NewKeyResponse { + secret: SecretKey, + #[serde(skip_serializing_if = "Option::is_none")] + address: Option
, + #[serde( + serialize_with = "serialize_option_to_string", + skip_serializing_if = "Option::is_none" + )] + peer_id: Option, + #[serde(rename = "type")] + typ: KeyType, +} + +fn serialize_option_to_string( + opt: &Option, + serializer: S, +) -> Result +where + S: serde::Serializer, + T: ToString, +{ + if let Some(value) = opt.as_ref() { + value.to_string().serialize(serializer) + } else { + serializer.serialize_none() + } +} + +pub fn new_key(key_type: KeyType) -> anyhow::Result { + let mut rng = StdRng::from_entropy(); + let secret = SecretKey::random(&mut rng); + let public_key = secret.public_key(); + + Ok(match key_type { + KeyType::BlockProduction => { + let address = Input::owner(&public_key); + NewKeyResponse { + secret, + address: Some(address), + peer_id: None, + typ: key_type, + } + } + KeyType::Peering => { + let mut bytes = *secret.deref(); + let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes) + .expect("Should be a valid private key"); + let p2p_keypair = secp256k1::Keypair::from(p2p_secret); + let libp2p_keypair = Keypair::from(p2p_keypair); + let peer_id = PeerId::from_public_key(&libp2p_keypair.public()); + NewKeyResponse { + secret, + address: None, + peer_id: Some(peer_id), + typ: key_type, + } + } + }) +} + +pub fn parse_secret( + key_type: KeyType, + secret: &str, +) -> anyhow::Result { + let secret = + SecretKey::from_str(secret).map_err(|_| anyhow::anyhow!("invalid secret key"))?; + Ok(match key_type { + KeyType::BlockProduction => { + let address = Input::owner(&secret.public_key()); + ParseSecretResponse { + address: Some(address), + peer_id: None, + typ: key_type, + } + } + KeyType::Peering => { + let mut bytes = *secret.deref(); + let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes) + .expect("Should be a valid private key"); + let p2p_keypair = secp256k1::Keypair::from(p2p_secret); + let libp2p_keypair = Keypair::from(p2p_keypair); + let peer_id = PeerId::from_public_key(&libp2p_keypair.public()); + ParseSecretResponse { + address: None, + peer_id: Some(peer_id), + typ: key_type, + } + } + }) +}