From 98fc59674c5b0394ea950755c8241f65c6bd3dec Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Fri, 21 Feb 2020 14:59:15 +0800 Subject: [PATCH 1/3] update: toml --- bin/node/cli/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index 34fb6ef82..bd48ed13b 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -8,10 +8,10 @@ edition = "2018" default-run = "darwinia" [badges] -travis-ci = { repository = "paritytech/substrate", branch = "master" } +travis-ci = { repository = "darwinia-network/darwinia", branch = "develop" } maintenance = { status = "actively-developed" } -is-it-maintained-issue-resolution = { repository = "paritytech/substrate" } -is-it-maintained-open-issues = { repository = "paritytech/substrate" } +is-it-maintained-issue-resolution = { repository = "darwinia-network/darwinia" } +is-it-maintained-open-issues = { repository = "darwinia-network/darwinia" } [[bin]] name = "darwinia" From 379e2af2a17c0aa751a2774a1dfff513d19b39a9 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Fri, 21 Feb 2020 15:39:25 +0800 Subject: [PATCH 2/3] add: cli --- Cargo.lock | 37 +- Cargo.toml | 1 + bin/node/cli/Cargo.toml | 9 +- client/cli/Cargo.toml | 49 ++ client/cli/README.adoc | 6 + client/cli/src/error.rs | 64 ++ client/cli/src/execution_strategy.rs | 34 + client/cli/src/informant.rs | 80 ++ client/cli/src/informant/display.rs | 154 ++++ client/cli/src/lib.rs | 1189 ++++++++++++++++++++++++++ client/cli/src/params.rs | 990 +++++++++++++++++++++ client/cli/src/traits.rs | 23 + 12 files changed, 2632 insertions(+), 4 deletions(-) create mode 100644 client/cli/Cargo.toml create mode 100644 client/cli/README.adoc create mode 100644 client/cli/src/error.rs create mode 100644 client/cli/src/execution_strategy.rs create mode 100644 client/cli/src/informant.rs create mode 100644 client/cli/src/informant/display.rs create mode 100644 client/cli/src/lib.rs create mode 100644 client/cli/src/params.rs create mode 100644 client/cli/src/traits.rs diff --git a/Cargo.lock b/Cargo.lock index 53253d6ab..03e982ce2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -921,6 +921,41 @@ dependencies = [ "zeroize 1.1.0", ] +[[package]] +name = "darwinia-cli" +version = "2.0.0" +dependencies = [ + "ansi_term 0.12.1", + "app_dirs", + "atty", + "clap", + "derive_more", + "env_logger 0.7.1", + "fdlimit", + "futures 0.3.4", + "lazy_static", + "log 0.4.8", + "names", + "regex", + "rpassword", + "sc-client-api", + "sc-network", + "sc-service", + "sc-telemetry", + "sc-tracing", + "serde_json", + "sp-blockchain", + "sp-core", + "sp-keyring", + "sp-panic-handler", + "sp-runtime", + "sp-state-machine", + "structopt", + "tempfile", + "time", + "tokio 0.2.11", +] + [[package]] name = "darwinia-eth-backing" version = "0.4.0" @@ -3357,6 +3392,7 @@ dependencies = [ "console_error_panic_hook", "console_log", "ctrlc", + "darwinia-cli", "frame-support", "frame-system", "futures 0.1.29", @@ -3384,7 +3420,6 @@ dependencies = [ "sc-authority-discovery", "sc-basic-authority", "sc-chain-spec", - "sc-cli", "sc-client", "sc-client-api", "sc-client-db", diff --git a/Cargo.toml b/Cargo.toml index bd6e73cb1..3d8bd6308 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "bin/node/rpc-client", "bin/node/rpc", "bin/node/runtime", + "client/cli", "frame/balances/kton", "frame/balances/ring", "frame/chainrelay/eth/backing", diff --git a/bin/node/cli/Cargo.toml b/bin/node/cli/Cargo.toml index bd48ed13b..887233f2a 100644 --- a/bin/node/cli/Cargo.toml +++ b/bin/node/cli/Cargo.toml @@ -82,9 +82,10 @@ node-runtime = { path = "../runtime" } # CLI-specific dependencies ctrlc = { version = "3.1.3", features = ["termination"], optional = true } node-transaction-factory = { version = "2.0.0", optional = true, git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } -sc-cli = { version = "2.0.0", optional = true, git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } tokio = { version = "0.1.22", optional = true } +sc-cli = { package = "darwinia-cli", optional = true, path = "../../../client/cli" } + # WASM-specific dependencies clear_on_drop = { version = "0.2.3", features = ["no_cc"], optional = true } # Imported just for the `no_cc` feature console_error_panic_hook = { version = "0.1.1", optional = true } @@ -97,11 +98,13 @@ wasm-bindgen = { version = "0.2.45", optional = true } wasm-bindgen-futures = { version = "0.3.22", optional = true } [build-dependencies] -build-script-utils = { version = "2.0.0", package = "substrate-build-script-utils", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } -sc-cli = { version = "2.0.0", package = "sc-cli", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } structopt = "=0.3.7" vergen = "3.0.4" +build-script-utils = { version = "2.0.0", package = "substrate-build-script-utils", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } + +sc-cli = { package = "darwinia-cli", path = "../../../client/cli" } + [features] default = ["cli"] browser = [ diff --git a/client/cli/Cargo.toml b/client/cli/Cargo.toml new file mode 100644 index 000000000..245d46e57 --- /dev/null +++ b/client/cli/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "darwinia-cli" +version = "2.0.0" +authors = ["Darwinia Network "] +description = "Darwinia CLI interface." +edition = "2018" + +[dependencies] +# crates.io +ansi_term = "0.12.1" +app_dirs = "1.2.1" +atty = "0.2.13" +clap = "2.33.0" +derive_more = "0.99.2" +env_logger = "0.7.0" +fdlimit = "0.1.1" +futures = { version = "0.3.1", features = ["compat"] } +lazy_static = "1.4.0" +log = "0.4.8" +names = "0.11.0" +regex = "1.3.1" +serde_json = "1.0.41" +structopt = "=0.3.7" +time = "0.1.42" +tokio = "0.2.1" + +# github.com +sc-client-api = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sc-network = { version = "0.8", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sc-service = { version = "2.0.0", default-features = false, git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sc-tracing = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sp-blockchain = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sp-core = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sp-keyring = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sp-panic-handler = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sp-runtime = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sp-state-machine = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } +sc-telemetry = { version = "2.0.0", git = "https://github.com/paritytech/substrate.git", tag = "pre-v2.0-3e65111" } + +[target.'cfg(not(target_os = "unknown"))'.dependencies] +rpassword = "4.0.1" + +[dev-dependencies] +tempfile = "3.1.0" + +[features] +wasmtime = [ + "sc-service/wasmtime", +] diff --git a/client/cli/README.adoc b/client/cli/README.adoc new file mode 100644 index 000000000..fc58908fd --- /dev/null +++ b/client/cli/README.adoc @@ -0,0 +1,6 @@ + += Substrate CLI + +Substrate CLI library + +include::doc/shell-completion.adoc[] diff --git a/client/cli/src/error.rs b/client/cli/src/error.rs new file mode 100644 index 000000000..359d178f9 --- /dev/null +++ b/client/cli/src/error.rs @@ -0,0 +1,64 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Initialization errors. + +/// Result type alias for the CLI. +pub type Result = std::result::Result; + +/// Error type for the CLI. +#[derive(Debug, derive_more::Display, derive_more::From)] +pub enum Error { + /// Io error + Io(std::io::Error), + /// Cli error + Cli(clap::Error), + /// Service error + Service(sc_service::Error), + /// Client error + Client(sp_blockchain::Error), + /// Input error + #[from(ignore)] + Input(String), + /// Invalid listen multiaddress + #[display(fmt = "Invalid listen multiaddress")] + InvalidListenMultiaddress, + /// Other uncategorized error. + #[from(ignore)] + Other(String), +} + +/// Must be implemented explicitly because `derive_more` won't generate this +/// case due to conflicting derive for `Other(String)`. +impl std::convert::From for Error { + fn from(s: String) -> Error { + Error::Input(s) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Io(ref err) => Some(err), + Error::Cli(ref err) => Some(err), + Error::Service(ref err) => Some(err), + Error::Client(ref err) => Some(err), + Error::Input(_) => None, + Error::InvalidListenMultiaddress => None, + Error::Other(_) => None, + } + } +} diff --git a/client/cli/src/execution_strategy.rs b/client/cli/src/execution_strategy.rs new file mode 100644 index 000000000..a0165f872 --- /dev/null +++ b/client/cli/src/execution_strategy.rs @@ -0,0 +1,34 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +#![allow(missing_docs)] + +use structopt::clap::arg_enum; + +arg_enum! { + /// How to execute blocks + #[derive(Debug, Clone, Copy)] + pub enum ExecutionStrategy { + // Execute with native build (if available, WebAssembly otherwise). + Native, + // Only execute with the WebAssembly build. + Wasm, + // Execute with both native (where available) and WebAssembly builds. + Both, + // Execute with the native build if possible; if it fails, then execute with WebAssembly. + NativeElseWasm, + } +} diff --git a/client/cli/src/informant.rs b/client/cli/src/informant.rs new file mode 100644 index 000000000..42e54b90f --- /dev/null +++ b/client/cli/src/informant.rs @@ -0,0 +1,80 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Console informant. Prints sync progress and block events. Runs on the calling thread. + +use futures::{compat::Stream01CompatExt, future, FutureExt, StreamExt, TryStreamExt}; +use log::{info, warn}; +use sc_client_api::BlockchainEvents; +use sc_service::AbstractService; +use sp_runtime::traits::Header; +use std::time::Duration; + +mod display; + +/// Creates an informant in the form of a `Future` that must be polled regularly. +pub fn build(service: &impl AbstractService) -> impl futures::Future { + let client = service.client(); + + let mut display = display::InformantDisplay::new(); + + let display_notifications = service + .network_status(Duration::from_millis(5000)) + .compat() + .try_for_each(move |(net_status, _)| { + let info = client.usage_info(); + display.display(&info, net_status); + future::ok(()) + }); + + let client = service.client(); + let mut last_best = { + let info = client.usage_info(); + Some((info.chain.best_number, info.chain.best_hash)) + }; + + let display_block_import = client.import_notification_stream().for_each(move |n| { + // detect and log reorganizations. + if let Some((ref last_num, ref last_hash)) = last_best { + if n.header.parent_hash() != last_hash && n.is_new_best { + let maybe_ancestor = sp_blockchain::lowest_common_ancestor(&*client, last_hash.clone(), n.hash); + + match maybe_ancestor { + Ok(ref ancestor) if ancestor.hash != *last_hash => info!( + "Reorg from #{},{} to #{},{}, common ancestor #{},{}", + last_num, + last_hash, + n.header.number(), + n.hash, + ancestor.number, + ancestor.hash, + ), + Ok(_) => {} + Err(e) => warn!("Error computing tree route: {}", e), + } + } + } + + if n.is_new_best { + last_best = Some((n.header.number().clone(), n.hash.clone())); + } + + info!(target: "substrate", "Imported #{} ({})", n.header.number(), n.hash); + future::ready(()) + }); + + future::join(display_notifications, display_block_import).map(|_| ()) +} diff --git a/client/cli/src/informant/display.rs b/client/cli/src/informant/display.rs new file mode 100644 index 000000000..f22fc5830 --- /dev/null +++ b/client/cli/src/informant/display.rs @@ -0,0 +1,154 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +use ansi_term::Colour; +use log::info; +use sc_client_api::ClientInfo; +use sc_network::SyncState; +use sc_service::NetworkStatus; +use sp_runtime::traits::{Block as BlockT, CheckedDiv, NumberFor, Saturating, Zero}; +use std::{ + convert::{TryFrom, TryInto}, + fmt, time, +}; + +/// State of the informant display system. +/// +/// This is the system that handles the line that gets regularly printed and that looks something +/// like: +/// +/// > Syncing 5.4 bps, target=#531028 (4 peers), best: #90683 (0x4ca8…51b8), +/// > finalized #360 (0x6f24…a38b), ⬇ 5.5kiB/s ⬆ 0.9kiB/s +/// +/// # Usage +/// +/// Call `InformantDisplay::new` to initialize the state, then regularly call `display` with the +/// information to display. +/// +pub struct InformantDisplay { + /// Head of chain block number from the last time `display` has been called. + /// `None` if `display` has never been called. + last_number: Option>, + /// The last time `display` or `new` has been called. + last_update: time::Instant, +} + +impl InformantDisplay { + /// Builds a new informant display system. + pub fn new() -> InformantDisplay { + InformantDisplay { + last_number: None, + last_update: time::Instant::now(), + } + } + + /// Displays the informant by calling `info!`. + pub fn display(&mut self, info: &ClientInfo, net_status: NetworkStatus) { + let best_number = info.chain.best_number; + let best_hash = info.chain.best_hash; + let speed = speed::(best_number, self.last_number, self.last_update); + self.last_update = time::Instant::now(); + self.last_number = Some(best_number); + + let (status, target) = match (net_status.sync_state, net_status.best_seen_block) { + (SyncState::Idle, _) => ("Idle".into(), "".into()), + (SyncState::Downloading, None) => (format!("Syncing{}", speed), "".into()), + (SyncState::Downloading, Some(n)) => (format!("Syncing{}", speed), format!(", target=#{}", n)), + }; + + info!( + target: "substrate", + "{}{} ({} peers), best: #{} ({}), finalized #{} ({}), ⬇ {} ⬆ {}", + Colour::White.bold().paint(&status), + target, + Colour::White.bold().paint(format!("{}", net_status.num_connected_peers)), + Colour::White.paint(format!("{}", best_number)), + best_hash, + Colour::White.paint(format!("{}", info.chain.finalized_number)), + info.chain.finalized_hash, + TransferRateFormat(net_status.average_download_per_sec), + TransferRateFormat(net_status.average_upload_per_sec), + ); + } +} + +/// Calculates `(best_number - last_number) / (now - last_update)` and returns a `String` +/// representing the speed of import. +fn speed( + best_number: NumberFor, + last_number: Option>, + last_update: time::Instant, +) -> String { + // Number of milliseconds elapsed since last time. + let elapsed_ms = { + let elapsed = last_update.elapsed(); + let since_last_millis = elapsed.as_secs() * 1000; + let since_last_subsec_millis = elapsed.subsec_millis() as u64; + since_last_millis + since_last_subsec_millis + }; + + // Number of blocks that have been imported since last time. + let diff = match last_number { + None => return String::new(), + Some(n) => best_number.saturating_sub(n), + }; + + if let Ok(diff) = TryInto::::try_into(diff) { + // If the number of blocks can be converted to a regular integer, then it's easy: just + // do the math and turn it into a `f64`. + let speed = diff + .saturating_mul(10_000) + .checked_div(u128::from(elapsed_ms)) + .map_or(0.0, |s| s as f64) + / 10.0; + format!(" {:4.1} bps", speed) + } else { + // If the number of blocks can't be converted to a regular integer, then we need a more + // algebraic approach and we stay within the realm of integers. + let one_thousand = NumberFor::::from(1_000); + let elapsed = NumberFor::::from(>::try_from(elapsed_ms).unwrap_or(u32::max_value())); + + let speed = diff + .saturating_mul(one_thousand) + .checked_div(&elapsed) + .unwrap_or_else(Zero::zero); + format!(" {} bps", speed) + } +} + +/// Contains a number of bytes per second. Implements `fmt::Display` and shows this number of bytes +/// per second in a nice way. +struct TransferRateFormat(u64); +impl fmt::Display for TransferRateFormat { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Special case 0. + if self.0 == 0 { + return write!(f, "0"); + } + + // Under 0.1 kiB, display plain bytes. + if self.0 < 100 { + return write!(f, "{} B/s", self.0); + } + + // Under 1.0 MiB/sec, display the value in kiB/sec. + if self.0 < 1024 * 1024 { + return write!(f, "{:.1}kiB/s", self.0 as f64 / 1024.0); + } + + write!(f, "{:.1}MiB/s", self.0 as f64 / (1024.0 * 1024.0)) + } +} diff --git a/client/cli/src/lib.rs b/client/cli/src/lib.rs new file mode 100644 index 000000000..f572e4d02 --- /dev/null +++ b/client/cli/src/lib.rs @@ -0,0 +1,1189 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Substrate CLI library. + +#![warn(missing_docs)] +#![warn(unused_extern_crates)] + +#[macro_use] +mod traits; +pub mod error; +mod execution_strategy; +pub mod informant; +mod params; + +use sc_client_api::execution_extensions::ExecutionStrategies; +use sc_network::{ + self, + config::{build_multiaddr, NetworkConfiguration, NodeKeyConfig, NonReservedPeerMode, TransportConfig}, + multiaddr::Protocol, +}; +use sc_service::{ + config::{Configuration, DatabaseConfig}, + ChainSpec, ChainSpecExtension, PruningMode, RuntimeGenesis, ServiceBuilderCommand, +}; +use sp_core::H256; + +use std::{ + fmt::Debug, + fs::{self, File}, + io::{stdin, stdout, Cursor, ErrorKind, Read, Seek, Write}, + iter, + net::{Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, + pin::Pin, + str::FromStr, + task::Poll, +}; + +use app_dirs::{AppDataType, AppInfo}; +use futures::{compat::Future01CompatExt, executor::block_on, Future}; +use lazy_static::lazy_static; +use log::info; +use names::{Generator, Name}; +use params::{ + BuildSpecCmd, CheckBlockCmd, Cors, ExportBlocksCmd, ImportBlocksCmd, MergeParameters, NetworkConfigurationParams, + NodeKeyParams, NodeKeyType, PurgeChainCmd, RevertCmd, RunCmd, TransactionPoolParams, +}; +pub use params::{CoreParams, ExecutionStrategy, ImportParams, NoCustom, SharedParams}; +use regex::Regex; +use sc_telemetry::TelemetryEndpoints; +use sp_runtime::generic::BlockId; +use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; +#[doc(hidden)] +pub use structopt::clap::App; +use structopt::{clap::AppSettings, StructOpt, StructOptInternal}; +pub use traits::GetSharedParams; + +/// default sub directory to store network config +const DEFAULT_NETWORK_CONFIG_PATH: &'static str = "network"; +/// default sub directory to store database +const DEFAULT_DB_CONFIG_PATH: &'static str = "db"; +/// default sub directory for the key store +const DEFAULT_KEYSTORE_CONFIG_PATH: &'static str = "keystore"; + +/// The maximum number of characters for a node name. +const NODE_NAME_MAX_LENGTH: usize = 32; + +/// The file name of the node's Ed25519 secret key inside the chain-specific +/// network config directory, if neither `--node-key` nor `--node-key-file` +/// is specified in combination with `--node-key-type=ed25519`. +const NODE_KEY_ED25519_FILE: &str = "secret_ed25519"; + +/// Executable version. Used to pass version information from the root crate. +#[derive(Clone)] +pub struct VersionInfo { + /// Implementaiton name. + pub name: &'static str, + /// Implementation version. + pub version: &'static str, + /// SCM Commit hash. + pub commit: &'static str, + /// Executable file name. + pub executable_name: &'static str, + /// Executable file description. + pub description: &'static str, + /// Executable file author. + pub author: &'static str, + /// Support URL. + pub support_url: &'static str, +} + +/// Something that can be converted into an exit signal. +pub trait IntoExit { + /// Exit signal type. + type Exit: Future + Unpin + Send + 'static; + /// Convert into exit signal. + fn into_exit(self) -> Self::Exit; +} + +fn get_chain_key(cli: &SharedParams) -> String { + match cli.chain { + Some(ref chain) => chain.clone(), + None => { + if cli.dev { + "dev".into() + } else { + "".into() + } + } + } +} + +fn generate_node_name() -> String { + let result = loop { + let node_name = Generator::with_naming(Name::Numbered).next().unwrap(); + let count = node_name.chars().count(); + + if count < NODE_NAME_MAX_LENGTH { + break node_name; + } + }; + + result +} + +fn load_spec(cli: &SharedParams, factory: F) -> error::Result> +where + G: RuntimeGenesis, + E: ChainSpecExtension, + F: FnOnce(&str) -> Result>, String>, +{ + let chain_key = get_chain_key(cli); + let spec = match factory(&chain_key)? { + Some(spec) => spec, + None => ChainSpec::from_json_file(PathBuf::from(chain_key))?, + }; + Ok(spec) +} + +fn base_path(cli: &SharedParams, version: &VersionInfo) -> PathBuf { + cli.base_path.clone().unwrap_or_else(|| { + app_dirs::get_app_root( + AppDataType::UserData, + &AppInfo { + name: version.executable_name, + author: version.author, + }, + ) + .expect("app directories exist on all supported platforms; qed") + }) +} + +/// Check whether a node name is considered as valid +fn is_node_name_valid(_name: &str) -> Result<(), &str> { + let name = _name.to_string(); + if name.chars().count() >= NODE_NAME_MAX_LENGTH { + return Err("Node name too long"); + } + + let invalid_chars = r"[\\.@]"; + let re = Regex::new(invalid_chars).unwrap(); + if re.is_match(&name) { + return Err("Node name should not contain invalid chars such as '.' and '@'"); + } + + let invalid_patterns = r"(https?:\\/+)?(www)+"; + let re = Regex::new(invalid_patterns).unwrap(); + if re.is_match(&name) { + return Err("Node name should not contain urls"); + } + + Ok(()) +} + +/// Parse command line interface arguments and prepares the command for execution. +/// +/// Before returning, this function performs various initializations, such as initializing the +/// panic handler and the logger, or increasing the limit for file descriptors. +/// +/// # Remarks +/// +/// `CC` is a custom subcommand. This needs to be an `enum`! If no custom subcommand is required, +/// `NoCustom` can be used as type here. +/// +/// `RP` are custom parameters for the run command. This needs to be a `struct`! The custom +/// parameters are visible to the user as if they were normal run command parameters. If no custom +/// parameters are required, `NoCustom` can be used as type here. +pub fn parse_and_prepare<'a, CC, RP, I>( + version: &'a VersionInfo, + impl_name: &'static str, + args: I, +) -> ParseAndPrepare<'a, CC, RP> +where + CC: StructOpt + Clone + GetSharedParams, + RP: StructOpt + Clone + StructOptInternal, + I: IntoIterator, + ::Item: Into + Clone, +{ + let full_version = sc_service::config::full_version_from_strs(version.version, version.commit); + + sp_panic_handler::set(version.support_url, &full_version); + let matches = CoreParams::::clap() + .name(version.executable_name) + .author(version.author) + .about(version.description) + .version(&(full_version + "\n")[..]) + .setting(AppSettings::GlobalVersion) + .setting(AppSettings::ArgsNegateSubcommands) + .setting(AppSettings::SubcommandsNegateReqs) + .get_matches_from(args); + let cli_args = CoreParams::::from_clap(&matches); + fdlimit::raise_fd_limit(); + + let args = match cli_args { + params::CoreParams::Run(params) => ParseAndPrepare::Run(ParseAndPrepareRun { + params, + impl_name, + version, + }), + params::CoreParams::BuildSpec(params) => { + ParseAndPrepare::BuildSpec(ParseAndPrepareBuildSpec { params, version }) + } + params::CoreParams::ExportBlocks(params) => { + ParseAndPrepare::ExportBlocks(ParseAndPrepareExport { params, version }) + } + params::CoreParams::ImportBlocks(params) => { + ParseAndPrepare::ImportBlocks(ParseAndPrepareImport { params, version }) + } + params::CoreParams::CheckBlock(params) => ParseAndPrepare::CheckBlock(CheckBlock { params, version }), + params::CoreParams::PurgeChain(params) => ParseAndPrepare::PurgeChain(ParseAndPreparePurge { params, version }), + params::CoreParams::Revert(params) => ParseAndPrepare::RevertChain(ParseAndPrepareRevert { params, version }), + params::CoreParams::Custom(params) => ParseAndPrepare::CustomCommand(params), + }; + init_logger( + args.shared_params() + .and_then(|p| p.log.as_ref()) + .map(|v| v.as_ref()) + .unwrap_or(""), + ); + args +} + +/// Returns a string displaying the node role, special casing the sentry mode +/// (returning `SENTRY`), since the node technically has an `AUTHORITY` role but +/// doesn't participate. +pub fn display_role(config: &Configuration) -> String { + if config.sentry_mode { + "SENTRY".to_string() + } else { + format!("{:?}", config.roles) + } +} + +/// Output of calling `parse_and_prepare`. +#[must_use] +pub enum ParseAndPrepare<'a, CC, RP> { + /// Command ready to run the main client. + Run(ParseAndPrepareRun<'a, RP>), + /// Command ready to build chain specs. + BuildSpec(ParseAndPrepareBuildSpec<'a>), + /// Command ready to export the chain. + ExportBlocks(ParseAndPrepareExport<'a>), + /// Command ready to import the chain. + ImportBlocks(ParseAndPrepareImport<'a>), + /// Command to check a block. + CheckBlock(CheckBlock<'a>), + /// Command ready to purge the chain. + PurgeChain(ParseAndPreparePurge<'a>), + /// Command ready to revert the chain. + RevertChain(ParseAndPrepareRevert<'a>), + /// An additional custom command passed to `parse_and_prepare`. + CustomCommand(CC), +} + +impl<'a, CC, RP> ParseAndPrepare<'a, CC, RP> +where + CC: GetSharedParams, +{ + /// Return common set of parameters shared by all commands. + pub fn shared_params(&self) -> Option<&SharedParams> { + match self { + ParseAndPrepare::Run(c) => Some(&c.params.left.shared_params), + ParseAndPrepare::BuildSpec(c) => Some(&c.params.shared_params), + ParseAndPrepare::ExportBlocks(c) => Some(&c.params.shared_params), + ParseAndPrepare::ImportBlocks(c) => Some(&c.params.shared_params), + ParseAndPrepare::CheckBlock(c) => Some(&c.params.shared_params), + ParseAndPrepare::PurgeChain(c) => Some(&c.params.shared_params), + ParseAndPrepare::RevertChain(c) => Some(&c.params.shared_params), + ParseAndPrepare::CustomCommand(c) => c.shared_params(), + } + } +} + +/// Command ready to run the main client. +pub struct ParseAndPrepareRun<'a, RP> { + params: MergeParameters, + impl_name: &'static str, + version: &'a VersionInfo, +} + +impl<'a, RP> ParseAndPrepareRun<'a, RP> { + /// Runs the command and runs the main client. + pub fn run(self, spec_factory: S, exit: Exit, run_service: RS) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + E: Into, + RP: StructOpt + Clone, + C: Default, + G: RuntimeGenesis, + CE: ChainSpecExtension, + Exit: IntoExit, + RS: FnOnce(Exit, RunCmd, RP, Configuration) -> Result<(), E>, + { + let config = create_run_node_config(self.params.left.clone(), spec_factory, self.impl_name, self.version)?; + + run_service(exit, self.params.left, self.params.right, config).map_err(Into::into) + } +} + +/// Command ready to build chain specs. +pub struct ParseAndPrepareBuildSpec<'a> { + params: BuildSpecCmd, + version: &'a VersionInfo, +} + +impl<'a> ParseAndPrepareBuildSpec<'a> { + /// Runs the command and build the chain specs. + pub fn run(self, spec_factory: S) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + { + info!("Building chain spec"); + let raw_output = self.params.raw; + let mut spec = load_spec(&self.params.shared_params, spec_factory)?; + + if spec.boot_nodes().is_empty() && !self.params.disable_default_bootnode { + let base_path = base_path(&self.params.shared_params, self.version); + let cfg = + sc_service::Configuration::::default_with_spec_and_base_path(spec.clone(), Some(base_path)); + let node_key = node_key_config( + self.params.node_key_params, + &Some( + cfg.in_chain_config_dir(DEFAULT_NETWORK_CONFIG_PATH) + .expect("We provided a base_path"), + ), + )?; + let keys = node_key.into_keypair()?; + let peer_id = keys.public().into_peer_id(); + let addr = build_multiaddr![Ip4([127, 0, 0, 1]), Tcp(30333u16), P2p(peer_id)]; + spec.add_boot_node(addr) + } + + let json = sc_service::chain_ops::build_spec(spec, raw_output)?; + + print!("{}", json); + + Ok(()) + } +} + +/// Command ready to export the chain. +pub struct ParseAndPrepareExport<'a> { + params: ExportBlocksCmd, + version: &'a VersionInfo, +} + +impl<'a> ParseAndPrepareExport<'a> { + /// Runs the command and exports from the chain. + pub fn run_with_builder(self, builder: F, spec_factory: S, exit: Exit) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + F: FnOnce(Configuration) -> Result, + B: ServiceBuilderCommand, + <<<::Block as BlockT>::Header as HeaderT>::Number as FromStr>::Err: Debug, + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + Exit: IntoExit, + { + let config = create_config_with_db_path(spec_factory, &self.params.shared_params, self.version)?; + + if let DatabaseConfig::Path { ref path, .. } = &config.database { + info!("DB path: {}", path.display()); + } + let from = self.params.from.and_then(|f| f.parse().ok()).unwrap_or(1); + let to = self.params.to.and_then(|t| t.parse().ok()); + + let json = self.params.json; + + let file: Box = match self.params.output { + Some(filename) => Box::new(File::create(filename)?), + None => Box::new(stdout()), + }; + + // Note: while we would like the user to handle the exit themselves, we handle it here + // for backwards compatibility reasons. + let (exit_send, exit_recv) = std::sync::mpsc::channel(); + let exit = exit.into_exit(); + std::thread::spawn(move || { + block_on(exit); + let _ = exit_send.send(()); + }); + + let mut export_fut = builder(config)?.export_blocks(file, from.into(), to, json).compat(); + let fut = futures::future::poll_fn(|cx| { + if exit_recv.try_recv().is_ok() { + return Poll::Ready(Ok(())); + } + Pin::new(&mut export_fut).poll(cx) + }); + + let mut runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(fut)?; + Ok(()) + } +} + +/// Command ready to import the chain. +pub struct ParseAndPrepareImport<'a> { + params: ImportBlocksCmd, + version: &'a VersionInfo, +} + +impl<'a> ParseAndPrepareImport<'a> { + /// Runs the command and imports to the chain. + pub fn run_with_builder(self, builder: F, spec_factory: S, exit: Exit) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + F: FnOnce(Configuration) -> Result, + B: ServiceBuilderCommand, + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + Exit: IntoExit, + { + let mut config = create_config_with_db_path(spec_factory, &self.params.shared_params, self.version)?; + fill_import_params(&mut config, &self.params.import_params, sc_service::Roles::FULL)?; + + let file: Box = match self.params.input { + Some(filename) => Box::new(File::open(filename)?), + None => { + let mut buffer = Vec::new(); + stdin().read_to_end(&mut buffer)?; + Box::new(Cursor::new(buffer)) + } + }; + + // Note: while we would like the user to handle the exit themselves, we handle it here + // for backwards compatibility reasons. + let (exit_send, exit_recv) = std::sync::mpsc::channel(); + let exit = exit.into_exit(); + std::thread::spawn(move || { + block_on(exit); + let _ = exit_send.send(()); + }); + + let mut import_fut = builder(config)?.import_blocks(file, false).compat(); + let fut = futures::future::poll_fn(|cx| { + if exit_recv.try_recv().is_ok() { + return Poll::Ready(Ok(())); + } + Pin::new(&mut import_fut).poll(cx) + }); + + let mut runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(fut)?; + Ok(()) + } +} + +/// Command to check a block. +pub struct CheckBlock<'a> { + params: CheckBlockCmd, + version: &'a VersionInfo, +} + +impl<'a> CheckBlock<'a> { + /// Runs the command and imports to the chain. + pub fn run_with_builder(self, builder: F, spec_factory: S, _exit: Exit) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + F: FnOnce(Configuration) -> Result, + B: ServiceBuilderCommand, + <::Block as BlockT>::Hash: FromStr, + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + Exit: IntoExit, + { + let mut config = create_config_with_db_path(spec_factory, &self.params.shared_params, self.version)?; + fill_import_params(&mut config, &self.params.import_params, sc_service::Roles::FULL)?; + + let input = if self.params.input.starts_with("0x") { + &self.params.input[2..] + } else { + &self.params.input[..] + }; + let block_id = match FromStr::from_str(input) { + Ok(hash) => BlockId::hash(hash), + Err(_) => match self.params.input.parse::() { + Ok(n) => BlockId::number((n as u32).into()), + Err(_) => return Err(error::Error::Input("Invalid hash or number specified".into())), + }, + }; + + let start = std::time::Instant::now(); + let check = builder(config)?.check_block(block_id).compat(); + let mut runtime = tokio::runtime::Runtime::new().unwrap(); + runtime.block_on(check)?; + println!("Completed in {} ms.", start.elapsed().as_millis()); + Ok(()) + } +} + +/// Command ready to purge the chain. +pub struct ParseAndPreparePurge<'a> { + params: PurgeChainCmd, + version: &'a VersionInfo, +} + +impl<'a> ParseAndPreparePurge<'a> { + /// Runs the command and purges the chain. + pub fn run(self, spec_factory: S) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + G: RuntimeGenesis, + E: ChainSpecExtension, + { + let config = create_config_with_db_path::<(), _, _, _>(spec_factory, &self.params.shared_params, self.version)?; + let db_path = match config.database { + DatabaseConfig::Path { path, .. } => path, + _ => { + eprintln!("Cannot purge custom database implementation"); + return Ok(()); + } + }; + + if !self.params.yes { + print!("Are you sure to remove {:?}? [y/N]: ", &db_path); + stdout().flush().expect("failed to flush stdout"); + + let mut input = String::new(); + stdin().read_line(&mut input)?; + let input = input.trim(); + + match input.chars().nth(0) { + Some('y') | Some('Y') => {} + _ => { + println!("Aborted"); + return Ok(()); + } + } + } + + match fs::remove_dir_all(&db_path) { + Result::Ok(_) => { + println!("{:?} removed.", &db_path); + Ok(()) + } + Result::Err(ref err) if err.kind() == ErrorKind::NotFound => { + eprintln!("{:?} did not exist.", &db_path); + Ok(()) + } + Result::Err(err) => Result::Err(err.into()), + } + } +} + +/// Command ready to revert the chain. +pub struct ParseAndPrepareRevert<'a> { + params: RevertCmd, + version: &'a VersionInfo, +} + +impl<'a> ParseAndPrepareRevert<'a> { + /// Runs the command and reverts the chain. + pub fn run_with_builder(self, builder: F, spec_factory: S) -> error::Result<()> + where + S: FnOnce(&str) -> Result>, String>, + F: FnOnce(Configuration) -> Result, + B: ServiceBuilderCommand, + <<<::Block as BlockT>::Header as HeaderT>::Number as FromStr>::Err: Debug, + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + { + let config = create_config_with_db_path(spec_factory, &self.params.shared_params, self.version)?; + let blocks = self.params.num.parse()?; + builder(config)?.revert_chain(blocks)?; + Ok(()) + } +} + +/// Create a `NodeKeyConfig` from the given `NodeKeyParams` in the context +/// of an optional network config storage directory. +fn node_key_config

(params: NodeKeyParams, net_config_dir: &Option

) -> error::Result +where + P: AsRef, +{ + match params.node_key_type { + NodeKeyType::Ed25519 => params + .node_key + .as_ref() + .map(parse_ed25519_secret) + .unwrap_or_else(|| { + Ok(params + .node_key_file + .or_else(|| net_config_file(net_config_dir, NODE_KEY_ED25519_FILE)) + .map(sc_network::config::Secret::File) + .unwrap_or(sc_network::config::Secret::New)) + }) + .map(NodeKeyConfig::Ed25519), + } +} + +fn net_config_file

(net_config_dir: &Option

, name: &str) -> Option +where + P: AsRef, +{ + net_config_dir.as_ref().map(|d| d.as_ref().join(name)) +} + +/// Create an error caused by an invalid node key argument. +fn invalid_node_key(e: impl std::fmt::Display) -> error::Error { + error::Error::Input(format!("Invalid node key: {}", e)) +} + +/// Parse a Ed25519 secret key from a hex string into a `sc_network::Secret`. +fn parse_ed25519_secret(hex: &String) -> error::Result { + H256::from_str(&hex).map_err(invalid_node_key).and_then(|bytes| { + sc_network::config::identity::ed25519::SecretKey::from_bytes(bytes) + .map(sc_network::config::Secret::Input) + .map_err(invalid_node_key) + }) +} + +/// Fill the given `PoolConfiguration` by looking at the cli parameters. +fn fill_transaction_pool_configuration( + options: &mut Configuration, + params: TransactionPoolParams, +) -> error::Result<()> { + // ready queue + options.transaction_pool.ready.count = params.pool_limit; + options.transaction_pool.ready.total_bytes = params.pool_kbytes * 1024; + + // future queue + let factor = 10; + options.transaction_pool.future.count = params.pool_limit / factor; + options.transaction_pool.future.total_bytes = params.pool_kbytes * 1024 / factor; + + Ok(()) +} + +/// Fill the given `NetworkConfiguration` by looking at the cli parameters. +fn fill_network_configuration( + cli: NetworkConfigurationParams, + config_path: PathBuf, + config: &mut NetworkConfiguration, + client_id: String, + is_dev: bool, +) -> error::Result<()> { + config.boot_nodes.extend(cli.bootnodes.into_iter()); + config.config_path = Some(config_path.to_string_lossy().into()); + config.net_config_path = config.config_path.clone(); + + config.reserved_nodes.extend(cli.reserved_nodes.into_iter()); + if cli.reserved_only { + config.non_reserved_mode = NonReservedPeerMode::Deny; + } + + config.sentry_nodes.extend(cli.sentry_nodes.into_iter()); + + for addr in cli.listen_addr.iter() { + let addr = addr.parse().ok().ok_or(error::Error::InvalidListenMultiaddress)?; + config.listen_addresses.push(addr); + } + + if config.listen_addresses.is_empty() { + let port = match cli.port { + Some(port) => port, + None => 30333, + }; + + config.listen_addresses = vec![iter::once(Protocol::Ip4(Ipv4Addr::new(0, 0, 0, 0))) + .chain(iter::once(Protocol::Tcp(port))) + .collect()]; + } + + config.public_addresses = Vec::new(); + + config.client_version = client_id; + config.node_key = node_key_config(cli.node_key_params, &config.net_config_path)?; + + config.in_peers = cli.in_peers; + config.out_peers = cli.out_peers; + + config.transport = TransportConfig::Normal { + enable_mdns: !is_dev && !cli.no_mdns, + allow_private_ipv4: !cli.no_private_ipv4, + wasm_external_transport: None, + }; + + config.max_parallel_downloads = cli.max_parallel_downloads; + + Ok(()) +} + +#[cfg(not(target_os = "unknown"))] +fn input_keystore_password() -> Result { + rpassword::read_password_from_tty(Some("Keystore password: ")).map_err(|e| format!("{:?}", e)) +} + +/// Fill the password field of the given config instance. +fn fill_config_keystore_password( + config: &mut sc_service::Configuration, + cli: &RunCmd, +) -> Result<(), String> { + config.keystore_password = if cli.password_interactive { + #[cfg(not(target_os = "unknown"))] + { + Some(input_keystore_password()?.into()) + } + #[cfg(target_os = "unknown")] + None + } else if let Some(ref file) = cli.password_filename { + Some(fs::read_to_string(file).map_err(|e| format!("{}", e))?.into()) + } else if let Some(ref password) = cli.password { + Some(password.clone().into()) + } else { + None + }; + + Ok(()) +} + +/// Put block import CLI params into `config` object. +pub fn fill_import_params( + config: &mut Configuration, + cli: &ImportParams, + role: sc_service::Roles, +) -> error::Result<()> +where + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, +{ + match config.database { + DatabaseConfig::Path { ref mut cache_size, .. } => *cache_size = Some(cli.database_cache_size), + DatabaseConfig::Custom(_) => {} + } + + config.state_cache_size = cli.state_cache_size; + + // by default we disable pruning if the node is an authority (i.e. + // `ArchiveAll`), otherwise we keep state for the last 256 blocks. if the + // node is an authority and pruning is enabled explicitly, then we error + // unless `unsafe_pruning` is set. + config.pruning = match &cli.pruning { + Some(ref s) if s == "archive" => PruningMode::ArchiveAll, + None if role == sc_service::Roles::AUTHORITY => PruningMode::ArchiveAll, + None => PruningMode::default(), + Some(s) => { + if role == sc_service::Roles::AUTHORITY && !cli.unsafe_pruning { + return Err(error::Error::Input( + "Validators should run with state pruning disabled (i.e. archive). \ + You can ignore this check with `--unsafe-pruning`." + .to_string(), + )); + } + + PruningMode::keep_blocks( + s.parse() + .map_err(|_| error::Error::Input("Invalid pruning mode specified".to_string()))?, + ) + } + }; + + config.wasm_method = cli.wasm_method.into(); + + let exec = &cli.execution_strategies; + let exec_all_or = |strat: ExecutionStrategy| exec.execution.unwrap_or(strat).into(); + config.execution_strategies = ExecutionStrategies { + syncing: exec_all_or(exec.execution_syncing), + importing: exec_all_or(exec.execution_import_block), + block_construction: exec_all_or(exec.execution_block_construction), + offchain_worker: exec_all_or(exec.execution_offchain_worker), + other: exec_all_or(exec.execution_other), + }; + Ok(()) +} + +fn create_run_node_config( + cli: RunCmd, + spec_factory: S, + impl_name: &'static str, + version: &VersionInfo, +) -> error::Result> +where + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + S: FnOnce(&str) -> Result>, String>, +{ + let mut config = create_config_with_db_path(spec_factory, &cli.shared_params, &version)?; + + fill_config_keystore_password(&mut config, &cli)?; + + let is_dev = cli.shared_params.dev; + let is_authority = cli.validator || cli.sentry || is_dev || cli.keyring.account.is_some(); + let role = if cli.light { + sc_service::Roles::LIGHT + } else if is_authority { + sc_service::Roles::AUTHORITY + } else { + sc_service::Roles::FULL + }; + + fill_import_params(&mut config, &cli.import_params, role)?; + + config.impl_name = impl_name; + config.impl_commit = version.commit; + config.impl_version = version.version; + + config.name = match cli.name.or(cli.keyring.account.map(|a| a.to_string())) { + None => generate_node_name(), + Some(name) => name, + }; + match is_node_name_valid(&config.name) { + Ok(_) => (), + Err(msg) => Err(error::Error::Input(format!( + "Invalid node name '{}'. Reason: {}. If unsure, use none.", + config.name, msg + )))?, + } + + config.keystore_path = cli + .keystore_path + .or_else(|| config.in_chain_config_dir(DEFAULT_KEYSTORE_CONFIG_PATH)); + + // set sentry mode (i.e. act as an authority but **never** actively participate) + config.sentry_mode = cli.sentry; + + config.offchain_worker = match (cli.offchain_worker, role) { + (params::OffchainWorkerEnabled::WhenValidating, sc_service::Roles::AUTHORITY) => true, + (params::OffchainWorkerEnabled::Always, _) => true, + (params::OffchainWorkerEnabled::Never, _) => false, + (params::OffchainWorkerEnabled::WhenValidating, _) => false, + }; + + config.roles = role; + config.disable_grandpa = cli.no_grandpa; + + let client_id = config.client_id(); + fill_network_configuration( + cli.network_config, + config + .in_chain_config_dir(DEFAULT_NETWORK_CONFIG_PATH) + .expect("We provided a basepath"), + &mut config.network, + client_id, + is_dev, + )?; + + fill_transaction_pool_configuration(&mut config, cli.pool_config)?; + + config.dev_key_seed = + cli.keyring + .account + .map(|a| format!("//{}", a)) + .or_else(|| if is_dev { Some("//Alice".into()) } else { None }); + + let rpc_interface: &str = interface_str(cli.rpc_external, cli.unsafe_rpc_external, cli.validator)?; + let ws_interface: &str = interface_str(cli.ws_external, cli.unsafe_ws_external, cli.validator)?; + let grafana_interface: &str = if cli.grafana_external { "0.0.0.0" } else { "127.0.0.1" }; + + config.rpc_http = Some(parse_address(&format!("{}:{}", rpc_interface, 9933), cli.rpc_port)?); + config.rpc_ws = Some(parse_address(&format!("{}:{}", ws_interface, 9944), cli.ws_port)?); + config.grafana_port = Some(parse_address( + &format!("{}:{}", grafana_interface, 9955), + cli.grafana_port, + )?); + + config.rpc_ws_max_connections = cli.ws_max_connections; + config.rpc_cors = cli + .rpc_cors + .unwrap_or_else(|| { + if is_dev { + log::warn!("Running in --dev mode, RPC CORS has been disabled."); + Cors::All + } else { + Cors::List(vec![ + "http://localhost:*".into(), + "http://127.0.0.1:*".into(), + "https://localhost:*".into(), + "https://127.0.0.1:*".into(), + "https://polkadot.js.org".into(), + "https://substrate-ui.parity.io".into(), + ]) + } + }) + .into(); + + // Override telemetry + if cli.no_telemetry { + config.telemetry_endpoints = None; + } else if !cli.telemetry_endpoints.is_empty() { + config.telemetry_endpoints = Some(TelemetryEndpoints::new(cli.telemetry_endpoints)); + } + + config.tracing_targets = cli.tracing_targets.into(); + config.tracing_receiver = cli.tracing_receiver.into(); + + // Imply forced authoring on --dev + config.force_authoring = cli.shared_params.dev || cli.force_authoring; + + Ok(config) +} + +fn interface_str( + is_external: bool, + is_unsafe_external: bool, + is_validator: bool, +) -> Result<&'static str, error::Error> { + if is_external && is_validator { + return Err(error::Error::Input( + "--rpc-external and --ws-external options shouldn't be \ + used if the node is running as a validator. Use `--unsafe-rpc-external` if you understand \ + the risks. See the options description for more information." + .to_owned(), + )); + } + + if is_external || is_unsafe_external { + log::warn!( + "It isn't safe to expose RPC publicly without a proxy server that filters \ + available set of RPC methods." + ); + + Ok("0.0.0.0") + } else { + Ok("127.0.0.1") + } +} + +/// Creates a configuration including the database path. +pub fn create_config_with_db_path( + spec_factory: S, + cli: &SharedParams, + version: &VersionInfo, +) -> error::Result> +where + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + S: FnOnce(&str) -> Result>, String>, +{ + let spec = load_spec(cli, spec_factory)?; + let base_path = base_path(cli, version); + + let mut config = sc_service::Configuration::default_with_spec_and_base_path(spec.clone(), Some(base_path)); + + config.database = DatabaseConfig::Path { + path: config + .in_chain_config_dir(DEFAULT_DB_CONFIG_PATH) + .expect("We provided a base_path."), + cache_size: None, + }; + + Ok(config) +} + +/// Internal trait used to cast to a dynamic type that implements Read and Seek. +trait ReadPlusSeek: Read + Seek {} + +impl ReadPlusSeek for T {} + +fn parse_address(address: &str, port: Option) -> Result { + let mut address: SocketAddr = address.parse().map_err(|_| format!("Invalid address: {}", address))?; + if let Some(port) = port { + address.set_port(port); + } + + Ok(address) +} + +fn init_logger(pattern: &str) { + use ansi_term::Colour; + + let mut builder = env_logger::Builder::new(); + // Disable info logging by default for some modules: + builder.filter(Some("ws"), log::LevelFilter::Off); + builder.filter(Some("hyper"), log::LevelFilter::Warn); + builder.filter(Some("cranelift_wasm"), log::LevelFilter::Warn); + // Always log the special target `sc_tracing`, overrides global level + builder.filter(Some("sc_tracing"), log::LevelFilter::Info); + // Enable info for others. + builder.filter(None, log::LevelFilter::Info); + + if let Ok(lvl) = std::env::var("RUST_LOG") { + builder.parse_filters(&lvl); + } + + builder.parse_filters(pattern); + let isatty = atty::is(atty::Stream::Stderr); + let enable_color = isatty; + + builder.format(move |buf, record| { + let now = time::now(); + let timestamp = time::strftime("%Y-%m-%d %H:%M:%S", &now).expect("Error formatting log timestamp"); + + let mut output = if log::max_level() <= log::LevelFilter::Info { + format!("{} {}", Colour::Black.bold().paint(timestamp), record.args()) + } else { + let name = ::std::thread::current() + .name() + .map_or_else(Default::default, |x| format!("{}", Colour::Blue.bold().paint(x))); + let millis = (now.tm_nsec as f32 / 1000000.0).round() as usize; + let timestamp = format!("{}.{:03}", timestamp, millis); + format!( + "{} {} {} {} {}", + Colour::Black.bold().paint(timestamp), + name, + record.level(), + record.target(), + record.args() + ) + }; + + if !isatty && record.level() <= log::Level::Info && atty::is(atty::Stream::Stdout) { + // duplicate INFO/WARN output to console + println!("{}", output); + } + + if !enable_color { + output = kill_color(output.as_ref()); + } + + writeln!(buf, "{}", output) + }); + + if builder.try_init().is_err() { + info!("Not registering Substrate logger, as there is already a global logger registered!"); + } +} + +fn kill_color(s: &str) -> String { + lazy_static! { + static ref RE: Regex = Regex::new("\x1b\\[[^m]+m").expect("Error initializing color regex"); + } + RE.replace_all(s, "").to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use sc_network::config::identity::ed25519; + + #[test] + fn tests_node_name_good() { + assert!(is_node_name_valid("short name").is_ok()); + } + + #[test] + fn tests_node_name_bad() { + assert!(is_node_name_valid("long names are not very cool for the ui").is_err()); + assert!(is_node_name_valid("Dots.not.Ok").is_err()); + assert!(is_node_name_valid("http://visit.me").is_err()); + assert!(is_node_name_valid("https://visit.me").is_err()); + assert!(is_node_name_valid("www.visit.me").is_err()); + assert!(is_node_name_valid("email@domain").is_err()); + } + + #[test] + fn test_node_key_config_input() { + fn secret_input(net_config_dir: Option) -> error::Result<()> { + NodeKeyType::variants().into_iter().try_for_each(|t| { + let node_key_type = NodeKeyType::from_str(t).unwrap(); + let sk = match node_key_type { + NodeKeyType::Ed25519 => ed25519::SecretKey::generate().as_ref().to_vec(), + }; + let params = NodeKeyParams { + node_key_type, + node_key: Some(format!("{:x}", H256::from_slice(sk.as_ref()))), + node_key_file: None, + }; + node_key_config(params, &net_config_dir).and_then(|c| match c { + NodeKeyConfig::Ed25519(sc_network::config::Secret::Input(ref ski)) + if node_key_type == NodeKeyType::Ed25519 && &sk[..] == ski.as_ref() => + { + Ok(()) + } + _ => Err(error::Error::Input("Unexpected node key config".into())), + }) + }) + } + + assert!(secret_input(None).is_ok()); + assert!(secret_input(Some("x".to_string())).is_ok()); + } + + #[test] + fn test_node_key_config_file() { + fn secret_file(net_config_dir: Option) -> error::Result<()> { + NodeKeyType::variants().into_iter().try_for_each(|t| { + let node_key_type = NodeKeyType::from_str(t).unwrap(); + let tmp = tempfile::Builder::new().prefix("alice").tempdir()?; + let file = tmp.path().join(format!("{}_mysecret", t)).to_path_buf(); + let params = NodeKeyParams { + node_key_type, + node_key: None, + node_key_file: Some(file.clone()), + }; + node_key_config(params, &net_config_dir).and_then(|c| match c { + NodeKeyConfig::Ed25519(sc_network::config::Secret::File(ref f)) + if node_key_type == NodeKeyType::Ed25519 && f == &file => + { + Ok(()) + } + _ => Err(error::Error::Input("Unexpected node key config".into())), + }) + }) + } + + assert!(secret_file(None).is_ok()); + assert!(secret_file(Some("x".to_string())).is_ok()); + } + + #[test] + fn test_node_key_config_default() { + fn with_def_params(f: F) -> error::Result<()> + where + F: Fn(NodeKeyParams) -> error::Result<()>, + { + NodeKeyType::variants().into_iter().try_for_each(|t| { + let node_key_type = NodeKeyType::from_str(t).unwrap(); + f(NodeKeyParams { + node_key_type, + node_key: None, + node_key_file: None, + }) + }) + } + + fn no_config_dir() -> error::Result<()> { + with_def_params(|params| { + let typ = params.node_key_type; + node_key_config::(params, &None).and_then(|c| match c { + NodeKeyConfig::Ed25519(sc_network::config::Secret::New) if typ == NodeKeyType::Ed25519 => Ok(()), + _ => Err(error::Error::Input("Unexpected node key config".into())), + }) + }) + } + + fn some_config_dir(net_config_dir: String) -> error::Result<()> { + with_def_params(|params| { + let dir = PathBuf::from(net_config_dir.clone()); + let typ = params.node_key_type; + node_key_config(params, &Some(net_config_dir.clone())).and_then(move |c| match c { + NodeKeyConfig::Ed25519(sc_network::config::Secret::File(ref f)) + if typ == NodeKeyType::Ed25519 && f == &dir.join(NODE_KEY_ED25519_FILE) => + { + Ok(()) + } + _ => Err(error::Error::Input("Unexpected node key config".into())), + }) + }) + } + + assert!(no_config_dir().is_ok()); + assert!(some_config_dir("x".to_string()).is_ok()); + } +} diff --git a/client/cli/src/params.rs b/client/cli/src/params.rs new file mode 100644 index 000000000..3d7251634 --- /dev/null +++ b/client/cli/src/params.rs @@ -0,0 +1,990 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +use crate::traits::GetSharedParams; + +use std::{path::PathBuf, str::FromStr}; +use structopt::{ + clap::{arg_enum, App, AppSettings, Arg, SubCommand}, + StructOpt, StructOptInternal, +}; + +pub use crate::execution_strategy::ExecutionStrategy; + +impl Into for ExecutionStrategy { + fn into(self) -> sc_client_api::ExecutionStrategy { + match self { + ExecutionStrategy::Native => sc_client_api::ExecutionStrategy::NativeWhenPossible, + ExecutionStrategy::Wasm => sc_client_api::ExecutionStrategy::AlwaysWasm, + ExecutionStrategy::Both => sc_client_api::ExecutionStrategy::Both, + ExecutionStrategy::NativeElseWasm => sc_client_api::ExecutionStrategy::NativeElseWasm, + } + } +} + +arg_enum! { + /// How to execute Wasm runtime code + #[allow(missing_docs)] + #[derive(Debug, Clone, Copy)] + pub enum WasmExecutionMethod { + // Uses an interpreter. + Interpreted, + // Uses a compiled runtime. + Compiled, + } +} + +impl WasmExecutionMethod { + /// Returns list of variants that are not disabled by feature flags. + fn enabled_variants() -> Vec<&'static str> { + Self::variants() + .iter() + .cloned() + .filter(|&name| cfg!(feature = "wasmtime") || name != "Compiled") + .collect() + } +} + +impl Into for WasmExecutionMethod { + fn into(self) -> sc_service::config::WasmExecutionMethod { + match self { + WasmExecutionMethod::Interpreted => sc_service::config::WasmExecutionMethod::Interpreted, + #[cfg(feature = "wasmtime")] + WasmExecutionMethod::Compiled => sc_service::config::WasmExecutionMethod::Compiled, + #[cfg(not(feature = "wasmtime"))] + WasmExecutionMethod::Compiled => { + panic!("Substrate must be compiled with \"wasmtime\" feature for compiled Wasm execution") + } + } + } +} + +arg_enum! { + /// Whether off-chain workers are enabled. + #[allow(missing_docs)] + #[derive(Debug, Clone)] + pub enum OffchainWorkerEnabled { + Always, + Never, + WhenValidating, + } +} + +/// Shared parameters used by all `CoreParams`. +#[derive(Debug, StructOpt, Clone)] +pub struct SharedParams { + /// Specify the chain specification (one of dev, local or staging). + #[structopt(long = "chain", value_name = "CHAIN_SPEC")] + pub chain: Option, + + /// Specify the development chain. + #[structopt(long = "dev")] + pub dev: bool, + + /// Specify custom base path. + #[structopt(long = "base-path", short = "d", value_name = "PATH", parse(from_os_str))] + pub base_path: Option, + + /// Sets a custom logging filter. + #[structopt(short = "l", long = "log", value_name = "LOG_PATTERN")] + pub log: Option, +} + +/// Parameters for block import. +#[derive(Debug, StructOpt, Clone)] +pub struct ImportParams { + /// Specify the state pruning mode, a number of blocks to keep or 'archive'. + /// + /// Default is to keep all block states if the node is running as a + /// validator (i.e. 'archive'), otherwise state is only kept for the last + /// 256 blocks. + #[structopt(long = "pruning", value_name = "PRUNING_MODE")] + pub pruning: Option, + + /// Force start with unsafe pruning settings. + /// + /// When running as a validator it is highly recommended to disable state + /// pruning (i.e. 'archive') which is the default. The node will refuse to + /// start as a validator if pruning is enabled unless this option is set. + #[structopt(long = "unsafe-pruning")] + pub unsafe_pruning: bool, + + /// Method for executing Wasm runtime code. + #[structopt( + long = "wasm-execution", + value_name = "METHOD", + possible_values = &WasmExecutionMethod::enabled_variants(), + case_insensitive = true, + default_value = "Interpreted" + )] + pub wasm_method: WasmExecutionMethod, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub execution_strategies: ExecutionStrategies, + + /// Limit the memory the database cache can use. + #[structopt(long = "db-cache", value_name = "MiB", default_value = "1024")] + pub database_cache_size: u32, + + /// Specify the state cache size. + #[structopt(long = "state-cache-size", value_name = "Bytes", default_value = "67108864")] + pub state_cache_size: usize, +} + +/// Parameters used to create the network configuration. +#[derive(Debug, StructOpt, Clone)] +pub struct NetworkConfigurationParams { + /// Specify a list of bootnodes. + #[structopt(long = "bootnodes", value_name = "URL")] + pub bootnodes: Vec, + + /// Specify a list of reserved node addresses. + #[structopt(long = "reserved-nodes", value_name = "URL")] + pub reserved_nodes: Vec, + + /// Whether to only allow connections to/from reserved nodes. + /// + /// If you are a validator your node might still connect to other validator + /// nodes regardless of whether they are defined as reserved nodes. + #[structopt(long = "reserved-only")] + pub reserved_only: bool, + + /// Specify a list of sentry node public addresses. + #[structopt( + long = "sentry-nodes", + value_name = "URL", + conflicts_with_all = &[ "sentry" ] + )] + pub sentry_nodes: Vec, + + /// Listen on this multiaddress. + #[structopt(long = "listen-addr", value_name = "LISTEN_ADDR")] + pub listen_addr: Vec, + + /// Specify p2p protocol TCP port. + /// + /// Only used if --listen-addr is not specified. + #[structopt(long = "port", value_name = "PORT")] + pub port: Option, + + /// Allow connecting to private IPv4 addresses (as specified in + /// [RFC1918](https://tools.ietf.org/html/rfc1918)), unless the address was passed with + /// `--reserved-nodes` or `--bootnodes`. + #[structopt(long = "no-private-ipv4")] + pub no_private_ipv4: bool, + + /// Specify the number of outgoing connections we're trying to maintain. + #[structopt(long = "out-peers", value_name = "COUNT", default_value = "25")] + pub out_peers: u32, + + /// Specify the maximum number of incoming connections we're accepting. + #[structopt(long = "in-peers", value_name = "COUNT", default_value = "25")] + pub in_peers: u32, + + /// Disable mDNS discovery. + /// + /// By default, the network will use mDNS to discover other nodes on the + /// local network. This disables it. Automatically implied when using --dev. + #[structopt(long = "no-mdns")] + pub no_mdns: bool, + + /// Maximum number of peers to ask the same blocks in parallel. + /// + /// This allows downlading announced blocks from multiple peers. Decrease to save + /// traffic and risk increased latency. + #[structopt(long = "max-parallel-downloads", value_name = "COUNT", default_value = "5")] + pub max_parallel_downloads: u32, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub node_key_params: NodeKeyParams, +} + +arg_enum! { + #[allow(missing_docs)] + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub enum NodeKeyType { + Ed25519 + } +} + +/// Parameters used to create the `NodeKeyConfig`, which determines the keypair +/// used for libp2p networking. +#[derive(Debug, StructOpt, Clone)] +pub struct NodeKeyParams { + /// The secret key to use for libp2p networking. + /// + /// The value is a string that is parsed according to the choice of + /// `--node-key-type` as follows: + /// + /// `ed25519`: + /// The value is parsed as a hex-encoded Ed25519 32 bytes secret key, + /// i.e. 64 hex characters. + /// + /// The value of this option takes precedence over `--node-key-file`. + /// + /// WARNING: Secrets provided as command-line arguments are easily exposed. + /// Use of this option should be limited to development and testing. To use + /// an externally managed secret key, use `--node-key-file` instead. + #[structopt(long = "node-key", value_name = "KEY")] + pub node_key: Option, + + /// The type of secret key to use for libp2p networking. + /// + /// The secret key of the node is obtained as follows: + /// + /// * If the `--node-key` option is given, the value is parsed as a secret key + /// according to the type. See the documentation for `--node-key`. + /// + /// * If the `--node-key-file` option is given, the secret key is read from the + /// specified file. See the documentation for `--node-key-file`. + /// + /// * Otherwise, the secret key is read from a file with a predetermined, + /// type-specific name from the chain-specific network config directory + /// inside the base directory specified by `--base-dir`. If this file does + /// not exist, it is created with a newly generated secret key of the + /// chosen type. + /// + /// The node's secret key determines the corresponding public key and hence the + /// node's peer ID in the context of libp2p. + #[structopt( + long = "node-key-type", + value_name = "TYPE", + possible_values = &NodeKeyType::variants(), + case_insensitive = true, + default_value = "Ed25519" + )] + pub node_key_type: NodeKeyType, + + /// The file from which to read the node's secret key to use for libp2p networking. + /// + /// The contents of the file are parsed according to the choice of `--node-key-type` + /// as follows: + /// + /// `ed25519`: + /// The file must contain an unencoded 32 bytes Ed25519 secret key. + /// + /// If the file does not exist, it is created with a newly generated secret key of + /// the chosen type. + #[structopt(long = "node-key-file", value_name = "FILE")] + pub node_key_file: Option, +} + +/// Parameters used to create the pool configuration. +#[derive(Debug, StructOpt, Clone)] +pub struct TransactionPoolParams { + /// Maximum number of transactions in the transaction pool. + #[structopt(long = "pool-limit", value_name = "COUNT", default_value = "512")] + pub pool_limit: usize, + /// Maximum number of kilobytes of all transactions stored in the pool. + #[structopt(long = "pool-kbytes", value_name = "COUNT", default_value = "10240")] + pub pool_kbytes: usize, +} + +arg_enum! { + #[allow(missing_docs)] + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub enum TracingReceiver { + Log, + Telemetry, + Grafana, + } +} + +impl Into for TracingReceiver { + fn into(self) -> sc_tracing::TracingReceiver { + match self { + TracingReceiver::Log => sc_tracing::TracingReceiver::Log, + TracingReceiver::Telemetry => sc_tracing::TracingReceiver::Telemetry, + TracingReceiver::Grafana => sc_tracing::TracingReceiver::Grafana, + } + } +} + +/// Execution strategies parameters. +#[derive(Debug, StructOpt, Clone)] +pub struct ExecutionStrategies { + /// The means of execution used when calling into the runtime while syncing blocks. + #[structopt( + long = "execution-syncing", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + default_value = "NativeElseWasm" + )] + pub execution_syncing: ExecutionStrategy, + + /// The means of execution used when calling into the runtime while importing blocks. + #[structopt( + long = "execution-import-block", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + default_value = "NativeElseWasm" + )] + pub execution_import_block: ExecutionStrategy, + + /// The means of execution used when calling into the runtime while constructing blocks. + #[structopt( + long = "execution-block-construction", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + default_value = "Wasm" + )] + pub execution_block_construction: ExecutionStrategy, + + /// The means of execution used when calling into the runtime while using an off-chain worker. + #[structopt( + long = "execution-offchain-worker", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + default_value = "Native" + )] + pub execution_offchain_worker: ExecutionStrategy, + + /// The means of execution used when calling into the runtime while not syncing, importing or constructing blocks. + #[structopt( + long = "execution-other", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + default_value = "Native" + )] + pub execution_other: ExecutionStrategy, + + /// The execution strategy that should be used by all execution contexts. + #[structopt( + long = "execution", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + conflicts_with_all = &[ + "execution-other", + "execution-offchain-worker", + "execution-block-construction", + "execution-import-block", + "execution-syncing", + ] + )] + pub execution: Option, +} + +/// The `run` command used to run a node. +#[derive(Debug, StructOpt, Clone)] +pub struct RunCmd { + /// Enable validator mode. + /// + /// The node will be started with the authority role and actively + /// participate in any consensus task that it can (e.g. depending on + /// availability of local keys). + #[structopt( + long = "validator", + conflicts_with_all = &[ "sentry" ] + )] + pub validator: bool, + + /// Enable sentry mode. + /// + /// The node will be started with the authority role and participate in + /// consensus tasks as an "observer", it will never actively participate + /// regardless of whether it could (e.g. keys are available locally). This + /// mode is useful as a secure proxy for validators (which would run + /// detached from the network), since we want this node to participate in + /// the full consensus protocols in order to have all needed consensus data + /// available to relay to private nodes. + #[structopt( + long = "sentry", + conflicts_with_all = &[ "validator" ] + )] + pub sentry: bool, + + /// Disable GRANDPA voter when running in validator mode, otherwise disables the GRANDPA observer. + #[structopt(long = "no-grandpa")] + pub no_grandpa: bool, + + /// Experimental: Run in light client mode. + #[structopt(long = "light")] + pub light: bool, + + /// Listen to all RPC interfaces. + /// + /// Default is local. Note: not all RPC methods are safe to be exposed publicly. Use a RPC proxy + /// server to filter out dangerous methods. More details: https://github.com/paritytech/substrate/wiki/Public-RPC. + /// Use `--unsafe-rpc-external` to suppress the warning if you understand the risks. + #[structopt(long = "rpc-external")] + pub rpc_external: bool, + + /// Listen to all RPC interfaces. + /// + /// Same as `--rpc-external`. + #[structopt(long = "unsafe-rpc-external")] + pub unsafe_rpc_external: bool, + + /// Listen to all Websocket interfaces. + /// + /// Default is local. Note: not all RPC methods are safe to be exposed publicly. Use a RPC proxy + /// server to filter out dangerous methods. More details: https://github.com/paritytech/substrate/wiki/Public-RPC. + /// Use `--unsafe-ws-external` to suppress the warning if you understand the risks. + #[structopt(long = "ws-external")] + pub ws_external: bool, + + /// Listen to all Websocket interfaces. + /// + /// Same as `--ws-external`. + #[structopt(long = "unsafe-ws-external")] + pub unsafe_ws_external: bool, + + /// Listen to all Grafana data source interfaces. + /// + /// Default is local. + #[structopt(long = "grafana-external")] + pub grafana_external: bool, + + /// Specify HTTP RPC server TCP port. + #[structopt(long = "rpc-port", value_name = "PORT")] + pub rpc_port: Option, + + /// Specify WebSockets RPC server TCP port. + #[structopt(long = "ws-port", value_name = "PORT")] + pub ws_port: Option, + + /// Maximum number of WS RPC server connections. + #[structopt(long = "ws-max-connections", value_name = "COUNT")] + pub ws_max_connections: Option, + + /// Specify browser Origins allowed to access the HTTP & WS RPC servers. + /// + /// A comma-separated list of origins (protocol://domain or special `null` + /// value). Value of `all` will disable origin validation. Default is to + /// allow localhost, https://polkadot.js.org and + /// https://substrate-ui.parity.io origins. When running in --dev mode the + /// default is to allow all origins. + #[structopt(long = "rpc-cors", value_name = "ORIGINS", parse(try_from_str = parse_cors))] + pub rpc_cors: Option, + + /// Specify Grafana data source server TCP Port. + #[structopt(long = "grafana-port", value_name = "PORT")] + pub grafana_port: Option, + + /// The human-readable name for this node. + /// + /// The node name will be reported to the telemetry server, if enabled. + #[structopt(long = "name", value_name = "NAME")] + pub name: Option, + + /// Disable connecting to the Substrate telemetry server. + /// + /// Telemetry is on by default on global chains. + #[structopt(long = "no-telemetry")] + pub no_telemetry: bool, + + /// The URL of the telemetry server to connect to. + /// + /// This flag can be passed multiple times as a mean to specify multiple + /// telemetry endpoints. Verbosity levels range from 0-9, with 0 denoting + /// the least verbosity. If no verbosity level is specified the default is + /// 0. + #[structopt(long = "telemetry-url", value_name = "URL VERBOSITY", parse(try_from_str = parse_telemetry_endpoints))] + pub telemetry_endpoints: Vec<(String, u8)>, + + /// Should execute offchain workers on every block. + /// + /// By default it's only enabled for nodes that are authoring new blocks. + #[structopt( + long = "offchain-worker", + value_name = "ENABLED", + possible_values = &OffchainWorkerEnabled::variants(), + case_insensitive = true, + default_value = "WhenValidating" + )] + pub offchain_worker: OffchainWorkerEnabled, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub import_params: ImportParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub network_config: NetworkConfigurationParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub pool_config: TransactionPoolParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub keyring: Keyring, + + /// Enable authoring even when offline. + #[structopt(long = "force-authoring")] + pub force_authoring: bool, + + /// Comma separated list of targets for tracing + #[structopt(long = "tracing-targets", value_name = "TARGETS")] + pub tracing_targets: Option, + + /// Receiver to process tracing messages + #[structopt( + long = "tracing-receiver", + value_name = "RECEIVER", + possible_values = &TracingReceiver::variants(), + case_insensitive = true, + default_value = "Log" + )] + pub tracing_receiver: TracingReceiver, + + /// Specify custom keystore path. + #[structopt(long = "keystore-path", value_name = "PATH", parse(from_os_str))] + pub keystore_path: Option, + + /// Use interactive shell for entering the password used by the keystore. + #[structopt( + long = "password-interactive", + conflicts_with_all = &[ "password", "password-filename" ] + )] + pub password_interactive: bool, + + /// Password used by the keystore. + #[structopt( + long = "password", + conflicts_with_all = &[ "password-interactive", "password-filename" ] + )] + pub password: Option, + + /// File that contains the password used by the keystore. + #[structopt( + long = "password-filename", + value_name = "PATH", + parse(from_os_str), + conflicts_with_all = &[ "password-interactive", "password" ] + )] + pub password_filename: Option, +} + +/// Stores all required Cli values for a keyring test account. +struct KeyringTestAccountCliValues { + help: String, + conflicts_with: Vec, + name: String, + variant: sp_keyring::Sr25519Keyring, +} + +lazy_static::lazy_static! { + /// The Cli values for all test accounts. + static ref TEST_ACCOUNTS_CLI_VALUES: Vec = { + sp_keyring::Sr25519Keyring::iter().map(|a| { + let help = format!( + "Shortcut for `--name {} --validator` with session keys for `{}` added to keystore.", + a, + a, + ); + let conflicts_with = sp_keyring::Sr25519Keyring::iter() + .filter(|b| a != *b) + .map(|b| b.to_string().to_lowercase()) + .chain(std::iter::once("name".to_string())) + .collect::>(); + let name = a.to_string().to_lowercase(); + + KeyringTestAccountCliValues { + help, + conflicts_with, + name, + variant: a, + } + }).collect() + }; +} + +/// Wrapper for exposing the keyring test accounts into the Cli. +#[derive(Debug, Clone)] +pub struct Keyring { + pub account: Option, +} + +impl StructOpt for Keyring { + fn clap<'a, 'b>() -> App<'a, 'b> { + unimplemented!("Should not be called for `TestAccounts`.") + } + + fn from_clap(m: &structopt::clap::ArgMatches) -> Self { + Keyring { + account: TEST_ACCOUNTS_CLI_VALUES + .iter() + .find(|a| m.is_present(&a.name)) + .map(|a| a.variant), + } + } +} + +impl StructOptInternal for Keyring { + fn augment_clap<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + TEST_ACCOUNTS_CLI_VALUES.iter().fold(app, |app, a| { + let conflicts_with_strs = a.conflicts_with.iter().map(|s| s.as_str()).collect::>(); + + app.arg( + Arg::with_name(&a.name) + .long(&a.name) + .help(&a.help) + .conflicts_with_all(&conflicts_with_strs) + .takes_value(false), + ) + }) + } +} + +/// Default to verbosity level 0, if none is provided. +fn parse_telemetry_endpoints(s: &str) -> Result<(String, u8), Box> { + let pos = s.find(' '); + match pos { + None => Ok((s.to_owned(), 0)), + Some(pos_) => { + let verbosity = s[pos_ + 1..].parse()?; + let url = s[..pos_].parse()?; + Ok((url, verbosity)) + } + } +} + +/// CORS setting +/// +/// The type is introduced to overcome `Option>` +/// handling of `structopt`. +#[derive(Clone, Debug)] +pub enum Cors { + /// All hosts allowed + All, + /// Only hosts on the list are allowed. + List(Vec), +} + +impl From for Option> { + fn from(cors: Cors) -> Self { + match cors { + Cors::All => None, + Cors::List(list) => Some(list), + } + } +} + +/// Parse cors origins +fn parse_cors(s: &str) -> Result> { + let mut is_all = false; + let mut origins = Vec::new(); + for part in s.split(',') { + match part { + "all" | "*" => { + is_all = true; + break; + } + other => origins.push(other.to_owned()), + } + } + + Ok(if is_all { Cors::All } else { Cors::List(origins) }) +} + +/// The `build-spec` command used to build a specification. +#[derive(Debug, StructOpt, Clone)] +pub struct BuildSpecCmd { + /// Force raw genesis storage output. + #[structopt(long = "raw")] + pub raw: bool, + + /// Disable adding the default bootnode to the specification. + /// + /// By default the `/ip4/127.0.0.1/tcp/30333/p2p/NODE_PEER_ID` bootnode is added to the + /// specification when no bootnode exists. + #[structopt(long = "disable-default-bootnode")] + pub disable_default_bootnode: bool, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub node_key_params: NodeKeyParams, +} + +/// Wrapper type of `String` which holds an arbitary sized unsigned integer formatted as decimal. +#[derive(Debug, Clone)] +pub struct BlockNumber(String); + +impl FromStr for BlockNumber { + type Err = String; + + fn from_str(block_number: &str) -> Result { + if block_number.chars().any(|d| !d.is_digit(10)) { + Err(format!( + "Invalid block number: {}, expected decimal formatted unsigned integer", + block_number + )) + } else { + Ok(Self(block_number.to_owned())) + } + } +} + +impl BlockNumber { + /// Wrapper on top of `std::str::parse` but with `Error` as a `String` + /// + /// See `https://doc.rust-lang.org/std/primitive.str.html#method.parse` for more elaborate + /// documentation. + pub fn parse(&self) -> Result + where + N: FromStr, + N::Err: std::fmt::Debug, + { + self.0 + .parse() + .map_err(|e| format!("BlockNumber: {} parsing failed because of {:?}", self.0, e)) + } +} + +/// The `export-blocks` command used to export blocks. +#[derive(Debug, StructOpt, Clone)] +pub struct ExportBlocksCmd { + /// Output file name or stdout if unspecified. + #[structopt(parse(from_os_str))] + pub output: Option, + + /// Specify starting block number. + /// + /// Default is 1. + #[structopt(long = "from", value_name = "BLOCK")] + pub from: Option, + + /// Specify last block number. + /// + /// Default is best block. + #[structopt(long = "to", value_name = "BLOCK")] + pub to: Option, + + /// Use JSON output rather than binary. + #[structopt(long = "json")] + pub json: bool, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, +} + +/// The `import-blocks` command used to import blocks. +#[derive(Debug, StructOpt, Clone)] +pub struct ImportBlocksCmd { + /// Input file or stdin if unspecified. + #[structopt(parse(from_os_str))] + pub input: Option, + + /// The default number of 64KB pages to ever allocate for Wasm execution. + /// + /// Don't alter this unless you know what you're doing. + #[structopt(long = "default-heap-pages", value_name = "COUNT")] + pub default_heap_pages: Option, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub import_params: ImportParams, +} + +/// The `check-block` command used to validate blocks. +#[derive(Debug, StructOpt, Clone)] +pub struct CheckBlockCmd { + /// Block hash or number + #[structopt(value_name = "HASH or NUMBER")] + pub input: String, + + /// The default number of 64KB pages to ever allocate for Wasm execution. + /// + /// Don't alter this unless you know what you're doing. + #[structopt(long = "default-heap-pages", value_name = "COUNT")] + pub default_heap_pages: Option, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub import_params: ImportParams, +} + +/// The `revert` command used revert the chain to a previous state. +#[derive(Debug, StructOpt, Clone)] +pub struct RevertCmd { + /// Number of blocks to revert. + #[structopt(default_value = "256")] + pub num: BlockNumber, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, +} + +/// The `purge-chain` command used to remove the whole chain. +#[derive(Debug, StructOpt, Clone)] +pub struct PurgeChainCmd { + /// Skip interactive prompt by answering yes automatically. + #[structopt(short = "y")] + pub yes: bool, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, +} + +/// All core commands that are provided by default. +/// +/// The core commands are split into multiple subcommands and `Run` is the default subcommand. From +/// the CLI user perspective, it is not visible that `Run` is a subcommand. So, all parameters of +/// `Run` are exported as main executable parameters. +#[derive(Debug, Clone)] +pub enum CoreParams { + /// Run a node. + Run(MergeParameters), + + /// Build a spec.json file, outputing to stdout. + BuildSpec(BuildSpecCmd), + + /// Export blocks to a file. + ExportBlocks(ExportBlocksCmd), + + /// Import blocks from file. + ImportBlocks(ImportBlocksCmd), + + /// Validte a single block. + CheckBlock(CheckBlockCmd), + + /// Revert chain to the previous state. + Revert(RevertCmd), + + /// Remove the whole chain data. + PurgeChain(PurgeChainCmd), + + /// Further custom subcommands. + Custom(CC), +} + +impl StructOpt for CoreParams +where + CC: StructOpt + GetSharedParams, + RP: StructOpt + StructOptInternal, +{ + fn clap<'a, 'b>() -> App<'a, 'b> { + RP::augment_clap(RunCmd::augment_clap( + CC::clap().unset_setting(AppSettings::SubcommandRequiredElseHelp), + )) + .subcommand( + BuildSpecCmd::augment_clap(SubCommand::with_name("build-spec")) + .about("Build a spec.json file, outputting to stdout."), + ) + .subcommand( + ExportBlocksCmd::augment_clap(SubCommand::with_name("export-blocks")).about( + "Export blocks to a file. This file can only be re-imported \ + if it is in binary format (not JSON!).", + ), + ) + .subcommand( + ImportBlocksCmd::augment_clap(SubCommand::with_name("import-blocks")).about("Import blocks from file."), + ) + .subcommand( + CheckBlockCmd::augment_clap(SubCommand::with_name("check-block")).about("Re-validate a known block."), + ) + .subcommand( + RevertCmd::augment_clap(SubCommand::with_name("revert")).about("Revert chain to the previous state."), + ) + .subcommand( + PurgeChainCmd::augment_clap(SubCommand::with_name("purge-chain")).about("Remove the whole chain data."), + ) + } + + fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self { + match matches.subcommand() { + ("build-spec", Some(matches)) => CoreParams::BuildSpec(BuildSpecCmd::from_clap(matches)), + ("export-blocks", Some(matches)) => CoreParams::ExportBlocks(ExportBlocksCmd::from_clap(matches)), + ("import-blocks", Some(matches)) => CoreParams::ImportBlocks(ImportBlocksCmd::from_clap(matches)), + ("check-block", Some(matches)) => CoreParams::CheckBlock(CheckBlockCmd::from_clap(matches)), + ("revert", Some(matches)) => CoreParams::Revert(RevertCmd::from_clap(matches)), + ("purge-chain", Some(matches)) => CoreParams::PurgeChain(PurgeChainCmd::from_clap(matches)), + (_, None) => CoreParams::Run(MergeParameters::from_clap(matches)), + _ => CoreParams::Custom(CC::from_clap(matches)), + } + } +} + +/// A special commandline parameter that expands to nothing. +/// Should be used as custom subcommand/run arguments if no custom values are required. +#[derive(Clone, Debug, Default)] +pub struct NoCustom {} + +impl StructOpt for NoCustom { + fn clap<'a, 'b>() -> App<'a, 'b> { + App::new("NoCustom") + } + + fn from_clap(_: &::structopt::clap::ArgMatches) -> Self { + NoCustom {} + } +} + +impl StructOptInternal for NoCustom { + fn augment_clap<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app + } +} + +impl GetSharedParams for NoCustom { + fn shared_params(&self) -> Option<&SharedParams> { + None + } +} + +/// Merge all CLI parameters of `L` and `R` into the same level. +#[derive(Clone, Debug)] +pub struct MergeParameters { + /// The left side parameters. + pub left: L, + /// The right side parameters. + pub right: R, +} + +impl StructOpt for MergeParameters +where + L: StructOpt + StructOptInternal, + R: StructOpt, +{ + fn clap<'a, 'b>() -> App<'a, 'b> { + L::augment_clap(R::clap()) + } + + fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self { + MergeParameters { + left: L::from_clap(matches), + right: R::from_clap(matches), + } + } +} diff --git a/client/cli/src/traits.rs b/client/cli/src/traits.rs new file mode 100644 index 000000000..96216a172 --- /dev/null +++ b/client/cli/src/traits.rs @@ -0,0 +1,23 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +use crate::params::SharedParams; + +/// Supports getting common params. +pub trait GetSharedParams { + /// Returns shared params if any. + fn shared_params(&self) -> Option<&SharedParams>; +} From 7ad3435f00828234ed683e1dd153a2f55e276b7f Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Sat, 22 Feb 2020 01:02:07 +0800 Subject: [PATCH 3/3] add: boot from configuration file --- Cargo.lock | 7 +- client/cli/Cargo.toml | 1 + client/cli/src/execution_strategy.rs | 3 +- client/cli/src/lib.rs | 124 +++++++++++++++++++---- client/cli/src/params.rs | 143 ++++++++++++++++++++++++--- 5 files changed, 241 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03e982ce2..0e36800b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -943,6 +943,7 @@ dependencies = [ "sc-service", "sc-telemetry", "sc-tracing", + "serde", "serde_json", "sp-blockchain", "sp-core", @@ -2621,9 +2622,9 @@ checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" [[package]] name = "libc" -version = "0.2.66" +version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" [[package]] name = "libloading" @@ -7359,7 +7360,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bfd5b7557925ce778ff9b9ef90e3ade34c524b5ff10e239c69a42d546d2af56" dependencies = [ - "rand 0.5.6", + "rand 0.7.3", ] [[package]] diff --git a/client/cli/Cargo.toml b/client/cli/Cargo.toml index 245d46e57..5c84464cf 100644 --- a/client/cli/Cargo.toml +++ b/client/cli/Cargo.toml @@ -19,6 +19,7 @@ lazy_static = "1.4.0" log = "0.4.8" names = "0.11.0" regex = "1.3.1" +serde = "1.0.102" serde_json = "1.0.41" structopt = "=0.3.7" time = "0.1.42" diff --git a/client/cli/src/execution_strategy.rs b/client/cli/src/execution_strategy.rs index a0165f872..4e82216ea 100644 --- a/client/cli/src/execution_strategy.rs +++ b/client/cli/src/execution_strategy.rs @@ -16,11 +16,12 @@ #![allow(missing_docs)] +use serde::Deserialize; use structopt::clap::arg_enum; arg_enum! { /// How to execute blocks - #[derive(Debug, Clone, Copy)] + #[derive(Clone, Copy, Debug, Deserialize)] pub enum ExecutionStrategy { // Execute with native build (if available, WebAssembly otherwise). Native, diff --git a/client/cli/src/lib.rs b/client/cli/src/lib.rs index f572e4d02..b67106df3 100644 --- a/client/cli/src/lib.rs +++ b/client/cli/src/lib.rs @@ -19,24 +19,18 @@ #![warn(missing_docs)] #![warn(unused_extern_crates)] +pub mod error; +pub mod informant; + #[macro_use] mod traits; -pub mod error; mod execution_strategy; -pub mod informant; mod params; -use sc_client_api::execution_extensions::ExecutionStrategies; -use sc_network::{ - self, - config::{build_multiaddr, NetworkConfiguration, NodeKeyConfig, NonReservedPeerMode, TransportConfig}, - multiaddr::Protocol, -}; -use sc_service::{ - config::{Configuration, DatabaseConfig}, - ChainSpec, ChainSpecExtension, PruningMode, RuntimeGenesis, ServiceBuilderCommand, -}; -use sp_core::H256; +pub use params::{CoreParams, ExecutionStrategy, ImportParams, NoCustom, SharedParams}; +#[doc(hidden)] +pub use structopt::clap::App; +pub use traits::GetSharedParams; use std::{ fmt::Debug, @@ -59,15 +53,26 @@ use params::{ BuildSpecCmd, CheckBlockCmd, Cors, ExportBlocksCmd, ImportBlocksCmd, MergeParameters, NetworkConfigurationParams, NodeKeyParams, NodeKeyType, PurgeChainCmd, RevertCmd, RunCmd, TransactionPoolParams, }; -pub use params::{CoreParams, ExecutionStrategy, ImportParams, NoCustom, SharedParams}; use regex::Regex; +use sc_client_api::execution_extensions::ExecutionStrategies; +use sc_network::{ + self, + config::{build_multiaddr, NetworkConfiguration, NodeKeyConfig, NonReservedPeerMode, TransportConfig}, + multiaddr::Protocol, +}; +use sc_service::{ + config::{Configuration, DatabaseConfig}, + ChainSpec, ChainSpecExtension, PruningMode, RuntimeGenesis, ServiceBuilderCommand, +}; use sc_telemetry::TelemetryEndpoints; -use sp_runtime::generic::BlockId; -use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; -#[doc(hidden)] -pub use structopt::clap::App; +use sp_core::H256; +use sp_runtime::{ + generic::BlockId, + traits::{Block as BlockT, Header as HeaderT}, +}; use structopt::{clap::AppSettings, StructOpt, StructOptInternal}; -pub use traits::GetSharedParams; + +use params::Conf; /// default sub directory to store network config const DEFAULT_NETWORK_CONFIG_PATH: &'static str = "network"; @@ -806,8 +811,85 @@ where Ok(()) } +// TODO: check conflict options +fn load_conf_from_file(cli: &mut RunCmd) -> error::Result<()> { + if cli.conf.is_none() { + return Ok(()); + } + + let conf: Conf = { + let f = File::open(cli.conf.as_ref().unwrap())?; + serde_json::from_reader(f).map_err(|e| format!("{}", e))? + }; + + // println!("{:#?}", conf); + + if let Some(shared_params) = conf.shared_params.as_ref() { + cli.shared_params.dev = shared_params.dev; + } + if let Some(validator) = conf.validator { + cli.validator = validator; + } + if let Some(sentry) = conf.sentry { + cli.sentry = sentry; + } + // TODO: keyring + if let Some(light) = conf.light { + cli.light = light; + } + cli.name = conf.name; + cli.keystore_path = conf.keystore_path; + if let Some(offchain_worker) = conf.offchain_worker { + cli.offchain_worker = offchain_worker; + } + if let Some(no_grandpa) = conf.no_grandpa { + cli.no_grandpa = no_grandpa; + } + if let Some(network_config) = conf.network_config { + cli.network_config = network_config; + } + if let Some(pool_config) = conf.pool_config { + cli.pool_config = pool_config; + } + if let Some(rpc_external) = conf.rpc_external { + cli.rpc_external = rpc_external; + } + if let Some(unsafe_rpc_external) = conf.unsafe_rpc_external { + cli.unsafe_rpc_external = unsafe_rpc_external; + } + if let Some(ws_external) = conf.ws_external { + cli.ws_external = ws_external; + } + if let Some(unsafe_ws_external) = conf.unsafe_ws_external { + cli.unsafe_ws_external = unsafe_ws_external; + } + if let Some(grafana_external) = conf.grafana_external { + cli.grafana_external = grafana_external; + } + cli.rpc_port = conf.rpc_port; + cli.ws_port = conf.ws_port; + cli.grafana_port = conf.grafana_port; + cli.ws_max_connections = conf.ws_max_connections; + cli.rpc_cors = conf.rpc_cors; + if let Some(no_telemetry) = conf.no_telemetry { + cli.no_telemetry = no_telemetry; + } + if let Some(telemetry_endpoints) = conf.telemetry_endpoints { + cli.telemetry_endpoints = telemetry_endpoints; + } + cli.tracing_targets = conf.tracing_targets; + if let Some(tracing_receiver) = conf.tracing_receiver { + cli.tracing_receiver = tracing_receiver; + } + if let Some(force_authoring) = conf.force_authoring { + cli.force_authoring = force_authoring; + } + + Ok(()) +} + fn create_run_node_config( - cli: RunCmd, + mut cli: RunCmd, spec_factory: S, impl_name: &'static str, version: &VersionInfo, @@ -820,6 +902,8 @@ where { let mut config = create_config_with_db_path(spec_factory, &cli.shared_params, &version)?; + load_conf_from_file(&mut cli)?; + fill_config_keystore_password(&mut config, &cli)?; let is_dev = cli.shared_params.dev; diff --git a/client/cli/src/params.rs b/client/cli/src/params.rs index 3d7251634..ba92e316b 100644 --- a/client/cli/src/params.rs +++ b/client/cli/src/params.rs @@ -14,15 +14,17 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -use crate::traits::GetSharedParams; +pub use crate::execution_strategy::ExecutionStrategy; use std::{path::PathBuf, str::FromStr}; + +use serde::Deserialize; use structopt::{ clap::{arg_enum, App, AppSettings, Arg, SubCommand}, StructOpt, StructOptInternal, }; -pub use crate::execution_strategy::ExecutionStrategy; +use crate::traits::GetSharedParams; impl Into for ExecutionStrategy { fn into(self) -> sc_client_api::ExecutionStrategy { @@ -38,7 +40,8 @@ impl Into for ExecutionStrategy { arg_enum! { /// How to execute Wasm runtime code #[allow(missing_docs)] - #[derive(Debug, Clone, Copy)] + #[derive(Clone, Copy, Debug, Deserialize)] + #[serde(rename_all = "kebab-case")] pub enum WasmExecutionMethod { // Uses an interpreter. Interpreted, @@ -75,7 +78,8 @@ impl Into for WasmExecutionMethod { arg_enum! { /// Whether off-chain workers are enabled. #[allow(missing_docs)] - #[derive(Debug, Clone)] + #[derive(Clone, Debug, Deserialize)] + #[serde(rename_all = "kebab-case")] pub enum OffchainWorkerEnabled { Always, Never, @@ -84,7 +88,8 @@ arg_enum! { } /// Shared parameters used by all `CoreParams`. -#[derive(Debug, StructOpt, Clone)] +#[derive(Clone, Debug, Default, Deserialize, StructOpt)] +#[serde(default, rename_all = "kebab-case")] pub struct SharedParams { /// Specify the chain specification (one of dev, local or staging). #[structopt(long = "chain", value_name = "CHAIN_SPEC")] @@ -104,7 +109,8 @@ pub struct SharedParams { } /// Parameters for block import. -#[derive(Debug, StructOpt, Clone)] +#[derive(Clone, Debug, Deserialize, StructOpt)] +#[serde(default, rename_all = "kebab-case")] pub struct ImportParams { /// Specify the state pruning mode, a number of blocks to keep or 'archive'. /// @@ -133,10 +139,12 @@ pub struct ImportParams { pub wasm_method: WasmExecutionMethod, #[allow(missing_docs)] + #[serde(flatten)] #[structopt(flatten)] pub execution_strategies: ExecutionStrategies, /// Limit the memory the database cache can use. + #[serde(rename = "db-cache")] #[structopt(long = "db-cache", value_name = "MiB", default_value = "1024")] pub database_cache_size: u32, @@ -145,8 +153,22 @@ pub struct ImportParams { pub state_cache_size: usize, } +impl Default for ImportParams { + fn default() -> Self { + Self { + pruning: None, + unsafe_pruning: false, + wasm_method: WasmExecutionMethod::Interpreted, + execution_strategies: Default::default(), + database_cache_size: 1024, + state_cache_size: 67108864, + } + } +} + /// Parameters used to create the network configuration. -#[derive(Debug, StructOpt, Clone)] +#[derive(Clone, Debug, Deserialize, StructOpt)] +#[serde(default, rename_all = "kebab-case")] pub struct NetworkConfigurationParams { /// Specify a list of bootnodes. #[structopt(long = "bootnodes", value_name = "URL")] @@ -210,13 +232,34 @@ pub struct NetworkConfigurationParams { pub max_parallel_downloads: u32, #[allow(missing_docs)] + #[serde(flatten)] #[structopt(flatten)] pub node_key_params: NodeKeyParams, } +impl Default for NetworkConfigurationParams { + fn default() -> Self { + Self { + bootnodes: vec![], + reserved_nodes: vec![], + reserved_only: false, + sentry_nodes: vec![], + listen_addr: vec![], + port: None, + no_private_ipv4: false, + out_peers: 25, + in_peers: 25, + no_mdns: false, + max_parallel_downloads: 5, + node_key_params: Default::default(), + } + } +} + arg_enum! { #[allow(missing_docs)] - #[derive(Debug, Copy, Clone, PartialEq, Eq)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] + #[serde(rename_all = "kebab-case")] pub enum NodeKeyType { Ed25519 } @@ -224,7 +267,8 @@ arg_enum! { /// Parameters used to create the `NodeKeyConfig`, which determines the keypair /// used for libp2p networking. -#[derive(Debug, StructOpt, Clone)] +#[derive(Clone, Debug, Deserialize, StructOpt)] +#[serde(default, rename_all = "kebab-case")] pub struct NodeKeyParams { /// The secret key to use for libp2p networking. /// @@ -284,8 +328,19 @@ pub struct NodeKeyParams { pub node_key_file: Option, } +impl Default for NodeKeyParams { + fn default() -> Self { + Self { + node_key: None, + node_key_type: NodeKeyType::Ed25519, + node_key_file: None, + } + } +} + /// Parameters used to create the pool configuration. -#[derive(Debug, StructOpt, Clone)] +#[derive(Clone, Debug, Deserialize, StructOpt)] +#[serde(default, rename_all = "kebab-case")] pub struct TransactionPoolParams { /// Maximum number of transactions in the transaction pool. #[structopt(long = "pool-limit", value_name = "COUNT", default_value = "512")] @@ -295,9 +350,19 @@ pub struct TransactionPoolParams { pub pool_kbytes: usize, } +impl Default for TransactionPoolParams { + fn default() -> Self { + Self { + pool_limit: 512, + pool_kbytes: 10240, + } + } +} + arg_enum! { #[allow(missing_docs)] - #[derive(Debug, Copy, Clone, PartialEq, Eq)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] + #[serde(rename_all = "kebab-case")] pub enum TracingReceiver { Log, Telemetry, @@ -316,7 +381,8 @@ impl Into for TracingReceiver { } /// Execution strategies parameters. -#[derive(Debug, StructOpt, Clone)] +#[derive(Clone, Debug, Deserialize, StructOpt)] +#[serde(default, rename_all = "kebab-case")] pub struct ExecutionStrategies { /// The means of execution used when calling into the runtime while syncing blocks. #[structopt( @@ -385,6 +451,19 @@ pub struct ExecutionStrategies { pub execution: Option, } +impl Default for ExecutionStrategies { + fn default() -> Self { + Self { + execution_syncing: ExecutionStrategy::NativeElseWasm, + execution_import_block: ExecutionStrategy::NativeElseWasm, + execution_block_construction: ExecutionStrategy::Wasm, + execution_offchain_worker: ExecutionStrategy::Native, + execution_other: ExecutionStrategy::Native, + execution: None, + } + } +} + /// The `run` command used to run a node. #[derive(Debug, StructOpt, Clone)] pub struct RunCmd { @@ -579,6 +658,10 @@ pub struct RunCmd { conflicts_with_all = &[ "password-interactive", "password" ] )] pub password_filename: Option, + + /// Specify the boot configuration json file . All command line input will be overwritten by this. + #[structopt(long = "conf", value_name = "PATH")] + pub conf: Option, } /// Stores all required Cli values for a keyring test account. @@ -669,7 +752,8 @@ fn parse_telemetry_endpoints(s: &str) -> Result<(String, u8), Box>` /// handling of `structopt`. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum Cors { /// All hosts allowed All, @@ -988,3 +1072,36 @@ where } } } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Conf { + pub shared_params: Option, + pub validator: Option, + pub sentry: Option, + // TODO: derive Deserialize + // pub keyring: Option, + pub light: Option, + pub import_params: Option, + pub name: Option, + pub keystore_path: Option, + pub offchain_worker: Option, + pub no_grandpa: Option, + pub network_config: Option, + pub pool_config: Option, + pub rpc_external: Option, + pub unsafe_rpc_external: Option, + pub ws_external: Option, + pub unsafe_ws_external: Option, + pub grafana_external: Option, + pub rpc_port: Option, + pub ws_port: Option, + pub grafana_port: Option, + pub ws_max_connections: Option, + pub rpc_cors: Option, + pub no_telemetry: Option, + pub telemetry_endpoints: Option>, + pub tracing_targets: Option, + pub tracing_receiver: Option, + pub force_authoring: Option, +}