From 2cc6e0a0d7e4742795e050507b37de41a54c4aad Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Wed, 24 Jan 2024 01:07:16 +0200 Subject: [PATCH 1/3] Refactor `subspace-node domain insert-key` to `subspace-node domain key insert` as we'll have more subcommands there --- crates/subspace-node/src/domain/cli.rs | 13 ++++++++++--- crates/subspace-node/src/main.rs | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/subspace-node/src/domain/cli.rs b/crates/subspace-node/src/domain/cli.rs index d732ff8ae8..22a3c6614e 100644 --- a/crates/subspace-node/src/domain/cli.rs +++ b/crates/subspace-node/src/domain/cli.rs @@ -39,12 +39,13 @@ use std::net::SocketAddr; use std::path::Path; use subspace_runtime::Block; -/// Sub-commands supported by the executor. +/// Sub-commands supported by the operator. #[derive(Debug, clap::Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Subcommand { - /// Insert key into domain's keystore - InsertKey(InsertDomainKeyOptions), + /// Domain key management + #[clap(subcommand)] + Key(DomainKey), /// Export the state of a given block into a chain spec. ExportState(sc_cli::ExportStateCmd), @@ -63,6 +64,12 @@ pub enum Subcommand { ExportExecutionReceipt(ExportExecutionReceiptCmd), } +#[derive(Debug, clap::Subcommand)] +pub enum DomainKey { + /// Insert key into domain's keystore + Insert(InsertDomainKeyOptions), +} + #[derive(Debug, Parser)] pub struct DomainCli { /// Run a domain node. diff --git a/crates/subspace-node/src/main.rs b/crates/subspace-node/src/main.rs index ecc142462d..69ce924512 100644 --- a/crates/subspace-node/src/main.rs +++ b/crates/subspace-node/src/main.rs @@ -24,6 +24,7 @@ mod cli; mod domain; use crate::cli::{Cli, SubspaceCliPlaceholder}; +use crate::domain::cli::DomainKey; use crate::domain::{DomainCli, DomainSubcommand}; use clap::Parser; use domain_runtime_primitives::opaque::Block as DomainBlock; @@ -311,7 +312,7 @@ fn main() -> Result<(), Error> { })?; } Cli::Domain(domain_cmd) => match domain_cmd { - DomainSubcommand::InsertKey(insert_domain_key_options) => { + DomainSubcommand::Key(DomainKey::Insert(insert_domain_key_options)) => { commands::insert_domain_key(insert_domain_key_options)?; } DomainSubcommand::Benchmark(cmd) => { From 96643dab32314c680fe6366a6299ae46b68f356d Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Wed, 24 Jan 2024 01:13:24 +0200 Subject: [PATCH 2/3] Refactor common keystore options for better reuse --- crates/subspace-node/src/commands.rs | 4 ++-- .../{insert_domain_key.rs => domain_key.rs} | 17 +++++++++------ .../subspace-node/src/commands/run/domain.rs | 21 ++++++++++++------- crates/subspace-node/src/commands/shared.rs | 9 +------- 4 files changed, 28 insertions(+), 23 deletions(-) rename crates/subspace-node/src/commands/{insert_domain_key.rs => domain_key.rs} (81%) diff --git a/crates/subspace-node/src/commands.rs b/crates/subspace-node/src/commands.rs index 6f9227ea5c..5805bf26a3 100644 --- a/crates/subspace-node/src/commands.rs +++ b/crates/subspace-node/src/commands.rs @@ -1,8 +1,8 @@ -mod insert_domain_key; +mod domain_key; mod run; mod shared; mod wipe; -pub use insert_domain_key::{insert_domain_key, InsertDomainKeyOptions}; +pub use domain_key::{insert_domain_key, InsertDomainKeyOptions}; pub use run::{run, RunOptions}; pub use wipe::{wipe, WipeOptions}; diff --git a/crates/subspace-node/src/commands/insert_domain_key.rs b/crates/subspace-node/src/commands/domain_key.rs similarity index 81% rename from crates/subspace-node/src/commands/insert_domain_key.rs rename to crates/subspace-node/src/commands/domain_key.rs index 4ed69df851..0af4d45faf 100644 --- a/crates/subspace-node/src/commands/insert_domain_key.rs +++ b/crates/subspace-node/src/commands/domain_key.rs @@ -2,11 +2,12 @@ use crate::commands::shared::{init_logger, store_key_in_keystore, KeystoreOption use clap::Parser; use sc_cli::{Error, KeystoreParams}; use sc_service::config::KeystoreConfig; +use sp_core::crypto::SecretString; use sp_domains::DomainId; use std::path::PathBuf; use tracing::info; -/// Options for running a node +/// Options for inserting domain key #[derive(Debug, Parser)] pub struct InsertDomainKeyOptions { /// Base path where to store node files @@ -15,9 +16,16 @@ pub struct InsertDomainKeyOptions { /// ID of the domain to store key for #[arg(long, required = true)] domain_id: DomainId, + /// Operator secret key URI to insert into keystore. + /// + /// Example: "//Alice". + /// + /// If the value is a file, the file content is used as URI. + #[arg(long, required = true)] + keystore_suri: SecretString, /// Options for domain keystore #[clap(flatten)] - keystore_options: KeystoreOptions, + keystore_options: KeystoreOptions, } pub fn insert_domain_key(options: InsertDomainKeyOptions) -> Result<(), Error> { @@ -26,6 +34,7 @@ pub fn insert_domain_key(options: InsertDomainKeyOptions) -> Result<(), Error> { let InsertDomainKeyOptions { base_path, domain_id, + keystore_suri, keystore_options, } = options; let domain_path = base_path.join("domains").join(domain_id.to_string()); @@ -39,10 +48,6 @@ pub fn insert_domain_key(options: InsertDomainKeyOptions) -> Result<(), Error> { let keystore_config = keystore_params.keystore_config(&domain_path)?; - let Some(keystore_suri) = keystore_options.keystore_suri else { - unreachable!("--keystore-suri is set to required; qed"); - }; - let (path, password) = match &keystore_config { KeystoreConfig::Path { path, password, .. } => (path.clone(), password.clone()), KeystoreConfig::InMemory => { diff --git a/crates/subspace-node/src/commands/run/domain.rs b/crates/subspace-node/src/commands/run/domain.rs index 0ff43fbacd..b6fbc7d2fd 100644 --- a/crates/subspace-node/src/commands/run/domain.rs +++ b/crates/subspace-node/src/commands/run/domain.rs @@ -105,9 +105,17 @@ pub(super) struct DomainOptions { #[clap(flatten)] network_options: SubstrateNetworkOptions, + /// Operator secret key URI to insert into keystore. + /// + /// Example: "//Alice". + /// + /// If the value is a file, the file content is used as URI. + #[arg(long)] + keystore_suri: Option, + /// Options for domain keystore #[clap(flatten)] - keystore_options: KeystoreOptions, + keystore_options: KeystoreOptions, /// Options for transaction pool #[clap(flatten)] @@ -138,7 +146,8 @@ pub(super) fn create_domain_configuration( prometheus_listen_on, pruning_params, network_options, - mut keystore_options, + mut keystore_suri, + keystore_options, pool_config, additional_args, } = domain_options; @@ -152,10 +161,8 @@ pub(super) fn create_domain_configuration( if operator_id.is_none() { operator_id.replace(OperatorId::default()); } - if keystore_options.keystore_suri.is_none() { - keystore_options - .keystore_suri - .replace(SecretString::new("//Alice".to_string())); + if keystore_suri.is_none() { + keystore_suri.replace(SecretString::new("//Alice".to_string())); } } @@ -285,7 +292,7 @@ pub(super) fn create_domain_configuration( let keystore_config = keystore_params.keystore_config(&base_path)?; - if let Some(keystore_suri) = keystore_options.keystore_suri { + if let Some(keystore_suri) = keystore_suri { let (path, password) = match &keystore_config { KeystoreConfig::Path { path, password, .. } => (path.clone(), password.clone()), KeystoreConfig::InMemory => { diff --git a/crates/subspace-node/src/commands/shared.rs b/crates/subspace-node/src/commands/shared.rs index d551fa7e17..c3566b12ab 100644 --- a/crates/subspace-node/src/commands/shared.rs +++ b/crates/subspace-node/src/commands/shared.rs @@ -12,14 +12,7 @@ use tracing_subscriber::{fmt, EnvFilter}; /// Options used for keystore #[derive(Debug, Parser)] -pub(super) struct KeystoreOptions { - /// Operator secret key URI to insert into keystore. - /// - /// Example: "//Alice". - /// - /// If the value is a file, the file content is used as URI. - #[arg(long, required = SURI_REQUIRED)] - pub(super) keystore_suri: Option, +pub(super) struct KeystoreOptions { /// Use interactive shell for entering the password used by the keystore. #[arg(long, conflicts_with_all = &["keystore_password", "keystore_password_filename"])] pub(super) keystore_password_interactive: bool, From 526a7b1a2c7f4a86be426e14f132ec16c94146e5 Mon Sep 17 00:00:00 2001 From: Nazar Mokrynskyi Date: Wed, 24 Jan 2024 01:38:41 +0200 Subject: [PATCH 3/3] Add `subspace-node domain key create` subcommand --- Cargo.lock | 1 + crates/subspace-node/Cargo.toml | 1 + crates/subspace-node/src/commands.rs | 4 +- .../subspace-node/src/commands/domain_key.rs | 73 ++++++++++++++++++- .../subspace-node/src/commands/run/domain.rs | 2 +- crates/subspace-node/src/commands/shared.rs | 23 ++++-- crates/subspace-node/src/domain/cli.rs | 4 +- crates/subspace-node/src/main.rs | 3 + 8 files changed, 96 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5074d660db..adcc5d94f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11648,6 +11648,7 @@ dependencies = [ name = "subspace-node" version = "0.1.0" dependencies = [ + "bip39", "clap", "cross-domain-message-gossip", "dirs", diff --git a/crates/subspace-node/Cargo.toml b/crates/subspace-node/Cargo.toml index c5c2c2d464..2c0bb85204 100644 --- a/crates/subspace-node/Cargo.toml +++ b/crates/subspace-node/Cargo.toml @@ -20,6 +20,7 @@ include = [ targets = ["x86_64-unknown-linux-gnu"] [dependencies] +bip39 = "2.0.0" clap = { version = "4.4.18", features = ["derive"] } cross-domain-message-gossip = { version = "0.1.0", path = "../../domains/client/cross-domain-message-gossip" } dirs = "5.0.1" diff --git a/crates/subspace-node/src/commands.rs b/crates/subspace-node/src/commands.rs index 5805bf26a3..95b8c3ca35 100644 --- a/crates/subspace-node/src/commands.rs +++ b/crates/subspace-node/src/commands.rs @@ -3,6 +3,8 @@ mod run; mod shared; mod wipe; -pub use domain_key::{insert_domain_key, InsertDomainKeyOptions}; +pub use domain_key::{ + create_domain_key, insert_domain_key, CreateDomainKeyOptions, InsertDomainKeyOptions, +}; pub use run::{run, RunOptions}; pub use wipe::{wipe, WipeOptions}; diff --git a/crates/subspace-node/src/commands/domain_key.rs b/crates/subspace-node/src/commands/domain_key.rs index 0af4d45faf..0e8b638118 100644 --- a/crates/subspace-node/src/commands/domain_key.rs +++ b/crates/subspace-node/src/commands/domain_key.rs @@ -1,11 +1,76 @@ -use crate::commands::shared::{init_logger, store_key_in_keystore, KeystoreOptions}; +use crate::commands::shared::{ + derive_keypair, init_logger, store_key_in_keystore, KeystoreOptions, +}; +use bip39::Mnemonic; use clap::Parser; use sc_cli::{Error, KeystoreParams}; use sc_service::config::KeystoreConfig; -use sp_core::crypto::SecretString; +use sp_core::crypto::{ExposeSecret, SecretString}; +use sp_core::Pair; use sp_domains::DomainId; use std::path::PathBuf; -use tracing::info; +use tracing::{info, warn}; + +/// Options for creating domain key +#[derive(Debug, Parser)] +pub struct CreateDomainKeyOptions { + /// Base path where to store node files + #[arg(long)] + base_path: PathBuf, + /// ID of the domain to store key for + #[arg(long, required = true)] + domain_id: DomainId, + /// Options for domain keystore + #[clap(flatten)] + keystore_options: KeystoreOptions, +} + +pub fn create_domain_key(options: CreateDomainKeyOptions) -> Result<(), Error> { + init_logger(); + + let CreateDomainKeyOptions { + base_path, + domain_id, + keystore_options, + } = options; + let domain_path = base_path.join("domains").join(domain_id.to_string()); + + let keystore_params = KeystoreParams { + keystore_path: None, + password_interactive: keystore_options.keystore_password_interactive, + password: keystore_options.keystore_password, + password_filename: keystore_options.keystore_password_filename, + }; + + let keystore_config = keystore_params.keystore_config(&domain_path)?; + + let (path, password) = match &keystore_config { + KeystoreConfig::Path { path, password, .. } => (path.clone(), password.clone()), + KeystoreConfig::InMemory => { + unreachable!("Just constructed non-memory keystore config; qed"); + } + }; + + let has_password = password.is_some(); + + let mnemonic = Mnemonic::generate(12) + .map_err(|error| Error::Input(format!("Mnemonic generation failed: {error}")))?; + let phrase = SecretString::from(mnemonic.to_string()); + + let public_key = derive_keypair(&phrase, &password)?.public(); + + store_key_in_keystore(path, &phrase, password)?; + + info!("Successfully generated and imported keypair!"); + info!("Public key: {}", hex::encode(public_key.0)); + info!("Seed: \"{}\"", phrase.expose_secret()); + if has_password { + info!("Password: as specified in CLI options"); + } + warn!("⚠ Make sure to keep ^ seed secure and never share with anyone to avoid loss of funds ⚠"); + + Ok(()) +} /// Options for inserting domain key #[derive(Debug, Parser)] @@ -55,7 +120,7 @@ pub fn insert_domain_key(options: InsertDomainKeyOptions) -> Result<(), Error> { } }; - store_key_in_keystore(path, password, &keystore_suri)?; + store_key_in_keystore(path, &keystore_suri, password)?; info!("Success"); diff --git a/crates/subspace-node/src/commands/run/domain.rs b/crates/subspace-node/src/commands/run/domain.rs index b6fbc7d2fd..5ef44d08fa 100644 --- a/crates/subspace-node/src/commands/run/domain.rs +++ b/crates/subspace-node/src/commands/run/domain.rs @@ -300,7 +300,7 @@ pub(super) fn create_domain_configuration( } }; - store_key_in_keystore(path, password, &keystore_suri)?; + store_key_in_keystore(path, &keystore_suri, password)?; } keystore_config diff --git a/crates/subspace-node/src/commands/shared.rs b/crates/subspace-node/src/commands/shared.rs index c3566b12ab..10b88c24b1 100644 --- a/crates/subspace-node/src/commands/shared.rs +++ b/crates/subspace-node/src/commands/shared.rs @@ -2,7 +2,8 @@ use clap::Parser; use sc_cli::Error; use sc_keystore::LocalKeystore; use sp_core::crypto::{ExposeSecret, SecretString}; -use sp_core::Pair; +use sp_core::sr25519::Pair; +use sp_core::Pair as PairT; use sp_domains::KEY_TYPE; use sp_keystore::Keystore; use std::path::PathBuf; @@ -25,20 +26,26 @@ pub(super) struct KeystoreOptions { pub(super) keystore_password_filename: Option, } -pub(super) fn store_key_in_keystore( - keystore_path: PathBuf, - password: Option, +pub(super) fn derive_keypair( suri: &SecretString, -) -> Result<(), Error> { - let keypair_result = sp_core::sr25519::Pair::from_string( + password: &Option, +) -> Result { + let keypair_result = Pair::from_string( suri.expose_secret(), password .as_ref() .map(|password| password.expose_secret().as_str()), ); - let keypair = - keypair_result.map_err(|err| Error::Input(format!("Invalid password {:?}", err)))?; + keypair_result.map_err(|err| Error::Input(format!("Invalid password {:?}", err))) +} + +pub(super) fn store_key_in_keystore( + keystore_path: PathBuf, + suri: &SecretString, + password: Option, +) -> Result<(), Error> { + let keypair = derive_keypair(suri, &password)?; LocalKeystore::open(keystore_path, password)? .insert(KEY_TYPE, suri.expose_secret(), &keypair.public()) diff --git a/crates/subspace-node/src/domain/cli.rs b/crates/subspace-node/src/domain/cli.rs index 22a3c6614e..24b9e52d32 100644 --- a/crates/subspace-node/src/domain/cli.rs +++ b/crates/subspace-node/src/domain/cli.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::commands::InsertDomainKeyOptions; +use crate::commands::{CreateDomainKeyOptions, InsertDomainKeyOptions}; use crate::domain::evm_chain_spec; use clap::Parser; use domain_runtime_primitives::opaque::Block as DomainBlock; @@ -66,6 +66,8 @@ pub enum Subcommand { #[derive(Debug, clap::Subcommand)] pub enum DomainKey { + /// Create key and import into domain's keystore + Create(CreateDomainKeyOptions), /// Insert key into domain's keystore Insert(InsertDomainKeyOptions), } diff --git a/crates/subspace-node/src/main.rs b/crates/subspace-node/src/main.rs index 69ce924512..7311feee57 100644 --- a/crates/subspace-node/src/main.rs +++ b/crates/subspace-node/src/main.rs @@ -312,6 +312,9 @@ fn main() -> Result<(), Error> { })?; } Cli::Domain(domain_cmd) => match domain_cmd { + DomainSubcommand::Key(DomainKey::Create(create_domain_key_options)) => { + commands::create_domain_key(create_domain_key_options)?; + } DomainSubcommand::Key(DomainKey::Insert(insert_domain_key_options)) => { commands::insert_domain_key(insert_domain_key_options)?; }