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",