From 8a323ad75ea021b2bf09125f5ad452acf1e7e6b6 Mon Sep 17 00:00:00 2001 From: Cesar <142530682+cr-fuel@users.noreply.github.com> Date: Wed, 1 Nov 2023 02:16:42 -0300 Subject: [PATCH] Improvements for keygen cli and crates (#1465) Import improvements from https://github.com/FuelLabs/sway/pull/5153. Once this PR is merged and a new release is created sway will reference this crate instead of having its own copy (https://github.com/FuelLabs/sway/issues/5170) --- CHANGELOG.md | 1 + Cargo.lock | 55 +++++++++++- bin/keygen/Cargo.toml | 4 + bin/keygen/src/main.rs | 108 +++++++++++++++++++++-- crates/keygen/Cargo.toml | 4 +- crates/keygen/src/keygen.rs | 171 ------------------------------------ crates/keygen/src/lib.rs | 148 +++++++++++++++++++++++++++++-- 7 files changed, 301 insertions(+), 190 deletions(-) delete mode 100644 crates/keygen/src/keygen.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cf39b2f0b9d..1ebe7d31552 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 e7a8f3f9fbc..23496ab71a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1456,6 +1456,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.4.1", + "crossterm_winapi", + "libc", + "mio", + "parking_lot 0.12.1", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -2946,12 +2971,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]] @@ -2959,8 +2982,12 @@ name = "fuel-core-keygen-bin" version = "0.20.8" dependencies = [ "anyhow", + "atty", "clap 4.4.7", + "crossterm", "fuel-core-keygen", + "serde_json", + "termion", ] [[package]] @@ -5040,6 +5067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", + "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -7367,6 +7395,27 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" diff --git a/bin/keygen/Cargo.toml b/bin/keygen/Cargo.toml index 71735bcc491..31b27d1eb87 100644 --- a/bin/keygen/Cargo.toml +++ b/bin/keygen/Cargo.toml @@ -11,5 +11,9 @@ description = "Command line utilities for fuel-core key management" [dependencies] anyhow = { workspace = true } +atty = "0.2.14" clap = { workspace = true, features = ["derive", "env"] } +crossterm = "0.27.0" 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..8bc1f3c9398 100644 --- a/bin/keygen/src/main.rs +++ b/bin/keygen/src/main.rs @@ -1,25 +1,117 @@ //! A simple keygen cli utility tool for configuring fuel-core - +use atty::Stream; use clap::Parser; -use fuel_core_keygen::keygen; +use crossterm::terminal; +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]; + terminal::enable_raw_mode().expect("enable_raw_mode failed"); + stdin().read_exact(&mut single_key).unwrap(); + terminal::disable_raw_mode().expect("disable_raw_mode failed"); +} + +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 f714510391f..3676936731c 100644 --- a/crates/keygen/src/lib.rs +++ b/crates/keygen/src/lib.rs @@ -1,8 +1,146 @@ -//! Keygen crate - #![deny(clippy::cast_possible_truncation)] -pub const BLOCK_PRODUCTION: &str = "block-production"; -pub const P2P: &str = "p2p"; +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, +}; + +#[derive(Clone, Copy, Debug, Default, Serialize, ValueEnum)] +#[serde(rename_all = "kebab-case")] +pub enum KeyType { + #[default] + BlockProduction, + Peering, +} + +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 mod keygen; +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, + } + } + }) +}