diff --git a/.ci/build_and_test.sh b/.ci/build_and_test.sh index 8277240..404589f 100755 --- a/.ci/build_and_test.sh +++ b/.ci/build_and_test.sh @@ -15,4 +15,11 @@ if [ "$CFG_RELEASE_CHANNEL" == "nightly" ]; then else cargo build --locked fi -cargo test \ No newline at end of file + +cargo test + +if [ "$(uname -s)" != "Darwin" ]; then + # The MacOS CI doesn't support Docker because of licensing issues, so only run them on Linux. + # Also, run the docker tests on a single thread to avoid concurrency issues. + cargo test -F docker-test -- --test-threads 1 docker +fi diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9fe342c --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +./target diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 78b333d..0591fd3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -26,7 +26,7 @@ jobs: - name: install rustup run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh - sh rustup-init.sh -y --default-toolchain none + sh rustup-init.sh -y --default-toolchain ${{ matrix.cfg_release_channel }} rustup target add ${{ matrix.target }} - name: Build and Test diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 49918f7..5a3fb27 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -27,7 +27,7 @@ jobs: - name: install rustup run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup-init.sh - sh rustup-init.sh -y --default-toolchain none + sh rustup-init.sh -y --default-toolchain ${{ matrix.cfg_release_channel }} rustup target add ${{ matrix.target }} - name: Build and Test diff --git a/Dockerfile b/Dockerfile index 472b884..7143a4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,15 @@ -ARG TARGETPLATFORM=linux/amd64 -FROM rust:1.80 AS builder +ARG BUILD_PLATFORM=linux/amd64 +ARG RUST_VERSION=1.80 +ARG CARGO_STYLUS_VERSION=0.5.6 + +FROM --platform=${BUILD_PLATFORM} rust:${RUST_VERSION} AS builder RUN apt-get update && apt-get install -y git RUN rustup target add x86_64-unknown-linux-gnu -RUN git clone https://github.com/offchainlabs/cargo-stylus.git +ARG CARGO_STYLUS_VERSION +RUN test -n "$CARGO_STYLUS_VERSION" +RUN git clone --branch v$CARGO_STYLUS_VERSION https://github.com/offchainlabs/cargo-stylus.git WORKDIR /cargo-stylus -RUN git checkout v0.5.6 RUN cargo build --release --manifest-path main/Cargo.toml -FROM rust:1.80 +FROM --platform=${BUILD_PLATFORM} rust:${RUST_VERSION} AS cargo-stylus-base COPY --from=builder /cargo-stylus/target/release/cargo-stylus /usr/local/bin/cargo-stylus diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b94b6d --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +CARGO_STYLUS_VERSION := $(shell cargo pkgid --manifest-path main/Cargo.toml | cut -d '@' -f 2) + +.PHONY: build +build: + cargo build + +.PHONY: test +test: + cargo test + +.PHONY: bench +bench: + cargo +nightly bench -F nightly + +.PHONY: fmt +fmt: + cargo fmt + +.PHONY: lint +lint: + cargo clippy --package cargo-stylus --package cargo-stylus-example + +.PHONY: install +install: fmt lint + cargo install --path main + +.PHONY: docker +docker: + docker build -t cargo-stylus-base:$(CARGO_STYLUS_VERSION) --build-arg CARGO_STYLUS_VERSION=$(CARGO_STYLUS_VERSION) . diff --git a/install.sh b/install.sh deleted file mode 100755 index fbed7b3..0000000 --- a/install.sh +++ /dev/null @@ -1,3 +0,0 @@ -cargo fmt -cargo clippy --package cargo-stylus --package cargo-stylus-example -cargo install --path main diff --git a/main/Cargo.toml b/main/Cargo.toml index 98215aa..d09fc9b 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -10,6 +10,10 @@ license.workspace = true version.workspace = true repository.workspace = true +[features] +docker-test = [] +nightly = [] + [dependencies] alloy-primitives.workspace = true alloy-json-abi.workspace = true diff --git a/main/src/activate.rs b/main/src/activate.rs index 8526d8f..ccb59e5 100644 --- a/main/src/activate.rs +++ b/main/src/activate.rs @@ -1,24 +1,22 @@ // Copyright 2023-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md +use crate::check::check_activate; +use crate::constants::ARB_WASM_H160; +use crate::macros::greyln; use crate::util::color::{Color, DebugColor}; use crate::util::sys; +use crate::ActivateConfig; use alloy_primitives::Address; use alloy_sol_macro::sol; use alloy_sol_types::SolCall; use ethers::middleware::{Middleware, SignerMiddleware}; use ethers::signers::Signer; use ethers::types::transaction::eip2718::TypedTransaction; -use ethers::types::{Eip1559TransactionRequest, U256}; +use ethers::types::Eip1559TransactionRequest; use ethers::utils::format_units; use eyre::{bail, Context, Result}; -use crate::check::check_activate; -use crate::constants::ARB_WASM_H160; -use crate::macros::greyln; - -use crate::ActivateConfig; - sol! { interface ArbWasm { function activateProgram(address program) @@ -41,25 +39,14 @@ pub async fn activate_contract(cfg: &ActivateConfig) -> Result<()> { let client = SignerMiddleware::new(provider.clone(), wallet); let code = client.get_code(cfg.address, None).await?; - let data_fee = check_activate(code, cfg.address, &provider).await?; - let mut data_fee = alloy_ethers_typecast::alloy_u256_to_ethers(data_fee); - - greyln!( - "obtained estimated activation data fee {}", - format_units(data_fee, "ether")?.debug_lavender() - ); - greyln!( - "bumping estimated activation data fee by {}%", - cfg.data_fee_bump_percent.debug_lavender() - ); - data_fee = bump_data_fee(data_fee, cfg.data_fee_bump_percent); + let data_fee = check_activate(code, cfg.address, &cfg.data_fee, &provider).await?; let contract: Address = cfg.address.to_fixed_bytes().into(); let data = ArbWasm::activateProgramCall { program: contract }.abi_encode(); let tx = Eip1559TransactionRequest::new() .from(client.address()) .to(*ARB_WASM_H160) - .value(data_fee) + .value(alloy_ethers_typecast::alloy_u256_to_ethers(data_fee)) .data(data); let tx = TypedTransaction::Eip1559(tx); if cfg.estimate_gas { @@ -96,8 +83,3 @@ pub async fn activate_contract(cfg: &ActivateConfig) -> Result<()> { } Ok(()) } - -fn bump_data_fee(fee: U256, pct: u64) -> U256 { - let num = 100 + pct; - fee * U256::from(num) / U256::from(100) -} diff --git a/main/src/check.rs b/main/src/check.rs index 5c31ea6..d9e99c1 100644 --- a/main/src/check.rs +++ b/main/src/check.rs @@ -1,13 +1,16 @@ // Copyright 2023-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md -use crate::util::{color::Color, sys, text}; use crate::{ check::ArbWasm::ArbWasmErrors, constants::{ARB_WASM_H160, ONE_ETH, TOOLCHAIN_FILE_NAME}, macros::*, project::{self, extract_toolchain_channel, BuildConfig}, - CheckConfig, + util::{ + color::{Color, GREY, LAVENDER}, + sys, text, + }, + CheckConfig, DataFeeOpts, }; use alloy_primitives::{Address, B256, U256}; use alloy_sol_macro::sol; @@ -87,9 +90,7 @@ pub async fn check(cfg: &CheckConfig) -> Result { } let address = cfg.contract_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} ETH"); + let fee = check_activate(code.clone().into(), address, &cfg.data_fee, &provider).await?; Ok(ContractCheck::Ready { code, fee }) } @@ -112,7 +113,7 @@ impl ContractCheck { pub fn suggest_fee(&self) -> U256 { match self { Self::Active { .. } => U256::default(), - Self::Ready { fee, .. } => fee * U256::from(120) / U256::from(100), + Self::Ready { fee, .. } => *fee, } } } @@ -128,7 +129,7 @@ impl CheckConfig { 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)?; + project::hash_project(self.common_cfg.source_files_for_project_hash.clone(), cfg)?; Ok((wasm, project_hash)) } } @@ -148,17 +149,19 @@ pub fn format_file_size(len: usize, mid: u64, max: u64) -> String { } /// Pretty-prints a data fee. -fn format_data_fee(fee: U256) -> Result { - let fee: u64 = (fee / U256::from(1e9)).try_into()?; +fn format_data_fee(fee: U256) -> String { + let Ok(fee): Result = (fee / U256::from(1e9)).try_into() else { + return ("???").red(); + }; let fee: f64 = fee as f64 / 1e9; - let text = format!("{fee:.6}"); - Ok(if fee <= 5e14 { + let text = format!("{fee:.6} ETH"); + if fee <= 5e14 { text.mint() } else if fee <= 5e15 { text.yellow() } else { text.pink() - }) + } } pub struct EthCallError { @@ -247,7 +250,12 @@ Perhaps the Arbitrum node for the endpoint you are connecting to has not yet upg } /// Checks contract activation, returning the data fee. -pub async fn check_activate(code: Bytes, address: H160, provider: &Provider) -> Result { +pub async fn check_activate( + code: Bytes, + address: H160, + opts: &DataFeeOpts, + provider: &Provider, +) -> Result { let contract = Address::from(address.to_fixed_bytes()); let data = ArbWasm::activateProgramCall { program: contract }.abi_encode(); let tx = Eip1559TransactionRequest::new() @@ -256,8 +264,17 @@ pub async fn check_activate(code: Bytes, address: H160, provider: &Provider, Wallet>; /// Deploys a stylus contract, activating if needed. pub async fn deploy(cfg: DeployConfig) -> Result<()> { - macro_rules! run { - ($expr:expr) => { - $expr.await? - }; - ($expr:expr, $($msg:expr),+) => { - $expr.await.wrap_err_with(|| eyre!($($msg),+))? - }; - } - - let contract = run!(check::check(&cfg.check_config), "cargo stylus check failed"); + let contract = check::check(&cfg.check_config) + .await + .expect("cargo stylus check failed"); let verbose = cfg.check_config.common_cfg.verbose; let client = sys::new_provider(&cfg.check_config.common_cfg.endpoint)?; - let chain_id = run!(client.get_chainid(), "failed to get chain id"); + let chain_id = client.get_chainid().await.expect("failed to get chain id"); let wallet = cfg.auth.wallet().wrap_err("failed to load wallet")?; let wallet = wallet.with_chain_id(chain_id.as_u64()); @@ -67,7 +60,10 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { if let ContractCheck::Ready { .. } = &contract { // check balance early - let balance = run!(client.get_balance(sender, None), "failed to get balance"); + let balance = client + .get_balance(sender, None) + .await + .expect("failed to get balance"); let balance = alloy_ethers_typecast::ethers_u256_to_alloy(balance); if balance < data_fee && !cfg.estimate_gas { @@ -93,8 +89,16 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { match contract { ContractCheck::Ready { .. } => { - cfg.activate(sender, contract_addr, data_fee, &client) - .await? + if cfg.no_activate { + mintln!( + r#"NOTE: You must activate the stylus contract before calling it. To do so, we recommend running: +cargo stylus activate --address {}"#, + hex::encode(contract_addr) + ); + } else { + cfg.activate(sender, contract_addr, data_fee, &client) + .await? + } } ContractCheck::Active { .. } => greyln!("wasm already activated!"), } diff --git a/main/src/docker.rs b/main/src/docker.rs index 62ad2c1..c293727 100644 --- a/main/src/docker.rs +++ b/main/src/docker.rs @@ -12,8 +12,14 @@ use crate::constants::TOOLCHAIN_FILE_NAME; use crate::macros::greyln; use crate::project::extract_toolchain_channel; -fn image_exists(cargo_stylus_version: &str) -> Result { - let image_name = format!("cargo-stylus-base:{}", cargo_stylus_version); +fn image_name(cargo_stylus_version: &str, toolchain_version: &str) -> String { + format!( + "cargo-stylus-base-{}-toolchain-{}", + cargo_stylus_version, toolchain_version + ) +} + +fn image_exists(image_name: &str) -> Result { let output = Command::new("docker") .arg("images") .arg(image_name) @@ -37,21 +43,19 @@ a reproducible deployment, or opt out by using the --no-verify flag for local bu Ok(output.stdout.iter().filter(|c| **c == b'\n').count() > 1) } -fn create_image(cargo_stylus_version: Option, version: &str) -> Result<()> { - let cargo_stylus_version = - cargo_stylus_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); - if image_exists(&cargo_stylus_version)? { +fn create_image(cargo_stylus_version: &str, toolchain_version: &str) -> Result<()> { + let image_name = image_name(cargo_stylus_version, toolchain_version); + if image_exists(&image_name)? { return Ok(()); } - let name = format!( - "cargo-stylus-base-{}-toolchain-{}", - cargo_stylus_version, version + println!( + "Building Docker image for Rust toolchain {}", + toolchain_version ); - println!("Building Docker image for Rust toolchain {}", version,); let mut child = Command::new("docker") .arg("build") .arg("-t") - .arg(name) + .arg(image_name) .arg(".") .arg("-f-") .stdin(Stdio::piped()) @@ -60,32 +64,28 @@ fn create_image(cargo_stylus_version: Option, version: &str) -> Result<( write!( child.stdin.as_mut().unwrap(), "\ - FROM --platform=linux/amd64 offchainlabs/cargo-stylus-base:{} as base + ARG BUILD_PLATFORM=linux/amd64 + FROM --platform=${{BUILD_PLATFORM}} offchainlabs/cargo-stylus-base:{} AS base 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 component add rust-src --toolchain {}-x86_64-unknown-linux-gnu ", cargo_stylus_version, - version, - version, - version, + toolchain_version, + toolchain_version, + toolchain_version, )?; child.wait().map_err(|e| eyre!("wait failed: {e}"))?; Ok(()) } fn run_in_docker_container( - cargo_stylus_version: Option, + cargo_stylus_version: &str, toolchain_version: &str, command_line: &[&str], ) -> Result<()> { - let cargo_stylus_version = - cargo_stylus_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); - let name = format!( - "cargo-stylus-base-{}-toolchain-{}", - cargo_stylus_version, toolchain_version - ); + let image_name = image_name(cargo_stylus_version, toolchain_version); let dir = std::env::current_dir().map_err(|e| eyre!("failed to find current directory: {e}"))?; Command::new("docker") @@ -96,7 +96,7 @@ fn run_in_docker_container( .arg("/source") .arg("-v") .arg(format!("{}:/source", dir.as_os_str().to_str().unwrap())) - .arg(name) + .arg(image_name) .args(command_line) .spawn() .map_err(|e| eyre!("failed to execute Docker command: {e}"))? @@ -116,12 +116,14 @@ pub fn run_reproducible( "Running reproducible Stylus command with toolchain {}", toolchain_channel.mint() ); + let cargo_stylus_version = + cargo_stylus_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); let mut command = vec!["cargo", "stylus"]; for s in command_line.iter() { command.push(s); } - create_image(cargo_stylus_version.clone(), &toolchain_channel)?; - run_in_docker_container(cargo_stylus_version, &toolchain_channel, &command) + create_image(&cargo_stylus_version, &toolchain_channel)?; + run_in_docker_container(&cargo_stylus_version, &toolchain_channel, &command) } fn verify_valid_host() -> Result<()> { @@ -146,3 +148,31 @@ fn verify_valid_host() -> Result<()> { } Ok(()) } + +#[cfg(all(test, feature = "docker-test"))] +mod tests { + use super::*; + + #[test] + fn test_create_image_and_check_it_exists() { + let toolchain_version = "1.80.0"; + let cargo_stylus_version = "0.5.3"; + let image_name = image_name(&cargo_stylus_version, toolchain_version); + println!("image name: {}", image_name); + + // Remove existing docker image + Command::new("docker") + .arg("image") + .arg("rm") + .arg("-f") + .arg(&image_name) + .spawn() + .expect("failed to spawn docker image rm") + .wait() + .expect("failed to run docker image rm"); + + assert!(!image_exists(&image_name).unwrap()); + create_image(&cargo_stylus_version, toolchain_version).unwrap(); + assert!(image_exists(&image_name).unwrap()); + } +} diff --git a/main/src/main.rs b/main/src/main.rs index 59c4b9b..acb510f 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -1,6 +1,9 @@ // Copyright 2023-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md +// Enable unstable test feature for benchmarks when nightly is available +#![cfg_attr(feature = "nightly", feature(test))] + use alloy_primitives::TxHash; use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand}; use constants::DEFAULT_ENDPOINT; @@ -181,15 +184,14 @@ pub struct CacheSuggestionsConfig { pub struct ActivateConfig { #[command(flatten)] common_cfg: CommonConfig, + #[command(flatten)] + data_fee: DataFeeOpts, /// Wallet source to use. #[command(flatten)] auth: AuthOpts, /// Deployed Stylus contract address to activate. #[arg(long)] address: H160, - /// Percent to bump the estimated activation data fee by. Default of 20% - #[arg(long, default_value = "20")] - data_fee_bump_percent: u64, /// Whether or not to just estimate gas without sending a tx. #[arg(long)] estimate_gas: bool, @@ -199,6 +201,8 @@ pub struct ActivateConfig { pub struct CheckConfig { #[command(flatten)] common_cfg: CommonConfig, + #[command(flatten)] + data_fee: DataFeeOpts, /// The WASM to check (defaults to any found in the current directory). #[arg(long)] wasm_file: Option, @@ -225,6 +229,9 @@ struct DeployConfig { /// If not set, uses the default version of the local cargo stylus binary. #[arg(long)] cargo_stylus_version: Option, + /// If set, do not activate the program after deploying it + #[arg(long)] + no_activate: bool, } #[derive(Args, Clone, Debug)] @@ -311,6 +318,13 @@ pub struct SimulateArgs { use_native_tracer: bool, } +#[derive(Clone, Debug, Args)] +struct DataFeeOpts { + /// Percent to bump the estimated activation data fee by. + #[arg(long, default_value = "20")] + data_fee_bump_percent: u64, +} + #[derive(Clone, Debug, Args)] #[clap(group(ArgGroup::new("key").required(true).args(&["private_key_path", "private_key", "keystore_path"])))] struct AuthOpts { diff --git a/main/src/new.rs b/main/src/new.rs index a9ecc2b..6a1fdf8 100644 --- a/main/src/new.rs +++ b/main/src/new.rs @@ -7,31 +7,18 @@ use crate::util::{ sys, }; use eyre::{bail, Context, Result}; -use std::{env::current_dir, path::Path}; +use std::{env, fs, path::Path}; -/// Creates a new Stylus project in the current directory -pub fn new(name: &Path, minimal: bool) -> Result<()> { - let repo = match minimal { - true => GITHUB_TEMPLATE_REPO_MINIMAL, - false => GITHUB_TEMPLATE_REPO, - }; - let output = sys::new_command("git") - .arg("clone") - .arg(repo) - .arg(name) - .output() - .wrap_err("git clone failed")?; - - if !output.status.success() { - bail!("git clone command failed"); - } - let path = current_dir().wrap_err("no current dir")?.join(name); - println!("{GREY}new project at: {}", path.to_string_lossy().mint()); - Ok(()) +/// Creates a new directory given the path and then initialize a stylus project. +pub fn new(path: &Path, minimal: bool) -> Result<()> { + fs::create_dir_all(path).wrap_err("failed to create project dir")?; + env::set_current_dir(path).wrap_err("failed to set project dir")?; + init(minimal) } +/// Creates a new Stylus project in the current directory. pub fn init(minimal: bool) -> Result<()> { - let current_dir = current_dir().wrap_err("no current dir")?; + let current_dir = env::current_dir().wrap_err("no current dir")?; let repo = if minimal { GITHUB_TEMPLATE_REPO_MINIMAL } else { @@ -51,6 +38,17 @@ pub fn init(minimal: bool) -> Result<()> { bail!("git clone command failed"); } + let output = sys::new_command("git") + .arg("remote") + .arg("remove") + .arg("origin") + .output() + .wrap_err("git remote remove failed")?; + + if !output.status.success() { + bail!("git remote remove command failed"); + } + println!( "{GREY}initialized project in: {}", current_dir.to_string_lossy().mint() diff --git a/main/src/project.rs b/main/src/project.rs index 0f0fd4e..4df27d1 100644 --- a/main/src/project.rs +++ b/main/src/project.rs @@ -18,6 +18,8 @@ use std::{ io::Read, path::{Path, PathBuf}, process, + sync::mpsc, + thread, }; use std::{ops::Range, process::Command}; use tiny_keccak::{Hasher, Keccak}; @@ -228,8 +230,22 @@ pub fn extract_cargo_toml_version(cargo_toml_path: &PathBuf) -> Result { Ok(version.to_string()) } -pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result<[u8; 32]> { - let mut keccak = Keccak::v256(); +pub fn read_file_preimage(filename: &Path) -> Result> { + let mut contents = Vec::with_capacity(1024); + { + let filename = filename.as_os_str(); + contents.extend_from_slice(&(filename.len() as u64).to_be_bytes()); + contents.extend_from_slice(filename.as_encoded_bytes()); + } + let mut file = std::fs::File::open(filename) + .map_err(|e| eyre!("failed to open file {}: {e}", filename.display()))?; + contents.extend_from_slice(&file.metadata().unwrap().len().to_be_bytes()); + file.read_to_end(&mut contents) + .map_err(|e| eyre!("Unable to read file {}: {e}", filename.display()))?; + Ok(contents) +} + +pub fn hash_project(source_file_patterns: Vec, cfg: BuildConfig) -> Result<[u8; 32]> { let mut cmd = Command::new("cargo"); cmd.arg("--version"); let output = cmd @@ -238,33 +254,23 @@ pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result if !output.status.success() { bail!("cargo version command failed"); } - keccak.update(&output.stdout); + + hash_files(&output.stdout, source_file_patterns, cfg) +} + +pub fn hash_files( + cargo_version_output: &[u8], + source_file_patterns: Vec, + cfg: BuildConfig, +) -> Result<[u8; 32]> { + let mut keccak = Keccak::v256(); + keccak.update(cargo_version_output); 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(()) - }; - // 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); @@ -277,12 +283,20 @@ pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result paths.push(toolchain_file_path); paths.sort(); - for filename in paths.iter() { - greyln!( - "File used for deployment hash: {}", - filename.as_os_str().to_string_lossy() - ); - hash_file(filename)?; + // Read the file contents in another thread and process the keccak in the main thread. + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + for filename in paths.iter() { + greyln!( + "File used for deployment hash: {}", + filename.as_os_str().to_string_lossy() + ); + tx.send(read_file_preimage(filename)) + .expect("failed to send preimage (impossible)"); + } + }); + for result in rx { + keccak.update(result?.as_slice()); } let mut hash = [0u8; 32]; @@ -405,9 +419,50 @@ fn strip_user_metadata(wasm_file_bytes: &[u8]) -> Result> { #[cfg(test)] mod test { use super::*; - use std::fs::{self, File}; - use std::io::Write; - use tempfile::tempdir; + use std::{ + env, + fs::{self, File}, + io::Write, + path::Path, + }; + use tempfile::{tempdir, TempDir}; + + #[cfg(feature = "nightly")] + extern crate test; + + fn write_valid_toolchain_file(toolchain_file_path: &Path) -> Result<()> { + let toolchain_contents = r#" + [toolchain] + channel = "nightly-2020-07-10" + components = [ "rustfmt", "rustc-dev" ] + targets = [ "wasm32-unknown-unknown", "thumbv2-none-eabi" ] + profile = "minimal" + "#; + fs::write(&toolchain_file_path, toolchain_contents)?; + Ok(()) + } + + fn write_hash_files(num_files: usize, num_lines: usize) -> Result { + let dir = tempdir()?; + env::set_current_dir(dir.path())?; + + let toolchain_file_path = dir.path().join(TOOLCHAIN_FILE_NAME); + write_valid_toolchain_file(&toolchain_file_path)?; + + fs::create_dir(dir.path().join("src"))?; + let mut contents = String::new(); + for _ in 0..num_lines { + contents.push_str("// foo"); + } + for i in 0..num_files { + let file_path = dir.path().join(format!("src/f{i}.rs")); + fs::write(&file_path, &contents)?; + } + fs::write(dir.path().join("Cargo.toml"), "")?; + fs::write(dir.path().join("Cargo.lock"), "")?; + + Ok(dir) + } #[test] fn test_extract_toolchain_channel() -> Result<()> { @@ -438,15 +493,7 @@ mod test { }; 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)?; - + write_valid_toolchain_file(&toolchain_file_path)?; let channel = extract_toolchain_channel(&toolchain_file_path)?; assert_eq!(channel, "nightly-2020-07-10"); Ok(()) @@ -496,4 +543,28 @@ mod test { Ok(()) } + + #[test] + pub fn test_hash_files() -> Result<()> { + let _dir = write_hash_files(10, 100)?; + let rust_version = "cargo 1.80.0 (376290515 2024-07-16)\n".as_bytes(); + let hash = hash_files(rust_version, vec![], BuildConfig::new(false))?; + assert_eq!( + hex::encode(hash), + "06b50fcc53e0804f043eac3257c825226e59123018b73895cb946676148cb262" + ); + Ok(()) + } + + #[cfg(feature = "nightly")] + #[bench] + pub fn bench_hash_files(b: &mut test::Bencher) -> Result<()> { + let _dir = write_hash_files(1000, 10000)?; + let rust_version = "cargo 1.80.0 (376290515 2024-07-16)\n".as_bytes(); + b.iter(|| { + hash_files(rust_version, vec![], BuildConfig::new(false)) + .expect("failed to hash files"); + }); + Ok(()) + } } diff --git a/main/src/verify.rs b/main/src/verify.rs index c65223b..c8087f2 100644 --- a/main/src/verify.rs +++ b/main/src/verify.rs @@ -18,7 +18,7 @@ use crate::{ constants::TOOLCHAIN_FILE_NAME, deploy::{self, extract_compressed_wasm, extract_contract_evm_deployment_prelude}, project::{self, extract_toolchain_channel}, - CheckConfig, VerifyConfig, + CheckConfig, DataFeeOpts, VerifyConfig, }; #[derive(Debug, Deserialize, Serialize)] @@ -52,6 +52,9 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { } let check_cfg = CheckConfig { common_cfg: cfg.common_cfg.clone(), + data_fee: DataFeeOpts { + data_fee_bump_percent: 20, + }, wasm_file: None, contract_address: None, }; @@ -65,7 +68,7 @@ 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 project_hash = - project::hash_files(cfg.common_cfg.source_files_for_project_hash, build_cfg)?; + project::hash_project(cfg.common_cfg.source_files_for_project_hash, build_cfg)?; let (_, init_code) = project::compress_wasm(&wasm_file, project_hash)?; let deployment_data = deploy::contract_deployment_calldata(&init_code); if deployment_data == *result.input {