use anyhow::{
Context,
Result,
};
use colored::Colorize;
use contract_build::{
code_hash,
execute,
verbose_eprintln,
BuildArtifacts,
BuildInfo,
BuildMode,
ExecuteArgs,
ImageVariant,
ManifestPath,
Verbosity,
VerbosityFlags,
};
use contract_metadata::{
CodeHash,
ContractMetadata,
};
use std::{
fs::File,
path::PathBuf,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, clap::Args)]
#[clap(name = "verify")]
pub struct VerifyCommand {
#[clap(long, value_parser)]
manifest_path: Option<PathBuf>,
#[clap(long)]
contract: Option<PathBuf>,
#[clap(long, conflicts_with = "contract")]
wasm: Option<PathBuf>,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(long, conflicts_with = "verbose")]
output_json: bool,
}
impl VerifyCommand {
pub fn run(&self) -> Result<VerificationResult> {
let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?;
let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?;
if let Some(path) = &self.contract {
self.verify_contract(manifest_path, verbosity, path)
} else if let Some(path) = &self.wasm {
self.verify_wasm(manifest_path, verbosity, path)
} else {
anyhow::bail!("Either --wasm or --contract must be specified")
}
}
fn verify_wasm(
&self,
manifest_path: ManifestPath,
verbosity: Verbosity,
path: &PathBuf,
) -> Result<VerificationResult> {
let ref_buffer = std::fs::read(path)
.context(format!("Failed to read contract binary {}", path.display()))?;
let reference_code_hash = CodeHash(code_hash(&ref_buffer));
let args = ExecuteArgs {
manifest_path: manifest_path.clone(),
verbosity,
optimization_passes: Some(contract_build::OptimizationPasses::Z),
build_mode: BuildMode::Release,
build_artifact: BuildArtifacts::CodeOnly,
extra_lints: false,
..Default::default()
};
let build_result = execute(args)?;
let built_wasm_path = if let Some(m) = build_result.dest_wasm {
m
} else {
anyhow::bail!("\nThe workspace contract does not contain a Wasm binary,\n\
therefore we are unable to verify the contract."
.to_string()
.bright_yellow())
};
let target_buffer = std::fs::read(&built_wasm_path).context(format!(
"Failed to read contract binary {}",
built_wasm_path.display()
))?;
let output_code_hash = CodeHash(code_hash(&target_buffer));
if output_code_hash != reference_code_hash {
anyhow::bail!(format!(
"\nFailed to verify the authenticity of wasm binary at {} against the workspace \n\
found at {}.\n Expected {}, found {}",
format!("`{}`", path.display()).bright_white(),
format!("`{}`", built_wasm_path.display()).bright_white(),
format!("{}", reference_code_hash).bright_white(),
format!("{}", output_code_hash).bright_white())
);
}
Ok(VerificationResult {
is_verified: true,
image: None,
contract: built_wasm_path.display().to_string(),
reference_contract: path.display().to_string(),
output_json: self.output_json,
verbosity,
})
}
fn verify_contract(
&self,
manifest_path: ManifestPath,
verbosity: Verbosity,
path: &PathBuf,
) -> Result<VerificationResult> {
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,
);
let build_mode = if metadata.image.is_some() {
BuildMode::Verifiable
} else {
build_info.build_mode
};
if build_mode != BuildMode::Verifiable {
let expected_rust_toolchain = build_info.rust_toolchain;
let rust_toolchain = contract_build::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)?;
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()
);
}
let args = ExecuteArgs {
manifest_path: manifest_path.clone(),
verbosity,
build_mode,
build_artifact: BuildArtifacts::All,
optimization_passes: Some(build_info.wasm_opt_settings.optimization_passes),
keep_debug_symbols: build_info.wasm_opt_settings.keep_debug_symbols,
image: ImageVariant::from(metadata.image.clone()),
extra_lints: false,
..Default::default()
};
let build_result = execute(args)?;
let reference_code_hash = metadata.source.hash;
let built_contract_path = if let Some(m) = build_result.metadata_result {
m
} else {
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 target_bundle = &built_contract_path.dest_bundle;
let file = File::open(target_bundle.clone()).context(format!(
"Failed to open contract bundle {}",
target_bundle.display()
))?;
let built_contract: ContractMetadata =
serde_json::from_reader(file).context(format!(
"Failed to deserialize contract bundle {}",
target_bundle.display()
))?;
let target_code_hash = built_contract.source.hash;
if reference_code_hash != target_code_hash {
verbose_eprintln!(
verbosity,
"Expected Code Hash: '{}'\n\nGot Code Hash: `{}`",
&reference_code_hash,
&target_code_hash
);
anyhow::bail!(format!(
"\nFailed to verify the authenticity of {} contract against the workspace \n\
found at {}.",
format!("`{}`", metadata.contract.name).bright_white(),
format!("{:?}", manifest_path.as_ref()).bright_white()).bright_red()
);
}
Ok(VerificationResult {
is_verified: true,
image: metadata.image,
contract: target_bundle.display().to_string(),
reference_contract: path.display().to_string(),
output_json: self.output_json,
verbosity,
})
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct VerificationResult {
pub is_verified: bool,
pub image: Option<String>,
pub contract: String,
pub reference_contract: String,
#[serde(skip_serializing, skip_deserializing)]
pub output_json: bool,
#[serde(skip_serializing, skip_deserializing)]
pub verbosity: Verbosity,
}
impl VerificationResult {
pub fn display(&self) -> String {
format!(
"\n{} {} against reference contract {}",
"Successfully verified contract".bright_green().bold(),
format!("`{}`", &self.contract).bold(),
format!("`{}`!", &self.reference_contract).bold()
)
}
pub fn serialize_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(self)?)
}
}