diff --git a/Cargo.lock b/Cargo.lock index 6b70faa..ccbfe01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -694,6 +694,7 @@ dependencies = [ "sc-consensus-slots", "sc-executor", "sc-executor-wasmtime", + "sc-keystore", "sc-network", "sc-network-sync", "sc-offchain", diff --git a/node/Cargo.toml b/node/Cargo.toml index 93acd40..2b7f7ed 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -25,7 +25,6 @@ thiserror = { workspace = true } # Substrate pallet-im-online = { workspace = true, features = ["std"] } -substrate-prometheus-endpoint = { workspace = true } sc-basic-authorship = { workspace = true } sc-chain-spec = { workspace = true } sc-cli = { workspace = true } @@ -36,10 +35,11 @@ sc-consensus-babe = { workspace = true } sc-consensus-babe-rpc = { workspace = true } sc-consensus-grandpa = { workspace = true } sc-consensus-grandpa-rpc = { workspace = true } -sc-consensus-slots = { workspace = true } sc-consensus-manual-seal = { workspace = true } +sc-consensus-slots = { workspace = true } sc-executor = { workspace = true } sc-executor-wasmtime = { workspace = true } +sc-keystore = { workspace = true } sc-network = { workspace = true } sc-network-sync = { workspace = true } sc-offchain = { workspace = true } @@ -66,6 +66,7 @@ sp-state-machine = { workspace = true, features = ["default"] } sp-timestamp = { workspace = true, features = ["default"] } sp-transaction-pool = { workspace = true, features = ["default"] } sp-transaction-storage-proof = { workspace = true, features = ["default"] } +substrate-prometheus-endpoint = { workspace = true } # These dependencies are used for RPC frame-system-rpc-runtime-api = { workspace = true } pallet-transaction-payment-rpc = { workspace = true } diff --git a/node/src/cli.rs b/node/src/cli.rs index 4ed1793..aa0e6fa 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -1,5 +1,8 @@ +use self::validator::ValidatorCmd; use crate::service::EthConfiguration; +mod validator; + /// Available Sealing methods. #[derive(Copy, Clone, Debug, Default, clap::ValueEnum)] pub enum Sealing { @@ -66,5 +69,9 @@ pub enum Subcommand { /// Db meta columns information. FrontierDb(fc_cli::FrontierDbCmd), + /// Return the runtime version. RuntimeVersion, + + /// Validator related commands. + Validator(ValidatorCmd), } diff --git a/node/src/cli/validator.rs b/node/src/cli/validator.rs new file mode 100644 index 0000000..22dd990 --- /dev/null +++ b/node/src/cli/validator.rs @@ -0,0 +1,249 @@ +use clap::{Parser, Subcommand}; +use sc_cli::{CliConfiguration, Error, KeystoreParams, SharedParams, SubstrateCli}; +use sc_keystore::LocalKeystore; +use sc_service::config::{BasePath, KeystoreConfig}; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_blockchain::HeaderBackend; +use sp_core::Bytes; +use sp_keystore::{KeystoreExt, KeystorePtr}; +use sp_session::SessionKeys; + +use atleta_runtime::opaque; + +use crate::service::FullClient; + +/// Validator related commands. +#[derive(Debug, clap::Parser)] +pub struct ValidatorCmd { + #[allow(missing_docs)] + #[command(subcommand)] + subcommand: Option, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, +} + +impl ValidatorCmd { + pub fn run(&self, cli: &Cli, client: &FullClient) -> Result<(), Error> { + match &self.subcommand { + Some(sc) => sc.run(cli, client), + _ => Ok(()), + } + } +} + +impl CliConfiguration for ValidatorCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } +} + +#[derive(Debug, Subcommand)] +pub enum ValidatorSubcommands { + /// Generate session keys and insert them into the keystore. + GenerateSessionKeys(GenerateSessionKeysCmd), + + /// Decode session keys. + DecodeSessionKeys(DecodeSessionKeysCmd), + + /// Insert a session key into the keystore. + InsertKey(InsertKeyCmd), +} + +impl ValidatorSubcommands { + /// Runs the command. + pub fn run(&self, cli: &Cli, client: &FullClient) -> Result<(), Error> { + match self { + Self::GenerateSessionKeys(cmd) => cmd.run(cli, client), + Self::DecodeSessionKeys(cmd) => cmd.run(), + Self::InsertKey(cmd) => cmd.run(cli), + } + } +} + +/// `generate-session-keys` subcommand. +#[derive(Debug, Clone, Parser)] +pub struct GenerateSessionKeysCmd { + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, +} + +impl GenerateSessionKeysCmd { + /// Run the command + pub fn run(&self, cli: &Cli, client: &FullClient) -> Result<(), Error> { + let keystore = init_keystore(cli, &self.shared_params, &self.keystore_params)?; + let best_block_hash = client.info().best_hash; + let mut runtime_api = client.runtime_api(); + + runtime_api.register_extension(KeystoreExt::from(keystore.clone())); + + let keys = runtime_api + .generate_session_keys(best_block_hash, None) + .map_err(|api_err| Error::Application(Box::new(api_err).into()))?; + + println!("{}", sp_core::bytes::to_hex(&keys, true)); + + Ok(()) + } +} + +impl CliConfiguration for GenerateSessionKeysCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn keystore_params(&self) -> Option<&KeystoreParams> { + Some(&self.keystore_params) + } +} + +/// `insert-key` subcommand. +#[derive(Debug, Clone, Parser)] +pub struct InsertKeyCmd { + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + /// Key type + #[arg(long)] + pub key_type: String, + + /// Secret URI + #[arg(long)] + pub suri: String, + + /// Public key + #[arg(long)] + pub public_key: Bytes, +} + +impl InsertKeyCmd { + /// Run the command + pub fn run(&self, cli: &Cli) -> Result<(), Error> { + let keystore = init_keystore(cli, &self.shared_params, &self.keystore_params)?; + let key_type = self.key_type.as_str().try_into().map_err(|_| Error::KeyTypeInvalid)?; + keystore + .insert(key_type, &self.suri, &self.public_key[..]) + .map_err(|_| Error::KeystoreOperation)?; + + Ok(()) + } +} + +impl CliConfiguration for InsertKeyCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn keystore_params(&self) -> Option<&KeystoreParams> { + Some(&self.keystore_params) + } +} + +fn init_keystore( + cli: &Cli, + shared_params: &SharedParams, + keystore_params: &KeystoreParams, +) -> Result { + let base_path = shared_params + .base_path()? + .unwrap_or_else(|| BasePath::from_project("", "", &Cli::executable_name())); + let chain_id = shared_params.chain_id(shared_params.is_dev()); + let chain_spec = cli.load_spec(&chain_id)?; + let config_dir = base_path.config_dir(chain_spec.id()); + + match keystore_params.keystore_config(&config_dir)? { + KeystoreConfig::Path { path, password } => Ok(LocalKeystore::open(path, password)?.into()), + _ => unreachable!("keystore_config always returns path and password; qed"), + } +} + +/// `decode-session-keys` subcommand. +#[derive(Debug, Clone, Parser)] +pub struct DecodeSessionKeysCmd { + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub keystore_params: KeystoreParams, + + /// Hex-encoded session keys. + #[arg(value_name = "SESSION KEYS")] + pub keys: String, +} + +impl DecodeSessionKeysCmd { + /// Run the command + pub fn run(&self) -> Result<(), Error> { + for key_line in decode_readable(&self.keys)? { + println!("{}: {}", key_line.0, key_line.1); + } + Ok(()) + } +} + +fn decode_readable(keys: &str) -> Result, Error> { + let bytes: Vec = sp_core::bytes::from_hex(keys) + .map_err(|convert_err| Error::Application(Box::new(convert_err).into()))?; + let decoded = opaque::SessionKeys::decode_into_raw_public_keys(&bytes) + .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Error decoding session keys"))?; + + Ok(decoded + .into_iter() + .map(|(value, key_id)| { + ( + String::from_utf8(key_id.0.to_vec()).expect("KeyTypeId string is valid"), + sp_core::bytes::to_hex(&value, true), + ) + }) + .collect()) +} + +impl CliConfiguration for DecodeSessionKeysCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decoding_session_keys_works() { + let keys = "0xeafcb752d8b82fc872f3b4dcb6c55104b80de3b49796a1e61b9ef310eb5da42dd58e4f129f85b11a2b65d2a8b6cfc3f95c1d43505a1ec25bdcae7ff28471c20140b743a501c25bb8fa033dde00de6c73ebf8c62421d4d1bd67780e8098e8aa23"; + + assert_eq!( + decode_readable(keys).unwrap(), + vec![ + ( + "babe".to_string(), + "0xeafcb752d8b82fc872f3b4dcb6c55104b80de3b49796a1e61b9ef310eb5da42d" + .to_string() + ), + ( + "gran".to_string(), + "0xd58e4f129f85b11a2b65d2a8b6cfc3f95c1d43505a1ec25bdcae7ff28471c201" + .to_string() + ), + ( + "imon".to_string(), + "0x40b743a501c25bb8fa033dde00de6c73ebf8c62421d4d1bd67780e8098e8aa23" + .to_string() + ), + ] + ) + } +} diff --git a/node/src/command.rs b/node/src/command.rs index fdc206b..6733cee 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -1,20 +1,3 @@ -// This file is part of Substrate. - -// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - use futures::TryFutureExt; // Substrate use sc_cli::{ChainSpec, SubstrateCli}; @@ -275,6 +258,14 @@ pub fn run() -> sc_cli::Result<()> { Ok(()) }, + Some(Subcommand::Validator(cmd)) => { + let runner = cli.create_runner(cmd)?; + log::set_max_level(log::LevelFilter::Error); + runner.sync_run(|mut config| { + let (client, ..) = service::new_chain_ops(&mut config, &cli.eth)?; + cmd.run(&cli, &client) + }) + }, None => { let runner = cli.create_runner(&cli.run)?; runner.run_node_until_exit(|config| async move {