diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index eca9c8f1fd..b557048f9d 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3377,6 +3377,7 @@ dependencies = [ "log 0.4.8", "nix", "node-executor", + "node-inspect", "node-primitives", "node-rpc", "node-runtime", @@ -3463,6 +3464,22 @@ dependencies = [ "wabt", ] +[[package]] +name = "node-inspect" +version = "0.8.0" +dependencies = [ + "derive_more", + "log 0.4.8", + "parity-scale-codec", + "sc-cli", + "sc-client-api", + "sc-service", + "sp-blockchain", + "sp-core", + "sp-runtime", + "structopt", +] + [[package]] name = "node-primitives" version = "2.0.0" @@ -7047,7 +7064,7 @@ dependencies = [ "hash256-std-hasher", "hex", "hex-literal", - "impl-serde 0.2.3", + "impl-serde 0.3.0", "lazy_static", "libsecp256k1", "log 0.4.8", diff --git a/substrate/bin/node/cli/Cargo.toml b/substrate/bin/node/cli/Cargo.toml index 528d7e5faa..079e8d13e2 100644 --- a/substrate/bin/node/cli/Cargo.toml +++ b/substrate/bin/node/cli/Cargo.toml @@ -90,6 +90,7 @@ node-executor = { version = "2.0.0", path = "../executor" } # CLI-specific dependencies sc-cli = { version = "0.8.0", optional = true, path = "../../../client/cli" } node-transaction-factory = { version = "0.8.0", optional = true, path = "../transaction-factory" } +node-inspect = { version = "0.8.0", optional = true, path = "../inspect" } # WASM-specific dependencies wasm-bindgen = { version = "0.2.57", optional = true } @@ -110,6 +111,7 @@ nix = "0.17" build-script-utils = { version = "2.0.0", package = "substrate-build-script-utils", path = "../../../utils/build-script-utils" } structopt = { version = "0.3.8", optional = true } node-transaction-factory = { version = "0.8.0", optional = true, path = "../transaction-factory" } +node-inspect = { version = "0.8.0", optional = true, path = "../inspect" } [build-dependencies.sc-cli] version = "0.8.0" @@ -129,12 +131,13 @@ browser = [ "wasm-bindgen-futures", ] cli = [ - "sc-cli", - "node-transaction-factory", - "sc-service/rocksdb", "node-executor/wasmi-errno", - "vergen", + "node-inspect", + "node-transaction-factory", + "sc-cli", + "sc-service/rocksdb", "structopt", + "vergen", ] wasmtime = [ "cli", diff --git a/substrate/bin/node/cli/src/cli.rs b/substrate/bin/node/cli/src/cli.rs index 7844c2c0a5..7dcd02699d 100644 --- a/substrate/bin/node/cli/src/cli.rs +++ b/substrate/bin/node/cli/src/cli.rs @@ -17,7 +17,7 @@ use sc_cli::{SharedParams, ImportParams, RunCmd}; use structopt::StructOpt; -#[allow(missing_docs)] +/// An overarching CLI command definition. #[derive(Clone, Debug, StructOpt)] #[structopt(settings = &[ structopt::clap::AppSettings::GlobalVersion, @@ -25,7 +25,7 @@ use structopt::StructOpt; structopt::clap::AppSettings::SubcommandsNegateReqs, ])] pub struct Cli { - #[allow(missing_docs)] + /// Possible subcommand with parameters. #[structopt(subcommand)] pub subcommand: Option, #[allow(missing_docs)] @@ -33,10 +33,10 @@ pub struct Cli { pub run: RunCmd, } -#[allow(missing_docs)] +/// Possible subcommands of the main binary. #[derive(Clone, Debug, StructOpt)] pub enum Subcommand { - #[allow(missing_docs)] + /// A set of base subcommands handled by `sc_cli`. #[structopt(flatten)] Base(sc_cli::Subcommand), /// The custom factory subcommmand for manufacturing transactions. @@ -46,6 +46,13 @@ pub enum Subcommand { Only supported for development or local testnet." )] Factory(FactoryCmd), + + /// The custom inspect subcommmand for decoding blocks and extrinsics. + #[structopt( + name = "inspect", + about = "Decode given block or extrinsic using current native runtime." + )] + Inspect(node_inspect::cli::InspectCmd), } /// The `factory` command used to generate transactions. diff --git a/substrate/bin/node/cli/src/command.rs b/substrate/bin/node/cli/src/command.rs index 61d1051796..ba0f2785c1 100644 --- a/substrate/bin/node/cli/src/command.rs +++ b/substrate/bin/node/cli/src/command.rs @@ -39,6 +39,16 @@ where load_spec, &version, ), + Some(Subcommand::Inspect(cmd)) => { + cmd.init(&mut config, load_spec, &version)?; + + let client = sc_service::new_full_client::< + node_runtime::Block,node_runtime::RuntimeApi, node_executor::Executor, _, _, + >(&config)?; + let inspect = node_inspect::Inspector::::new(client); + + cmd.run(inspect) + }, Some(Subcommand::Factory(cli_args)) => { sc_cli::init(&cli_args.shared_params, &version)?; sc_cli::init_config(&mut config, &cli_args.shared_params, &version, load_spec)?; diff --git a/substrate/bin/node/cli/src/lib.rs b/substrate/bin/node/cli/src/lib.rs index f5b915a2be..789d6a6913 100644 --- a/substrate/bin/node/cli/src/lib.rs +++ b/substrate/bin/node/cli/src/lib.rs @@ -27,7 +27,6 @@ //! hasn't been tested. #![warn(missing_docs)] -#![warn(unused_extern_crates)] pub mod chain_spec; diff --git a/substrate/bin/node/inspect/Cargo.toml b/substrate/bin/node/inspect/Cargo.toml new file mode 100644 index 0000000000..cbce7e4589 --- /dev/null +++ b/substrate/bin/node/inspect/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "node-inspect" +version = "0.8.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "1.0.0" } +derive_more = "0.99" +log = "0.4.8" +sc-cli = { version = "0.8.0", path = "../../../client/cli" } +sc-client-api = { version = "2.0.0", path = "../../../client/api" } +sc-service = { version = "0.8", default-features = false, path = "../../../client/service" } +sp-blockchain = { version = "2.0.0", path = "../../../primitives/blockchain" } +sp-core = { version = "2.0.0", path = "../../../primitives/core" } +sp-runtime = { version = "2.0.0", path = "../../../primitives/runtime" } +structopt = "0.3.8" diff --git a/substrate/bin/node/inspect/src/cli.rs b/substrate/bin/node/inspect/src/cli.rs new file mode 100644 index 0000000000..27afcfff91 --- /dev/null +++ b/substrate/bin/node/inspect/src/cli.rs @@ -0,0 +1,240 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Structs to easily compose inspect sub-command for CLI. + +use std::{ + fmt::Debug, + str::FromStr, +}; +use crate::{Inspector, PrettyPrinter}; +use sc_cli::{ImportParams, SharedParams, error}; +use structopt::StructOpt; + +/// The `inspect` command used to print decoded chain data. +#[derive(Debug, StructOpt, Clone)] +pub struct InspectCmd { + #[allow(missing_docs)] + #[structopt(flatten)] + pub command: InspectSubCmd, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub shared_params: SharedParams, + + #[allow(missing_docs)] + #[structopt(flatten)] + pub import_params: ImportParams, +} + +/// A possible inspect sub-commands. +#[derive(Debug, StructOpt, Clone)] +pub enum InspectSubCmd { + /// Decode block with native version of runtime and print out the details. + Block { + /// Address of the block to print out. + /// + /// Can be either a block hash (no 0x prefix) or a number to retrieve existing block, + /// or a 0x-prefixed bytes hex string, representing SCALE encoding of + /// a block. + #[structopt(value_name = "HASH or NUMBER or BYTES")] + input: String, + }, + /// Decode extrinsic with native version of runtime and print out the details. + Extrinsic { + /// Address of an extrinsic to print out. + /// + /// Can be either a block hash (no 0x prefix) or number and the index, in the form + /// of `{block}:{index}` or a 0x-prefixed bytes hex string, + /// representing SCALE encoding of an extrinsic. + #[structopt(value_name = "BLOCK:INDEX or BYTES")] + input: String, + }, +} + +impl InspectCmd { + /// Parse CLI arguments and initialize given config. + pub fn init( + &self, + config: &mut sc_service::config::Configuration, + spec_factory: impl FnOnce(&str) -> Result>, String>, + version: &sc_cli::VersionInfo, + ) -> error::Result<()> where + G: sc_service::RuntimeGenesis, + E: sc_service::ChainSpecExtension, + { + sc_cli::init_config(config, &self.shared_params, version, spec_factory)?; + // make sure to configure keystore + sc_cli::fill_config_keystore_in_memory(config)?; + // and all import params (especially pruning that has to match db meta) + sc_cli::fill_import_params( + config, + &self.import_params, + sc_service::Roles::FULL, + self.shared_params.dev, + )?; + Ok(()) + } + + /// Run the inspect command, passing the inspector. + pub fn run( + self, + inspect: Inspector, + ) -> error::Result<()> where + B: sp_runtime::traits::Block, + B::Hash: FromStr, + P: PrettyPrinter, + { + match self.command { + InspectSubCmd::Block { input } => { + let input = input.parse()?; + let res = inspect.block(input) + .map_err(|e| format!("{}", e))?; + println!("{}", res); + Ok(()) + }, + InspectSubCmd::Extrinsic { input } => { + let input = input.parse()?; + let res = inspect.extrinsic(input) + .map_err(|e| format!("{}", e))?; + println!("{}", res); + Ok(()) + }, + } + } +} + + +/// A block to retrieve. +#[derive(Debug, Clone, PartialEq)] +pub enum BlockAddress { + /// Get block by hash. + Hash(Hash), + /// Get block by number. + Number(Number), + /// Raw SCALE-encoded bytes. + Bytes(Vec), +} + +impl FromStr for BlockAddress { + type Err = String; + + fn from_str(s: &str) -> Result { + // try to parse hash first + if let Ok(hash) = s.parse() { + return Ok(Self::Hash(hash)) + } + + // then number + if let Ok(number) = s.parse() { + return Ok(Self::Number(number)) + } + + // then assume it's bytes (hex-encoded) + sp_core::bytes::from_hex(s) + .map(Self::Bytes) + .map_err(|e| format!( + "Given string does not look like hash or number. It could not be parsed as bytes either: {}", + e + )) + } +} + +/// An extrinsic address to decode and print out. +#[derive(Debug, Clone, PartialEq)] +pub enum ExtrinsicAddress { + /// Extrinsic as part of existing block. + Block(BlockAddress, usize), + /// Raw SCALE-encoded extrinsic bytes. + Bytes(Vec), +} + +impl FromStr for ExtrinsicAddress { + type Err = String; + + fn from_str(s: &str) -> Result { + // first try raw bytes + if let Ok(bytes) = sp_core::bytes::from_hex(s).map(Self::Bytes) { + return Ok(bytes) + } + + // split by a bunch of different characters + let mut it = s.split(|c| c == '.' || c == ':' || c == ' '); + let block = it.next() + .expect("First element of split iterator is never empty; qed") + .parse()?; + + let index = it.next() + .ok_or_else(|| format!("Extrinsic index missing: example \"5:0\""))? + .parse() + .map_err(|e| format!("Invalid index format: {}", e))?; + + Ok(Self::Block(block, index)) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use sp_core::hash::H160 as Hash; + + #[test] + fn should_parse_block_strings() { + type BlockAddress = super::BlockAddress; + + let b0 = BlockAddress::from_str("3BfC20f0B9aFcAcE800D73D2191166FF16540258"); + let b1 = BlockAddress::from_str("1234"); + let b2 = BlockAddress::from_str("0"); + let b3 = BlockAddress::from_str("0x0012345f"); + + + assert_eq!(b0, Ok(BlockAddress::Hash( + "3BfC20f0B9aFcAcE800D73D2191166FF16540258".parse().unwrap() + ))); + assert_eq!(b1, Ok(BlockAddress::Number(1234))); + assert_eq!(b2, Ok(BlockAddress::Number(0))); + assert_eq!(b3, Ok(BlockAddress::Bytes(vec![0, 0x12, 0x34, 0x5f]))); + } + + #[test] + fn should_parse_extrinsic_address() { + type BlockAddress = super::BlockAddress; + type ExtrinsicAddress = super::ExtrinsicAddress; + + let e0 = ExtrinsicAddress::from_str("1234"); + let b0 = ExtrinsicAddress::from_str("3BfC20f0B9aFcAcE800D73D2191166FF16540258:5"); + let b1 = ExtrinsicAddress::from_str("1234:0"); + let b2 = ExtrinsicAddress::from_str("0 0"); + let b3 = ExtrinsicAddress::from_str("0x0012345f"); + + + assert_eq!(e0, Err("Extrinsic index missing: example \"5:0\"".into())); + assert_eq!(b0, Ok(ExtrinsicAddress::Block( + BlockAddress::Hash("3BfC20f0B9aFcAcE800D73D2191166FF16540258".parse().unwrap()), + 5 + ))); + assert_eq!(b1, Ok(ExtrinsicAddress::Block( + BlockAddress::Number(1234), + 0 + ))); + assert_eq!(b2, Ok(ExtrinsicAddress::Block( + BlockAddress::Number(0), + 0 + ))); + assert_eq!(b3, Ok(ExtrinsicAddress::Bytes(vec![0, 0x12, 0x34, 0x5f]))); + } +} diff --git a/substrate/bin/node/inspect/src/lib.rs b/substrate/bin/node/inspect/src/lib.rs new file mode 100644 index 0000000000..5c4e18c0a7 --- /dev/null +++ b/substrate/bin/node/inspect/src/lib.rs @@ -0,0 +1,204 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! A CLI extension for substrate node, adding sub-command to pretty print debug info +//! about blocks and extrinsics. +//! +//! The blocks and extrinsics can either be retrieved from the database (on-chain), +//! or a raw SCALE-encoding can be provided. + +#![warn(missing_docs)] + +pub mod cli; + +use std::{ + fmt, + marker::PhantomData +}; +use codec::{Encode, Decode}; +use sc_client_api::BlockBody; +use sp_blockchain::HeaderBackend; +use sp_core::hexdisplay::HexDisplay; +use sp_runtime::{ + generic::BlockId, + traits::{Block, HashFor, NumberFor, Hash} +}; + +/// A helper type for a generic block input. +pub type BlockAddressFor = cli::BlockAddress< + as Hash>::Output, + NumberFor +>; + +/// A Pretty formatter implementation. +pub trait PrettyPrinter { + /// Nicely format block. + fn fmt_block(&self, fmt: &mut fmt::Formatter, block: &TBlock) -> fmt::Result; + /// Nicely format extrinsic. + fn fmt_extrinsic(&self, fmt: &mut fmt::Formatter, extrinsic: &TBlock::Extrinsic) -> fmt::Result; +} + +/// Default dummy debug printer. +#[derive(Default)] +pub struct DebugPrinter; +impl PrettyPrinter for DebugPrinter { + fn fmt_block(&self, fmt: &mut fmt::Formatter, block: &TBlock) -> fmt::Result { + writeln!(fmt, "Header:")?; + writeln!(fmt, "{:?}", block.header())?; + writeln!(fmt, "Block bytes: {:?}", HexDisplay::from(&block.encode()))?; + writeln!(fmt, "Extrinsics ({})", block.extrinsics().len())?; + for (idx, ex) in block.extrinsics().iter().enumerate() { + writeln!(fmt, "- {}:", idx)?; + >::fmt_extrinsic(self, fmt, ex)?; + } + Ok(()) + } + + fn fmt_extrinsic(&self, fmt: &mut fmt::Formatter, extrinsic: &TBlock::Extrinsic) -> fmt::Result { + writeln!(fmt, " {:?}", extrinsic)?; + writeln!(fmt, " Bytes: {:?}", HexDisplay::from(&extrinsic.encode()))?; + Ok(()) + } +} + +/// Aggregated error for `Inspector` operations. +#[derive(Debug, derive_more::From, derive_more::Display)] +pub enum Error { + /// Could not decode Block or Extrinsic. + Codec(codec::Error), + /// Error accessing blockchain DB. + Blockchain(sp_blockchain::Error), + /// Given block has not been found. + NotFound(String), +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + Self::Codec(ref e) => Some(e), + Self::Blockchain(ref e) => Some(e), + Self::NotFound(_) => None, + } + } +} + +/// A helper trait to access block headers and bodies. +pub trait ChainAccess: + HeaderBackend + + BlockBody +{} + +impl ChainAccess for T where + TBlock: Block, + T: sp_blockchain::HeaderBackend + sc_client_api::BlockBody, +{} + +/// Blockchain inspector. +pub struct Inspector = DebugPrinter> { + printer: TPrinter, + chain: Box>, + _block: PhantomData, +} + +impl> Inspector { + /// Create new instance of the inspector with default printer. + pub fn new( + chain: impl ChainAccess + 'static, + ) -> Self where TPrinter: Default { + Self::with_printer(chain, Default::default()) + } + + /// Customize pretty-printing of the data. + pub fn with_printer( + chain: impl ChainAccess + 'static, + printer: TPrinter, + ) -> Self { + Inspector { + chain: Box::new(chain) as _, + printer, + _block: Default::default(), + } + } + + /// Get a pretty-printed block. + pub fn block(&self, input: BlockAddressFor) -> Result { + struct BlockPrinter<'a, A, B>(A, &'a B); + impl<'a, A: Block, B: PrettyPrinter> fmt::Display for BlockPrinter<'a, A, B> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + self.1.fmt_block(fmt, &self.0) + } + } + + let block = self.get_block(input)?; + Ok(format!("{}", BlockPrinter(block, &self.printer))) + } + + fn get_block(&self, input: BlockAddressFor) -> Result { + Ok(match input { + cli::BlockAddress::Bytes(bytes) => { + TBlock::decode(&mut &*bytes)? + }, + cli::BlockAddress::Number(number) => { + let id = BlockId::number(number); + let not_found = format!("Could not find block {:?}", id); + let body = self.chain.block_body(&id)? + .ok_or_else(|| Error::NotFound(not_found.clone()))?; + let header = self.chain.header(id)? + .ok_or_else(|| Error::NotFound(not_found.clone()))?; + TBlock::new(header, body) + }, + cli::BlockAddress::Hash(hash) => { + let id = BlockId::hash(hash); + let not_found = format!("Could not find block {:?}", id); + let body = self.chain.block_body(&id)? + .ok_or_else(|| Error::NotFound(not_found.clone()))?; + let header = self.chain.header(id)? + .ok_or_else(|| Error::NotFound(not_found.clone()))?; + TBlock::new(header, body) + }, + }) + } + + /// Get a pretty-printed extrinsic. + pub fn extrinsic( + &self, + input: cli::ExtrinsicAddress< as Hash>::Output, NumberFor>, + ) -> Result { + struct ExtrinsicPrinter<'a, A: Block, B>(A::Extrinsic, &'a B); + impl<'a, A: Block, B: PrettyPrinter> fmt::Display for ExtrinsicPrinter<'a, A, B> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + self.1.fmt_extrinsic(fmt, &self.0) + } + } + + let ext = match input { + cli::ExtrinsicAddress::Block(block, index) => { + let block = self.get_block(block)?; + block.extrinsics() + .get(index) + .cloned() + .ok_or_else(|| Error::NotFound(format!( + "Could not find extrinsic {} in block {:?}", index, block + )))? + }, + cli::ExtrinsicAddress::Bytes(bytes) => { + TBlock::Extrinsic::decode(&mut &*bytes)? + } + }; + + Ok(format!("{}", ExtrinsicPrinter(ext, &self.printer))) + } +} diff --git a/substrate/client/service/src/config.rs b/substrate/client/service/src/config.rs index f4043d533e..2099c600df 100644 --- a/substrate/client/service/src/config.rs +++ b/substrate/client/service/src/config.rs @@ -137,7 +137,7 @@ pub enum KeystoreConfig { password: Option> }, /// In-memory keystore. Recommended for in-browser nodes. - InMemory + InMemory, } impl KeystoreConfig { diff --git a/substrate/primitives/core/Cargo.toml b/substrate/primitives/core/Cargo.toml index 3293769023..0d28fe139a 100644 --- a/substrate/primitives/core/Cargo.toml +++ b/substrate/primitives/core/Cargo.toml @@ -13,7 +13,7 @@ log = { version = "0.4.8", default-features = false } serde = { version = "1.0.101", optional = true, features = ["derive"] } byteorder = { version = "1.3.2", default-features = false } primitive-types = { version = "0.6.2", default-features = false, features = ["codec"] } -impl-serde = { version = "0.2.3", optional = true } +impl-serde = { version = "0.3.0", optional = true } wasmi = { version = "0.6.2", optional = true } hash-db = { version = "0.15.2", default-features = false } hash256-std-hasher = { version = "0.15.2", default-features = false } diff --git a/substrate/primitives/core/src/lib.rs b/substrate/primitives/core/src/lib.rs index f8c48ee970..01a96d8853 100644 --- a/substrate/primitives/core/src/lib.rs +++ b/substrate/primitives/core/src/lib.rs @@ -133,6 +133,15 @@ impl Deref for Bytes { fn deref(&self) -> &[u8] { &self.0[..] } } +#[cfg(feature = "std")] +impl sp_std::str::FromStr for Bytes { + type Err = bytes::FromHexError; + + fn from_str(s: &str) -> Result { + bytes::from_hex(s).map(Bytes) + } +} + /// Stores the encoded `RuntimeMetadata` for the native side as opaque type. #[derive(Encode, Decode, PartialEq)] pub struct OpaqueMetadata(Vec);