diff --git a/Cargo.lock b/Cargo.lock index b85b2e1..e16de6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -70,11 +82,11 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786689872ec4e7d354810ab0dffd48bb40b838c047522eb031cbd47d15634849" +checksum = "bc05b04ac331a9f07e3a4036ef7926e49a8bf84a99a1ccfc7e2ab55a5fcbb372" dependencies = [ - "alloy-primitives 0.7.2", + "alloy-primitives 0.7.7", "alloy-sol-type-parser", "serde", "serde_json", @@ -104,9 +116,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525448f6afc1b70dd0f9d0a8145631bf2f5e434678ab23ab18409ca264cae6b3" +checksum = "ccb3ead547f4532bc8af961649942f0b9c16ee9226e26caa3f38420651cc0bf4" dependencies = [ "alloy-rlp", "bytes", @@ -136,13 +148,27 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c80a2cb97e7aa48611cbb63950336f9824a174cdf670527cc6465078a26ea1" +checksum = "2b40397ddcdcc266f59f959770f601ce1280e699a91fc1862f29cef91707cd09" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "867a5469d61480fea08c7333ffeca52d5b621f5ca2e44f271b117ec1fc9a0525" dependencies = [ "alloy-sol-macro-input", "const-hex", - "heck 0.4.1", + "heck 0.5.0", "indexmap 2.1.0", "proc-macro-error", "proc-macro2", @@ -154,9 +180,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58894b58ac50979eeac6249661991ac40b9d541830d9a725f7714cc9ef08c23" +checksum = "2e482dc33a32b6fadbc0f599adea520bd3aaa585c141a80b404d0a3e3fa72528" dependencies = [ "const-hex", "dunce", @@ -169,20 +195,21 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8e71ea68e780cc203919e03f69f59e7afe92d2696fb1dcb6662f61e4031b6" +checksum = "cbcba3ca07cf7975f15d871b721fb18031eec8bce51103907f6dcce00b255d98" dependencies = [ + "serde", "winnow 0.6.7", ] [[package]] name = "alloy-sol-types" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399287f68d1081ed8b1f4903c49687658b95b142207d7cb4ae2f4813915343ef" +checksum = "a91ca40fa20793ae9c3841b83e74569d1cc9af29a2f5237314fd3452d51e38c7" dependencies = [ - "alloy-primitives 0.7.2", + "alloy-primitives 0.7.7", "alloy-sol-macro", "const-hex", "serde", @@ -572,6 +599,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "byteorder" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" + [[package]] name = "byteorder" version = "1.5.0" @@ -634,7 +667,7 @@ dependencies = [ [[package]] name = "cargo-stylus" -version = "0.4.0" +version = "0.4.1" dependencies = [ "cargo-stylus-util", "clap", @@ -643,7 +676,7 @@ dependencies = [ [[package]] name = "cargo-stylus-cgen" -version = "0.4.0" +version = "0.4.1" dependencies = [ "alloy-json-abi", "clap", @@ -654,11 +687,11 @@ dependencies = [ [[package]] name = "cargo-stylus-check" -version = "0.4.0" +version = "0.4.1" dependencies = [ "alloy-ethers-typecast", "alloy-json-abi", - "alloy-primitives 0.7.2", + "alloy-primitives 0.7.7", "alloy-sol-macro", "alloy-sol-types", "brotli2", @@ -677,21 +710,25 @@ dependencies = [ "thiserror", "tiny-keccak", "tokio", + "toml", + "wasm-encoder 0.213.0", + "wasm-gen", "wasmer", + "wasmparser 0.213.0", ] [[package]] name = "cargo-stylus-example" -version = "0.4.0" +version = "0.4.1" dependencies = [ "clap", ] [[package]] name = "cargo-stylus-replay" -version = "0.4.0" +version = "0.4.1" dependencies = [ - "alloy-primitives 0.7.2", + "alloy-primitives 0.7.7", "cargo-stylus-util", "clap", "ethers", @@ -709,7 +746,7 @@ dependencies = [ [[package]] name = "cargo-stylus-util" -version = "0.4.0" +version = "0.4.1" dependencies = [ "ethers", "eyre", @@ -1766,7 +1803,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ - "byteorder", + "byteorder 1.5.0", "rand", "rustc-hex", "static_assertions", @@ -1949,7 +1986,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" dependencies = [ - "byteorder", + "byteorder 1.5.0", ] [[package]] @@ -2045,7 +2082,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.7", ] [[package]] @@ -2053,6 +2090,10 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.11", + "serde", +] [[package]] name = "hashers" @@ -2264,6 +2305,7 @@ checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -3366,9 +3408,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608a5726529f2f0ef81b8fde9873c4bb829d6b5b5ca6be4d97345ddf0749c825" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -3390,9 +3432,9 @@ dependencies = [ [[package]] name = "ruint-macro" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e666a5496a0b2186dbcd0ff6106e29e093c15591bde62c20d3842007c6978a09" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rustc-demangle" @@ -3674,9 +3716,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -3950,9 +3992,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa0cefd02f532035d83cfec82647c6eb53140b0485220760e669f4bad489e36" +checksum = "c837dc8852cb7074e46b444afb81783140dab12c58867b49fb3898fbafedf7ea" dependencies = [ "paste", "proc-macro2", @@ -4158,21 +4200,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.0", + "toml_edit 0.22.15", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -4204,12 +4246,23 @@ name = "toml_edit" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow 0.5.34", +] + +[[package]] +name = "toml_edit" +version = "0.22.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.5.34", + "winnow 0.6.7", ] [[package]] @@ -4271,7 +4324,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ - "byteorder", + "byteorder 1.5.0", "bytes", "data-encoding", "http", @@ -4303,7 +4356,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ - "byteorder", + "byteorder 1.5.0", "crunchy", "hex", "static_assertions", @@ -4549,6 +4602,25 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.213.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850e4e6a56413a8f33567741a2388c8f6dafd841a939d945c7248671a8739dd8" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasm-gen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b854b1461005a7b3365742310f7faa3cac3add809d66928c64a40c7e9e842ebb" +dependencies = [ + "byteorder 0.5.3", + "leb128", +] + [[package]] name = "wasmer" version = "3.3.0" @@ -4596,7 +4668,7 @@ dependencies = [ "thiserror", "wasmer-types", "wasmer-vm", - "wasmparser", + "wasmparser 0.95.0", "winapi", ] @@ -4684,6 +4756,20 @@ dependencies = [ "url", ] +[[package]] +name = "wasmparser" +version = "0.213.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48e5a90a9e0afc2990437f5600b8de682a32b18cbaaf6f2b5db185352868b6b" +dependencies = [ + "ahash 0.8.11", + "bitflags 2.4.2", + "hashbrown 0.14.3", + "indexmap 2.1.0", + "semver 1.0.21", + "serde", +] + [[package]] name = "wast" version = "70.0.0" @@ -4693,7 +4779,7 @@ dependencies = [ "leb128", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.39.0", ] [[package]] @@ -4989,6 +5075,26 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "zeroize" version = "1.7.0" @@ -5016,7 +5122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "aes", - "byteorder", + "byteorder 1.5.0", "bzip2", "constant_time_eq", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 55aed5d..4cd4354 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,17 +4,17 @@ resolver = "2" [workspace.package] authors = ["Offchain Labs"] -version = "0.4.0" +version = "0.4.1" edition = "2021" homepage = "https://arbitrum.io" license = "MIT OR Apache-2.0" repository = "https://github.com/OffchainLabs/cargo-stylus" [workspace.dependencies] -alloy-primitives = "0.7.2" -alloy-json-abi = "0.7.2" -alloy-sol-macro = "0.7.2" -alloy-sol-types = "0.7.2" +alloy-primitives = "0.7.6" +alloy-json-abi = "0.7.6" +alloy-sol-macro = "0.7.6" +alloy-sol-types = "0.7.6" alloy-ethers-typecast = "0.2.0" clap = { version = "4.5.4", features = [ "derive", "color" ] } ethers = "2.0.10" @@ -34,4 +34,4 @@ parking_lot = "0.12.1" sneks = "0.1.2" # members -cargo-stylus-util = { path = "util", version = "0.4.0" } +cargo-stylus-util = { path = "util", version = "0.4.1" } diff --git a/check/Cargo.toml b/check/Cargo.toml index c428200..1cd89e4 100644 --- a/check/Cargo.toml +++ b/check/Cargo.toml @@ -33,3 +33,7 @@ tokio.workspace = true wasmer = "3.1.0" glob = "0.3.1" tempfile = "3.10.1" +wasmparser = "0.213.0" +wasm-encoder = "0.213.0" +wasm-gen = "0.1.4" +toml = "0.8.14" diff --git a/check/src/check.rs b/check/src/check.rs index fe3fa0d..f893c9e 100644 --- a/check/src/check.rs +++ b/check/src/check.rs @@ -59,12 +59,22 @@ pub async fn check(cfg: &CheckConfig) -> Result { greyln!("reading wasm file at {}", wasm.to_string_lossy().lavender()); } - let (wasm, code) = project::compress_wasm(&wasm).wrap_err("failed to compress WASM")?; + // Next, we include the project's hash as a custom section + // in the user's WASM so it can be verified by Cargo stylus' + // reproducible verification. This hash is added as a section that is + // ignored by WASM runtimes, so it will only exist in the file + // for metadata purposes. + // add_project_hash_to_wasm_file(wasm, project_hash) + let (wasm_file_bytes, code) = + project::compress_wasm(&wasm, project_hash).wrap_err("failed to compress WASM")?; greyln!("contract size: {}", format_file_size(code.len(), 16, 24)); if verbose { - greyln!("wasm size: {}", format_file_size(wasm.len(), 96, 128)); + greyln!( + "wasm size: {}", + format_file_size(wasm_file_bytes.len(), 96, 128) + ); greyln!("connecting to RPC: {}", &cfg.common_cfg.endpoint.lavender()); } @@ -73,34 +83,23 @@ pub async fn check(cfg: &CheckConfig) -> Result { let codehash = alloy_primitives::keccak256(&code); if program_exists(codehash, &provider).await? { - return Ok(ProgramCheck::Active { code, project_hash }); + return Ok(ProgramCheck::Active { code }); } let address = cfg.program_address.unwrap_or(H160::random()); let fee = check_activate(code.clone().into(), address, &provider).await?; let visual_fee = format_data_fee(fee).unwrap_or("???".red()); greyln!("wasm data fee: {visual_fee}"); - Ok(ProgramCheck::Ready { - code, - fee, - project_hash, - }) + Ok(ProgramCheck::Ready { code, fee }) } /// Whether a program is active, or needs activation. #[derive(PartialEq)] pub enum ProgramCheck { /// Program already exists onchain. - Active { - code: Vec, - project_hash: [u8; 32], - }, + Active { code: Vec }, /// Program can be activated with the given data fee. - Ready { - code: Vec, - fee: U256, - project_hash: [u8; 32], - }, + Ready { code: Vec, fee: U256 }, } impl ProgramCheck { @@ -110,14 +109,6 @@ impl ProgramCheck { Self::Ready { code, .. } => code, } } - - pub fn project_hash(&self) -> &[u8; 32] { - match self { - Self::Active { project_hash, .. } => project_hash, - Self::Ready { project_hash, .. } => project_hash, - } - } - pub fn suggest_fee(&self) -> U256 { match self { Self::Active { .. } => U256::default(), @@ -132,11 +123,9 @@ impl CheckConfig { return Ok((wasm, [0u8; 32])); } let cfg = BuildConfig::new(self.common_cfg.rust_stable); - let project_hash = project::hash_files( - self.common_cfg.source_files_for_project_hash.clone(), - cfg.clone(), - )?; - let wasm = project::build_dylib(cfg)?; + let wasm = project::build_dylib(cfg.clone())?; + let project_hash = + project::hash_files(self.common_cfg.source_files_for_project_hash.clone(), cfg)?; Ok((wasm, project_hash)) } } diff --git a/check/src/constants.rs b/check/src/constants.rs index 25aa4c1..ca30860 100644 --- a/check/src/constants.rs +++ b/check/src/constants.rs @@ -36,3 +36,10 @@ pub const GITHUB_TEMPLATE_REPO_MINIMAL: &str = /// One ether in wei. pub const ONE_ETH: U256 = U256([1000000000000000000, 0, 0, 0]); + +/// Name of the custom wasm section that is added to contracts deployed with cargo stylus +/// to include a hash of the Rust project's source files for reproducible verification of builds. +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"; diff --git a/check/src/deploy.rs b/check/src/deploy.rs index df54bdd..e8dc8b7 100644 --- a/check/src/deploy.rs +++ b/check/src/deploy.rs @@ -83,9 +83,7 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { } } - let contract = cfg - .deploy_contract(program.code(), program.project_hash(), sender, &client) - .await?; + let contract = cfg.deploy_contract(program.code(), sender, &client).await?; match program { ProgramCheck::Ready { .. } => cfg.activate(sender, contract, data_fee, &client).await?, @@ -98,11 +96,10 @@ impl DeployConfig { async fn deploy_contract( &self, code: &[u8], - project_hash: &[u8; 32], sender: H160, client: &SignerClient, ) -> Result { - let init_code = program_deployment_calldata(code, project_hash); + let init_code = program_deployment_calldata(code); let tx = Eip1559TransactionRequest::new() .from(sender) @@ -231,7 +228,7 @@ pub async fn run_tx( } /// Prepares an EVM bytecode prelude for contract creation. -pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec { +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![]; @@ -239,7 +236,7 @@ pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec { deploy.extend(code_len); deploy.push(0x80); // DUP1 deploy.push(0x60); // PUSH1 - deploy.push(42 + 1 + 32); // prelude + version + hash + deploy.push(42 + 1); // prelude + version deploy.push(0x60); // PUSH1 deploy.push(0x00); deploy.push(0x39); // CODECOPY @@ -247,11 +244,24 @@ pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec { deploy.push(0x00); deploy.push(0xf3); // RETURN deploy.push(0x00); // version - deploy.extend(hash); deploy.extend(code); deploy } +pub fn extract_program_evm_deployment_prelude(calldata: &[u8]) -> Vec { + // The length of the prelude, version part is 42 + 1 as per the code + let metadata_length = 42 + 1; + // Extract and return the metadata part + calldata[0..metadata_length].to_vec() +} + +pub fn extract_compressed_wasm(calldata: &[u8]) -> Vec { + // The length of the prelude, version part is 42 + 1 as per the code + let metadata_length = 42 + 1; + // Extract and return the metadata part + calldata[metadata_length..].to_vec() +} + pub fn format_gas(gas: U256) -> String { let gas: u64 = gas.try_into().unwrap_or(u64::MAX); let text = format!("{gas} gas"); diff --git a/check/src/docker.rs b/check/src/docker.rs index 1b17e90..5a18e4a 100644 --- a/check/src/docker.rs +++ b/check/src/docker.rs @@ -2,10 +2,14 @@ // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md use std::io::Write; +use std::path::PathBuf; use std::process::{Command, Stdio}; use eyre::{bail, eyre, Result}; +use crate::constants::TOOLCHAIN_FILE_NAME; +use crate::project::extract_toolchain_channel; + fn version_to_image_name(version: &str) -> String { format!("cargo-stylus-{}", version) } @@ -24,6 +28,8 @@ fn create_image(version: &str) -> Result<()> { 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 mut child = Command::new("docker") .arg("build") .arg("-t") @@ -37,15 +43,19 @@ fn create_image(version: &str) -> Result<()> { child.stdin.as_mut().unwrap(), "\ FROM rust:{} as builder\n\ + RUN rustup toolchain install {} && rustup default {} 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 ", - version + version, + toolchain_channel, + toolchain_channel, )?; child.wait().map_err(|e| eyre!("wait failed: {e}"))?; Ok(()) @@ -76,10 +86,14 @@ fn run_in_docker_container(version: &str, command_line: &[&str]) -> Result<()> { } pub fn run_reproducible(version: &str, command_line: &[String]) -> Result<()> { + let version: String = version + .chars() + .filter(|c| c.is_alphanumeric() || *c == '.') + .collect(); 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(&version)?; + run_in_docker_container(&version, &command) } diff --git a/check/src/project.rs b/check/src/project.rs index 774499a..eab27f8 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -2,7 +2,10 @@ // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md use crate::{ - constants::{BROTLI_COMPRESSION_LEVEL, EOF_PREFIX_NO_DICT, RUST_TARGET}, + constants::{ + BROTLI_COMPRESSION_LEVEL, EOF_PREFIX_NO_DICT, PROJECT_HASH_SECTION_NAME, RUST_TARGET, + TOOLCHAIN_FILE_NAME, + }, macros::*, }; use brotli2::read::BrotliEncoder; @@ -18,6 +21,7 @@ use std::{ process, }; use tiny_keccak::{Hasher, Keccak}; +use toml::Value; #[derive(Default, Clone, PartialEq)] pub enum OptLevel { @@ -107,7 +111,8 @@ pub fn build_dylib(cfg: BuildConfig) -> Result { }) .ok_or(BuildError::NoWasmFound { path: release_path })?; - let (wasm, code) = compress_wasm(&wasm_file_path).wrap_err("failed to compress WASM")?; + let (wasm, code) = + compress_wasm(&wasm_file_path, [0u8; 32]).wrap_err("failed to compress WASM")?; greyln!( "contract size: {}", @@ -161,6 +166,32 @@ 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( + "expected to find a rust-toolchain.toml file in project directory \ + to specify your Rust toolchain for reproducible verification", + )?; + let toolchain_toml: Value = + toml::from_str(&toolchain_file_contents).wrap_err("failed to parse rust-toolchain.toml")?; + + // Extract the channel from the toolchain section + let Some(toolchain) = toolchain_toml.get("toolchain") else { + bail!("toolchain section not found in rust-toolchain.toml"); + }; + let Some(channel) = toolchain.get("channel") else { + bail!("could not find channel in rust-toolchain.toml's toolchain section"); + }; + let Some(channel) = channel.as_str() else { + bail!("channel in rust-toolchain.toml's toolchain section is not a string"); + }; + // Next, parse the Rust version from the toolchain project, only allowing alphanumeric chars and dashes. + let channel = channel + .chars() + .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '.') + .collect(); + Ok(channel) +} + pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result<[u8; 32]> { let mut keccak = Keccak::v256(); let mut cmd = Command::new("cargo"); @@ -201,11 +232,20 @@ pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result Ok(()) }; + // Fetch the Rust toolchain toml file from the project root. Assert that it exists and add it to the + // files in the directory to hash. + let toolchain_file_path = PathBuf::from(".").as_path().join(TOOLCHAIN_FILE_NAME); + let _ = std::fs::metadata(&toolchain_file_path).wrap_err( + "expected to find a rust-toolchain.toml file in project directory \ + to specify your Rust toolchain for reproducible verification", + )?; + let mut paths = all_paths(PathBuf::from(".").as_path(), source_file_patterns)?; + paths.push(toolchain_file_path); paths.sort(); for filename in paths.iter() { - println!( + greyln!( "File used for deployment hash: {}", filename.as_os_str().to_string_lossy() ); @@ -214,7 +254,7 @@ pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result let mut hash = [0u8; 32]; keccak.finalize(&mut hash); - println!( + greyln!( "Project hash computed on deployment: {:?}", hex::encode(hash) ); @@ -235,10 +275,12 @@ fn expand_glob_patterns(patterns: Vec) -> Result> { } /// Reads a WASM file at a specified path and returns its brotli compressed bytes. -pub fn compress_wasm(wasm: &PathBuf) -> Result<(Vec, Vec)> { +pub fn compress_wasm(wasm: &PathBuf, project_hash: [u8; 32]) -> Result<(Vec, Vec)> { let wasm = fs::read(wasm).wrap_err_with(|| eyre!("failed to read Wasm {}", wasm.to_string_lossy()))?; + let wasm = add_project_hash_to_wasm_file(&wasm, project_hash) + .wrap_err("failed to add project hash to wasm file as custom section")?; let wasm = wasmer::wat2wasm(&wasm).wrap_err("failed to parse Wasm")?; let mut compressor = BrotliEncoder::new(&*wasm, BROTLI_COMPRESSION_LEVEL); @@ -253,6 +295,44 @@ pub fn compress_wasm(wasm: &PathBuf) -> Result<(Vec, Vec)> { Ok((wasm.to_vec(), contract_code)) } +// Adds the hash of the project's source files to the wasm as a custom section +// if it does not already exist. This allows for reproducible builds by cargo stylus +// for all Rust stylus programs. See `cargo stylus verify --help` for more information. +fn add_project_hash_to_wasm_file( + wasm_file_bytes: &[u8], + project_hash: [u8; 32], +) -> Result> { + let section_exists = has_project_hash_section(wasm_file_bytes)?; + if section_exists { + greyln!("Wasm file bytes already contains a custom section with a project hash, not overwriting'"); + return Ok(wasm_file_bytes.to_vec()); + } + Ok(add_custom_section(wasm_file_bytes, project_hash)) +} + +pub fn has_project_hash_section(wasm_file_bytes: &[u8]) -> Result { + let parser = wasmparser::Parser::new(0); + for payload in parser.parse_all(wasm_file_bytes) { + if let wasmparser::Payload::CustomSection(reader) = payload? { + if reader.name() == PROJECT_HASH_SECTION_NAME { + println!( + "Found the project hash custom section name {}", + hex::encode(reader.data()) + ); + return Ok(true); + } + } + } + Ok(false) +} + +fn add_custom_section(wasm_file_bytes: &[u8], project_hash: [u8; 32]) -> Vec { + let mut bytes = vec![]; + bytes.extend_from_slice(wasm_file_bytes); + wasm_gen::write_custom_section(&mut bytes, PROJECT_HASH_SECTION_NAME, &project_hash); + bytes +} + #[cfg(test)] mod test { use super::*; @@ -260,6 +340,49 @@ mod test { use std::io::Write; use tempfile::tempdir; + #[test] + fn test_extract_toolchain_channel() -> Result<()> { + let dir = tempdir()?; + let dir_path = dir.path(); + + let toolchain_file_path = dir_path.join(TOOLCHAIN_FILE_NAME); + let toolchain_contents = r#" + [toolchain] + "#; + std::fs::write(&toolchain_file_path, toolchain_contents)?; + + let channel = extract_toolchain_channel(&toolchain_file_path); + let Err(err_details) = channel else { + panic!("expected an error"); + }; + assert!(err_details.to_string().contains("could not find channel"),); + + let toolchain_contents = r#" + [toolchain] + channel = 32390293 + "#; + std::fs::write(&toolchain_file_path, toolchain_contents)?; + + let channel = extract_toolchain_channel(&toolchain_file_path); + let Err(err_details) = channel else { + panic!("expected an error"); + }; + assert!(err_details.to_string().contains("is not a string"),); + + let toolchain_contents = r#" + [toolchain] + channel = "nightly-2020-07-10" + components = [ "rustfmt", "rustc-dev" ] + targets = [ "wasm32-unknown-unknown", "thumbv2-none-eabi" ] + profile = "minimal" + "#; + std::fs::write(&toolchain_file_path, toolchain_contents)?; + + let channel = extract_toolchain_channel(&toolchain_file_path)?; + assert_eq!(channel, "nightly-2020-07-10"); + Ok(()) + } + #[test] fn test_all_paths() -> Result<()> { let dir = tempdir()?; diff --git a/check/src/verify.rs b/check/src/verify.rs index 9cb4faf..f3e0ca0 100644 --- a/check/src/verify.rs +++ b/check/src/verify.rs @@ -12,7 +12,11 @@ use ethers::types::H256; use serde::{Deserialize, Serialize}; -use crate::{check, deploy, project, CheckConfig, VerifyConfig}; +use crate::{ + check, + deploy::{self, extract_compressed_wasm, extract_program_evm_deployment_prelude}, + project, CheckConfig, VerifyConfig, +}; use cargo_stylus_util::{color::Color, sys}; #[derive(Debug, Deserialize, Serialize)] @@ -55,16 +59,37 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { }; let wasm_file: PathBuf = project::build_dylib(build_cfg.clone()) .map_err(|e| eyre!("could not build project to WASM: {e}"))?; - let (_, init_code) = project::compress_wasm(&wasm_file)?; - let hash = project::hash_files(cfg.common_cfg.source_files_for_project_hash, build_cfg)?; - let deployment_data = deploy::program_deployment_calldata(&init_code, &hash); + let project_hash = + project::hash_files(cfg.common_cfg.source_files_for_project_hash, build_cfg)?; + let (_, init_code) = project::compress_wasm(&wasm_file, project_hash)?; + let deployment_data = deploy::program_deployment_calldata(&init_code); if deployment_data == *result.input { println!("Verified - program matches local project's file hashes"); } else { + let tx_prelude = extract_program_evm_deployment_prelude(&*result.input); + let reconstructed_prelude = extract_program_evm_deployment_prelude(&deployment_data); println!( "{} - program deployment did not verify against local project's file hashes", "FAILED".red() ); + if tx_prelude != reconstructed_prelude { + println!("Prelude mismatch"); + println!("Deployment tx prelude {}", hex::encode(tx_prelude)); + println!( + "Reconstructed prelude {}", + hex::encode(reconstructed_prelude) + ); + } else { + println!("Compressed WASM bytecode mismatch"); + } + println!( + "Compressed code length of locally reconstructed {}", + init_code.len() + ); + println!( + "Compressed code length of deployment tx {}", + extract_compressed_wasm(&*result.input).len() + ); } Ok(()) }