use anyhow::{
Context,
Result,
};
use colored::Colorize;
use contract_build::{
code_hash,
execute,
util::decode_hex,
verbose_eprintln,
BuildArtifacts,
BuildInfo,
BuildMode,
ExecuteArgs,
ImageVariant,
ManifestPath,
MetadataArtifacts,
Verbosity,
VerbosityFlags,
};
use contract_metadata::{
CodeHash,
ContractMetadata,
};
use regex::Regex;
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_bundle: Option<PathBuf>,
#[clap(long, conflicts_with = "contract_bundle")]
contract_binary: 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_bundle {
self.verify_contract(manifest_path, verbosity, path)
} else if let Some(path) = &self.contract_binary {
self.verify_contract_binary(manifest_path, verbosity, path)
} else {
anyhow::bail!(
"Either --contract-binary or --contract-bundle must be specified"
)
}
}
fn verify_contract_binary(
&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,
build_mode: BuildMode::Release,
build_artifact: BuildArtifacts::CodeOnly,
extra_lints: false,
..Default::default()
};
let build_result = execute(args)?;
let built_polkavm_path = if let Some(m) = build_result.dest_binary {
m
} else {
anyhow::bail!("\nThe workspace contract does not contain a contract binary (`.polkavm`),\n\
therefore we are unable to verify the contract."
.to_string()
.bright_yellow())
};
let target_buffer = std::fs::read(&built_polkavm_path).context(format!(
"Failed to read contract binary {}",
built_polkavm_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 the polkavm binary at {} against the workspace \n\
found at {}.\n Expected {}, found {}",
format!("`{}`", path.display()).bright_white(),
format!("`{}`", built_polkavm_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_polkavm_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.");
validate_toolchain_name(&expected_rust_toolchain)?;
validate_toolchain_name(&rust_toolchain)?;
let rustc_matches = rust_toolchain == expected_rust_toolchain;
let mismatched_rustc = format!(
"\nYou are trying to `verify` a contract using the following toolchain:\n\
{rust_toolchain}\n\n\
However, the original contract was built using this one:\n\
{expected_rust_toolchain}\n\n\
Please install the correct toolchain and re-run the `verify` command:\n\
rustup install {expected_rust_toolchain}");
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\n\
However, the original contract was built using `cargo-contract` version \
`{expected_cargo_contract_version}`.\n\n\
Please install the matching version and re-run the `verify` command:\n\
cargo install --force --locked cargo-contract --version {expected_cargo_contract_version}",
);
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,
image: ImageVariant::from(metadata.image.clone()),
extra_lints: false,
..Default::default()
};
let build_result = execute(args)?;
let reference_polkavm_blob = decode_hex(
&metadata
.source
.contract_binary
.expect("no `source.polkavm` field exists in metadata")
.to_string(),
)
.expect("decoding the `source.polkavm` hex failed");
let reference_code_hash = CodeHash(code_hash(&reference_polkavm_blob));
let built_contract_path = if let Some(MetadataArtifacts::Ink(m)) =
build_result.metadata_result
{
m
} else {
anyhow::bail!(
"\nThe metadata for the workspace contract does not contain a contract 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 from reference contract ({}): {}\nGot Code Hash: {}\n",
&path.display(),
&reference_code_hash,
&target_code_hash
);
anyhow::bail!(format!(
"\nFailed to verify `{}` against the workspace at `{}`: the hashed polkavm blobs are not matching.",
format!("{}", &path.display()).bright_white(),
format!("{}", manifest_path.as_ref().display()).bright_white()
)
.bright_red());
}
if reference_code_hash != metadata.source.hash {
verbose_eprintln!(
verbosity,
"Expected code hash from reference metadata ({}): {}\nGot Code Hash: {}\n",
&path.display(),
&reference_code_hash,
&metadata.source.hash
);
anyhow::bail!(format!(
"\nThe reference contract `{}` metadata is corrupt: the `source.hash` does not match the `source.polkavm` hash.",
format!("{}", &path.display()).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)?)
}
}
fn validate_toolchain_name(toolchain: &str) -> Result<()> {
let re = Regex::new(r"^[a-zA-Z._\-0-9]+$").expect("failed creating regex");
if re.is_match(toolchain) {
return Ok(());
}
anyhow::bail!("Invalid toolchain name: {}", toolchain)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_toolchain_names() {
assert!(validate_toolchain_name("nightly").is_ok());
assert!(validate_toolchain_name("stable").is_ok());
assert!(validate_toolchain_name("beta").is_ok());
assert!(validate_toolchain_name("nightly-2023-01-01").is_ok());
assert!(validate_toolchain_name("beta-2024-01-02").is_ok());
assert!(validate_toolchain_name("stable-2022-03-03").is_ok());
assert!(validate_toolchain_name("1.56.0").is_ok());
assert!(validate_toolchain_name("1.70").is_ok());
assert!(validate_toolchain_name("1.70-aarch64-apple-darwin").is_ok());
assert!(
validate_toolchain_name("nightly-2024-11-05-aarch64-apple-darwin").is_ok()
);
assert!(validate_toolchain_name("stable-x86_64-unknown-linux-gnu").is_ok());
}
#[test]
fn invalid_toolchain_names() {
assert!(validate_toolchain_name("https://sh.rust-toolchain.rs").is_err());
assert!(validate_toolchain_name("_ $").is_err());
assert!(validate_toolchain_name(
"nightly', please install https://sh.rust-toolchain.rs"
)
.is_err());
}
}