diff --git a/CHANGELOG.md b/CHANGELOG.md index 126b48304..121d82466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add Rust specific build info to metadata - [#680](https://github.com/paritytech/cargo-contract/pull/680) +- Add `verify` command - [#696](https://github.com/paritytech/cargo-contract/pull/696) ### Changed - Removed requirement to install binaryen. The `wasm-opt` tool is now compiled into `cargo-contract`. diff --git a/crates/cargo-contract/src/cmd/build/mod.rs b/crates/cargo-contract/src/cmd/build/mod.rs index b3fd8f429..ce51b950f 100644 --- a/crates/cargo-contract/src/cmd/build/mod.rs +++ b/crates/cargo-contract/src/cmd/build/mod.rs @@ -68,7 +68,7 @@ use std::{ const MAX_MEMORY_PAGES: u32 = 16; /// Version of the currently executing `cargo-contract` binary. -const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Arguments to use when executing `build` or `check` commands. #[derive(Default)] diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 31a73208f..c1b2b7302 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -19,6 +19,7 @@ pub mod decode; pub mod metadata; pub mod new; pub mod test; +pub mod verify; pub(crate) use self::{ build::{ @@ -27,6 +28,7 @@ pub(crate) use self::{ }, decode::DecodeCommand, test::TestCommand, + verify::VerifyCommand, }; mod extrinsics; diff --git a/crates/cargo-contract/src/cmd/verify.rs b/crates/cargo-contract/src/cmd/verify.rs new file mode 100644 index 000000000..d0f262892 --- /dev/null +++ b/crates/cargo-contract/src/cmd/verify.rs @@ -0,0 +1,196 @@ +// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract 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. +// +// cargo-contract 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 cargo-contract. If not, see . + +use crate::{ + cmd::{ + build::{ + execute, + ExecuteArgs, + VERSION, + }, + metadata::BuildInfo, + }, + maybe_println, + workspace::ManifestPath, + BuildArtifacts, + Verbosity, + VerbosityFlags, +}; +use anyhow::{ + Context, + Result, +}; +use colored::Colorize; +use contract_metadata::{ + ContractMetadata, + SourceWasm, +}; + +use std::{ + fs::File, + path::PathBuf, +}; + +/// Checks if a contract in the given workspace matches that of a reference contract. +#[derive(Debug, clap::Args)] +#[clap(name = "verify")] +pub struct VerifyCommand { + /// Path to the `Cargo.toml` of the contract to verify. + #[clap(long, value_parser)] + manifest_path: Option, + /// The reference Wasm contract (`*.contract`) that the workspace will be checked against. + contract: PathBuf, + /// Denotes if output should be printed to stdout. + #[clap(flatten)] + verbosity: VerbosityFlags, +} + +impl VerifyCommand { + pub fn run(&self) -> Result<()> { + let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?; + let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; + + // 1. Read the given metadata, and pull out the `BuildInfo` + let path = &self.contract; + let file = File::open(path) + .context(format!("Failed to open contract bundle {}", path.display()))?; + + let metadata: ContractMetadata = serde_json::from_reader(&file).context( + format!("Failed to deserialize contract bundle {}", path.display()), + )?; + let build_info = if let Some(info) = metadata.source.build_info { + info + } else { + anyhow::bail!( + "\nThe metadata does not contain any build information which can be used to \ + verify a contract." + .to_string() + .bright_yellow() + ) + }; + + let build_info: BuildInfo = + serde_json::from_value(build_info.into()).context(format!( + "Failed to deserialize the build info from {}", + path.display() + ))?; + + tracing::debug!( + "Parsed the following build info from the metadata: {:?}", + &build_info, + ); + + // 2. Check that the build info from the metadata matches our current setup. + let expected_rust_toolchain = build_info.rust_toolchain; + let rust_toolchain = crate::util::rust_toolchain() + .expect("`rustc` always has a version associated with it."); + + let rustc_matches = rust_toolchain == expected_rust_toolchain; + let mismatched_rustc = format!( + "\nYou are trying to `verify` a contract using the `{rust_toolchain}` toolchain.\n\ + However, the original contract was built using `{expected_rust_toolchain}`. Please\n\ + install the correct toolchain (`rustup install {expected_rust_toolchain}`) and\n\ + re-run the `verify` command.",); + anyhow::ensure!(rustc_matches, mismatched_rustc.bright_yellow()); + + let expected_cargo_contract_version = build_info.cargo_contract_version; + let cargo_contract_version = semver::Version::parse(VERSION)?; + + // Note, assuming both versions of `cargo-contract` were installed with the same lockfile + // (e.g `--locked`) then the versions of `wasm-opt` should also match. + let cargo_contract_matches = + cargo_contract_version == expected_cargo_contract_version; + let mismatched_cargo_contract = format!( + "\nYou are trying to `verify` a contract using `cargo-contract` version \ + `{cargo_contract_version}`.\n\ + However, the original contract was built using `cargo-contract` version \ + `{expected_cargo_contract_version}`.\n\ + Please install the matching version and re-run the `verify` command.", + ); + anyhow::ensure!( + cargo_contract_matches, + mismatched_cargo_contract.bright_yellow() + ); + + // 3. Call `cargo contract build` with the `BuildInfo` from the metadata. + let args = ExecuteArgs { + manifest_path: manifest_path.clone(), + verbosity, + build_mode: build_info.build_mode, + network: Default::default(), + build_artifact: BuildArtifacts::CodeOnly, + unstable_flags: Default::default(), + optimization_passes: build_info.wasm_opt_settings.optimization_passes, + keep_debug_symbols: build_info.wasm_opt_settings.keep_debug_symbols, + skip_linting: true, + output_type: Default::default(), + }; + + let build_result = execute(args)?; + + // 4. Grab the built Wasm contract and compare it with the Wasm from the metadata. + let reference_wasm = if let Some(wasm) = metadata.source.wasm { + wasm + } else { + anyhow::bail!( + "\nThe metadata for the reference contract does not contain a Wasm binary,\n\ + therefore we are unable to verify the contract." + .to_string() + .bright_yellow() + ) + }; + + let built_wasm_path = if let Some(wasm) = build_result.dest_wasm { + wasm + } else { + // Since we're building the contract ourselves this should always be populated, + // but we'll bail out here just in case. + anyhow::bail!( + "\nThe metadata for the workspace contract does not contain a Wasm binary,\n\ + therefore we are unable to verify the contract." + .to_string() + .bright_yellow() + ) + }; + + let fs_wasm = std::fs::read(built_wasm_path)?; + let built_wasm = SourceWasm::new(fs_wasm); + + if reference_wasm != built_wasm { + tracing::debug!( + "Expected Wasm Binary '{}'\n\nGot Wasm Binary `{}`", + &reference_wasm, + &built_wasm + ); + + anyhow::bail!(format!( + "\nFailed to verify the authenticity of {} contract againt the workspace \n\ + found at {}.", + format!("`{}`", metadata.contract.name).bright_white(), + format!("{:?}", manifest_path.as_ref()).bright_white()).bright_red() + ); + } + + maybe_println!( + verbosity, + " \n{} {}", + "Succesfully verified contract".bright_green().bold(), + format!("`{}`!", &metadata.contract.name).bold(), + ); + + Ok(()) + } +} diff --git a/crates/cargo-contract/src/main.rs b/crates/cargo-contract/src/main.rs index afdbf2363..6d5806c1f 100644 --- a/crates/cargo-contract/src/main.rs +++ b/crates/cargo-contract/src/main.rs @@ -34,6 +34,7 @@ use self::{ InstantiateCommand, TestCommand, UploadCommand, + VerifyCommand, }, util::DEFAULT_KEY_COL_WIDTH, workspace::ManifestPath, @@ -504,6 +505,9 @@ enum Command { /// Decodes a contracts input or output data (supplied in hex-encoding) #[clap(name = "decode")] Decode(DecodeCommand), + /// Verifies that a given contract binary matches the build result of the specified workspace. + #[clap(name = "verify")] + Verify(VerifyCommand), } fn main() { @@ -570,6 +574,7 @@ fn exec(cmd: Command) -> Result<()> { .map_err(|err| map_extrinsic_err(err, call.is_json())) } Command::Decode(decode) => decode.run().map_err(format_err), + Command::Verify(verify) => verify.run(), } } diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index 4735dabaf..41786bbf2 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -166,7 +166,7 @@ impl Source { } /// The bytes of the compiled Wasm smart contract. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct SourceWasm( #[serde( serialize_with = "byte_str::serialize_as_byte_str",