From 98b202d390aa54dba32d194988538b76393454bf Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 12 Jan 2024 14:29:56 -0800 Subject: [PATCH] verify and reproducible subcommands --- Cargo.lock | 122 +++++++++++++++++++++++++++++++++++++------------ README.md | 21 +++++++++ src/check.rs | 10 ++-- src/deploy.rs | 46 ++++++------------- src/docker.rs | 81 ++++++++++++++++++++++++++++++++ src/main.rs | 61 +++++++++++++++++++++---- src/project.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++- src/verify.rs | 79 ++++++++++++++++++++++++++++++++ src/wallet.rs | 6 +-- 9 files changed, 459 insertions(+), 79 deletions(-) create mode 100644 src/docker.rs create mode 100644 src/verify.rs diff --git a/Cargo.lock b/Cargo.lock index 2069d37..b9416e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1147,23 +1147,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] @@ -2171,9 +2160,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -2193,9 +2182,9 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -2468,7 +2457,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec 1.11.0", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2863,6 +2852,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_users" version = "0.4.3" @@ -3118,15 +3116,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.10" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3660,15 +3658,15 @@ checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4412,7 +4410,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -4421,21 +4428,42 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.33.0" @@ -4448,6 +4476,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.33.0" @@ -4460,6 +4494,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.33.0" @@ -4472,6 +4512,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.33.0" @@ -4484,12 +4530,24 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.33.0" @@ -4502,6 +4560,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.15" diff --git a/README.md b/README.md index 0fcb053..cf13cf4 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,27 @@ Usage: cargo stylus deploy [OPTIONS] See `--help` for all available flags and default values. +## Verifying Stylus Programs + +**cargo stylus verify** + +Verifies that a deployed smart contract is identical to that produced by the +current project. Since Stylus smart contracts include a hash of all project +files, this additionally verifies that code comments and other files are +identical. To ensure build reproducibility, if a program is to be verified, +it should be both deployed and verified using `cargo stylus reproducible`. + +See `--help` for all available flags and default values. + +## Reproducibly Deploying and Verifying + +**cargo stylus reproducible** + +Runs a `cargo stylus` command in a Docker container to ensure build +reproducibility. + +See `--help` for all available flags and default values. + ## Deploying Non-Rust WASM Projects The Stylus tool can also be used to deploy non-Rust, WASM projects to Stylus by specifying the WASM file directly with the `--wasm-file-path` flag to any of the cargo stylus commands. diff --git a/src/check.rs b/src/check.rs index 9270e06..9b0abab 100644 --- a/src/check.rs +++ b/src/check.rs @@ -65,16 +65,16 @@ pub async fn run_checks(cfg: CheckConfig) -> eyre::Result { Some(path) => PathBuf::from_str(path).unwrap(), None => project::build_project_dylib(BuildConfig { opt_level: project::OptLevel::default(), - nightly: cfg.nightly, + nightly: cfg.common_cfg.nightly, rebuild: true, - skip_contract_size_check: cfg.skip_contract_size_check, + skip_contract_size_check: cfg.common_cfg.skip_contract_size_check, }) .map_err(|e| eyre!("failed to build project to WASM: {e}"))?, }; println!("Reading WASM file at {}", wasm_file_path.display().grey()); let (precompressed_bytes, init_code) = - project::compress_wasm(&wasm_file_path, cfg.skip_contract_size_check) + project::compress_wasm(&wasm_file_path, cfg.common_cfg.skip_contract_size_check) .map_err(|e| eyre!("failed to get compressed WASM bytes: {e}"))?; let precompressed_size = FileByteSize::new(precompressed_bytes.len() as u64); @@ -85,10 +85,10 @@ pub async fn run_checks(cfg: CheckConfig) -> eyre::Result { println!( "Connecting to Stylus RPC endpoint: {}", - &cfg.endpoint.mint() + &cfg.common_cfg.endpoint.mint() ); - let provider = util::new_provider(&cfg.endpoint)?; + let provider = util::new_provider(&cfg.common_cfg.endpoint)?; let mut expected_program_addr = cfg.clone().expected_program_address; diff --git a/src/deploy.rs b/src/deploy.rs index 9f68b63..ec50a74 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -44,7 +44,7 @@ pub async fn deploy(cfg: DeployConfig) -> eyre::Result<()> { .map_err(|e| eyre!("Stylus checks failed: {e}"))?; let wallet = wallet::load(&cfg.check_cfg).map_err(|e| eyre!("could not load wallet: {e}"))?; - let provider = util::new_provider(&cfg.check_cfg.endpoint)?; + let provider = util::new_provider(&cfg.check_cfg.common_cfg.endpoint)?; let chain_id = provider .get_chainid() @@ -112,25 +112,29 @@ programs to Stylus chains here https://docs.arbitrum.io/stylus/stylus-quickstart } if deploy { + let build_cfg = BuildConfig { + opt_level: project::OptLevel::default(), + nightly: cfg.check_cfg.common_cfg.nightly, + rebuild: false, // The check step at the start of this command rebuilt. + skip_contract_size_check: cfg.check_cfg.common_cfg.skip_contract_size_check, + }; let wasm_file_path: PathBuf = match &cfg.check_cfg.wasm_file_path { Some(path) => PathBuf::from_str(path).unwrap(), - None => project::build_project_dylib(BuildConfig { - opt_level: project::OptLevel::default(), - nightly: cfg.check_cfg.nightly, - rebuild: false, // The check step at the start of this command rebuilt. - skip_contract_size_check: cfg.check_cfg.skip_contract_size_check, - }) - .map_err(|e| eyre!("could not build project to WASM: {e}"))?, + None => project::build_project_dylib(build_cfg) + .map_err(|e| eyre!("could not build project to WASM: {e}"))?, }; - let (_, init_code) = - project::compress_wasm(&wasm_file_path, cfg.check_cfg.skip_contract_size_check)?; + let hash = project::hash_files(build_cfg)?; + let (_, init_code) = project::compress_wasm( + &wasm_file_path, + cfg.check_cfg.common_cfg.skip_contract_size_check, + )?; println!(""); println!("{}", "====DEPLOYMENT====".grey()); println!( "Deploying program to address {}", to_checksum(&expected_program_addr, None).mint(), ); - let deployment_calldata = program_deployment_calldata(&init_code); + let deployment_calldata = project::program_deployment_calldata(&init_code, &hash); // Output the tx data to a user's specified directory if desired. if let Some(tx_data_output_dir) = output_dir { @@ -205,26 +209,6 @@ pub fn activation_calldata(program_addr: &H160) -> Vec { activate_calldata } -/// Prepares an EVM bytecode prelude for contract creation. -pub fn program_deployment_calldata(code: &[u8]) -> Vec { - let mut code_len = [0u8; 32]; - U256::from(code.len()).to_big_endian(&mut code_len); - let mut deploy: Vec = vec![]; - deploy.push(0x7f); // PUSH32 - deploy.extend(code_len); - deploy.push(0x80); // DUP1 - deploy.push(0x60); // PUSH1 - deploy.push(0x2a); // 42 the prelude length - deploy.push(0x60); // PUSH1 - deploy.push(0x00); - deploy.push(0x39); // CODECOPY - deploy.push(0x60); // PUSH1 - deploy.push(0x00); - deploy.push(0xf3); // RETURN - deploy.extend(code); - deploy -} - fn write_tx_data(tx_kind: TxKind, path: &Path, data: &[u8]) -> eyre::Result<()> { let file_name = format!("{tx_kind}_tx_data"); let path = path.join(file_name); diff --git a/src/docker.rs b/src/docker.rs new file mode 100644 index 0000000..39069e9 --- /dev/null +++ b/src/docker.rs @@ -0,0 +1,81 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md + +use std::io::Write; +use std::process::{Command, Stdio}; + +use eyre::{bail, eyre, Result}; + +fn version_to_image_name(version: &str) -> String { + format!("cargo-stylus-{}", version) +} + +fn image_exists(name: &str) -> Result { + let output = Command::new("docker") + .arg("images") + .arg(name) + .output() + .map_err(|e| eyre!("failed to execure Docker command: {e}"))?; + Ok(output.stdout.iter().filter(|c| **c == b'\n').count() > 1) +} + +fn create_image(version: &str) -> Result<()> { + let name = version_to_image_name(version); + if image_exists(&name)? { + return Ok(()); + } + let mut child = Command::new("docker") + .arg("build") + .arg("-t") + .arg(name) + .arg(".") + .arg("-f-") + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| eyre!("failed to execure Docker command: {e}"))?; + write!( + child.stdin.as_mut().unwrap(), + "\ + FROM rust:{} as builder\n\ + RUN rustup target add wasm32-unknown-unknown + RUN rustup target add wasm32-wasi + RUN cargo install cargo-stylus + ", + version + )?; + child.wait().map_err(|e| eyre!("wait failed: {e}"))?; + Ok(()) +} + +fn run_in_docker_container(version: &str, command_line: &[&str]) -> Result<()> { + let name = version_to_image_name(version); + if !image_exists(&name)? { + bail!("Docker image {name} doesn't exist"); + } + let dir = + std::env::current_dir().map_err(|e| eyre!("failed to find current directory: {e}"))?; + Command::new("docker") + .arg("run") + .arg("--network") + .arg("host") + .arg("-w") + .arg("/source") + .arg("-v") + .arg(format!("{}:/source", dir.as_os_str().to_str().unwrap())) + .arg(name) + .args(command_line) + .spawn() + .map_err(|e| eyre!("failed to execure Docker command: {e}"))? + .wait() + .map_err(|e| eyre!("wait failed: {e}"))?; + Ok(()) +} + +pub fn run_reproducible(version: &str, command_line: &[String]) -> Result<()> { + let mut command = vec!["cargo", "stylus"]; + for s in command_line.iter() { + command.push(s); + } + create_image(version)?; + run_in_docker_container(version, &command) +} diff --git a/src/main.rs b/src/main.rs index d92ea9c..eb8ed23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,12 +13,14 @@ mod check; mod color; mod constants; mod deploy; +mod docker; mod export_abi; mod new; mod project; mod replay; mod tx; mod util; +mod verify; mod wallet; #[derive(Parser, Debug)] @@ -51,6 +53,19 @@ struct StylusArgs { #[derive(Parser, Debug, Clone)] enum Subcommands { + /// Build in a Docker container to ensure reproducibility. + /// + /// Specify the Rust version to use, followed by the cargo stylus subcommand. + /// Example: `cargo stylus reproducible 1.75 check` + Reproducible { + /// Rust version to use. + #[arg()] + version: String, + + /// Stylus subcommand. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + stylus: Vec, + }, /// Create a new Rust project. New { /// Project name. @@ -79,6 +94,9 @@ enum Subcommands { /// Deploy a stylus contract. #[command(alias = "d")] Deploy(DeployConfig), + /// Verify a stylus contact. + #[command(alias = "v")] + Verify(VerifyConfig), /// Replay a transaction in gdb. #[command(alias = "r")] Replay(ReplayConfig), @@ -88,18 +106,10 @@ enum Subcommands { } #[derive(Args, Clone, Debug)] -pub struct CheckConfig { +pub struct CommonConfig { /// RPC endpoint of the Stylus node to connect to. #[arg(short, long, default_value = "https://stylus-testnet.arbitrum.io/rpc")] endpoint: String, - /// Specifies a WASM file instead of looking for one in the current directory. - #[arg(long)] - wasm_file_path: Option, - /// Specify the program address we want to check activation for. If unspecified, it will - /// compute the next program address from the user's wallet address and nonce, which will require - /// wallet-related flags to be specified. - #[arg(long, default_value = "0x0000000000000000000000000000000000000000")] - expected_program_address: H160, /// File path to a text file containing a private key. #[arg(long)] private_key_path: Option, @@ -120,6 +130,20 @@ pub struct CheckConfig { skip_contract_size_check: bool, } +#[derive(Args, Clone, Debug)] +pub struct CheckConfig { + #[command(flatten)] + common_cfg: CommonConfig, + /// Specifies a WASM file instead of looking for one in the current directory. + #[arg(long)] + wasm_file_path: Option, + /// Specify the program address we want to check activation for. If unspecified, it will + /// compute the next program address from the user's wallet address and nonce, which will require + /// wallet-related flags to be specified. + #[arg(long, default_value = "0x0000000000000000000000000000000000000000")] + expected_program_address: H160, +} + #[derive(Args, Clone, Debug)] pub struct DeployConfig { #[command(flatten)] @@ -139,6 +163,16 @@ pub struct DeployConfig { tx_sending_opts: TxSendingOpts, } +#[derive(Args, Clone, Debug)] +pub struct VerifyConfig { + #[command(flatten)] + common_cfg: CommonConfig, + + /// Hash of the deployment transaction. + #[arg(long)] + deployment_tx: String, +} + #[derive(Args, Clone, Debug)] pub struct ReplayConfig { /// RPC endpoint. @@ -254,12 +288,21 @@ async fn main_impl(args: StylusArgs) -> Result<()> { Subcommands::Deploy(config) => { run!(deploy::deploy(config).await, "failed to deploy"); } + Subcommands::Verify(config) => { + run!(verify::verify(config).await, "failed to verify"); + } Subcommands::Replay(config) => { run!(replay::replay(config).await, "failed to replay tx"); } Subcommands::Trace(config) => { run!(replay::trace(config).await, "failed to trace"); } + Subcommands::Reproducible { version, stylus } => { + run!( + docker::run_reproducible(&version, &stylus), + "failed reproducible run" + ); + } } Ok(()) } diff --git a/src/project.rs b/src/project.rs index 2188bf4..9f12f60 100644 --- a/src/project.rs +++ b/src/project.rs @@ -9,16 +9,25 @@ use crate::{ }; use brotli2::read::BrotliEncoder; use bytesize::ByteSize; +use ethers::types::U256; use eyre::{bail, eyre, Result}; -use std::{env::current_dir, io::Read, path::PathBuf}; +use std::process::Command; +use std::str::FromStr as _; +use std::{ + env::current_dir, + io::Read, + path::{Path, PathBuf}, +}; +use tiny_keccak::{Hasher, Keccak}; -#[derive(Default, PartialEq)] +#[derive(Clone, Copy, Default, PartialEq)] pub enum OptLevel { #[default] S, Z, } +#[derive(Clone, Copy)] pub struct BuildConfig { pub opt_level: OptLevel, pub nightly: bool, @@ -49,6 +58,105 @@ https://github.com/OffchainLabs/cargo-stylus/blob/main/OPTIMIZING_BINARIES.md"#) MaxPrecompressedSizeExceeded { got: ByteSize, want: ByteSize }, } +fn all_paths() -> Result> { + let mut files = Vec::::new(); + let mut directories = Vec::::new(); + directories.push(PathBuf::from_str(".").unwrap()); + while let Some(dir) = directories.pop() { + for f in std::fs::read_dir(&dir) + .map_err(|e| eyre!("Unable to read directory {}: {e}", dir.display()))? + { + let f = f.map_err(|e| eyre!("Error finding file in {}: {e}", dir.display()))?; + let mut pathbuf = dir.clone(); + pathbuf.push(f.file_name()); + let bytes = dir.as_os_str().as_encoded_bytes(); + if bytes == b"./target" || bytes == b"./.git" || bytes == b"./.gitignore" { + continue; + } + if pathbuf.is_dir() { + directories.push(pathbuf); + } else { + files.push(pathbuf); + } + } + } + Ok(files) +} + +pub fn hash_files(cfg: BuildConfig) -> Result<[u8; 32]> { + let mut keccak = Keccak::v256(); + let mut cmd = Command::new("cargo"); + if cfg.nightly { + cmd.arg("+nightly"); + } + cmd.arg("--version"); + let output = cmd + .output() + .map_err(|e| eyre!("failed to execute cargo command: {e}"))?; + if !output.status.success() { + bail!("cargo version command failed"); + } + keccak.update(&*output.stdout); + if cfg.opt_level == OptLevel::Z { + keccak.update(&[0]); + } else { + keccak.update(&[1]); + } + + let mut buf = vec![0u8; 0x100000]; + + let mut hash_file = |filename: &Path| -> Result<()> { + keccak.update(&(filename.as_os_str().len() as u64).to_be_bytes()); + keccak.update(filename.as_os_str().as_encoded_bytes()); + let mut file = std::fs::File::open(filename) + .map_err(|e| eyre!("failed to open file {}: {e}", filename.display()))?; + keccak.update(&file.metadata().unwrap().len().to_be_bytes()); + loop { + let bytes_read = file + .read(&mut *buf) + .map_err(|e| eyre!("Unable to read file {}: {e}", filename.display()))?; + if bytes_read == 0 { + break; + } + keccak.update(&buf[..bytes_read]); + } + Ok(()) + }; + + let mut paths = all_paths()?; + paths.sort(); + + for filename in paths.iter() { + hash_file(filename)?; + } + + let mut hash = [0u8; 32]; + keccak.finalize(&mut hash); + Ok(hash) +} + +/// Prepares an EVM bytecode prelude for contract creation. +pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec { + let mut code_len = [0u8; 32]; + U256::from(code.len()).to_big_endian(&mut code_len); + let mut deploy: Vec = vec![]; + deploy.push(0x7f); // PUSH32 + deploy.extend(code_len); + deploy.push(0x80); // DUP1 + deploy.push(0x60); // PUSH1 + deploy.push(42 + 1 + 32); // prelude + version + hash + deploy.push(0x60); // PUSH1 + deploy.push(0x00); + deploy.push(0x39); // CODECOPY + deploy.push(0x60); // PUSH1 + deploy.push(0x00); + deploy.push(0xf3); // RETURN + deploy.push(0x00); // version + deploy.extend(hash); + deploy.extend(code); + deploy +} + /// Build a Rust project to WASM and return the path to the compiled WASM file. pub fn build_project_dylib(cfg: BuildConfig) -> Result { let cwd: PathBuf = current_dir().map_err(|e| eyre!("could not get current dir: {e}"))?; diff --git a/src/verify.rs b/src/verify.rs new file mode 100644 index 0000000..9eb32e7 --- /dev/null +++ b/src/verify.rs @@ -0,0 +1,79 @@ +// Copyright 2023, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md + +#![allow(clippy::println_empty_string)] + +use std::path::PathBuf; + +use eyre::{bail, eyre}; + +use ethers::types::H160; + +use serde::{Deserialize, Serialize}; + +use crate::project::BuildConfig; +use crate::util; +use crate::{check, project, CheckConfig, VerifyConfig}; + +#[derive(Debug, Deserialize, Serialize)] +struct RpcResult { + input: String, +} + +fn hex_str_to_bytes(s: &str) -> eyre::Result> { + let s = if s.starts_with("0x") { &s[2..] } else { s }; + let mut result = Vec::with_capacity(s.len() / 2); + for i in 0..s.len() / 2 { + let j = 2 * i; + let val = + u8::from_str_radix(&s[j..j + 2], 16).map_err(|e| eyre!("Invalid hex data: {e}"))?; + result.push(val); + } + Ok(result) +} + +pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { + let provider = util::new_provider(&cfg.common_cfg.endpoint)?; + let result: RpcResult = provider + .request("eth_getTransactionByHash", [cfg.deployment_tx]) + .await + .map_err(|e| eyre!("RPC failed: {e}"))?; + let remote_data = + hex_str_to_bytes(&result.input).map_err(|e| eyre!("Failed parsing RPC data: {e}"))?; + + let output = util::new_command("cargo") + .arg("clean") + .output() + .map_err(|e| eyre!("failed to execute cargo clean: {e}"))?; + if !output.status.success() { + bail!("cargo clean command failed"); + } + let check_cfg = CheckConfig { + common_cfg: cfg.common_cfg.clone(), + wasm_file_path: None, + expected_program_address: H160::zero(), + }; + check::run_checks(check_cfg) + .await + .map_err(|e| eyre!("Stylus checks failed: {e}"))?; + let build_cfg = BuildConfig { + opt_level: project::OptLevel::default(), + nightly: cfg.common_cfg.nightly, + rebuild: false, + skip_contract_size_check: cfg.common_cfg.skip_contract_size_check, + }; + let wasm_file_path: PathBuf = project::build_project_dylib(build_cfg) + .map_err(|e| eyre!("could not build project to WASM: {e}"))?; + let (_, init_code) = + project::compress_wasm(&wasm_file_path, cfg.common_cfg.skip_contract_size_check)?; + let hash = project::hash_files(build_cfg)?; + let deployment_data = project::program_deployment_calldata(&init_code, &hash); + + if deployment_data == remote_data { + println!("Verified - data matches!"); + } else { + println!("Not verified - data does not match!"); + } + + Ok(()) +} diff --git a/src/wallet.rs b/src/wallet.rs index 47742ca..23ddecb 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -6,17 +6,17 @@ use std::str::FromStr; use ethers::signers::LocalWallet; use eyre::{bail, eyre}; -use crate::CheckConfig; +use crate::{CheckConfig, CommonConfig}; /// Loads a wallet for signing transactions either from a private key file path. /// or a keystore along with a keystore password file. pub fn load(cfg: &CheckConfig) -> eyre::Result { - let CheckConfig { + let CommonConfig { private_key_path, keystore_opts, private_key, .. - } = cfg; + } = &cfg.common_cfg; if private_key.is_some() && private_key_path.is_some() { bail!("cannot provide both --private-key and --private-key-path"); }