diff --git a/Cargo.lock b/Cargo.lock index f536651..d9529ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,7 +200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbcba3ca07cf7975f15d871b721fb18031eec8bce51103907f6dcce00b255d98" dependencies = [ "serde", - "winnow 0.6.15", + "winnow 0.6.16", ] [[package]] @@ -706,6 +706,7 @@ dependencies = [ "lazy_static", "serde", "serde_json", + "sys-info", "tempfile", "thiserror", "tiny-keccak", @@ -3691,9 +3692,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -3983,6 +3984,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sys-info" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -4181,21 +4192,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.16", + "toml_edit 0.22.17", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] @@ -4213,15 +4224,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.15", + "winnow 0.6.16", ] [[package]] @@ -4991,9 +5002,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.15" +version = "0.6.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" dependencies = [ "memchr", ] diff --git a/check/Cargo.toml b/check/Cargo.toml index 1cd89e4..d94052f 100644 --- a/check/Cargo.toml +++ b/check/Cargo.toml @@ -37,3 +37,4 @@ wasmparser = "0.213.0" wasm-encoder = "0.213.0" wasm-gen = "0.1.4" toml = "0.8.14" +sys-info = "0.9.1" diff --git a/check/src/check.rs b/check/src/check.rs index f893c9e..ebe2626 100644 --- a/check/src/check.rs +++ b/check/src/check.rs @@ -3,9 +3,9 @@ use crate::{ check::ArbWasm::ArbWasmErrors, - constants::{ARB_WASM_H160, ONE_ETH}, + constants::{ARB_WASM_H160, ONE_ETH, TOOLCHAIN_FILE_NAME}, macros::*, - project::{self, BuildConfig}, + project::{self, extract_toolchain_channel, BuildConfig}, CheckConfig, }; use alloy_primitives::{Address, B256, U256}; @@ -122,7 +122,10 @@ impl CheckConfig { if let Some(wasm) = self.wasm_file.clone() { return Ok((wasm, [0u8; 32])); } - let cfg = BuildConfig::new(self.common_cfg.rust_stable); + let toolchain_file_path = PathBuf::from(".").as_path().join(TOOLCHAIN_FILE_NAME); + let toolchain_channel = extract_toolchain_channel(&toolchain_file_path)?; + let rust_stable = !toolchain_channel.contains("nightly"); + let cfg = BuildConfig::new(rust_stable); let wasm = project::build_dylib(cfg.clone())?; let project_hash = project::hash_files(self.common_cfg.source_files_for_project_hash.clone(), cfg)?; diff --git a/check/src/constants.rs b/check/src/constants.rs index b564876..d2bb12a 100644 --- a/check/src/constants.rs +++ b/check/src/constants.rs @@ -43,3 +43,8 @@ pub const PROJECT_HASH_SECTION_NAME: &str = "project_hash"; /// Name of the toolchain file used to specify the Rust toolchain version for a project. pub const TOOLCHAIN_FILE_NAME: &str = "rust-toolchain.toml"; + +/// Base Rust image version to be used for reproducible builds. This simply installs cargo and the Rust +/// compiler, but the user will specify the exact version of the Rust toolchain to use for building within +/// the docker container. +pub const RUST_BASE_IMAGE_VERSION: &str = "1.79.0"; diff --git a/check/src/deploy.rs b/check/src/deploy.rs index db3c93f..600de07 100644 --- a/check/src/deploy.rs +++ b/check/src/deploy.rs @@ -2,7 +2,6 @@ // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md #![allow(clippy::println_empty_string)] - use crate::{ check::{self, ProgramCheck}, constants::ARB_WASM_H160, @@ -140,7 +139,7 @@ impl DeployConfig { greyln!("deployed code at address: {address}"); } let tx_hash = receipt.transaction_hash.debug_lavender(); - greyln!("Deployment tx hash: {tx_hash}"); + greyln!("deployment tx hash: {tx_hash}"); println!( r#"we recommend running cargo stylus cache --address={} to cache your activated program in ArbOS. Cached programs benefit from cheaper calls. To read more about the Stylus program cache, see diff --git a/check/src/docker.rs b/check/src/docker.rs index 5a18e4a..0c327d6 100644 --- a/check/src/docker.rs +++ b/check/src/docker.rs @@ -5,9 +5,11 @@ use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; +use cargo_stylus_util::color::Color; use eyre::{bail, eyre, Result}; -use crate::constants::TOOLCHAIN_FILE_NAME; +use crate::constants::{RUST_BASE_IMAGE_VERSION, TOOLCHAIN_FILE_NAME}; +use crate::macros::greyln; use crate::project::extract_toolchain_channel; fn version_to_image_name(version: &str) -> String { @@ -20,16 +22,38 @@ fn image_exists(name: &str) -> Result { .arg(name) .output() .map_err(|e| eyre!("failed to execute Docker command: {e}"))?; + + if !output.status.success() { + let stderr = std::str::from_utf8(&output.stderr) + .map_err(|e| eyre!("failed to read Docker command stderr: {e}"))?; + if stderr.contains("Cannot connect to the Docker daemon") { + println!( + r#"Cargo stylus deploy|check|verify run in a Docker container by default to ensure deployments +are reproducible, but Docker is not found in your system. Please install Docker if you wish to create +a reproducible deployment, or opt out by using the --no-verify flag for local builds"# + ); + bail!("Docker not running"); + } + bail!(stderr.to_string()) + } + Ok(output.stdout.iter().filter(|c| **c == b'\n').count() > 1) } fn create_image(version: &str) -> Result<()> { - let name = version_to_image_name(version); + let name = version_to_image_name(&version); if image_exists(&name)? { return Ok(()); } - let toolchain_file_path = PathBuf::from(".").as_path().join(TOOLCHAIN_FILE_NAME); - let toolchain_channel = extract_toolchain_channel(&toolchain_file_path)?; + let cargo_stylus_version = env!("CARGO_PKG_VERSION"); + let cargo_stylus_version: String = cargo_stylus_version + .chars() + .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '.') + .collect(); + println!( + "Building Docker image for cargo-stylus version {} and Rust toolchain {}", + cargo_stylus_version, version, + ); let mut child = Command::new("docker") .arg("build") .arg("-t") @@ -42,20 +66,20 @@ fn create_image(version: &str) -> Result<()> { write!( child.stdin.as_mut().unwrap(), "\ - FROM rust:{} as builder\n\ - RUN rustup toolchain install {} && rustup default {} + FROM --platform=linux/amd64 rust:{} as builder\n\ + RUN rustup toolchain install {}-x86_64-unknown-linux-gnu + RUN rustup default {}-x86_64-unknown-linux-gnu RUN rustup target add wasm32-unknown-unknown RUN rustup target add wasm32-wasi - RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add x86_64-unknown-linux-gnu - RUN cargo install cargo-stylus - RUN cargo install --force cargo-stylus-check - RUN cargo install --force cargo-stylus-replay - RUN cargo install --force cargo-stylus-cgen + RUN cargo install cargo-stylus-check --version {} --force + RUN cargo install cargo-stylus --version {} --force ", + RUST_BASE_IMAGE_VERSION, + version, version, - toolchain_channel, - toolchain_channel, + cargo_stylus_version, + cargo_stylus_version, )?; child.wait().map_err(|e| eyre!("wait failed: {e}"))?; Ok(()) @@ -79,21 +103,47 @@ fn run_in_docker_container(version: &str, command_line: &[&str]) -> Result<()> { .arg(name) .args(command_line) .spawn() - .map_err(|e| eyre!("failed to execure Docker command: {e}"))? + .map_err(|e| eyre!("failed to execute Docker command: {e}"))? .wait() .map_err(|e| eyre!("wait failed: {e}"))?; Ok(()) } -pub fn run_reproducible(version: &str, command_line: &[String]) -> Result<()> { - let version: String = version - .chars() - .filter(|c| c.is_alphanumeric() || *c == '.') - .collect(); +pub fn run_reproducible(command_line: &[String]) -> Result<()> { + verify_valid_host()?; + let toolchain_file_path = PathBuf::from(".").as_path().join(TOOLCHAIN_FILE_NAME); + let toolchain_channel = extract_toolchain_channel(&toolchain_file_path)?; + greyln!( + "Running reproducible Stylus command with toolchain {}", + toolchain_channel.mint() + ); let mut command = vec!["cargo", "stylus"]; for s in command_line.iter() { command.push(s); } - create_image(&version)?; - run_in_docker_container(&version, &command) + create_image(&toolchain_channel)?; + run_in_docker_container(&toolchain_channel, &command) +} + +fn verify_valid_host() -> Result<()> { + let Ok(os_type) = sys_info::os_type() else { + bail!("unable to determine host OS type"); + }; + if os_type == "Windows" { + // Check for WSL environment + let Ok(kernel_version) = sys_info::os_release() else { + bail!("unable to determine kernel version"); + }; + if kernel_version.contains("microsoft") || kernel_version.contains("WSL") { + greyln!("Detected Windows Linux Subsystem host"); + } else { + bail!( + "Reproducible cargo stylus commands on Windows are only supported \ + in Windows Linux Subsystem (WSL). Please install within WSL. \ + To instead opt out of reproducible builds, add the --no-verify \ + flag to your commands." + ); + } + } + Ok(()) } diff --git a/check/src/main.rs b/check/src/main.rs index da3c0b6..c7f60bf 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -4,6 +4,7 @@ use clap::{ArgGroup, Args, Parser}; use ethers::types::{H160, U256}; use eyre::{eyre, Context, Result}; +use std::fmt; use std::path::PathBuf; use tokio::runtime::Builder; @@ -57,19 +58,6 @@ enum Apis { /// Deploy a contract. #[command(alias = "d")] Deploy(DeployConfig), - /// 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.77 check` - Reproducible { - /// Rust version to use. - #[arg()] - rust_version: String, - - /// Stylus subcommand. - #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - stylus: Vec, - }, /// Verify the deployment of a Stylus program. #[command(alias = "v")] Verify(VerifyConfig), @@ -80,9 +68,6 @@ struct CommonConfig { /// Arbitrum RPC endpoint. #[arg(short, long, default_value = "https://sepolia-rollup.arbitrum.io/rpc")] endpoint: String, - /// Whether to use stable Rust. - #[arg(long)] - rust_stable: bool, /// Whether to print debug info. #[arg(long)] verbose: bool, @@ -123,6 +108,10 @@ pub struct CheckConfig { /// Where to deploy and activate the program (defaults to a random address). #[arg(long)] program_address: Option, + /// If specified, will not run the command in a reproducible docker container. Useful for local + /// builds, but at the risk of not having a reproducible contract for verification purposes. + #[arg(long)] + no_verify: bool, } #[derive(Args, Clone, Debug)] @@ -141,10 +130,13 @@ struct DeployConfig { pub struct VerifyConfig { #[command(flatten)] common_cfg: CommonConfig, - /// Hash of the deployment transaction. #[arg(long)] deployment_tx: String, + #[arg(long)] + /// If specified, will not run the command in a reproducible docker container. Useful for local + /// builds, but at the risk of not having a reproducible contract for verification purposes. + no_verify: bool, } #[derive(Clone, Debug, Args)] @@ -164,6 +156,110 @@ struct AuthOpts { keystore_password_path: Option, } +impl fmt::Display for CommonConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Convert the vector of source files to a comma-separated string + let mut source_files: String = "".to_string(); + if !self.source_files_for_project_hash.is_empty() { + source_files = format!( + "--source-files-for-project-hash={}", + self.source_files_for_project_hash.join(", ") + ); + } + write!( + f, + "--endpoint={} {} {} {}", + self.endpoint, + match self.verbose { + true => "--verbose", + false => "", + }, + source_files, + match &self.max_fee_per_gas_gwei { + Some(fee) => format!("--max-fee-per-gas-gwei {}", fee), + None => "".to_string(), + } + ) + } +} + +impl fmt::Display for CheckConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {} {}", + self.common_cfg.to_string(), + match &self.wasm_file { + Some(path) => format!("--wasm-file={}", path.display().to_string()), + None => "".to_string(), + }, + match &self.program_address { + Some(addr) => format!("--program-address={:?}", addr), + None => "".to_string(), + }, + match self.no_verify { + true => "--no-verify".to_string(), + false => "".to_string(), + }, + ) + } +} + +impl fmt::Display for DeployConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {}", + self.check_config.to_string(), + self.auth.to_string(), + match self.estimate_gas { + true => "--estimate-gas".to_string(), + false => "".to_string(), + }, + ) + } +} + +impl fmt::Display for AuthOpts { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} {} {} {}", + match &self.private_key_path { + Some(path) => format!("--private-key-path={}", path.display().to_string()), + None => "".to_string(), + }, + match &self.private_key { + Some(key) => format!("--private-key={}", key.clone()), + None => "".to_string(), + }, + match &self.keystore_path { + Some(path) => format!("--keystore-path={}", path.clone()), + None => "".to_string(), + }, + match &self.keystore_password_path { + Some(path) => format!("--keystore-password-path={}", path.display().to_string()), + None => "".to_string(), + } + ) + } +} + +impl fmt::Display for VerifyConfig { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} --deployment-tx={} {}", + self.common_cfg.to_string(), + self.deployment_tx, + match self.no_verify { + true => "--no-verify".to_string(), + false => "".to_string(), + } + ) + } +} + fn main() -> Result<()> { let args = Opts::parse(); let runtime = Builder::new_multi_thread().enable_all().build()?; @@ -188,22 +284,61 @@ async fn main_impl(args: Opts) -> Result<()> { run!(cache::cache_program(&config).await, "stylus cache failed"); } Apis::Check(config) => { - run!(check::check(&config).await, "stylus checks failed"); + if config.no_verify { + run!(check::check(&config).await, "stylus checks failed"); + } else { + let mut commands: Vec = + vec![String::from("check"), String::from("--no-verify")]; + let config_args = config + .to_string() + .split(' ') + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect::>(); + commands.extend(config_args); + run!( + docker::run_reproducible(&commands), + "failed reproducible run" + ); + } } Apis::Deploy(config) => { - run!(deploy::deploy(config).await, "failed to deploy"); - } - Apis::Reproducible { - rust_version, - stylus, - } => { - run!( - docker::run_reproducible(&rust_version, &stylus), - "failed reproducible run" - ); + if config.check_config.no_verify { + run!(deploy::deploy(config).await, "stylus deploy failed"); + } else { + let mut commands: Vec = + vec![String::from("deploy"), String::from("--no-verify")]; + let config_args = config + .to_string() + .split(' ') + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect::>(); + commands.extend(config_args); + run!( + docker::run_reproducible(&commands), + "failed reproducible run" + ); + } } Apis::Verify(config) => { - run!(verify::verify(config).await, "failed to verify"); + if config.no_verify { + run!(verify::verify(config).await, "failed to verify"); + } else { + let mut commands: Vec = + vec![String::from("verify"), String::from("--no-verify")]; + let config_args = config + .to_string() + .split(' ') + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect::>(); + commands.extend(config_args); + run!( + docker::run_reproducible(&commands), + "failed reproducible run" + ); + } } } Ok(()) diff --git a/check/src/project.rs b/check/src/project.rs index eab27f8..0530ca6 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -167,12 +167,17 @@ fn all_paths(root_dir: &Path, source_file_patterns: Vec) -> Result Result { - let toolchain_file_contents = std::fs::read_to_string(toolchain_file_path).wrap_err( + let toolchain_file_contents = fs::read_to_string(toolchain_file_path).context( "expected to find a rust-toolchain.toml file in project directory \ - to specify your Rust toolchain for reproducible verification", + to specify your Rust toolchain for reproducible verification. The channel in your project's rust-toolchain.toml's \ + toolchain section must be a specific version e.g., '1.80.0' or 'nightly-YYYY-MM-DD'. \ + To ensure reproducibility, it cannot be a generic channel like 'stable', 'nightly', or 'beta'. Read more about \ + the toolchain file in https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file or see \ + the file in https://github.com/OffchainLabs/stylus-hello-world for an example", )?; + let toolchain_toml: Value = - toml::from_str(&toolchain_file_contents).wrap_err("failed to parse rust-toolchain.toml")?; + toml::from_str(&toolchain_file_contents).context("failed to parse rust-toolchain.toml")?; // Extract the channel from the toolchain section let Some(toolchain) = toolchain_toml.get("toolchain") else { @@ -184,11 +189,19 @@ pub fn extract_toolchain_channel(toolchain_file_path: &PathBuf) -> Result, cfg: BuildConfig) -> Result let mut hash = [0u8; 32]; keccak.finalize(&mut hash); greyln!( - "Project hash computed on deployment: {:?}", + "project metadata hash computed on deployment: {:?}", hex::encode(hash) ); Ok(hash) diff --git a/check/src/verify.rs b/check/src/verify.rs index f3e0ca0..668deee 100644 --- a/check/src/verify.rs +++ b/check/src/verify.rs @@ -14,8 +14,10 @@ use serde::{Deserialize, Serialize}; use crate::{ check, + constants::TOOLCHAIN_FILE_NAME, deploy::{self, extract_compressed_wasm, extract_program_evm_deployment_prelude}, - project, CheckConfig, VerifyConfig, + project::{self, extract_toolchain_channel}, + CheckConfig, VerifyConfig, }; use cargo_stylus_util::{color::Color, sys}; @@ -30,6 +32,9 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { if hash.len() != 32 { bail!("Invalid hash"); } + let toolchain_file_path = PathBuf::from(".").as_path().join(TOOLCHAIN_FILE_NAME); + let toolchain_channel = extract_toolchain_channel(&toolchain_file_path)?; + let rust_stable = !toolchain_channel.contains("nightly"); let Some(result) = provider .get_transaction(H256::from_slice(&hash)) .await @@ -49,13 +54,14 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { common_cfg: cfg.common_cfg.clone(), wasm_file: None, program_address: None, + no_verify: cfg.no_verify, }; let _ = check::check(&check_cfg) .await .map_err(|e| eyre!("Stylus checks failed: {e}"))?; let build_cfg = project::BuildConfig { opt_level: project::OptLevel::default(), - stable: cfg.common_cfg.rust_stable, + stable: rust_stable, }; let wasm_file: PathBuf = project::build_dylib(build_cfg.clone()) .map_err(|e| eyre!("could not build project to WASM: {e}"))?; diff --git a/main/src/main.rs b/main/src/main.rs index 43da340..175ce04 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -50,9 +50,6 @@ enum Subcommands { /// Verify the deployment of a Stylus program against a local project. #[command(alias = "v")] Verify, - /// Run cargo stylus commands in a Docker container for reproducibility. - #[command()] - Reproducible, /// Generate C code. #[command()] CGen, @@ -74,7 +71,6 @@ const COMMANDS: &[Binary] = &[ "check", "deploy", "verify", - "reproducible", "n", "x", "c",