diff --git a/Cargo.lock b/Cargo.lock index 30028be59..c14aada54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -730,6 +730,43 @@ dependencies = [ "substrate-primitives 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", ] +[[package]] +name = "darwinia-cli" +version = "0.1.0" +dependencies = [ + "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "app_dirs 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "derive_more 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "exit-future 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "futures-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", + "names 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rpassword 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.103 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", + "sr-primitives 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "substrate-client 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-header-metadata 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-keyring 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-network 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-panic-handler 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-primitives 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-service 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-state-machine 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "substrate-telemetry 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "darwinia-ethereum-bridge" version = "0.2.0" @@ -2656,6 +2693,7 @@ dependencies = [ "console_log 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "darwinia-balances 2.0.0", + "darwinia-cli 0.1.0", "exit-future 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "futures-preview 0.3.0-alpha.19 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2687,7 +2725,6 @@ dependencies = [ "substrate-basic-authorship 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", "substrate-build-script-utils 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", "substrate-chain-spec 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", - "substrate-cli 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", "substrate-client 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", "substrate-client-db 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", "substrate-consensus-babe 2.0.0 (git+https://github.com/darwinia-network/substrate.git?branch=darwinia-develop)", diff --git a/Cargo.toml b/Cargo.toml index 89580d2ae..95c7068ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ panic = 'unwind' [workspace] members = [ + "core/cli", "core/merkle-mountain-range", "core/fly-client", "core/sr-eth-primitives", diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml new file mode 100644 index 000000000..93efb900b --- /dev/null +++ b/core/cli/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "darwinia-cli" +version = "0.1.0" +authors = ["Darwinia Network "] +description = "Darwinia CLI interface." +edition = "2018" + +[dependencies] +ansi_term = "0.12.1" +app_dirs = "1.2.1" +atty = "0.2.13" +clap = "2.33.0" +derive_more = "0.15.0" +env_logger = "0.7.0" +exit-future = "0.1.4" +futures = "0.1.29" +futures03 = { package = "futures-preview", version = "=0.3.0-alpha.19", features = ["compat"] } +fdlimit = "0.1.1" +lazy_static = "1.4.0" +log = "0.4.8" +names = "0.11.0" +regex = "1.3.1" +rpassword = "4.0.1" +serde = "1.0.103" +serde_json = "1.0.41" +structopt = "0.3.3" +time = "0.1.42" +tokio = "0.1.22" + +panic-handler = { package = "substrate-panic-handler", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +client = { package = "substrate-client", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +header-metadata = { package = "substrate-header-metadata", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +network = { package = "substrate-network", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +sr-primitives = { git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +primitives = { package = "substrate-primitives", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +service = { package = "substrate-service", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop", default-features = false } +state-machine = { package = "substrate-state-machine", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +substrate-telemetry = { git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } +keyring = { package = "substrate-keyring", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } + +[dev-dependencies] +tempdir = "0.3.7" + +[features] +wasmtime = [ + "service/wasmtime", +] diff --git a/core/cli/src/error.rs b/core/cli/src/error.rs new file mode 100644 index 000000000..e77bd9e1d --- /dev/null +++ b/core/cli/src/error.rs @@ -0,0 +1,64 @@ +// Copyright 2017-2019 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. + +use client; + +/// 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(service::Error), + /// Client error + Client(client::error::Error), + /// Input error + Input(String), + /// Invalid listen multiaddress + #[display(fmt = "Invalid listen multiaddress")] + InvalidListenMultiaddress, + /// Other uncategorized error. + 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/core/cli/src/execution_strategy.rs b/core/cli/src/execution_strategy.rs new file mode 100644 index 000000000..c77c188d1 --- /dev/null +++ b/core/cli/src/execution_strategy.rs @@ -0,0 +1,36 @@ +// Copyright 2018-2019 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 serde::Deserialize; +use structopt::clap::arg_enum; + +arg_enum! { + /// How to execute blocks + #[derive(Clone, Copy, Debug, Deserialize)] + #[serde(rename_all = "kebab-case")] + 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/core/cli/src/informant.rs b/core/cli/src/informant.rs new file mode 100644 index 000000000..2f2ae63e7 --- /dev/null +++ b/core/cli/src/informant.rs @@ -0,0 +1,84 @@ +// Copyright 2017-2019 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 client::BlockchainEvents; +use futures::{Future, Stream}; +use futures03::{StreamExt as _, TryStreamExt as _}; +use log::{info, warn}; +use service::AbstractService; +use sr_primitives::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 Future { + let client = service.client(); + + let mut display = display::InformantDisplay::new(); + + let display_notifications = service + .network_status(Duration::from_millis(5000)) + .for_each(move |(net_status, _)| { + let info = client.info(); + display.display(&info, net_status); + Ok(()) + }); + + let client = service.client(); + let mut last_best = { + let info = client.info(); + Some((info.chain.best_number, info.chain.best_hash)) + }; + + let display_block_import = client + .import_notification_stream() + .map(|v| Ok::<_, ()>(v)) + .compat() + .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 = header_metadata::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); + Ok(()) + }); + + display_notifications.join(display_block_import).map(|((), ())| ()) +} diff --git a/core/cli/src/informant/display.rs b/core/cli/src/informant/display.rs new file mode 100644 index 000000000..8d22ea4b9 --- /dev/null +++ b/core/cli/src/informant/display.rs @@ -0,0 +1,154 @@ +// Copyright 2019 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 client::ClientInfo; +use log::info; +use network::SyncState; +use service::NetworkStatus; +use sr_primitives::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/core/cli/src/lib.rs b/core/cli/src/lib.rs new file mode 100644 index 000000000..e09ec1c15 --- /dev/null +++ b/core/cli/src/lib.rs @@ -0,0 +1,1121 @@ +// Copyright 2017-2019 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)] + +pub mod error; +pub mod informant; + +#[macro_use] +mod traits; +mod execution_strategy; +mod params; + +pub use params::{CoreParams, ExecutionStrategy as ExecutionStrategyParam, NoCustom, SharedParams}; +#[doc(hidden)] +pub use structopt::clap::App; +pub use traits::{AugmentClap, GetLogFilter}; + +use std::{ + fs::{self, File}, + io::{stdin, stdout, Cursor, ErrorKind, Read, Seek, Write}, + iter, + net::{Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use app_dirs::{AppDataType, AppInfo}; +use client::ExecutionStrategies; +use futures::Future; +use lazy_static::lazy_static; +use log::info; +use names::{Generator, Name}; +use network::{ + self, + config::{build_multiaddr, NetworkConfiguration, NodeKeyConfig, NonReservedPeerMode, TransportConfig}, + multiaddr::Protocol, +}; +use primitives::H256; +use regex::Regex; +use service::{ + config::{Configuration, DatabaseConfig}, + ChainSpec, ChainSpecExtension, PruningMode, RuntimeGenesis, ServiceBuilderExport, ServiceBuilderImport, + ServiceBuilderRevert, +}; +use structopt::{clap::AppSettings, StructOpt}; +use substrate_telemetry::TelemetryEndpoints; + +use params::{ + BuildSpecCmd, Conf, Cors, ExportBlocksCmd, ImportBlocksCmd, MergeParameters, NetworkConfigurationParams, + NodeKeyParams, NodeKeyType, PurgeChainCmd, RevertCmd, RunCmd, TransactionPoolParams, +}; + +/// 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 + 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 + GetLogFilter, + RP: StructOpt + Clone + AugmentClap, + I: IntoIterator, + ::Item: Into + Clone, +{ + let full_version = service::config::full_version_from_strs(version.version, version.commit); + + 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); + + init_logger(cli_args.get_log_filter().as_ref().map(|v| v.as_ref()).unwrap_or("")); + fdlimit::raise_fd_limit(); + + 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::PurgeChain(params) => ParseAndPrepare::PurgeChain(ParseAndPreparePurge { params, version }), + params::CoreParams::Revert(params) => ParseAndPrepare::RevertChain(ParseAndPrepareRevert { params, version }), + params::CoreParams::Custom(params) => ParseAndPrepare::CustomCommand(params), + } +} + +/// 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 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), +} + +/// 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 = 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 = 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: ServiceBuilderExport, + 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.unwrap_or(1); + let to = self.params.to; + let json = self.params.json; + + let file: Box = match self.params.output { + Some(filename) => Box::new(File::create(filename)?), + None => Box::new(stdout()), + }; + + builder(config)?.export_blocks(exit.into_exit(), file, from.into(), to.map(Into::into), json)?; + 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: ServiceBuilderImport, + C: Default, + G: RuntimeGenesis, + E: ChainSpecExtension, + Exit: IntoExit, + { + let mut config = create_config_with_db_path(spec_factory, &self.params.shared_params, self.version)?; + config.wasm_method = self.params.wasm_method.into(); + config.execution_strategies = ExecutionStrategies { + importing: self.params.execution.into(), + other: self.params.execution.into(), + ..Default::default() + }; + + 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)) + } + }; + + let fut = builder(config)?.import_blocks(exit.into_exit(), file)?; + tokio::run(fut); + 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: ServiceBuilderRevert, + 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; + builder(config)?.revert_chain(blocks.into())?; + 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(network::config::Secret::File) + .unwrap_or(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 `network::Secret`. +fn parse_ed25519_secret(hex: &String) -> error::Result { + H256::from_str(&hex).map_err(invalid_node_key).and_then(|bytes| { + network::config::identity::ed25519::SecretKey::from_bytes(bytes) + .map(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; + } + + 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(()) +} + +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 service::Configuration, + cli: &RunCmd, +) -> Result<(), String> { + config.keystore_password = if cli.password_interactive { + Some(input_keystore_password()?.into()) + } 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(()) +} + +// 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); + + cli.name = conf.name; + + cli.keystore_path = conf.keystore_path; + + cli.database_cache_size = conf.database_cache_size; + if let Some(state_cache_size) = conf.state_cache_size { + cli.state_cache_size = state_cache_size; + } + + if let Some(shared_params) = conf.shared { + 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.pruning = conf.pruning; + if let Some(unsafe_pruning) = conf.unsafe_pruning { + cli.unsafe_pruning = unsafe_pruning; + } + + if let Some(wasm_method) = conf.wasm_method { + cli.wasm_method = wasm_method; + } + + if let Some(execution_strategies) = conf.execution_strategies { + cli.execution_strategies = execution_strategies; + } + + 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; + } + cli.rpc_port = conf.rpc_port; + + if let Some(ws_external) = conf.ws_external { + cli.ws_external = ws_external; + } + cli.ws_port = conf.ws_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; + } + + if let Some(force_authoring) = conf.force_authoring { + cli.force_authoring = force_authoring; + } + + Ok(()) +} + +fn create_run_node_config( + mut 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 spec = load_spec(&cli.shared_params, spec_factory)?; + let base_path = base_path(&cli.shared_params, &version); + let mut config = service::Configuration::default_with_spec_and_base_path(spec.clone(), Some(base_path)); + + load_conf_from_file(&mut cli)?; + + fill_config_keystore_password(&mut config, &cli)?; + + 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)); + + config.database = DatabaseConfig::Path { + path: config + .in_chain_config_dir(DEFAULT_DB_CONFIG_PATH) + .expect("We provided a base_path."), + cache_size: cli.database_cache_size, + }; + config.state_cache_size = cli.state_cache_size; + + 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 { + service::Roles::LIGHT + } else if is_authority { + service::Roles::AUTHORITY + } else { + service::Roles::FULL + }; + + // set sentry mode (i.e. act as an authority but **never** actively participate) + config.sentry_mode = cli.sentry; + + // 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 == service::Roles::AUTHORITY => PruningMode::ArchiveAll, + None => PruningMode::default(), + Some(s) => { + if role == 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: params::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), + }; + + config.offchain_worker = match (cli.offchain_worker, role) { + (params::OffchainWorkerEnabled::WhenValidating, 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 = if cli.rpc_external { "0.0.0.0" } else { "127.0.0.1" }; + let ws_interface: &str = if cli.ws_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.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)); + } + + // Imply forced authoring on --dev + config.force_authoring = cli.shared_params.dev || cli.force_authoring; + + Ok(config) +} + +/// 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 = 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); + // 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 !enable_color { + output = kill_color(output.as_ref()); + } + + if !isatty && record.level() <= log::Level::Info && atty::is(atty::Stream::Stdout) { + // duplicate INFO/WARN output to console + println!("{}", output); + } + 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 network::config::identity::ed25519; + use tempdir::TempDir; + + #[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(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 = TempDir::new("alice")?; + 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(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(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(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/core/cli/src/params.rs b/core/cli/src/params.rs new file mode 100644 index 000000000..844c59c70 --- /dev/null +++ b/core/cli/src/params.rs @@ -0,0 +1,1019 @@ +// Copyright 2018-2019 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 . + +pub use crate::execution_strategy::ExecutionStrategy; + +use std::path::PathBuf; + +use serde::Deserialize; +use structopt::{ + clap::{arg_enum, App, AppSettings, Arg, SubCommand}, + StructOpt, +}; + +use crate::traits::{AugmentClap, GetLogFilter}; + +/// Auxiliary macro to implement `GetLogFilter` for all types that have the `shared_params` field. +macro_rules! impl_get_log_filter { + ( $type:ident ) => { + impl $crate::GetLogFilter for $type { + fn get_log_filter(&self) -> Option { + self.shared_params.get_log_filter() + } + } + }; +} + +impl Into for ExecutionStrategy { + fn into(self) -> client::ExecutionStrategy { + match self { + ExecutionStrategy::Native => client::ExecutionStrategy::NativeWhenPossible, + ExecutionStrategy::Wasm => client::ExecutionStrategy::AlwaysWasm, + ExecutionStrategy::Both => client::ExecutionStrategy::Both, + ExecutionStrategy::NativeElseWasm => client::ExecutionStrategy::NativeElseWasm, + } + } +} + +arg_enum! { + /// How to execute Wasm runtime code + #[allow(missing_docs)] + #[derive(Clone, Debug, Deserialize)] + #[serde(rename_all = "kebab-case")] + 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) -> service::config::WasmExecutionMethod { + match self { + WasmExecutionMethod::Interpreted => service::config::WasmExecutionMethod::Interpreted, + #[cfg(feature = "wasmtime")] + WasmExecutionMethod::Compiled => 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(Clone, Debug, Deserialize)] + #[serde(rename_all = "kebab-case")] + pub enum OffchainWorkerEnabled { + Always, + Never, + WhenValidating, + } +} + +/// Shared parameters used by all `CoreParams`. +#[derive(Clone, Default, Debug, StructOpt, Deserialize)] +#[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")] + 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, +} + +impl GetLogFilter for SharedParams { + fn get_log_filter(&self) -> Option { + self.log.clone() + } +} + +/// Parameters used to create the network configuration. +#[derive(Clone, Debug, StructOpt, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +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, + + /// 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, +} + +impl Default for NetworkConfigurationParams { + fn default() -> Self { + Self { + bootnodes: vec![], + reserved_nodes: vec![], + reserved_only: false, + 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(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] + #[serde(rename_all = "kebab-case")] + pub enum NodeKeyType { + Ed25519 + } +} + +/// Parameters used to create the `NodeKeyConfig`, which determines the keypair +/// used for libp2p networking. +#[derive(Clone, Debug, StructOpt, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +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, +} + +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(Clone, Debug, StructOpt, Deserialize)] +#[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")] + 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, +} + +impl Default for TransactionPoolParams { + fn default() -> Self { + Self { + pool_limit: 512, + pool_kbytes: 10240, + } + } +} + +/// Execution strategies parameters. +#[derive(Clone, Debug, StructOpt, Deserialize)] +#[serde(rename_all = "kebab-case")] +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, + + /// Limit the memory the database cache can use. + #[structopt(long = "db-cache", value_name = "MiB")] + pub database_cache_size: Option, + + /// Specify the state cache size. + #[structopt(long = "state-cache-size", value_name = "Bytes", default_value = "67108864")] + pub state_cache_size: usize, + + /// Listen to all RPC interfaces. + /// + /// Default is local. + #[structopt(long = "rpc-external")] + pub rpc_external: bool, + + /// Listen to all Websocket interfaces. + /// + /// Default is local. + #[structopt(long = "ws-external")] + pub ws_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 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, + + /// 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, + + /// 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, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + #[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, + + /// 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, + + /// 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. +struct KeyringTestAccountCliValues { + help: String, + conflicts_with: Vec, + name: String, + variant: keyring::Sr25519Keyring, +} + +lazy_static::lazy_static! { + /// The Cli values for all test accounts. + static ref TEST_ACCOUNTS_CLI_VALUES: Vec = { + keyring::Sr25519Keyring::iter().map(|a| { + let help = format!( + "Shortcut for `--name {} --validator` with session keys for `{}` added to keystore.", + a, + a, + ); + let conflicts_with = 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 AugmentClap 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), + ) + }) + } +} + +impl Keyring { + fn is_subcommand() -> bool { + 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, Deserialize)] +#[serde(rename_all = "kebab-case")] +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) }) +} + +impl_augment_clap!(RunCmd); +impl_get_log_filter!(RunCmd); + +/// 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, +} + +impl_get_log_filter!(BuildSpecCmd); + +/// 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, +} + +impl_get_log_filter!(ExportBlocksCmd); + +/// 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, + + /// Method for executing Wasm runtime code. + #[structopt( + long = "wasm-execution", + value_name = "METHOD", + possible_values = &WasmExecutionMethod::variants(), + case_insensitive = true, + default_value = "Interpreted" + )] + pub wasm_method: WasmExecutionMethod, + + /// The means of execution used when calling into the runtime while importing blocks. + #[structopt( + long = "execution", + value_name = "STRATEGY", + possible_values = &ExecutionStrategy::variants(), + case_insensitive = true, + default_value = "NativeElseWasm" + )] + pub execution: ExecutionStrategy, +} + +impl_get_log_filter!(ImportBlocksCmd); + +/// 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: u32, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, +} + +impl_get_log_filter!(RevertCmd); + +/// 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, +} + +impl_get_log_filter!(PurgeChainCmd); + +/// 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), + + /// 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 + GetLogFilter, + RP: StructOpt + AugmentClap, +{ + 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( + 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)), + ("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)), + } + } +} + +impl GetLogFilter for CoreParams +where + CC: GetLogFilter, +{ + fn get_log_filter(&self) -> Option { + match self { + CoreParams::Run(c) => c.left.get_log_filter(), + CoreParams::BuildSpec(c) => c.get_log_filter(), + CoreParams::ExportBlocks(c) => c.get_log_filter(), + CoreParams::ImportBlocks(c) => c.get_log_filter(), + CoreParams::PurgeChain(c) => c.get_log_filter(), + CoreParams::Revert(c) => c.get_log_filter(), + CoreParams::Custom(c) => c.get_log_filter(), + } + } +} + +/// 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 AugmentClap for NoCustom { + fn augment_clap<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app + } +} + +impl GetLogFilter for NoCustom { + fn get_log_filter(&self) -> Option { + 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 + AugmentClap, + 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), + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Conf { + pub name: Option, + + pub keystore_path: Option, + + #[serde(rename = "db-cache")] + pub database_cache_size: Option, + pub state_cache_size: Option, + + pub shared: Option, + pub validator: Option, + pub sentry: Option, + // TODO: derive Deserialize + // pub keyring: Option, + pub light: Option, + + pub pruning: Option, + pub unsafe_pruning: Option, + + #[serde(rename = "wasm-execution")] + pub wasm_method: Option, + + pub execution_strategies: Option, + + pub offchain_worker: Option, + + pub no_grandpa: Option, + + pub network_config: Option, + pub pool_config: Option, + + pub rpc_external: Option, + pub rpc_port: Option, + + pub ws_external: Option, + pub ws_port: Option, + pub ws_max_connections: Option, + + pub rpc_cors: Option, + + pub no_telemetry: Option, + pub telemetry_endpoints: Option>, + + pub force_authoring: Option, +} diff --git a/core/cli/src/traits.rs b/core/cli/src/traits.rs new file mode 100644 index 000000000..8ea7e0468 --- /dev/null +++ b/core/cli/src/traits.rs @@ -0,0 +1,44 @@ +// Copyright 2017-2019 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 structopt::{clap::App, StructOpt}; + +/// Something that can augment a clap app with further parameters. +/// `derive(StructOpt)` is implementing this function by default, so a macro `impl_augment_clap!` +/// is provided to simplify the implementation of this trait. +pub trait AugmentClap: StructOpt { + /// Augment the given clap `App` with further parameters. + fn augment_clap<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b>; +} + +/// Macro for implementing the `AugmentClap` trait. +/// This requires that the given type uses `derive(StructOpt)`! +#[macro_export] +macro_rules! impl_augment_clap { + ( $type:ident ) => { + impl $crate::AugmentClap for $type { + fn augment_clap<'a, 'b>(app: $crate::App<'a, 'b>) -> $crate::App<'a, 'b> { + $type::augment_clap(app) + } + } + }; +} + +/// Returns the log filter given by the user as commandline argument. +pub trait GetLogFilter { + /// Returns the set log filter. + fn get_log_filter(&self) -> Option; +} diff --git a/darwinia_conf_example.json b/darwinia_conf_example.json new file mode 100644 index 000000000..beea8becd --- /dev/null +++ b/darwinia_conf_example.json @@ -0,0 +1,24 @@ +{ + "name": "darwinia conf example", + + "wasm-execution": "interpreted", + + "execution-strategies": { + "execution-syncing": "native", + "execution-import-block": "native", + "execution-block-construction": "native", + "execution-offchain-worker": "native", + "execution-other": "native", + "execution": "native" + }, + + "offchain-worker": "never", + + "no-grandpa": true, + + "rpc-external": false, + "rpc-port": 23333, + + "ws-external": false, + "ws-port": 23334 +} \ No newline at end of file diff --git a/node/cli/Cargo.toml b/node/cli/Cargo.toml index 720f85ed8..d52128197 100644 --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -68,37 +68,43 @@ node-primitives = { path = "../primitives" } node-executor = { path = "../executor" } # CLI-specific dependencies -tokio = { version = "0.1.22", optional = true } +ctrlc = { version = "3.1.3", features = ["termination"], optional = true } exit-future = { version = "0.1.4", optional = true } -substrate-cli = { git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop", optional = true } +tokio = { version = "0.1.22", optional = true } + transaction-factory = { git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop", optional = true } -ctrlc = { version = "3.1.3", features = ["termination"], optional = true } + +darwinia-cli = { path = "../../core/cli" } # WASM-specific dependencies -libp2p = { version = "0.13.0", default-features = false, optional = true } 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 } console_log = { version = "0.1.2", optional = true } js-sys = { version = "0.3.22", optional = true } wasm-bindgen = { version = "0.2.45", optional = true } wasm-bindgen-futures = { version = "0.3.22", optional = true } -kvdb-memorydb = { git = "https://github.com/paritytech/parity-common", rev = "b0317f649ab2c665b7987b8475878fc4d2e1f81d", optional = true } +libp2p = { version = "0.13.0", default-features = false, optional = true } rand6 = { package = "rand", version = "0.6", features = ["wasm-bindgen"], optional = true } # Imported just for the `wasm-bindgen` feature +kvdb-memorydb = { git = "https://github.com/paritytech/parity-common", rev = "b0317f649ab2c665b7987b8475878fc4d2e1f81d", optional = true } + [dev-dependencies] +tempfile = "3.1.0" +futures03 = { package = "futures-preview", version = "0.3.0-alpha.19" } + keystore = { package = "substrate-keystore", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } babe = { package = "substrate-consensus-babe", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop", features = ["test-helpers"] } consensus-common = { package = "substrate-consensus-common", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } service-test = { package = "substrate-service-test", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } -futures03 = { package = "futures-preview", version = "0.3.0-alpha.19" } -tempfile = "3.1.0" [build-dependencies] -substrate-cli = { git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop", optional = true } -build-script-utils = { package = "substrate-build-script-utils", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } structopt = "0.3.3" vergen = "3.0.4" +build-script-utils = { package = "substrate-build-script-utils", git = "https://github.com/darwinia-network/substrate.git", branch = "darwinia-develop" } + +darwinia-cli = { path = "../../core/cli", optional = true } + [features] default = ["cli"] browser = [ @@ -114,16 +120,18 @@ browser = [ "rand6" ] cli = [ - "substrate-cli", "transaction-factory", "tokio", "exit-future", "ctrlc", - "substrate-service/rocksdb" + "substrate-service/rocksdb", + + "darwinia-cli" ] wasmtime = [ "cli", "node-executor/wasmtime", - "substrate-cli/wasmtime", - "substrate-service/wasmtime" + "substrate-service/wasmtime", + + "darwinia-cli/wasmtime" ] diff --git a/node/cli/bin/main.rs b/node/cli/bin/main.rs index 41cf87939..63579d2a8 100644 --- a/node/cli/bin/main.rs +++ b/node/cli/bin/main.rs @@ -20,13 +20,13 @@ use std::cell::RefCell; +use darwinia_cli::VersionInfo; use futures::sync::oneshot; use futures::{future, Future}; -use substrate_cli::VersionInfo; // handles ctrl-c struct Exit; -impl substrate_cli::IntoExit for Exit { +impl darwinia_cli::IntoExit for Exit { type Exit = future::MapErr, fn(oneshot::Canceled) -> ()>; fn into_exit(self) -> Self::Exit { // can't use signal directly here because CtrlC takes only `Fn`. @@ -48,7 +48,7 @@ impl substrate_cli::IntoExit for Exit { } } -fn main() -> Result<(), substrate_cli::error::Error> { +fn main() -> Result<(), darwinia_cli::error::Error> { let version = VersionInfo { name: "Darwinia Crayfish Node", commit: env!("VERGEN_SHA_SHORT"), diff --git a/node/cli/build.rs b/node/cli/build.rs index 6f9337672..cb3a3bd34 100644 --- a/node/cli/build.rs +++ b/node/cli/build.rs @@ -16,8 +16,8 @@ use std::{env, fs, path::Path}; +use darwinia_cli::{CoreParams, NoCustom}; use structopt::{clap::Shell, StructOpt}; -use substrate_cli::{CoreParams, NoCustom}; use vergen::{generate_cargo_keys, ConstantsFlags}; fn main() { diff --git a/node/cli/src/cli.rs b/node/cli/src/cli.rs index b8e0b739a..c612ff8cd 100644 --- a/node/cli/src/cli.rs +++ b/node/cli/src/cli.rs @@ -14,12 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -pub use substrate_cli::{error, ExecutionStrategyParam, IntoExit, NoCustom, SharedParams, VersionInfo}; +pub use darwinia_cli::{error, ExecutionStrategyParam, IntoExit, NoCustom, SharedParams, VersionInfo}; use client::ExecutionStrategies; +use darwinia_cli::{parse_and_prepare, AugmentClap, GetLogFilter, ParseAndPrepare}; use log::info; use structopt::{clap::App, StructOpt}; -use substrate_cli::{parse_and_prepare, AugmentClap, GetLogFilter, ParseAndPrepare}; use substrate_service::{AbstractService, Configuration, Roles as ServiceRoles}; use tokio::{ prelude::Future, @@ -102,7 +102,7 @@ impl AugmentClap for FactoryCmd { } /// Parse command line arguments into service configuration. -pub fn run(args: I, exit: E, version: substrate_cli::VersionInfo) -> error::Result<()> +pub fn run(args: I, exit: E, version: darwinia_cli::VersionInfo) -> error::Result<()> where I: IntoIterator, T: Into + Clone, @@ -125,7 +125,7 @@ where info!(" |_____/ \\__,_|_| \\_/\\_/ |_|_| |_|_|\\__,_|"); info!("Chain specification: {}", config.chain_spec.name()); info!("Node name: {}", config.name); - info!("Roles: {:?}", substrate_cli::display_role(&config)); + info!("Roles: {:?}", darwinia_cli::display_role(&config)); let runtime = RuntimeBuilder::new() .name_prefix("main-tokio-") .build() @@ -150,7 +150,7 @@ where } ParseAndPrepare::CustomCommand(CustomSubcommands::Factory(cli_args)) => { let mut config: Config<_, _> = - substrate_cli::create_config_with_db_path(load_spec, &cli_args.shared_params, &version)?; + darwinia_cli::create_config_with_db_path(load_spec, &cli_args.shared_params, &version)?; config.execution_strategies = ExecutionStrategies { importing: cli_args.execution.into(), block_construction: cli_args.execution.into(), @@ -187,7 +187,7 @@ where { let (exit_send, exit) = exit_future::signal(); - let informant = substrate_cli::informant::build(&service); + let informant = darwinia_cli::informant::build(&service); runtime.executor().spawn(exit.until(informant).map(|_| ())); // we eagerly drop the service so that the internal exit future is fired, diff --git a/node/cli/src/lib.rs b/node/cli/src/lib.rs index baa78abab..145fe8c91 100644 --- a/node/cli/src/lib.rs +++ b/node/cli/src/lib.rs @@ -19,7 +19,7 @@ #![warn(missing_docs)] #![warn(unused_extern_crates)] -pub use substrate_cli::error; +pub use darwinia_cli::error; pub mod chain_spec; #[macro_use]