From 3e807f8df6b0548275cb0d59b05fb3d86df06f9b Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 09:07:37 -0500 Subject: [PATCH 01/23] port over verify repro --- Cargo.lock | 20 ++++++++++ Cargo.toml | 2 +- README.md | 21 +++++++++++ check/src/docker.rs | 81 ++++++++++++++++++++++++++++++++++++++++ check/src/main.rs | 21 +++++++++++ check/src/project.rs | 88 +++++++++++++++++++++++++++++++++++++++++++- reproduce/Cargo.toml | 0 verify/Cargo.toml | 28 ++++++++++++++ verify/src/main.rs | 44 ++++++++++++++++++++++ verify/src/verify.rs | 79 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 check/src/docker.rs create mode 100644 reproduce/Cargo.toml create mode 100644 verify/Cargo.toml create mode 100644 verify/src/main.rs create mode 100644 verify/src/verify.rs diff --git a/Cargo.lock b/Cargo.lock index 28373f4..bd86008 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -714,6 +714,26 @@ dependencies = [ "rustc-host", ] +[[package]] +name = "cargo-stylus-verify" +version = "0.3.1" +dependencies = [ + "alloy-primitives 0.7.2", + "cargo-stylus-util", + "clap", + "ethers", + "eyre", + "function_name", + "hex", + "lazy_static", + "libc", + "libloading", + "parking_lot", + "rustc-host", + "sneks", + "tokio", +] + [[package]] name = "cargo_metadata" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index 122c4d0..0759f3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cgen", "check", "example", "main", "replay", "util"] +members = ["cgen", "check", "example", "main", "replay", "util", "verify"] resolver = "2" [workspace.package] diff --git a/README.md b/README.md index 970a82c..2511565 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` flag to any of the cargo stylus commands. diff --git a/check/src/docker.rs b/check/src/docker.rs new file mode 100644 index 0000000..0fda3e8 --- /dev/null +++ b/check/src/docker.rs @@ -0,0 +1,81 @@ +// Copyright 2023-2024, 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/check/src/main.rs b/check/src/main.rs index 4de49b2..3452fa2 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -10,6 +10,7 @@ use tokio::runtime::Builder; mod check; mod constants; mod deploy; +mod docker; mod export_abi; mod macros; mod new; @@ -37,6 +38,19 @@ enum Apis { #[arg(long)] minimal: bool, }, + // /// 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(long)] + // version: String, + + // /// Stylus subcommand. + // #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + // stylus: Vec, + // }, /// Export a Solidity ABI. ExportAbi { /// The output file (defaults to stdout). @@ -85,6 +99,13 @@ struct DeployConfig { estimate_gas: bool, } +#[derive(Args, Clone, Debug)] +pub struct VerifyConfig { + /// Hash of the deployment transaction. + #[arg(long)] + deployment_tx: String, +} + #[derive(Clone, Debug, Args)] #[clap(group(ArgGroup::new("key").required(true).args(&["private_key_path", "private_key", "keystore_path"])))] struct AuthOpts { diff --git a/check/src/project.rs b/check/src/project.rs index fce5bcf..15cc7e8 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -8,7 +8,16 @@ use crate::{ use brotli2::read::BrotliEncoder; use cargo_stylus_util::{color::Color, sys}; use eyre::{eyre, Result, WrapErr}; -use std::{env::current_dir, fs, io::Read, path::PathBuf, process}; +use std::process::Command; +use std::str::FromStr as _; +use std::{ + env::current_dir, + fs, + io::Read, + path::{Path, PathBuf}, + process, +}; +use tiny_keccak::{Hasher, Keccak}; #[derive(Default, PartialEq)] pub enum OptLevel { @@ -112,6 +121,83 @@ pub fn build_dylib(cfg: BuildConfig) -> Result { Ok(wasm_file_path) } +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) +} + /// Reads a WASM file at a specified path and returns its brotli compressed bytes. pub fn compress_wasm(wasm: &PathBuf) -> Result<(Vec, Vec)> { let wasm = diff --git a/reproduce/Cargo.toml b/reproduce/Cargo.toml new file mode 100644 index 0000000..e69de29 diff --git a/verify/Cargo.toml b/verify/Cargo.toml new file mode 100644 index 0000000..b4959c8 --- /dev/null +++ b/verify/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "cargo-stylus-verify" +keywords = ["arbitrum", "ethereum", "stylus"] +description = "CLI tool for verifying reproducible builds of Stylus programs via Docker" + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +version.workspace = true +repository.workspace = true + +[dependencies] +alloy-primitives.workspace = true +cargo-stylus-util.workspace = true +cargo-stylus-check.workspace = true +clap.workspace = true +ethers.workspace = true +eyre.workspace = true +function_name.workspace = true +lazy_static.workspace = true +libc.workspace = true +libloading.workspace = true +parking_lot.workspace = true +rustc-host.workspace = true +sneks.workspace = true +tokio.workspace = true +hex.workspace = true \ No newline at end of file diff --git a/verify/src/main.rs b/verify/src/main.rs new file mode 100644 index 0000000..caee07a --- /dev/null +++ b/verify/src/main.rs @@ -0,0 +1,44 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md + +use clap::{Args, Parser}; +use eyre::{eyre, Context}; + +mod verify; + +#[derive(Parser, Debug)] +#[command(name = "verify")] +#[command(bin_name = "cargo stylus")] +#[command(author = "Offchain Labs, Inc.")] +#[command(about = "Generate C code for Stylus ABI bindings.", long_about = None)] +#[command(propagate_version = true)] +#[command(version)] +struct Opts { + #[command(subcommand)] + command: Apis, +} + +#[derive(Parser, Debug, Clone)] +enum Apis { + /// Verify a Stylus program deployment. + #[command()] + Verify(VerifyConfig), +} + +#[derive(Args, Clone, Debug)] +pub struct VerifyConfig { + /// Hash of the deployment transaction. + #[arg(long)] + deployment_tx: String, +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let args = Opts::parse(); + + match args.command { + Apis::Verify(config) => verify::verify(config) + .await + .wrap_err_with(|| eyre!("Failed to verify")), + } +} diff --git a/verify/src/verify.rs b/verify/src/verify.rs new file mode 100644 index 0000000..481dd03 --- /dev/null +++ b/verify/src/verify.rs @@ -0,0 +1,79 @@ +// Copyright 2023-2024, 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::middleware::Middleware; +use ethers::types::{H160, H256}; + +use serde::{Deserialize, Serialize}; + +use cargo_stylus_check::project::BuildConfig; +use cargo_stylus_util::util; + +#[derive(Debug, Deserialize, Serialize)] +struct RpcResult { + input: String, +} + +pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { + let provider = util::new_provider(&cfg.common_cfg.endpoint)?; + let hash = hex::decode( + cfg.deployment_tx + .as_str() + .strip_prefix("0x") + .unwrap_or(&cfg.deployment_tx) + .as_bytes(), + ) + .map_err(|e| eyre!("Invalid hash: {e}"))?; + if hash.len() != 32 { + bail!("Invalid hash"); + } + let Some(result) = provider + .get_transaction(H256::from_slice(&hash)) + .await + .map_err(|e| eyre!("RPC failed: {e}"))? + else { + bail!("No code at address"); + }; + + 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 == *result.input { + println!("Verified - data matches!"); + } else { + println!("Not verified - data does not match!"); + } + + Ok(()) +} From 10b04aacfde1c3d7e4617f118a0cf0b7774b6169 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 10:39:35 -0500 Subject: [PATCH 02/23] verify working --- Cargo.lock | 20 ----------- Cargo.toml | 2 +- check/src/check.rs | 53 ++++++++++++++++++--------- check/src/deploy.rs | 63 ++++++++++++++++++++------------- check/src/main.rs | 61 +++++++++++++++++++++---------- check/src/project.rs | 8 ++--- {verify => check}/src/verify.rs | 48 +++++++++++-------------- main/src/main.rs | 20 ++++++++++- reproduce/Cargo.toml | 0 verify/Cargo.toml | 28 --------------- verify/src/main.rs | 44 ----------------------- 11 files changed, 162 insertions(+), 185 deletions(-) rename {verify => check}/src/verify.rs (53%) delete mode 100644 reproduce/Cargo.toml delete mode 100644 verify/Cargo.toml delete mode 100644 verify/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index bd86008..28373f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -714,26 +714,6 @@ dependencies = [ "rustc-host", ] -[[package]] -name = "cargo-stylus-verify" -version = "0.3.1" -dependencies = [ - "alloy-primitives 0.7.2", - "cargo-stylus-util", - "clap", - "ethers", - "eyre", - "function_name", - "hex", - "lazy_static", - "libc", - "libloading", - "parking_lot", - "rustc-host", - "sneks", - "tokio", -] - [[package]] name = "cargo_metadata" version = "0.18.1" diff --git a/Cargo.toml b/Cargo.toml index 0759f3e..122c4d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cgen", "check", "example", "main", "replay", "util", "verify"] +members = ["cgen", "check", "example", "main", "replay", "util"] resolver = "2" [workspace.package] diff --git a/check/src/check.rs b/check/src/check.rs index 771168f..ca1f840 100644 --- a/check/src/check.rs +++ b/check/src/check.rs @@ -47,13 +47,13 @@ sol! { /// Checks that a program is valid and can be deployed onchain. /// Returns whether the WASM is already up-to-date and activated onchain, and the data fee. pub async fn check(cfg: &CheckConfig) -> Result { - if cfg.endpoint == "https://stylus-testnet.arbitrum.io/rpc" { + if cfg.common_cfg.endpoint == "https://stylus-testnet.arbitrum.io/rpc" { let version = "cargo stylus version 0.2.1".to_string().red(); bail!("The old Stylus testnet is no longer supported.\nPlease downgrade to {version}",); } - let verbose = cfg.verbose; - let wasm = cfg.build_wasm().wrap_err("failed to build wasm")?; + let verbose = cfg.common_cfg.verbose; + let (wasm, project_hash) = cfg.build_wasm().wrap_err("failed to build wasm")?; if verbose { greyln!("reading wasm file at {}", wasm.to_string_lossy().lavender()); @@ -65,55 +65,76 @@ pub async fn check(cfg: &CheckConfig) -> Result { if verbose { greyln!("wasm size: {}", format_file_size(wasm.len(), 96, 128)); - greyln!("connecting to RPC: {}", &cfg.endpoint.lavender()); + greyln!("connecting to RPC: {}", &cfg.common_cfg.endpoint.lavender()); } // check if the program already exists - let provider = sys::new_provider(&cfg.endpoint)?; + let provider = sys::new_provider(&cfg.common_cfg.endpoint)?; let codehash = alloy_primitives::keccak256(&code); if program_exists(codehash, &provider).await? { - return Ok(ProgramCheck::Active(code)); + return Ok(ProgramCheck::Active { code, project_hash }); } 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)) + Ok(ProgramCheck::Ready { + code, + fee, + project_hash, + }) } /// Whether a program is active, or needs activation. #[derive(PartialEq)] pub enum ProgramCheck { /// Program already exists onchain. - Active(Vec), + Active { + code: Vec, + project_hash: [u8; 32], + }, /// Program can be activated with the given data fee. - Ready(Vec, U256), + Ready { + code: Vec, + fee: U256, + project_hash: [u8; 32], + }, } impl ProgramCheck { pub fn code(&self) -> &[u8] { match self { - Self::Active(code) => code, - Self::Ready(code, _) => code, + Self::Active { code, .. } => code, + 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(), - Self::Ready(_, data_fee) => data_fee * U256::from(120) / U256::from(100), + Self::Active { .. } => U256::default(), + Self::Ready { fee, .. } => fee * U256::from(120) / U256::from(100), } } } impl CheckConfig { - fn build_wasm(&self) -> Result { + fn build_wasm(&self) -> Result<(PathBuf, [u8; 32])> { if let Some(wasm) = self.wasm_file.clone() { - return Ok(wasm); + return Ok((wasm, [0u8; 32])); } - project::build_dylib(BuildConfig::new(self.rust_stable)) + let cfg = BuildConfig::new(self.common_cfg.rust_stable); + let project_hash = project::hash_files(cfg.clone())?; + let wasm = project::build_dylib(cfg)?; + Ok((wasm, project_hash)) } } diff --git a/check/src/deploy.rs b/check/src/deploy.rs index 7f2792d..8326dfb 100644 --- a/check/src/deploy.rs +++ b/check/src/deploy.rs @@ -49,9 +49,9 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { } let program = run!(check::check(&cfg.check_config), "cargo stylus check failed"); - let verbose = cfg.check_config.verbose; + let verbose = cfg.check_config.common_cfg.verbose; - let client = sys::new_provider(&cfg.check_config.endpoint)?; + let client = sys::new_provider(&cfg.check_config.common_cfg.endpoint)?; let chain_id = run!(client.get_chainid(), "failed to get chain id"); let wallet = cfg.auth.wallet().wrap_err("failed to load wallet")?; @@ -65,7 +65,7 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { let data_fee = program.suggest_fee(); - if let ProgramCheck::Ready(..) = &program { + if let ProgramCheck::Ready { .. } = &program { // check balance early let balance = run!(client.get_balance(sender, None), "failed to get balance"); let balance = alloy_ethers_typecast::ethers_u256_to_alloy(balance); @@ -83,11 +83,13 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> { } } - let contract = cfg.deploy_contract(program.code(), sender, &client).await?; + let contract = cfg + .deploy_contract(program.code(), program.project_hash(), sender, &client) + .await?; match program { - ProgramCheck::Ready(..) => cfg.activate(sender, contract, data_fee, &client).await?, - ProgramCheck::Active(_) => greyln!("wasm already activated!"), + ProgramCheck::Ready { .. } => cfg.activate(sender, contract, data_fee, &client).await?, + ProgramCheck::Active { .. } => greyln!("wasm already activated!"), } Ok(()) } @@ -96,33 +98,22 @@ impl DeployConfig { async fn deploy_contract( &self, code: &[u8], + project_hash: &[u8; 32], sender: H160, client: &SignerClient, ) -> Result { - let mut init_code = Vec::with_capacity(42 + code.len()); - init_code.push(0x7f); // PUSH32 - init_code.extend(AU256::from(code.len()).to_be_bytes::<32>()); - init_code.push(0x80); // DUP1 - init_code.push(0x60); // PUSH1 - init_code.push(0x2a); // 42 the prelude length - init_code.push(0x60); // PUSH1 - init_code.push(0x00); - init_code.push(0x39); // CODECOPY - init_code.push(0x60); // PUSH1 - init_code.push(0x00); - init_code.push(0xf3); // RETURN - init_code.extend(code); + let init_code = program_deployment_calldata(code, project_hash); let tx = Eip1559TransactionRequest::new() .from(sender) .data(init_code); - let verbose = self.check_config.verbose; + let verbose = self.check_config.common_cfg.verbose; let gas = client .estimate_gas(&TypedTransaction::Eip1559(tx.clone()), None) .await?; - if self.check_config.verbose || self.estimate_gas { + if self.check_config.common_cfg.verbose || self.estimate_gas { greyln!("deploy gas estimate: {}", format_gas(gas)); } if self.estimate_gas { @@ -143,6 +134,8 @@ impl DeployConfig { } else { greyln!("deployed code at address: {address}"); } + let tx_hash = receipt.transaction_hash.debug_lavender(); + greyln!("Deployment tx hash: {tx_hash}"); Ok(contract) } @@ -153,7 +146,7 @@ impl DeployConfig { data_fee: AU256, client: &SignerClient, ) -> Result<()> { - let verbose = self.check_config.verbose; + let verbose = self.check_config.common_cfg.verbose; let data_fee = alloy_ethers_typecast::alloy_u256_to_ethers(data_fee); let program: Address = contract.to_fixed_bytes().into(); @@ -170,7 +163,7 @@ impl DeployConfig { .await .map_err(|e| eyre!("did not estimate correctly: {e}"))?; - if self.check_config.verbose || self.estimate_gas { + if self.check_config.common_cfg.verbose || self.estimate_gas { greyln!("activation gas estimate: {}", format_gas(gas)); } if self.estimate_gas { @@ -204,7 +197,7 @@ impl DeployConfig { let tx = client.send_transaction(tx, None).await?; let tx_hash = tx.tx_hash(); - let verbose = self.check_config.verbose; + let verbose = self.check_config.common_cfg.verbose; if verbose { greyln!("sent {name} tx: {}", tx_hash.debug_lavender()); @@ -219,6 +212,28 @@ impl DeployConfig { } } +/// 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 +} + 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/main.rs b/check/src/main.rs index 3452fa2..8648576 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -15,6 +15,7 @@ mod export_abi; mod macros; mod new; mod project; +mod verify; mod wallet; #[derive(Parser, Debug)] @@ -38,19 +39,6 @@ enum Apis { #[arg(long)] minimal: bool, }, - // /// 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(long)] - // version: String, - - // /// Stylus subcommand. - // #[arg(trailing_var_arg = true, allow_hyphen_values = true)] - // stylus: Vec, - // }, /// Export a Solidity ABI. ExportAbi { /// The output file (defaults to stdout). @@ -66,19 +54,30 @@ 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.75 check` + Reproducible { + /// Rust version to use. + #[arg()] + 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), } #[derive(Args, Clone, Debug)] -struct CheckConfig { +struct CommonConfig { /// Arbitrum RPC endpoint. #[arg(short, long, default_value = "https://stylusv2.arbitrum.io/rpc")] endpoint: String, /// The WASM to check (defaults to any found in the current directory). - #[arg(long)] - wasm_file: Option, - /// Where to deploy and activate the program (defaults to a random address). - #[arg(long)] - program_address: Option, /// Whether to use stable Rust. #[arg(long)] rust_stable: bool, @@ -87,6 +86,18 @@ struct CheckConfig { verbose: bool, } +#[derive(Args, Clone, Debug)] +pub struct CheckConfig { + #[command(flatten)] + common_cfg: CommonConfig, + /// The WASM to check (defaults to any found in the current directory). + #[arg(long)] + wasm_file: Option, + /// Where to deploy and activate the program (defaults to a random address). + #[arg(long)] + program_address: Option, +} + #[derive(Args, Clone, Debug)] struct DeployConfig { #[command(flatten)] @@ -101,6 +112,9 @@ struct DeployConfig { #[derive(Args, Clone, Debug)] pub struct VerifyConfig { + #[command(flatten)] + common_cfg: CommonConfig, + /// Hash of the deployment transaction. #[arg(long)] deployment_tx: String, @@ -149,6 +163,15 @@ async fn main_impl(args: Opts) -> Result<()> { Apis::Deploy(config) => { run!(deploy::deploy(config).await, "failed to deploy"); } + Apis::Reproducible { version, stylus } => { + run!( + docker::run_reproducible(&version, &stylus), + "failed reproducible run" + ); + } + Apis::Verify(config) => { + run!(verify::verify(config).await, "failed to verify"); + } } Ok(()) } diff --git a/check/src/project.rs b/check/src/project.rs index 15cc7e8..f3a1e45 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -7,7 +7,7 @@ use crate::{ }; use brotli2::read::BrotliEncoder; use cargo_stylus_util::{color::Color, sys}; -use eyre::{eyre, Result, WrapErr}; +use eyre::{bail, eyre, Result, WrapErr}; use std::process::Command; use std::str::FromStr as _; use std::{ @@ -19,14 +19,14 @@ use std::{ }; use tiny_keccak::{Hasher, Keccak}; -#[derive(Default, PartialEq)] +#[derive(Default, Clone, PartialEq)] pub enum OptLevel { #[default] S, Z, } -#[derive(Default)] +#[derive(Default, Clone)] pub struct BuildConfig { pub opt_level: OptLevel, pub stable: bool, @@ -149,7 +149,7 @@ fn all_paths() -> Result> { pub fn hash_files(cfg: BuildConfig) -> Result<[u8; 32]> { let mut keccak = Keccak::v256(); let mut cmd = Command::new("cargo"); - if cfg.nightly { + if !cfg.stable { cmd.arg("+nightly"); } cmd.arg("--version"); diff --git a/verify/src/verify.rs b/check/src/verify.rs similarity index 53% rename from verify/src/verify.rs rename to check/src/verify.rs index 481dd03..c2c2b22 100644 --- a/verify/src/verify.rs +++ b/check/src/verify.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024, Offchain Labs, Inc. +// Copyright 2023, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md #![allow(clippy::println_empty_string)] @@ -8,12 +8,12 @@ use std::path::PathBuf; use eyre::{bail, eyre}; use ethers::middleware::Middleware; -use ethers::types::{H160, H256}; +use ethers::types::H256; use serde::{Deserialize, Serialize}; -use cargo_stylus_check::project::BuildConfig; -use cargo_stylus_util::util; +use crate::{check, deploy, project, CheckConfig, VerifyConfig}; +use cargo_stylus_util::{color::Color, sys}; #[derive(Debug, Deserialize, Serialize)] struct RpcResult { @@ -21,15 +21,8 @@ struct RpcResult { } pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { - let provider = util::new_provider(&cfg.common_cfg.endpoint)?; - let hash = hex::decode( - cfg.deployment_tx - .as_str() - .strip_prefix("0x") - .unwrap_or(&cfg.deployment_tx) - .as_bytes(), - ) - .map_err(|e| eyre!("Invalid hash: {e}"))?; + let provider = sys::new_provider(&cfg.common_cfg.endpoint)?; + let hash = cargo_stylus_util::text::decode0x(cfg.deployment_tx)?; if hash.len() != 32 { bail!("Invalid hash"); } @@ -41,7 +34,7 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { bail!("No code at address"); }; - let output = util::new_command("cargo") + let output = sys::new_command("cargo") .arg("clean") .output() .map_err(|e| eyre!("failed to execute cargo clean: {e}"))?; @@ -50,30 +43,29 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { } let check_cfg = CheckConfig { common_cfg: cfg.common_cfg.clone(), - wasm_file_path: None, - expected_program_address: H160::zero(), + wasm_file: None, + program_address: None, }; - check::run_checks(check_cfg) + let _ = check::check(&check_cfg) .await .map_err(|e| eyre!("Stylus checks failed: {e}"))?; - let build_cfg = BuildConfig { + let build_cfg = project::BuildConfig { opt_level: project::OptLevel::default(), - nightly: cfg.common_cfg.nightly, + stable: cfg.common_cfg.rust_stable, rebuild: false, - skip_contract_size_check: cfg.common_cfg.skip_contract_size_check, }; - let wasm_file_path: PathBuf = project::build_project_dylib(build_cfg) + 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_path, cfg.common_cfg.skip_contract_size_check)?; + let (_, init_code) = project::compress_wasm(&wasm_file)?; let hash = project::hash_files(build_cfg)?; - let deployment_data = project::program_deployment_calldata(&init_code, &hash); - + let deployment_data = deploy::program_deployment_calldata(&init_code, &hash); if deployment_data == *result.input { - println!("Verified - data matches!"); + println!("Verified - program matches local project's file hashes"); } else { - println!("Not verified - data does not match!"); + println!( + "{} - program deployment did not verify against local project's file hashes", + "FAILED".red() + ); } - Ok(()) } diff --git a/main/src/main.rs b/main/src/main.rs index 601afb0..2759935 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -38,6 +38,12 @@ enum Subcommands { /// Trace a transaction. #[command()] Trace, + /// 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, @@ -52,7 +58,19 @@ struct Binary<'a> { const COMMANDS: &[Binary] = &[ Binary { name: "cargo-stylus-check", - apis: &["new", "export-abi", "check", "deploy", "n", "x", "c", "d"], + apis: &[ + "new", + "export-abi", + "check", + "deploy", + "verify", + "reproducible", + "n", + "x", + "c", + "d", + "v", + ], rust_flags: None, }, Binary { diff --git a/reproduce/Cargo.toml b/reproduce/Cargo.toml deleted file mode 100644 index e69de29..0000000 diff --git a/verify/Cargo.toml b/verify/Cargo.toml deleted file mode 100644 index b4959c8..0000000 --- a/verify/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "cargo-stylus-verify" -keywords = ["arbitrum", "ethereum", "stylus"] -description = "CLI tool for verifying reproducible builds of Stylus programs via Docker" - -authors.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -version.workspace = true -repository.workspace = true - -[dependencies] -alloy-primitives.workspace = true -cargo-stylus-util.workspace = true -cargo-stylus-check.workspace = true -clap.workspace = true -ethers.workspace = true -eyre.workspace = true -function_name.workspace = true -lazy_static.workspace = true -libc.workspace = true -libloading.workspace = true -parking_lot.workspace = true -rustc-host.workspace = true -sneks.workspace = true -tokio.workspace = true -hex.workspace = true \ No newline at end of file diff --git a/verify/src/main.rs b/verify/src/main.rs deleted file mode 100644 index caee07a..0000000 --- a/verify/src/main.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2023-2024, Offchain Labs, Inc. -// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md - -use clap::{Args, Parser}; -use eyre::{eyre, Context}; - -mod verify; - -#[derive(Parser, Debug)] -#[command(name = "verify")] -#[command(bin_name = "cargo stylus")] -#[command(author = "Offchain Labs, Inc.")] -#[command(about = "Generate C code for Stylus ABI bindings.", long_about = None)] -#[command(propagate_version = true)] -#[command(version)] -struct Opts { - #[command(subcommand)] - command: Apis, -} - -#[derive(Parser, Debug, Clone)] -enum Apis { - /// Verify a Stylus program deployment. - #[command()] - Verify(VerifyConfig), -} - -#[derive(Args, Clone, Debug)] -pub struct VerifyConfig { - /// Hash of the deployment transaction. - #[arg(long)] - deployment_tx: String, -} - -#[tokio::main] -async fn main() -> eyre::Result<()> { - let args = Opts::parse(); - - match args.command { - Apis::Verify(config) => verify::verify(config) - .await - .wrap_err_with(|| eyre!("Failed to verify")), - } -} From fed4dc00d8714e2de53e76f2f15cb309f36328f4 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 10:43:27 -0500 Subject: [PATCH 03/23] add items --- check/src/docker.rs | 3 +++ check/src/main.rs | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/check/src/docker.rs b/check/src/docker.rs index 0fda3e8..30fe471 100644 --- a/check/src/docker.rs +++ b/check/src/docker.rs @@ -40,6 +40,9 @@ fn create_image(version: &str) -> Result<()> { RUN rustup target add wasm32-unknown-unknown RUN rustup target add wasm32-wasi 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 )?; diff --git a/check/src/main.rs b/check/src/main.rs index 8648576..79d1287 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -57,11 +57,11 @@ enum Apis { /// 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` + /// Example: `cargo stylus reproducible 1.77 check` Reproducible { /// Rust version to use. #[arg()] - version: String, + rust_version: String, /// Stylus subcommand. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] @@ -163,9 +163,12 @@ async fn main_impl(args: Opts) -> Result<()> { Apis::Deploy(config) => { run!(deploy::deploy(config).await, "failed to deploy"); } - Apis::Reproducible { version, stylus } => { + Apis::Reproducible { + rust_version, + stylus, + } => { run!( - docker::run_reproducible(&version, &stylus), + docker::run_reproducible(&rust_version, &stylus), "failed reproducible run" ); } From 54bf7761a2ecaf924ee96689016033e3368cf952 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 10:48:34 -0500 Subject: [PATCH 04/23] bump versions --- Cargo.lock | 12 ++++++------ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28373f4..c0d3c92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -634,7 +634,7 @@ dependencies = [ [[package]] name = "cargo-stylus" -version = "0.3.1" +version = "0.3.2" dependencies = [ "cargo-stylus-util", "clap", @@ -643,7 +643,7 @@ dependencies = [ [[package]] name = "cargo-stylus-cgen" -version = "0.3.1" +version = "0.3.2" dependencies = [ "alloy-json-abi", "clap", @@ -654,7 +654,7 @@ dependencies = [ [[package]] name = "cargo-stylus-check" -version = "0.3.1" +version = "0.3.2" dependencies = [ "alloy-ethers-typecast", "alloy-json-abi", @@ -680,14 +680,14 @@ dependencies = [ [[package]] name = "cargo-stylus-example" -version = "0.3.1" +version = "0.3.2" dependencies = [ "clap", ] [[package]] name = "cargo-stylus-replay" -version = "0.3.1" +version = "0.3.2" dependencies = [ "alloy-primitives 0.7.2", "cargo-stylus-util", @@ -706,7 +706,7 @@ dependencies = [ [[package]] name = "cargo-stylus-util" -version = "0.3.1" +version = "0.3.2" dependencies = [ "ethers", "eyre", diff --git a/Cargo.toml b/Cargo.toml index 122c4d0..87e2cec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] authors = ["Offchain Labs"] -version = "0.3.1" +version = "0.3.2" edition = "2021" homepage = "https://arbitrum.io" license = "MIT OR Apache-2.0" @@ -34,4 +34,4 @@ parking_lot = "0.12.1" sneks = "0.1.2" # members -cargo-stylus-util = { path = "util", version = "0.3.1" } +cargo-stylus-util = { path = "util", version = "0.3.2" } From c4ce9732e1193e7888e59c4b8c4ab59f20d5cb2d Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 11:06:14 -0500 Subject: [PATCH 05/23] add arch --- check/src/docker.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/check/src/docker.rs b/check/src/docker.rs index 30fe471..29332a9 100644 --- a/check/src/docker.rs +++ b/check/src/docker.rs @@ -39,6 +39,8 @@ fn create_image(version: &str) -> Result<()> { FROM rust:{} as builder\n\ RUN rustup target add wasm32-unknown-unknown RUN rustup target add wasm32-wasi + RUN rustup target add nightly-aarch64-unknown-linux-gnu + RUN rustup target add aarch64-unknown-linux-gnu RUN cargo install cargo-stylus RUN cargo install --force cargo-stylus-check RUN cargo install --force cargo-stylus-replay From a5d0d125bd62f2defae8ee3626465304e096e73f Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 11:38:38 -0500 Subject: [PATCH 06/23] get toolchains out --- check/src/docker.rs | 3 +-- check/src/main.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/check/src/docker.rs b/check/src/docker.rs index 29332a9..1b17e90 100644 --- a/check/src/docker.rs +++ b/check/src/docker.rs @@ -15,7 +15,7 @@ fn image_exists(name: &str) -> Result { .arg("images") .arg(name) .output() - .map_err(|e| eyre!("failed to execure Docker command: {e}"))?; + .map_err(|e| eyre!("failed to execute Docker command: {e}"))?; Ok(output.stdout.iter().filter(|c| **c == b'\n').count() > 1) } @@ -39,7 +39,6 @@ fn create_image(version: &str) -> Result<()> { FROM rust:{} as builder\n\ RUN rustup target add wasm32-unknown-unknown RUN rustup target add wasm32-wasi - RUN rustup target add nightly-aarch64-unknown-linux-gnu RUN rustup target add aarch64-unknown-linux-gnu RUN cargo install cargo-stylus RUN cargo install --force cargo-stylus-check diff --git a/check/src/main.rs b/check/src/main.rs index 79d1287..ed3f233 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -77,7 +77,6 @@ struct CommonConfig { /// Arbitrum RPC endpoint. #[arg(short, long, default_value = "https://stylusv2.arbitrum.io/rpc")] endpoint: String, - /// The WASM to check (defaults to any found in the current directory). /// Whether to use stable Rust. #[arg(long)] rust_stable: bool, From 38d4381e34a2c1e5569cb14f24b0e39e696fab9e Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 6 Jun 2024 11:41:49 -0500 Subject: [PATCH 07/23] empty private key done --- check/src/wallet.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/check/src/wallet.rs b/check/src/wallet.rs index 55fb461..adcda45 100644 --- a/check/src/wallet.rs +++ b/check/src/wallet.rs @@ -18,6 +18,9 @@ impl AuthOpts { } if let Some(key) = &self.private_key { + if key.is_empty() { + return Err(eyre!("empty private key")); + } return wallet!(key); } From bf57dc63e4f5b8cce8a1736bf0702281bc61f68d Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Fri, 14 Jun 2024 15:38:58 -0500 Subject: [PATCH 08/23] provide paths --- Cargo.lock | 15 +++--- check/Cargo.toml | 2 + check/src/check.rs | 5 +- check/src/main.rs | 7 +++ check/src/project.rs | 116 ++++++++++++++++++++++++++++++++++++------- check/src/verify.rs | 2 +- 6 files changed, 121 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0d3c92..3575abc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -668,10 +668,12 @@ dependencies = [ "clap", "ethers", "eyre", + "glob", "hex", "lazy_static", "serde", "serde_json", + "tempfile", "thiserror", "tiny-keccak", "tokio", @@ -2429,9 +2431,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -3432,9 +3434,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.4.2", "errno", @@ -3992,13 +3994,12 @@ checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] diff --git a/check/Cargo.toml b/check/Cargo.toml index f223bc1..c428200 100644 --- a/check/Cargo.toml +++ b/check/Cargo.toml @@ -31,3 +31,5 @@ tiny-keccak = { version = "2.0.2", features = ["keccak"] } thiserror = "1.0.47" tokio.workspace = true wasmer = "3.1.0" +glob = "0.3.1" +tempfile = "3.10.1" diff --git a/check/src/check.rs b/check/src/check.rs index ca1f840..c0e8bac 100644 --- a/check/src/check.rs +++ b/check/src/check.rs @@ -132,7 +132,10 @@ impl CheckConfig { return Ok((wasm, [0u8; 32])); } let cfg = BuildConfig::new(self.common_cfg.rust_stable); - let project_hash = project::hash_files(cfg.clone())?; + let project_hash = project::hash_files( + self.common_cfg.source_files_for_project_hash.clone(), + cfg.clone(), + )?; let wasm = project::build_dylib(cfg)?; Ok((wasm, project_hash)) } diff --git a/check/src/main.rs b/check/src/main.rs index ed3f233..650fb95 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -83,6 +83,13 @@ struct CommonConfig { /// Whether to print debug info. #[arg(long)] verbose: bool, + /// The path to source files to include in the project hash, which + /// is included in the contract deployment init code transaction + /// to be used for verification of deployment integrity. + /// If not provided, all .rs files and Cargo.toml and Cargo.lock files + /// in project's directory tree are included. + #[arg(long)] + source_files_for_project_hash: Vec, } #[derive(Args, Clone, Debug)] diff --git a/check/src/project.rs b/check/src/project.rs index f3a1e45..db9e094 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -8,8 +8,8 @@ use crate::{ use brotli2::read::BrotliEncoder; use cargo_stylus_util::{color::Color, sys}; use eyre::{bail, eyre, Result, WrapErr}; +use glob::glob; use std::process::Command; -use std::str::FromStr as _; use std::{ env::current_dir, fs, @@ -121,32 +121,48 @@ pub fn build_dylib(cfg: BuildConfig) -> Result { Ok(wasm_file_path) } -fn all_paths() -> Result> { +fn all_paths(root_dir: &Path, source_file_patterns: Vec) -> Result> { let mut files = Vec::::new(); let mut directories = Vec::::new(); - directories.push(PathBuf::from_str(".").unwrap()); + directories.push(root_dir.to_path_buf()); // Using `from` directly + + let glob_paths = expand_glob_patterns(source_file_patterns)?; + while let Some(dir) = directories.pop() { - for f in std::fs::read_dir(&dir) + for entry in 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); + let entry = entry.map_err(|e| eyre!("Error finding file in {}: {e}", dir.display()))?; + let path = entry.path(); + + if path.is_dir() { + if path.ends_with("target") || path.ends_with(".git") { + continue; // Skip "target" and ".git" directories + } + directories.push(path); + } else if path.file_name().map_or(false, |f| { + // If the user has has specified a list of source file patterns, check if the file + // matches the pattern. + if glob_paths.len() > 0 { + for glob_path in glob_paths.iter() { + if glob_path == &path { + return true; + } + } + return false; + } else { + // Otherwise, by default include all rust files, Cargo.toml and Cargo.lock files. + f == "Cargo.toml" || f == "Cargo.lock" || f.to_string_lossy().ends_with(".rs") + } + }) { + files.push(path); } } } Ok(files) } -pub fn hash_files(cfg: BuildConfig) -> Result<[u8; 32]> { +pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result<[u8; 32]> { let mut keccak = Keccak::v256(); let mut cmd = Command::new("cargo"); if !cfg.stable { @@ -186,7 +202,7 @@ pub fn hash_files(cfg: BuildConfig) -> Result<[u8; 32]> { Ok(()) }; - let mut paths = all_paths()?; + let mut paths = all_paths(PathBuf::from(".").as_path(), source_file_patterns)?; paths.sort(); for filename in paths.iter() { @@ -198,6 +214,19 @@ pub fn hash_files(cfg: BuildConfig) -> Result<[u8; 32]> { Ok(hash) } +fn expand_glob_patterns(patterns: Vec) -> Result> { + let mut files_to_include = Vec::new(); + for pattern in patterns { + let paths = glob(&pattern) + .map_err(|e| eyre!("Failed to read glob pattern '{}': {}", pattern, e))?; + for path_result in paths { + let path = path_result.map_err(|e| eyre!("Error processing path: {}", e))?; + files_to_include.push(path); + } + } + Ok(files_to_include) +} + /// Reads a WASM file at a specified path and returns its brotli compressed bytes. pub fn compress_wasm(wasm: &PathBuf) -> Result<(Vec, Vec)> { let wasm = @@ -216,3 +245,56 @@ pub fn compress_wasm(wasm: &PathBuf) -> Result<(Vec, Vec)> { Ok((wasm.to_vec(), contract_code)) } + +#[cfg(test)] +mod test { + use super::*; + use std::fs::{self, File}; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn test_all_paths() -> Result<()> { + let dir = tempdir()?; + let dir_path = dir.path(); + + let files = vec!["file.rs", "ignore.me", "Cargo.toml", "Cargo.lock"]; + for file in files.iter() { + let file_path = dir_path.join(file); + let mut file = File::create(&file_path)?; + writeln!(file, "Test content")?; + } + + let dirs = vec!["nested", ".git", "target"]; + for d in dirs.iter() { + let subdir_path = dir_path.join(d); + if !subdir_path.exists() { + fs::create_dir(&subdir_path)?; + } + } + + let nested_dir = dir_path.join("nested"); + let nested_file = nested_dir.join("nested.rs"); + if !nested_file.exists() { + File::create(&nested_file)?; + } + + let found_files = all_paths( + dir_path, + vec![format!( + "{}/{}", + dir_path.as_os_str().to_string_lossy(), + "**/*.rs" + )], + )?; + + // Check that the correct files are included + assert!(found_files.contains(&dir_path.join("file.rs"))); + assert!(found_files.contains(&nested_dir.join("nested.rs"))); + assert!(!found_files.contains(&dir_path.join("ignore.me"))); + assert!(!found_files.contains(&dir_path.join("Cargo.toml"))); // Not matching *.rs + assert_eq!(found_files.len(), 2, "Should only find 2 Rust files."); + + Ok(()) + } +} diff --git a/check/src/verify.rs b/check/src/verify.rs index c2c2b22..5fce92b 100644 --- a/check/src/verify.rs +++ b/check/src/verify.rs @@ -57,7 +57,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 (_, init_code) = project::compress_wasm(&wasm_file)?; - let hash = project::hash_files(build_cfg)?; + 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); if deployment_data == *result.input { println!("Verified - program matches local project's file hashes"); From 67dc5f897595d38384f0d1f532c6d15af24e6165 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Fri, 14 Jun 2024 15:39:30 -0500 Subject: [PATCH 09/23] clippy --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 562d891..7780c2f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - toolchain: [stable, beta] + toolchain: [stable] steps: - uses: actions/checkout@v3 with: From 65820bb5f26acb08363cd4e5fb389b4f8d47d2da Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Fri, 14 Jun 2024 15:43:41 -0500 Subject: [PATCH 10/23] clippy --- check/src/project.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/check/src/project.rs b/check/src/project.rs index db9e094..d7e45bd 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -143,13 +143,13 @@ fn all_paths(root_dir: &Path, source_file_patterns: Vec) -> Result 0 { + if !glob_paths.is_empty() { for glob_path in glob_paths.iter() { if glob_path == &path { return true; } } - return false; + false } else { // Otherwise, by default include all rust files, Cargo.toml and Cargo.lock files. f == "Cargo.toml" || f == "Cargo.lock" || f.to_string_lossy().ends_with(".rs") From 8981de4f2479f04787ba3ffa56fef0ca2b2777e6 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 25 Jun 2024 16:17:46 -0500 Subject: [PATCH 11/23] added info --- check/src/project.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/check/src/project.rs b/check/src/project.rs index d7e45bd..7a06dab 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -206,11 +206,19 @@ pub fn hash_files(source_file_patterns: Vec, cfg: BuildConfig) -> Result paths.sort(); for filename in paths.iter() { + println!( + "File used for deployment hash: {}", + filename.as_os_str().to_string_lossy() + ); hash_file(filename)?; } let mut hash = [0u8; 32]; keccak.finalize(&mut hash); + println!( + "Project hash computed on deployment: {:?}", + hex::encode(hash) + ); Ok(hash) } From 36a97e0eb4dbd3ec723849349839d3f613a4210b Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Thu, 27 Jun 2024 17:16:17 -0500 Subject: [PATCH 12/23] cargo stylus cache program --- Cargo.lock | 17 ++++++ Cargo.toml | 2 +- cache/Cargo.toml | 24 ++++++++ cache/src/main.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 cache/Cargo.toml create mode 100644 cache/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 6bfee2a..aeec7f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,6 +641,23 @@ dependencies = [ "eyre", ] +[[package]] +name = "cargo-stylus-cache" +version = "0.3.1" +dependencies = [ + "alloy-primitives 0.7.2", + "alloy-sol-macro", + "alloy-sol-types", + "cargo-stylus-util", + "clap", + "ethers", + "eyre", + "hex", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "cargo-stylus-cgen" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 122c4d0..ad157b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cgen", "check", "example", "main", "replay", "util"] +members = ["cache", "cgen", "check", "example", "main", "replay", "util"] resolver = "2" [workspace.package] diff --git a/cache/Cargo.toml b/cache/Cargo.toml new file mode 100644 index 0000000..0f87ffa --- /dev/null +++ b/cache/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cargo-stylus-cache" +keywords = ["arbitrum", "ethereum", "stylus", "alloy"] +description = "CLI tool for interacting with the Stylus cache manager for Arbitrum chains" + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +version.workspace = true +repository.workspace = true + +[dependencies] +alloy-sol-macro.workspace = true +alloy-sol-types.workspace = true +alloy-primitives.workspace = true +cargo-stylus-util.workspace = true +hex.workspace = true +clap.workspace = true +ethers.workspace = true +eyre.workspace = true +serde = { version = "1.0.203", features = ["derive"] } +tokio.workspace = true +serde_json = "1.0.103" \ No newline at end of file diff --git a/cache/src/main.rs b/cache/src/main.rs new file mode 100644 index 0000000..c59a1ff --- /dev/null +++ b/cache/src/main.rs @@ -0,0 +1,148 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md + +use alloy_primitives::FixedBytes; +use alloy_sol_macro::sol; +use alloy_sol_types::{SolCall, SolInterface}; +use cargo_stylus_util::sys; +use clap::{Args, Parser}; +use ethers::middleware::Middleware; +use ethers::providers::{Http, Provider, ProviderError, RawCall}; +use ethers::types::spoof::State; +use ethers::types::transaction::eip2718::TypedTransaction; +use ethers::types::{Address, Eip1559TransactionRequest, U256}; +use eyre::{bail, eyre, Context, ErrReport, Result}; +use serde_json::Value; +use CacheManager::CacheManagerErrors; + +#[derive(Parser, Clone, Debug)] +#[command(name = "cargo-stylus-cache")] +#[command(bin_name = "cargo stylus cache")] +#[command(author = "Offchain Labs, Inc.")] +#[command(version = env!("CARGO_PKG_VERSION"))] +#[command(about = "Cargo command for interacting with the Arbitrum Stylus cache manager", long_about = None)] +#[command(propagate_version = true)] +pub struct Opts { + #[command(subcommand)] + command: Subcommands, +} + +#[derive(Parser, Debug, Clone)] +enum Subcommands { + #[command(alias = "p")] + Program(CacheArgs), +} + +#[derive(Args, Clone, Debug)] +struct CacheArgs { + /// RPC endpoint. + #[arg(short, long, default_value = "https://sepolia-rollup.arbitrum.io/rpc")] + endpoint: String, + /// Address of the Stylus program to cache. + #[arg(short, long)] + address: Address, + /// Bid, in wei, to place on the program cache. + #[arg(short, long, hide(true))] + bid: u64, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Opts::parse(); + macro_rules! run { + ($expr:expr, $($msg:expr),+) => { + $expr.await.wrap_err_with(|| eyre!($($msg),+)) + }; + } + match args.command { + Subcommands::Program(args) => run!( + self::cache_program(args), + "failed to submit program cache request" + ), + } +} + +sol! { + interface CacheManager { + function placeBid(bytes32 codehash) external payable; + + error NotChainOwner(address sender); + error AsmTooLarge(uint256 asm, uint256 queueSize, uint256 cacheSize); + error AlreadyCached(bytes32 codehash); + error BidTooSmall(uint192 bid, uint192 min); + error BidsArePaused(); + error MakeSpaceTooLarge(uint64 size, uint64 limit); + } +} + +async fn cache_program(args: CacheArgs) -> Result<()> { + let provider: Provider = sys::new_provider(&args.endpoint)?; + let chain_id = provider.get_chainid().await?; + println!("Connected to chain {}", chain_id); + + let program_code = provider + .get_code(args.address, None) + .await + .wrap_err("failed to fetch program code")?; + let codehash = ethers::utils::keccak256(&program_code); + let codehash = FixedBytes::<32>::from(codehash); + + let data = CacheManager::placeBidCall { codehash }.abi_encode(); + let tx = Eip1559TransactionRequest::new() + .data(data) + .value(U256::from(args.bid)); + if let Err(EthCallError { data, msg }) = + eth_call(tx.clone(), State::default(), &provider).await? + { + let Ok(error) = CacheManagerErrors::abi_decode(&data, true) else { + bail!("unknown CacheManager error: {msg}"); + }; + use CacheManagerErrors as C; + match error { + C::AsmTooLarge(_) => bail!("program too large"), + _ => bail!("unexpected CacheManager error: {msg}"), + } + } + + // Otherwise, we are ready to send the tx data if our call passed. + // TODO: Send. + Ok(()) +} + +struct EthCallError { + data: Vec, + msg: String, +} + +impl From for ErrReport { + fn from(value: EthCallError) -> Self { + eyre!(value.msg) + } +} + +async fn eth_call( + tx: Eip1559TransactionRequest, + mut state: State, + provider: &Provider, +) -> Result, EthCallError>> { + let tx = TypedTransaction::Eip1559(tx); + state.account(Default::default()).balance = Some(ethers::types::U256::MAX); // infinite balance + + match provider.call_raw(&tx).state(&state).await { + Ok(bytes) => Ok(Ok(bytes.to_vec())), + Err(ProviderError::JsonRpcClientError(error)) => { + let error = error + .as_error_response() + .ok_or_else(|| eyre!("json RPC failure: {error}"))?; + + let msg = error.message.clone(); + let data = match &error.data { + Some(Value::String(data)) => cargo_stylus_util::text::decode0x(data)?.to_vec(), + Some(value) => bail!("failed to decode RPC failure: {value}"), + None => vec![], + }; + Ok(Err(EthCallError { data, msg })) + } + Err(error) => Err(error.into()), + } +} From 7430b0b46a5c495536673486391f6599134df76c Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Mon, 1 Jul 2024 07:40:51 -0500 Subject: [PATCH 13/23] add in --- cache/src/main.rs | 61 ++++++++++++++++--- check/src/cache.rs | 146 +++++++++++++++++++++++++++++++++++++++++++++ check/src/main.rs | 3 + install.sh | 1 + 4 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 check/src/cache.rs diff --git a/cache/src/main.rs b/cache/src/main.rs index c59a1ff..f7cefa3 100644 --- a/cache/src/main.rs +++ b/cache/src/main.rs @@ -1,19 +1,24 @@ // Copyright 2023-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md +use std::str::FromStr; +use std::sync::Arc; + use alloy_primitives::FixedBytes; use alloy_sol_macro::sol; use alloy_sol_types::{SolCall, SolInterface}; use cargo_stylus_util::sys; use clap::{Args, Parser}; -use ethers::middleware::Middleware; +use ethers::middleware::{Middleware, SignerMiddleware}; use ethers::providers::{Http, Provider, ProviderError, RawCall}; +use ethers::signers::{LocalWallet, Signer}; use ethers::types::spoof::State; use ethers::types::transaction::eip2718::TypedTransaction; -use ethers::types::{Address, Eip1559TransactionRequest, U256}; +use ethers::types::{Address, Eip1559TransactionRequest, NameOrAddress, H160, U256}; +use ethers::utils::keccak256; use eyre::{bail, eyre, Context, ErrReport, Result}; +use hex::FromHex; use serde_json::Value; -use CacheManager::CacheManagerErrors; #[derive(Parser, Clone, Debug)] #[command(name = "cargo-stylus-cache")] @@ -84,26 +89,66 @@ async fn cache_program(args: CacheArgs) -> Result<()> { .get_code(args.address, None) .await .wrap_err("failed to fetch program code")?; - let codehash = ethers::utils::keccak256(&program_code); - let codehash = FixedBytes::<32>::from(codehash); + println!("Program code: {:?}", hex::encode(&program_code)); + // let codehash = ethers::utils::keccak256(&program_code); + // println!("Program codehash: {:#x}", &codehash); + + let raw_data = hex::decode("eff000001b242b23113662924a1045d5e667807a496e0e0117a87f49c7881338b04573d0e85a3033bcdcea73ffb8e29e93faa31153586c73d8f45c9b44c5d23c429259ffabd4f67ff7c86aab35a8c95a646a5f0f49269fe425cf8772da904e7b5b7173b548327220ce6c0e37c8d77c839b939618223409d01d04926f375575284b93b4dd9e2fb0fb101f11c543f92d811af817f6db3f498887b27b12593431444b18fa5d503686c138a4bf1971f630ce4f75a9aeb4b548e3dea02ff2a1ae2ae2900bd46c7d93793f826a35f9de2412d9b30a89441ea790482492422191f9268f432291482432f1bf54d3f6eddf5d920a691d422e7a123773b553eda616174b815800940f202f00941c4825523cc504f2648b9243084de92ae4d8b9693dbd0d4ffbc9ea67bc97b69aae2dc67d766ce18a8af461c86235ca6faa1bc34d7f8908f3f38f1ec3d5f5eea33615a404dcf717a52b3f9f5322e253b609ce9a95515359e6969729909a7076fd969ca97bb8c74c3ec7f3f5f5f1b3f2f9bc8ddff5a7e4af7ecf2454690b34ee1bf78fdbcf842bc2bad5bd67bcafe7f3f1b17390a8b7a825a25de3f1f4fd7917a2ce830765998741ffc3120ddabeeef8ff6fa285a10d1b46f3ba096b57513a8a0a05bdf09eaf54d04cdc7382ce58f259b219c98e23ab6c7fb778c5f6e04833ea959e0d71a689a98838088db962eb25a5d2fa9bdcaafae092b35dff97adc76fa1840fca5569c7dabbafa8a24cdf7ae53c8464b6d0624edd3e1823244fb0db774532f70207516e8fbddbfc840b794361a4dae013ac52a6b16ce7d11868cbbfe6fdb5afa241a3d30ca65f04f583b60c88cac830230c39e684a32aab8a2a814180eaac2eea743e3b5f9c4f4bd812b18452968a9446d88818a1d56cb5584debd83ab18e9aac299a34c6c6c4185dc7ae13d7d1349b16d371ced7a99c479cf37597ba27d86d93f99b4bb67340d8615879de8db610e59b2d038285dd81c63b9e545696c8ca450d2c06d648f6810a6bb5ea77c6c8c351b039078f16123db6a1d6c3cec2174298163b3e52909cc7277b436efc817a9e0a4a474d6fea25b828a6b2bce00fd4960ffc8386f263315bd3c252b6e662da9062aea603a9e666ba9066eea607e9e661fa90619e5ec42ec8342f935896799bd4b2cd87649643be406eb9c037282c37f801a5f549ab42ab7869181a19b136fdcab2e90f0176fd09e4b6fd00aa009f2d7378cce076278ccc16c2b0fc303b6bf89cbf4a2359b42a66a33e8892d08788ee3caa40bf66d9f38a935f484f8f6c1cd59a4d85adfa2381ecfa19e4a47b764faa624fcdc2afdf4141fd0714a63b8e4e6ac9b17a89a47e03a5f55750966e393fa905176a1065fd0aaad4896bb5e146cdd57e6023ba76458110915892cb4715a8d55f955aa3d5157f35cc2214190f1a9bc38507bf764ec212ddea2d8348a33398ac6a9fedc34016e06da95f7624bdc9d24353995ca16c683d34f52407dffb0fff597e105205da21e333344c5e0fb3899ad836331930c2124c00fbf5cf48015bc6ebe699a41088a3051b727858b70fb15aa29530065b036be102d4f7e6d7860ad9e68a1447b0fce30b28a1da13a49761d01fe06cbe08b80dadbdab6f77b8ca992818c207727f560a1e7c1d1167987c3171c87041f89ea181beb51413fdd99800bd7931cc2812b8c0dfdc024e09ca8f13125d1644ffd16f01e0810f88be11e180bc2c21c8ec519ff77525d9f44dd5d5ec7ffeda386c6a6487977536ec3bcccf22432f27d4aa3c54848569a0b1bf430a866da779a92fd275211c272a5cd79dd9c5488ce9ba23072a17eec399231b165ad15d775166428fe1babd2a370e9c5f6ce086ba32875e531a53f42fba36f7a8fd1330f89b8e96cd91da5b032cf6f58cdcdf1285bf204a36c068ef6784f6e70cf67744ec2f8984bd00ede30cb0e733667f4f787e45f86c06cdfe9d21ede50cc33f12a6bf260483eecd7c8a6cc512d912b91deec47dad8bd3963dec6d3decb234a074370b95549a9c031ded9458d43028cadb892faceb5961aff0af970f89090cc606922d27f864fb913094e176411e94c15888db50e063a6c201a01dea5bbb2c256f4149458dc608a70e8749104c82666f0ec90714bc1a386875afd70f1a501d1cfbc0557e61d80f4ce13781ded0cc2467ac53be5c9304573130f28f5e6de58c3d449b567d238562b6ff17ee17882986bbe93f0ccc99fff12786fc68638069d32c853b8c7a3ed638303bfd017b8ff103866a7c84f37c4fa9ce119ba3f15e22cdfe5669766ac484dfb282134f560db1c07f4558e1c0caffdcb68ec6582be46535cb04e4d4105140c7520dcaf7cac895393205d30f70bb4d3b25b7697d1f58e0362fb45b9b47eb6e8eb30966ac5c20ec36bc96bfcfa0c9f856a6ef3b417ecd773f19b101645a2554f74197761bb6dc801de436ad1ddaace4ffc51693ba92e42f43cce42db47e9cdb8670e7d12f0d9215825ab24a33a23ade7592ff781b258d49c843c84f5959210f4848166184271f6d64f1501dad901f9d324d93d64532cd03bd073fa8c543566d1fe48dfac1435c3bee588f726451e91f37f5fde0e2e4e4498fdaaa75e98887ab9cab798c0fe38e161d8c5d266b552a03fe4e5671c249fd0fcdde37b1a3b544a227c924886e2f63e41867d70b204e68e27766fcca3cc37b1320efccf3fef75067921d021669b08c601b3465b642a8dc0a4dbd0f7ab756b2dd202333d2a8b3f181a25e002013cb6f9d72c14269f4c9f40a9e0ce454095e65c19826c295ababa70ed6b7b8ab073d9710d3063f0e6d54fda32e982a27ba1daeecca71e2e4dca486358e59a1fdb2b228bb31e0b480064b83d5e03f849a5f9de6fc814da8e2688cfe9e27a087c622df42164fb83b59cc601c72c6fa98d2f79450f9f6469c1c97b7fa4e4b0b06a99dbaad59445bf4b4588b8c12b915a356007da01baf4542732c3c45468983e75004e8d88fc91889ffaf48c715d20121d0fa78bf23d630c1d2a9c3de747115391916901cb0db90b74ec295bb27981a2403169f04839a6a4dd3116beae91b3d32876c7dee09677f929efca2ed0a84f0d07122b47da465b697aef16dedb79ae399b15945ade1a0798d6eaa21449fb8f6bca866bbd360557fa94b96440b8ad0a926ac8eda7c65e1b89a095e1271a3b127142a541305892577c3dd0ae12600bfc2955f7169097540d040911d1419ac1ea9a760e41a658f495c329945afae5d253beed7b68d288c75a5e6f698769cd4c35a7a26abc1d718714c17fb9e219fb645a3be08d147b7475f5a2159631a8046c95733aee627c6dbd07e15d021ce423f437616e294e7027cf56da494f3196269f9966f59690b95a5755ac79655d4a96c1c96eab415bb32655da883aadc47681b5687258455a6b4f045b42c58f5240fe55106f175dde41409920dfdc64520ae4307e3eb94ff02fa14c9f39bbf76b5e40568d4e966f4479604aa682fab5198a387ff6033cf4ebaf3b61f68cd3729b19434b9e03d13c333c5c43c352c82c5f911c66e005dc14b858dba6a3c2b59ffe5207e1578cd61f719156406df6a29e4013b5eb0e20e273eb0e309377e70e20d2fffec05005ff8598b17969feeccc75abd20bfe42d34e751f916d602aa85a91fb861d5a3d75ef6e3205ca41f7f90364e3c289b271fb4ad530f8612142c38651a6cb8d2d7bbba051040000104104000bb4000bb404076815dc82e44b7f427ddc8d563378c5e3b7ed3d8f513b78cdf3879dbc4cd537748de3ae4027589a91b10d33e94070f8d5979319eb09cd174f1614c28108e7737af475bd999f7c6d33f57d95824d4aa04b896859cab3c18cff5ed6beee8e5395a7332ef285ed0f8aef46bdd83a507ff91df6a2ad1361d304dfbad929e333856287ad8e238bd354a8e0d38012c56e7dd2bcd906b04d3e1fead1071615b4b4e6a00cd7ed23577e20e75193df32f1a20e1f019a071a1d59ad484c16dc8dfe8aa665ff0e68914c3542a5108b718fd7516870c391cae2e925bdb031182435a30837373c6244ceecc4493f668759d44d634225562e5086e5c500481a2404454008a2eaf4d427664f1381e2578e12e0173142f98b5874c89f8264c7c3f244a24baed9c6dc450f6b35e6eef5e1a1bd06884ba75eacb16d444b3960c52a29f65ce00414f791c8a2d8fc8598b2879936025d0c8b310a553b042f9548bd8dfc384e278001b35437d18967a4f6e26db1e95eb1ba4fd95ca32372ff55c2a34200460b91fc957b1854f342528ff46155b83d66dd71342d37a8ba4ffe1e97898a5582aa32626cbd88ebf1b82b49ceabbbe40829b6ea32f982ff6bee370022d4a0f6b0e21cb1a548f045c0e8946d60d88f864b4b17819fb2b85bcf33c5a718275f90864e6442b4a95a515a5cae66fc575dcd1969fc9625fe2ede961993809ecbeb9269d653fad0fe4d92f53ab346f6e33985c25e86c3586e86d57a131b04a503ed17e56dc788a1da7ce3cbad9b9cd700fcfe704d985c411d89e323c0cddf313f31677efee5c859356023f0ec6d086846612b23825c8d02a60a9b6693c569cef51caca6138c0b1e515481490398d22047c8a9d6c3a6e35eb6031a1746f2f47d9a913e4e66992f5e0fb0a23911858749bd4a0b08d75afade96d0792335e7bb220ac45e3f8ee8e855579a845b597b1004add2490bb81d7e284b49e7cfd91f6cfd0f312c20caa40b59e2b9663d52a20e30fedc58767582ea04d08d0e8f3a1e1c6863a0f029f353c29009d0d7a0241781803c1c3346a2a20ef713470663143860d6371d8f0112e181428e0729f543718b9b420a08014e0e600c5dc2fe5fe306523e6912a95413e2e0c458bd4621ad23f83242b8ce8cd5e62313927f88a4affe88433e2920a1651a8346cc67c5b95101b34d3cfcc4f3a356aab65fb720e52db5848dec1e295d6b266a06d14c4b5240b048d8ae720c9d0a878f2e1166675e3405432e986057bc362c86a80321028600506dca84e9d632dd3aed60c5bf3920560f1a5236619bdbc62315ec9814c7cc49c258cb2171952066ac03093e61374c7134f3172161e528305c551762f8a6992f51c52e972263093309e1d554cb92610d7c44353a9c9d303a85cfa1b3547d95cc8a839f7c99b47809d73b131e2cd9e33b3272df4eeb340fbe58884127c8621828099af1ddf506232193acdb010a9accc0e909c20c8b01dda22153d4acca74a986ac5415de36d849b20a234f0f29d73247ad2946dac5838f12248cad06982895ca53ceead2bd61e450082ddc100b6754eb1c904afc4ad3263caecd375e36f16d4b1d73468415f6db6db9e03ba90cf34269ac10eccdbc7a130d2e2e4e6362f28d74de89a7c3e3b75009ef008417a930e2bd5b6880f44f69528e0a4d856cd2ec6b78eba4507f0a469ce9c743e921c172446e798ab8ad1f36dd303487c016bb8e380a85a76a22998c23b777563d28c8e839a5b2992538d2155beffb69b61d6a61f7d5f588e14ee24664d3b219ff51dc474cce37b7289c8aef57d04933c0b70b770cfbb60978ece02a78131bb35fae7809248633ee9302d8cbb05382244fe34650f415bfe786cc4c30ff5ed8bace88b2dbbb3ccf4eeb48d39853bd74b39511eedaacb986889361c456f29df49146d3f87bd6d5a548211bb4ea75d14ed808df459aaa53945a5ab2df57926512ef31a2d4bc9e8afe5eccd916505").unwrap(); + // if program_code != raw_data { + // bail!( + // "program code mismatch, got {} vs {}", + // hex::encode(program_code), + // hex::encode(raw_data) + // ); + // } + println!("got codehash {:?}", hex::encode(keccak256(&raw_data))); + let codehash = FixedBytes::<32>::from(keccak256(&raw_data)); let data = CacheManager::placeBidCall { codehash }.abi_encode(); + let to = H160::from_slice( + hex::decode("d1bbd579988f394a26d6ec16e77b3fa8a5e8fcee") + .unwrap() + .as_slice(), + ); let tx = Eip1559TransactionRequest::new() + .to(NameOrAddress::Address(to)) .data(data) .value(U256::from(args.bid)); + + // let privkey = "93690ac9d039285ed00f874a2694d951c1777ac3a165732f36ea773f16179a89".to_string(); + // let wallet = LocalWallet::from_str(&privkey)?; + // let chain_id = provider.get_chainid().await?.as_u64(); + // let client = Arc::new(SignerMiddleware::new( + // provider, + // wallet.clone().with_chain_id(chain_id), + // )); + // let pending_tx = client.send_transaction(tx, None).await?; + // let receipt = pending_tx.await?; + // match receipt { + // Some(receipt) => { + // println!("Receipt: {:?}", receipt); + // } + // None => { + // bail!("failed to cache program"); + // } + // } + if let Err(EthCallError { data, msg }) = eth_call(tx.clone(), State::default(), &provider).await? { - let Ok(error) = CacheManagerErrors::abi_decode(&data, true) else { - bail!("unknown CacheManager error: {msg}"); + println!("Got data {}, msg {:?}", hex::encode(&data), msg); + let error = match CacheManager::CacheManagerErrors::abi_decode(&data, true) { + Ok(err) => err, + Err(err_details) => bail!("unknown CacheManager error: {msg} and {:?}", err_details), }; - use CacheManagerErrors as C; + use CacheManager::CacheManagerErrors as C; match error { C::AsmTooLarge(_) => bail!("program too large"), _ => bail!("unexpected CacheManager error: {msg}"), } } + println!("Succeeded cache call"); // Otherwise, we are ready to send the tx data if our call passed. // TODO: Send. Ok(()) diff --git a/check/src/cache.rs b/check/src/cache.rs new file mode 100644 index 0000000..021ce33 --- /dev/null +++ b/check/src/cache.rs @@ -0,0 +1,146 @@ +// Copyright 2023-2024, Offchain Labs, Inc. +// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md + +use std::str::FromStr; +use std::sync::Arc; + +use alloy_primitives::FixedBytes; +use alloy_sol_macro::sol; +use alloy_sol_types::{SolCall, SolInterface}; +use cargo_stylus_util::sys; +use clap::{Args, Parser}; +use ethers::middleware::{Middleware, SignerMiddleware}; +use ethers::providers::{Http, Provider, ProviderError, RawCall}; +use ethers::signers::{LocalWallet, Signer}; +use ethers::types::spoof::State; +use ethers::types::transaction::eip2718::TypedTransaction; +use ethers::types::{Address, Eip1559TransactionRequest, NameOrAddress, H160, U256}; +use ethers::utils::keccak256; +use eyre::{bail, eyre, Context, ErrReport, Result}; +use hex::FromHex; +use serde_json::Value; + +sol! { + interface CacheManager { + function placeBid(bytes32 codehash) external payable; + + error NotChainOwner(address sender); + error AsmTooLarge(uint256 asm, uint256 queueSize, uint256 cacheSize); + error AlreadyCached(bytes32 codehash); + error BidTooSmall(uint192 bid, uint192 min); + error BidsArePaused(); + error MakeSpaceTooLarge(uint64 size, uint64 limit); + } +} + +async fn cache_program(args: CacheArgs) -> Result<()> { + let provider: Provider = sys::new_provider(&args.endpoint)?; + let chain_id = provider.get_chainid().await?; + println!("Connected to chain {}", chain_id); + + let program_code = provider + .get_code(args.address, None) + .await + .wrap_err("failed to fetch program code")?; + println!("Program code: {:?}", hex::encode(&program_code)); + // let codehash = ethers::utils::keccak256(&program_code); + // println!("Program codehash: {:#x}", &codehash); + + let raw_data = hex::decode("").unwrap(); + // if program_code != raw_data { + // bail!( + // "program code mismatch, got {} vs {}", + // hex::encode(program_code), + // hex::encode(raw_data) + // ); + // } + println!("got codehash {:?}", hex::encode(keccak256(&raw_data))); + let codehash = FixedBytes::<32>::from(keccak256(&raw_data)); + + let data = CacheManager::placeBidCall { codehash }.abi_encode(); + let to = H160::from_slice( + hex::decode("d1bbd579988f394a26d6ec16e77b3fa8a5e8fcee") + .unwrap() + .as_slice(), + ); + let tx = Eip1559TransactionRequest::new() + .to(NameOrAddress::Address(to)) + .data(data) + .value(U256::from(args.bid)); + + // let privkey = "93690ac9d039285ed00f874a2694d951c1777ac3a165732f36ea773f16179a89".to_string(); + // let wallet = LocalWallet::from_str(&privkey)?; + // let chain_id = provider.get_chainid().await?.as_u64(); + // let client = Arc::new(SignerMiddleware::new( + // provider, + // wallet.clone().with_chain_id(chain_id), + // )); + // let pending_tx = client.send_transaction(tx, None).await?; + // let receipt = pending_tx.await?; + // match receipt { + // Some(receipt) => { + // println!("Receipt: {:?}", receipt); + // } + // None => { + // bail!("failed to cache program"); + // } + // } + + if let Err(EthCallError { data, msg }) = + eth_call(tx.clone(), State::default(), &provider).await? + { + println!("Got data {}, msg {:?}", hex::encode(&data), msg); + let error = match CacheManager::CacheManagerErrors::abi_decode(&data, true) { + Ok(err) => err, + Err(err_details) => bail!("unknown CacheManager error: {msg} and {:?}", err_details), + }; + use CacheManager::CacheManagerErrors as C; + match error { + C::AsmTooLarge(_) => bail!("program too large"), + _ => bail!("unexpected CacheManager error: {msg}"), + } + } + + println!("Succeeded cache call"); + // Otherwise, we are ready to send the tx data if our call passed. + // TODO: Send. + Ok(()) +} + +struct EthCallError { + data: Vec, + msg: String, +} + +impl From for ErrReport { + fn from(value: EthCallError) -> Self { + eyre!(value.msg) + } +} + +async fn eth_call( + tx: Eip1559TransactionRequest, + mut state: State, + provider: &Provider, +) -> Result, EthCallError>> { + let tx = TypedTransaction::Eip1559(tx); + state.account(Default::default()).balance = Some(ethers::types::U256::MAX); // infinite balance + + match provider.call_raw(&tx).state(&state).await { + Ok(bytes) => Ok(Ok(bytes.to_vec())), + Err(ProviderError::JsonRpcClientError(error)) => { + let error = error + .as_error_response() + .ok_or_else(|| eyre!("json RPC failure: {error}"))?; + + let msg = error.message.clone(); + let data = match &error.data { + Some(Value::String(data)) => cargo_stylus_util::text::decode0x(data)?.to_vec(), + Some(value) => bail!("failed to decode RPC failure: {value}"), + None => vec![], + }; + Ok(Err(EthCallError { data, msg })) + } + Err(error) => Err(error.into()), + } +} diff --git a/check/src/main.rs b/check/src/main.rs index 25b49eb..bf2c0c8 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -7,6 +7,7 @@ use eyre::{eyre, Context, Result}; use std::path::PathBuf; use tokio::runtime::Builder; +mod cache; mod check; mod constants; mod deploy; @@ -46,6 +47,8 @@ enum Apis { #[arg(long)] json: bool, }, + /// Cache a contract using the Stylus CacheManager for Arbitrum chains. + Cache(CacheConfig), /// Check a contract. #[command(alias = "c")] Check(CheckConfig), diff --git a/install.sh b/install.sh index 682f003..cc52045 100755 --- a/install.sh +++ b/install.sh @@ -3,3 +3,4 @@ cargo clippy --package cargo-stylus --package cargo-stylus-cgen --package cargo- cargo install --path main cargo install --path cgen cargo install --path check +cargo install --path cache From b683d57f7a1cd1d24e3c2b06e6a232dffdbe6110 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Mon, 1 Jul 2024 08:52:51 -0500 Subject: [PATCH 14/23] cache with bid working --- Cargo.lock | 17 ---- Cargo.toml | 2 +- cache/Cargo.toml | 24 ----- cache/src/main.rs | 193 ----------------------------------------- check/src/cache.rs | 151 +++++++++++++------------------- check/src/check.rs | 27 +++--- check/src/constants.rs | 5 ++ check/src/deploy.rs | 70 ++++++++------- check/src/main.rs | 18 ++++ install.sh | 3 +- main/src/main.rs | 4 + 11 files changed, 148 insertions(+), 366 deletions(-) delete mode 100644 cache/Cargo.toml delete mode 100644 cache/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 36e9530..6739b34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,23 +641,6 @@ dependencies = [ "eyre", ] -[[package]] -name = "cargo-stylus-cache" -version = "0.3.1" -dependencies = [ - "alloy-primitives 0.7.2", - "alloy-sol-macro", - "alloy-sol-types", - "cargo-stylus-util", - "clap", - "ethers", - "eyre", - "hex", - "serde", - "serde_json", - "tokio", -] - [[package]] name = "cargo-stylus-cgen" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index e9dc0a3..87e2cec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["cache", "cgen", "check", "example", "main", "replay", "util"] +members = ["cgen", "check", "example", "main", "replay", "util"] resolver = "2" [workspace.package] diff --git a/cache/Cargo.toml b/cache/Cargo.toml deleted file mode 100644 index 0f87ffa..0000000 --- a/cache/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "cargo-stylus-cache" -keywords = ["arbitrum", "ethereum", "stylus", "alloy"] -description = "CLI tool for interacting with the Stylus cache manager for Arbitrum chains" - -authors.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -version.workspace = true -repository.workspace = true - -[dependencies] -alloy-sol-macro.workspace = true -alloy-sol-types.workspace = true -alloy-primitives.workspace = true -cargo-stylus-util.workspace = true -hex.workspace = true -clap.workspace = true -ethers.workspace = true -eyre.workspace = true -serde = { version = "1.0.203", features = ["derive"] } -tokio.workspace = true -serde_json = "1.0.103" \ No newline at end of file diff --git a/cache/src/main.rs b/cache/src/main.rs deleted file mode 100644 index f7cefa3..0000000 --- a/cache/src/main.rs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2023-2024, Offchain Labs, Inc. -// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md - -use std::str::FromStr; -use std::sync::Arc; - -use alloy_primitives::FixedBytes; -use alloy_sol_macro::sol; -use alloy_sol_types::{SolCall, SolInterface}; -use cargo_stylus_util::sys; -use clap::{Args, Parser}; -use ethers::middleware::{Middleware, SignerMiddleware}; -use ethers::providers::{Http, Provider, ProviderError, RawCall}; -use ethers::signers::{LocalWallet, Signer}; -use ethers::types::spoof::State; -use ethers::types::transaction::eip2718::TypedTransaction; -use ethers::types::{Address, Eip1559TransactionRequest, NameOrAddress, H160, U256}; -use ethers::utils::keccak256; -use eyre::{bail, eyre, Context, ErrReport, Result}; -use hex::FromHex; -use serde_json::Value; - -#[derive(Parser, Clone, Debug)] -#[command(name = "cargo-stylus-cache")] -#[command(bin_name = "cargo stylus cache")] -#[command(author = "Offchain Labs, Inc.")] -#[command(version = env!("CARGO_PKG_VERSION"))] -#[command(about = "Cargo command for interacting with the Arbitrum Stylus cache manager", long_about = None)] -#[command(propagate_version = true)] -pub struct Opts { - #[command(subcommand)] - command: Subcommands, -} - -#[derive(Parser, Debug, Clone)] -enum Subcommands { - #[command(alias = "p")] - Program(CacheArgs), -} - -#[derive(Args, Clone, Debug)] -struct CacheArgs { - /// RPC endpoint. - #[arg(short, long, default_value = "https://sepolia-rollup.arbitrum.io/rpc")] - endpoint: String, - /// Address of the Stylus program to cache. - #[arg(short, long)] - address: Address, - /// Bid, in wei, to place on the program cache. - #[arg(short, long, hide(true))] - bid: u64, -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Opts::parse(); - macro_rules! run { - ($expr:expr, $($msg:expr),+) => { - $expr.await.wrap_err_with(|| eyre!($($msg),+)) - }; - } - match args.command { - Subcommands::Program(args) => run!( - self::cache_program(args), - "failed to submit program cache request" - ), - } -} - -sol! { - interface CacheManager { - function placeBid(bytes32 codehash) external payable; - - error NotChainOwner(address sender); - error AsmTooLarge(uint256 asm, uint256 queueSize, uint256 cacheSize); - error AlreadyCached(bytes32 codehash); - error BidTooSmall(uint192 bid, uint192 min); - error BidsArePaused(); - error MakeSpaceTooLarge(uint64 size, uint64 limit); - } -} - -async fn cache_program(args: CacheArgs) -> Result<()> { - let provider: Provider = sys::new_provider(&args.endpoint)?; - let chain_id = provider.get_chainid().await?; - println!("Connected to chain {}", chain_id); - - let program_code = provider - .get_code(args.address, None) - .await - .wrap_err("failed to fetch program code")?; - println!("Program code: {:?}", hex::encode(&program_code)); - // let codehash = ethers::utils::keccak256(&program_code); - // println!("Program codehash: {:#x}", &codehash); - - let raw_data = hex::decode("eff000001b242b23113662924a1045d5e667807a496e0e0117a87f49c7881338b04573d0e85a3033bcdcea73ffb8e29e93faa31153586c73d8f45c9b44c5d23c429259ffabd4f67ff7c86aab35a8c95a646a5f0f49269fe425cf8772da904e7b5b7173b548327220ce6c0e37c8d77c839b939618223409d01d04926f375575284b93b4dd9e2fb0fb101f11c543f92d811af817f6db3f498887b27b12593431444b18fa5d503686c138a4bf1971f630ce4f75a9aeb4b548e3dea02ff2a1ae2ae2900bd46c7d93793f826a35f9de2412d9b30a89441ea790482492422191f9268f432291482432f1bf54d3f6eddf5d920a691d422e7a123773b553eda616174b815800940f202f00941c4825523cc504f2648b9243084de92ae4d8b9693dbd0d4ffbc9ea67bc97b69aae2dc67d766ce18a8af461c86235ca6faa1bc34d7f8908f3f38f1ec3d5f5eea33615a404dcf717a52b3f9f5322e253b609ce9a95515359e6969729909a7076fd969ca97bb8c74c3ec7f3f5f5f1b3f2f9bc8ddff5a7e4af7ecf2454690b34ee1bf78fdbcf842bc2bad5bd67bcafe7f3f1b17390a8b7a825a25de3f1f4fd7917a2ce830765998741ffc3120ddabeeef8ff6fa285a10d1b46f3ba096b57513a8a0a05bdf09eaf54d04cdc7382ce58f259b219c98e23ab6c7fb778c5f6e04833ea959e0d71a689a98838088db962eb25a5d2fa9bdcaafae092b35dff97adc76fa1840fca5569c7dabbafa8a24cdf7ae53c8464b6d0624edd3e1823244fb0db774532f70207516e8fbddbfc840b794361a4dae013ac52a6b16ce7d11868cbbfe6fdb5afa241a3d30ca65f04f583b60c88cac830230c39e684a32aab8a2a814180eaac2eea743e3b5f9c4f4bd812b18452968a9446d88818a1d56cb5584debd83ab18e9aac299a34c6c6c4185dc7ae13d7d1349b16d371ced7a99c479cf37597ba27d86d93f99b4bb67340d8615879de8db610e59b2d038285dd81c63b9e545696c8ca450d2c06d648f6810a6bb5ea77c6c8c351b039078f16123db6a1d6c3cec2174298163b3e52909cc7277b436efc817a9e0a4a474d6fea25b828a6b2bce00fd4960ffc8386f263315bd3c252b6e662da9062aea603a9e666ba9066eea607e9e661fa90619e5ec42ec8342f935896799bd4b2cd87649643be406eb9c037282c37f801a5f549ab42ab7869181a19b136fdcab2e90f0176fd09e4b6fd00aa009f2d7378cce076278ccc16c2b0fc303b6bf89cbf4a2359b42a66a33e8892d08788ee3caa40bf66d9f38a935f484f8f6c1cd59a4d85adfa2381ecfa19e4a47b764faa624fcdc2afdf4141fd0714a63b8e4e6ac9b17a89a47e03a5f55750966e393fa905176a1065fd0aaad4896bb5e146cdd57e6023ba76458110915892cb4715a8d55f955aa3d5157f35cc2214190f1a9bc38507bf764ec212ddea2d8348a33398ac6a9fedc34016e06da95f7624bdc9d24353995ca16c683d34f52407dffb0fff597e105205da21e333344c5e0fb3899ad836331930c2124c00fbf5cf48015bc6ebe699a41088a3051b727858b70fb15aa29530065b036be102d4f7e6d7860ad9e68a1447b0fce30b28a1da13a49761d01fe06cbe08b80dadbdab6f77b8ca992818c207727f560a1e7c1d1167987c3171c87041f89ea181beb51413fdd99800bd7931cc2812b8c0dfdc024e09ca8f13125d1644ffd16f01e0810f88be11e180bc2c21c8ec519ff77525d9f44dd5d5ec7ffeda386c6a6487977536ec3bcccf22432f27d4aa3c54848569a0b1bf430a866da779a92fd275211c272a5cd79dd9c5488ce9ba23072a17eec399231b165ad15d775166428fe1babd2a370e9c5f6ce086ba32875e531a53f42fba36f7a8fd1330f89b8e96cd91da5b032cf6f58cdcdf1285bf204a36c068ef6784f6e70cf67744ec2f8984bd00ede30cb0e733667f4f787e45f86c06cdfe9d21ede50cc33f12a6bf260483eecd7c8a6cc512d912b91deec47dad8bd3963dec6d3decb234a074370b95549a9c031ded9458d43028cadb892faceb5961aff0af970f89090cc606922d27f864fb913094e176411e94c15888db50e063a6c201a01dea5bbb2c256f4149458dc608a70e8749104c82666f0ec90714bc1a386875afd70f1a501d1cfbc0557e61d80f4ce13781ded0cc2467ac53be5c9304573130f28f5e6de58c3d449b567d238562b6ff17ee17882986bbe93f0ccc99fff12786fc68638069d32c853b8c7a3ed638303bfd017b8ff103866a7c84f37c4fa9ce119ba3f15e22cdfe5669766ac484dfb282134f560db1c07f4558e1c0caffdcb68ec6582be46535cb04e4d4105140c7520dcaf7cac895393205d30f70bb4d3b25b7697d1f58e0362fb45b9b47eb6e8eb30966ac5c20ec36bc96bfcfa0c9f856a6ef3b417ecd773f19b101645a2554f74197761bb6dc801de436ad1ddaace4ffc51693ba92e42f43cce42db47e9cdb8670e7d12f0d9215825ab24a33a23ade7592ff781b258d49c843c84f5959210f4848166184271f6d64f1501dad901f9d324d93d64532cd03bd073fa8c543566d1fe48dfac1435c3bee588f726451e91f37f5fde0e2e4e4498fdaaa75e98887ab9cab798c0fe38e161d8c5d266b552a03fe4e5671c249fd0fcdde37b1a3b544a227c924886e2f63e41867d70b204e68e27766fcca3cc37b1320efccf3fef75067921d021669b08c601b3465b642a8dc0a4dbd0f7ab756b2dd202333d2a8b3f181a25e002013cb6f9d72c14269f4c9f40a9e0ce454095e65c19826c295ababa70ed6b7b8ab073d9710d3063f0e6d54fda32e982a27ba1daeecca71e2e4dca486358e59a1fdb2b228bb31e0b480064b83d5e03f849a5f9de6fc814da8e2688cfe9e27a087c622df42164fb83b59cc601c72c6fa98d2f79450f9f6469c1c97b7fa4e4b0b06a99dbaad59445bf4b4588b8c12b915a356007da01baf4542732c3c45468983e75004e8d88fc91889ffaf48c715d20121d0fa78bf23d630c1d2a9c3de747115391916901cb0db90b74ec295bb27981a2403169f04839a6a4dd3116beae91b3d32876c7dee09677f929efca2ed0a84f0d07122b47da465b697aef16dedb79ae399b15945ade1a0798d6eaa21449fb8f6bca866bbd360557fa94b96440b8ad0a926ac8eda7c65e1b89a095e1271a3b127142a541305892577c3dd0ae12600bfc2955f7169097540d040911d1419ac1ea9a760e41a658f495c329945afae5d253beed7b68d288c75a5e6f698769cd4c35a7a26abc1d718714c17fb9e219fb645a3be08d147b7475f5a2159631a8046c95733aee627c6dbd07e15d021ce423f437616e294e7027cf56da494f3196269f9966f59690b95a5755ac79655d4a96c1c96eab415bb32655da883aadc47681b5687258455a6b4f045b42c58f5240fe55106f175dde41409920dfdc64520ae4307e3eb94ff02fa14c9f39bbf76b5e40568d4e966f4479604aa682fab5198a387ff6033cf4ebaf3b61f68cd3729b19434b9e03d13c333c5c43c352c82c5f911c66e005dc14b858dba6a3c2b59ffe5207e1578cd61f719156406df6a29e4013b5eb0e20e273eb0e309377e70e20d2fffec05005ff8598b17969feeccc75abd20bfe42d34e751f916d602aa85a91fb861d5a3d75ef6e3205ca41f7f90364e3c289b271fb4ad530f8612142c38651a6cb8d2d7bbba051040000104104000bb4000bb404076815dc82e44b7f427ddc8d563378c5e3b7ed3d8f513b78cdf3879dbc4cd537748de3ae4027589a91b10d33e94070f8d5979319eb09cd174f1614c28108e7737af475bd999f7c6d33f57d95824d4aa04b896859cab3c18cff5ed6beee8e5395a7332ef285ed0f8aef46bdd83a507ff91df6a2ad1361d304dfbad929e333856287ad8e238bd354a8e0d38012c56e7dd2bcd906b04d3e1fead1071615b4b4e6a00cd7ed23577e20e75193df32f1a20e1f019a071a1d59ad484c16dc8dfe8aa665ff0e68914c3542a5108b718fd7516870c391cae2e925bdb031182435a30837373c6244ceecc4493f668759d44d634225562e5086e5c500481a2404454008a2eaf4d427664f1381e2578e12e0173142f98b5874c89f8264c7c3f244a24baed9c6dc450f6b35e6eef5e1a1bd06884ba75eacb16d444b3960c52a29f65ce00414f791c8a2d8fc8598b2879936025d0c8b310a553b042f9548bd8dfc384e278001b35437d18967a4f6e26db1e95eb1ba4fd95ca32372ff55c2a34200460b91fc957b1854f342528ff46155b83d66dd71342d37a8ba4ffe1e97898a5582aa32626cbd88ebf1b82b49ceabbbe40829b6ea32f982ff6bee370022d4a0f6b0e21cb1a548f045c0e8946d60d88f864b4b17819fb2b85bcf33c5a718275f90864e6442b4a95a515a5cae66fc575dcd1969fc9625fe2ede961993809ecbeb9269d653fad0fe4d92f53ab346f6e33985c25e86c3586e86d57a131b04a503ed17e56dc788a1da7ce3cbad9b9cd700fcfe704d985c411d89e323c0cddf313f31677efee5c859356023f0ec6d086846612b23825c8d02a60a9b6693c569cef51caca6138c0b1e515481490398d22047c8a9d6c3a6e35eb6031a1746f2f47d9a913e4e66992f5e0fb0a23911858749bd4a0b08d75afade96d0792335e7bb220ac45e3f8ee8e855579a845b597b1004add2490bb81d7e284b49e7cfd91f6cfd0f312c20caa40b59e2b9663d52a20e30fedc58767582ea04d08d0e8f3a1e1c6863a0f029f353c29009d0d7a0241781803c1c3346a2a20ef713470663143860d6371d8f0112e181428e0729f543718b9b420a08014e0e600c5dc2fe5fe306523e6912a95413e2e0c458bd4621ad23f83242b8ce8cd5e62313927f88a4affe88433e2920a1651a8346cc67c5b95101b34d3cfcc4f3a356aab65fb720e52db5848dec1e295d6b266a06d14c4b5240b048d8ae720c9d0a878f2e1166675e3405432e986057bc362c86a80321028600506dca84e9d632dd3aed60c5bf3920560f1a5236619bdbc62315ec9814c7cc49c258cb2171952066ac03093e61374c7134f3172161e528305c551762f8a6992f51c52e972263093309e1d554cb92610d7c44353a9c9d303a85cfa1b3547d95cc8a839f7c99b47809d73b131e2cd9e33b3272df4eeb340fbe58884127c8621828099af1ddf506232193acdb010a9accc0e909c20c8b01dda22153d4acca74a986ac5415de36d849b20a234f0f29d73247ad2946dac5838f12248cad06982895ca53ceead2bd61e450082ddc100b6754eb1c904afc4ad3263caecd375e36f16d4b1d73468415f6db6db9e03ba90cf34269ac10eccdbc7a130d2e2e4e6362f28d74de89a7c3e3b75009ef008417a930e2bd5b6880f44f69528e0a4d856cd2ec6b78eba4507f0a469ce9c743e921c172446e798ab8ad1f36dd303487c016bb8e380a85a76a22998c23b777563d28c8e839a5b2992538d2155beffb69b61d6a61f7d5f588e14ee24664d3b219ff51dc474cce37b7289c8aef57d04933c0b70b770cfbb60978ece02a78131bb35fae7809248633ee9302d8cbb05382244fe34650f415bfe786cc4c30ff5ed8bace88b2dbbb3ccf4eeb48d39853bd74b39511eedaacb986889361c456f29df49146d3f87bd6d5a548211bb4ea75d14ed808df459aaa53945a5ab2df57926512ef31a2d4bc9e8afe5eccd916505").unwrap(); - // if program_code != raw_data { - // bail!( - // "program code mismatch, got {} vs {}", - // hex::encode(program_code), - // hex::encode(raw_data) - // ); - // } - println!("got codehash {:?}", hex::encode(keccak256(&raw_data))); - let codehash = FixedBytes::<32>::from(keccak256(&raw_data)); - - let data = CacheManager::placeBidCall { codehash }.abi_encode(); - let to = H160::from_slice( - hex::decode("d1bbd579988f394a26d6ec16e77b3fa8a5e8fcee") - .unwrap() - .as_slice(), - ); - let tx = Eip1559TransactionRequest::new() - .to(NameOrAddress::Address(to)) - .data(data) - .value(U256::from(args.bid)); - - // let privkey = "93690ac9d039285ed00f874a2694d951c1777ac3a165732f36ea773f16179a89".to_string(); - // let wallet = LocalWallet::from_str(&privkey)?; - // let chain_id = provider.get_chainid().await?.as_u64(); - // let client = Arc::new(SignerMiddleware::new( - // provider, - // wallet.clone().with_chain_id(chain_id), - // )); - // let pending_tx = client.send_transaction(tx, None).await?; - // let receipt = pending_tx.await?; - // match receipt { - // Some(receipt) => { - // println!("Receipt: {:?}", receipt); - // } - // None => { - // bail!("failed to cache program"); - // } - // } - - if let Err(EthCallError { data, msg }) = - eth_call(tx.clone(), State::default(), &provider).await? - { - println!("Got data {}, msg {:?}", hex::encode(&data), msg); - let error = match CacheManager::CacheManagerErrors::abi_decode(&data, true) { - Ok(err) => err, - Err(err_details) => bail!("unknown CacheManager error: {msg} and {:?}", err_details), - }; - use CacheManager::CacheManagerErrors as C; - match error { - C::AsmTooLarge(_) => bail!("program too large"), - _ => bail!("unexpected CacheManager error: {msg}"), - } - } - - println!("Succeeded cache call"); - // Otherwise, we are ready to send the tx data if our call passed. - // TODO: Send. - Ok(()) -} - -struct EthCallError { - data: Vec, - msg: String, -} - -impl From for ErrReport { - fn from(value: EthCallError) -> Self { - eyre!(value.msg) - } -} - -async fn eth_call( - tx: Eip1559TransactionRequest, - mut state: State, - provider: &Provider, -) -> Result, EthCallError>> { - let tx = TypedTransaction::Eip1559(tx); - state.account(Default::default()).balance = Some(ethers::types::U256::MAX); // infinite balance - - match provider.call_raw(&tx).state(&state).await { - Ok(bytes) => Ok(Ok(bytes.to_vec())), - Err(ProviderError::JsonRpcClientError(error)) => { - let error = error - .as_error_response() - .ok_or_else(|| eyre!("json RPC failure: {error}"))?; - - let msg = error.message.clone(); - let data = match &error.data { - Some(Value::String(data)) => cargo_stylus_util::text::decode0x(data)?.to_vec(), - Some(value) => bail!("failed to decode RPC failure: {value}"), - None => vec![], - }; - Ok(Err(EthCallError { data, msg })) - } - Err(error) => Err(error.into()), - } -} diff --git a/check/src/cache.rs b/check/src/cache.rs index 021ce33..1e3ad46 100644 --- a/check/src/cache.rs +++ b/check/src/cache.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use alloy_primitives::FixedBytes; use alloy_sol_macro::sol; use alloy_sol_types::{SolCall, SolInterface}; +use cargo_stylus_util::color::{Color, DebugColor}; use cargo_stylus_util::sys; use clap::{Args, Parser}; use ethers::middleware::{Middleware, SignerMiddleware}; @@ -17,9 +18,14 @@ use ethers::types::transaction::eip2718::TypedTransaction; use ethers::types::{Address, Eip1559TransactionRequest, NameOrAddress, H160, U256}; use ethers::utils::keccak256; use eyre::{bail, eyre, Context, ErrReport, Result}; -use hex::FromHex; use serde_json::Value; +use crate::check::{eth_call, EthCallError}; +use crate::constants::{CACHE_MANAGER_ADDRESS, CACHE_MANAGER_H160, EOF_PREFIX_NO_DICT}; +use crate::deploy::{format_gas, run_tx}; +use crate::macros::greyln; +use crate::CacheConfig; + sol! { interface CacheManager { function placeBid(bytes32 codehash) external payable; @@ -33,63 +39,54 @@ sol! { } } -async fn cache_program(args: CacheArgs) -> Result<()> { - let provider: Provider = sys::new_provider(&args.endpoint)?; - let chain_id = provider.get_chainid().await?; - println!("Connected to chain {}", chain_id); +pub async fn cache_program(cfg: &CacheConfig) -> Result<()> { + let provider = sys::new_provider(&cfg.common_cfg.endpoint)?; + let chain_id = provider + .get_chainid() + .await + .wrap_err("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()); + let client = SignerMiddleware::new(provider.clone(), wallet); - let program_code = provider - .get_code(args.address, None) + let program_code = client + .get_code(cfg.program_address, None) .await .wrap_err("failed to fetch program code")?; - println!("Program code: {:?}", hex::encode(&program_code)); - // let codehash = ethers::utils::keccak256(&program_code); - // println!("Program codehash: {:#x}", &codehash); - let raw_data = hex::decode("").unwrap(); - // if program_code != raw_data { - // bail!( - // "program code mismatch, got {} vs {}", - // hex::encode(program_code), - // hex::encode(raw_data) - // ); - // } - println!("got codehash {:?}", hex::encode(keccak256(&raw_data))); - let codehash = FixedBytes::<32>::from(keccak256(&raw_data)); - - let data = CacheManager::placeBidCall { codehash }.abi_encode(); - let to = H160::from_slice( - hex::decode("d1bbd579988f394a26d6ec16e77b3fa8a5e8fcee") - .unwrap() - .as_slice(), + greyln!( + "Program code length at address {}: {}", + cfg.program_address.debug_lavender(), + program_code.len() ); - let tx = Eip1559TransactionRequest::new() - .to(NameOrAddress::Address(to)) - .data(data) - .value(U256::from(args.bid)); + if !program_code.starts_with(hex::decode(EOF_PREFIX_NO_DICT).unwrap().as_slice()) { + bail!( + "program code does not start with Stylus prefix {}", + EOF_PREFIX_NO_DICT + ); + } + let codehash = FixedBytes::<32>::from(keccak256(&program_code)); + greyln!( + "Program codehash {}", + hex::encode(codehash).debug_lavender() + ); + let codehash = FixedBytes::<32>::from(keccak256(&program_code)); - // let privkey = "93690ac9d039285ed00f874a2694d951c1777ac3a165732f36ea773f16179a89".to_string(); - // let wallet = LocalWallet::from_str(&privkey)?; - // let chain_id = provider.get_chainid().await?.as_u64(); - // let client = Arc::new(SignerMiddleware::new( - // provider, - // wallet.clone().with_chain_id(chain_id), - // )); - // let pending_tx = client.send_transaction(tx, None).await?; - // let receipt = pending_tx.await?; - // match receipt { - // Some(receipt) => { - // println!("Receipt: {:?}", receipt); - // } - // None => { - // bail!("failed to cache program"); - // } - // } + let data = CacheManager::placeBidCall { codehash }.abi_encode(); + let mut tx = Eip1559TransactionRequest::new() + .to(*CACHE_MANAGER_H160) + .data(data); + + // If a bid is set, specify it. Otherwise, a zero bid will be sent. + if let Some(bid) = cfg.bid { + tx = tx.value(U256::from(bid)); + greyln!("Setting bid value of {} wei", bid.debug_mint()); + } if let Err(EthCallError { data, msg }) = eth_call(tx.clone(), State::default(), &provider).await? { - println!("Got data {}, msg {:?}", hex::encode(&data), msg); let error = match CacheManager::CacheManagerErrors::abi_decode(&data, true) { Ok(err) => err, Err(err_details) => bail!("unknown CacheManager error: {msg} and {:?}", err_details), @@ -100,47 +97,21 @@ async fn cache_program(args: CacheArgs) -> Result<()> { _ => bail!("unexpected CacheManager error: {msg}"), } } - - println!("Succeeded cache call"); - // Otherwise, we are ready to send the tx data if our call passed. - // TODO: Send. - Ok(()) -} - -struct EthCallError { - data: Vec, - msg: String, -} - -impl From for ErrReport { - fn from(value: EthCallError) -> Self { - eyre!(value.msg) - } -} - -async fn eth_call( - tx: Eip1559TransactionRequest, - mut state: State, - provider: &Provider, -) -> Result, EthCallError>> { - let tx = TypedTransaction::Eip1559(tx); - state.account(Default::default()).balance = Some(ethers::types::U256::MAX); // infinite balance - - match provider.call_raw(&tx).state(&state).await { - Ok(bytes) => Ok(Ok(bytes.to_vec())), - Err(ProviderError::JsonRpcClientError(error)) => { - let error = error - .as_error_response() - .ok_or_else(|| eyre!("json RPC failure: {error}"))?; - - let msg = error.message.clone(); - let data = match &error.data { - Some(Value::String(data)) => cargo_stylus_util::text::decode0x(data)?.to_vec(), - Some(value) => bail!("failed to decode RPC failure: {value}"), - None => vec![], - }; - Ok(Err(EthCallError { data, msg })) - } - Err(error) => Err(error.into()), + let verbose = cfg.common_cfg.verbose; + let receipt = run_tx("cache", tx, None, &client, verbose).await?; + + let address = cfg.program_address.debug_lavender(); + + if verbose { + let gas = format_gas(receipt.gas_used.unwrap_or_default()); + greyln!( + "Deployed code at address: {address} {} {gas}", + "with".grey() + ); + } else { + greyln!("Deployed code at address: {address}"); } + let tx_hash = receipt.transaction_hash.debug_lavender(); + greyln!("Sent Stylus cache tx with hash: {tx_hash}"); + Ok(()) } diff --git a/check/src/check.rs b/check/src/check.rs index c0e8bac..b742dc3 100644 --- a/check/src/check.rs +++ b/check/src/check.rs @@ -3,7 +3,7 @@ use crate::{ check::ArbWasm::ArbWasmErrors, - constants::{ARB_WASM_H160, ONE_ETH}, + constants::{ARB_WASM_ADDRESS, ARB_WASM_H160, ONE_ETH}, macros::*, project::{self, BuildConfig}, CheckConfig, @@ -169,9 +169,9 @@ fn format_data_fee(fee: U256) -> Result { }) } -struct EthCallError { - data: Vec, - msg: String, +pub struct EthCallError { + pub data: Vec, + pub msg: String, } impl From for ErrReport { @@ -180,13 +180,13 @@ impl From for ErrReport { } } -/// A funded eth_call to ArbWasm. -async fn eth_call( +/// A funded eth_call. +pub async fn eth_call( tx: Eip1559TransactionRequest, mut state: State, provider: &Provider, ) -> Result, EthCallError>> { - let tx = TypedTransaction::Eip1559(tx.to(*ARB_WASM_H160)); + let tx = TypedTransaction::Eip1559(tx); state.account(Default::default()).balance = Some(ethers::types::U256::MAX); // infinite balance match provider.call_raw(&tx).state(&state).await { @@ -211,7 +211,9 @@ async fn eth_call( /// Checks whether a program has already been activated with the most recent version of Stylus. async fn program_exists(codehash: B256, provider: &Provider) -> Result { let data = ArbWasm::codehashVersionCall { codehash }.abi_encode(); - let tx = Eip1559TransactionRequest::new().data(data); + let tx = Eip1559TransactionRequest::new() + .to(*ARB_WASM_H160) + .data(data); let outs = eth_call(tx, State::default(), provider).await?; let program_version = match outs { @@ -236,7 +238,9 @@ async fn program_exists(codehash: B256, provider: &Provider) -> Result) -> Result) -> Result { let program = Address::from(address.to_fixed_bytes()); let data = ArbWasm::activateProgramCall { program }.abi_encode(); - let tx = Eip1559TransactionRequest::new().data(data).value(ONE_ETH); + let tx = Eip1559TransactionRequest::new() + .to(*ARB_WASM_H160) + .data(data) + .value(ONE_ETH); let state = spoof::code(address, code); let outs = eth_call(tx, state, provider).await??; let ArbWasm::activateProgramReturn { dataFee, .. } = diff --git a/check/src/constants.rs b/check/src/constants.rs index 9c7b9a8..25aa4c1 100644 --- a/check/src/constants.rs +++ b/check/src/constants.rs @@ -14,11 +14,16 @@ pub const BROTLI_COMPRESSION_LEVEL: u32 = 11; lazy_static! { /// Address of the ArbWasm precompile. pub static ref ARB_WASM_H160: H160 = H160(*ARB_WASM_ADDRESS.0); + /// Address of the Stylus program cache manager. + pub static ref CACHE_MANAGER_H160: H160 = H160(*CACHE_MANAGER_ADDRESS.0); } /// Address of the ArbWasm precompile. pub const ARB_WASM_ADDRESS: Address = address!("0000000000000000000000000000000000000071"); +/// Address of the Stylus program cache manager for Arbitrum chains. +pub const CACHE_MANAGER_ADDRESS: Address = address!("d1bbd579988f394a26d6ec16e77b3fa8a5e8fcee"); + /// Target for compiled WASM folder in a Rust project pub const RUST_TARGET: &str = "wasm32-unknown-unknown"; diff --git a/check/src/deploy.rs b/check/src/deploy.rs index 8326dfb..b5d8fc0 100644 --- a/check/src/deploy.rs +++ b/check/src/deploy.rs @@ -35,7 +35,7 @@ sol! { } } -type SignerClient = SignerMiddleware, Wallet>; +pub type SignerClient = SignerMiddleware, Wallet>; /// Deploys a stylus program, activating if needed. pub async fn deploy(cfg: DeployConfig) -> Result<()> { @@ -121,7 +121,14 @@ impl DeployConfig { return Ok(ethers::utils::get_contract_address(sender, nonce)); } - let receipt = self.run_tx("deploy", tx, Some(gas), client).await?; + let receipt = run_tx( + "deploy", + tx, + Some(gas), + client, + self.check_config.common_cfg.verbose, + ) + .await?; let contract = receipt.contract_address.ok_or(eyre!("missing address"))?; let address = contract.debug_lavender(); @@ -170,7 +177,14 @@ impl DeployConfig { return Ok(()); } - let receipt = self.run_tx("activate", tx, Some(gas), client).await?; + let receipt = run_tx( + "activate", + tx, + Some(gas), + client, + self.check_config.common_cfg.verbose, + ) + .await?; if verbose { let gas = format_gas(receipt.gas_used.unwrap_or_default()); @@ -182,34 +196,32 @@ impl DeployConfig { ); Ok(()) } +} - async fn run_tx( - &self, - name: &str, - tx: Eip1559TransactionRequest, - gas: Option, - client: &SignerClient, - ) -> Result { - let mut tx = TypedTransaction::Eip1559(tx); - if let Some(gas) = gas { - tx.set_gas(gas); - } - - let tx = client.send_transaction(tx, None).await?; - let tx_hash = tx.tx_hash(); - let verbose = self.check_config.common_cfg.verbose; +pub async fn run_tx( + name: &str, + tx: Eip1559TransactionRequest, + gas: Option, + client: &SignerClient, + verbose: bool, +) -> Result { + let mut tx = TypedTransaction::Eip1559(tx); + if let Some(gas) = gas { + tx.set_gas(gas); + } - if verbose { - greyln!("sent {name} tx: {}", tx_hash.debug_lavender()); - } - let Some(receipt) = tx.await.wrap_err("tx failed to complete")? else { - bail!("failed to get receipt for tx {}", tx_hash.lavender()); - }; - if receipt.status != Some(U64::from(1)) { - bail!("{name} tx reverted {}", tx_hash.debug_red()); - } - Ok(receipt) + let tx = client.send_transaction(tx, None).await?; + let tx_hash = tx.tx_hash(); + if verbose { + greyln!("sent {name} tx: {}", tx_hash.debug_lavender()); + } + let Some(receipt) = tx.await.wrap_err("tx failed to complete")? else { + bail!("failed to get receipt for tx {}", tx_hash.lavender()); + }; + if receipt.status != Some(U64::from(1)) { + bail!("{name} tx reverted {}", tx_hash.debug_red()); } + Ok(receipt) } /// Prepares an EVM bytecode prelude for contract creation. @@ -234,7 +246,7 @@ pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec { deploy } -fn format_gas(gas: U256) -> String { +pub fn format_gas(gas: U256) -> String { let gas: u64 = gas.try_into().unwrap_or(u64::MAX); let text = format!("{gas} gas"); if gas <= 3_000_000 { diff --git a/check/src/main.rs b/check/src/main.rs index 8bacbcb..6081119 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -95,6 +95,21 @@ struct CommonConfig { source_files_for_project_hash: Vec, } +#[derive(Args, Clone, Debug)] +pub struct CacheConfig { + #[command(flatten)] + common_cfg: CommonConfig, + /// Wallet source to use. + #[command(flatten)] + auth: AuthOpts, + /// Deployed and activated program address to cache. + #[arg(long)] + program_address: H160, + /// Bid, in wei, to place on the program cache. + #[arg(short, long, hide(true))] + bid: Option, +} + #[derive(Args, Clone, Debug)] pub struct CheckConfig { #[command(flatten)] @@ -166,6 +181,9 @@ async fn main_impl(args: Opts) -> Result<()> { Apis::ExportAbi { json, output } => { run!(export_abi::export_abi(output, json), "failed to export abi"); } + Apis::Cache(config) => { + run!(cache::cache_program(&config).await, "stylus cache failed"); + } Apis::Check(config) => { run!(check::check(&config).await, "stylus checks failed"); } diff --git a/install.sh b/install.sh index cc52045..62b90f1 100755 --- a/install.sh +++ b/install.sh @@ -2,5 +2,4 @@ cargo fmt cargo clippy --package cargo-stylus --package cargo-stylus-cgen --package cargo-stylus-check cargo install --path main cargo install --path cgen -cargo install --path check -cargo install --path cache +cargo install --path check \ No newline at end of file diff --git a/main/src/main.rs b/main/src/main.rs index 2759935..47e285d 100644 --- a/main/src/main.rs +++ b/main/src/main.rs @@ -26,6 +26,9 @@ enum Subcommands { #[command(alias = "x")] /// Export a Solidity ABI. ExportAbi, + /// Cache a contract. + #[command(alias = "c")] + Cache, /// Check a contract. #[command(alias = "c")] Check, @@ -61,6 +64,7 @@ const COMMANDS: &[Binary] = &[ apis: &[ "new", "export-abi", + "cache", "check", "deploy", "verify", From b15961939f3d1bea8432e5cb23532509a2f6d8c4 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Mon, 1 Jul 2024 09:01:03 -0500 Subject: [PATCH 15/23] cache method done --- check/src/cache.rs | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/check/src/cache.rs b/check/src/cache.rs index 1e3ad46..d76a36d 100644 --- a/check/src/cache.rs +++ b/check/src/cache.rs @@ -1,27 +1,20 @@ // Copyright 2023-2024, Offchain Labs, Inc. // For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md -use std::str::FromStr; -use std::sync::Arc; - use alloy_primitives::FixedBytes; use alloy_sol_macro::sol; use alloy_sol_types::{SolCall, SolInterface}; use cargo_stylus_util::color::{Color, DebugColor}; use cargo_stylus_util::sys; -use clap::{Args, Parser}; use ethers::middleware::{Middleware, SignerMiddleware}; -use ethers::providers::{Http, Provider, ProviderError, RawCall}; -use ethers::signers::{LocalWallet, Signer}; +use ethers::signers::Signer; use ethers::types::spoof::State; -use ethers::types::transaction::eip2718::TypedTransaction; -use ethers::types::{Address, Eip1559TransactionRequest, NameOrAddress, H160, U256}; +use ethers::types::{Eip1559TransactionRequest, U256}; use ethers::utils::keccak256; -use eyre::{bail, eyre, Context, ErrReport, Result}; -use serde_json::Value; +use eyre::{bail, Context, Result}; use crate::check::{eth_call, EthCallError}; -use crate::constants::{CACHE_MANAGER_ADDRESS, CACHE_MANAGER_H160, EOF_PREFIX_NO_DICT}; +use crate::constants::{CACHE_MANAGER_H160, EOF_PREFIX_NO_DICT}; use crate::deploy::{format_gas, run_tx}; use crate::macros::greyln; use crate::CacheConfig; @@ -30,12 +23,10 @@ sol! { interface CacheManager { function placeBid(bytes32 codehash) external payable; - error NotChainOwner(address sender); error AsmTooLarge(uint256 asm, uint256 queueSize, uint256 cacheSize); error AlreadyCached(bytes32 codehash); error BidTooSmall(uint192 bid, uint192 min); error BidsArePaused(); - error MakeSpaceTooLarge(uint64 size, uint64 limit); } } @@ -55,11 +46,6 @@ pub async fn cache_program(cfg: &CacheConfig) -> Result<()> { .await .wrap_err("failed to fetch program code")?; - greyln!( - "Program code length at address {}: {}", - cfg.program_address.debug_lavender(), - program_code.len() - ); if !program_code.starts_with(hex::decode(EOF_PREFIX_NO_DICT).unwrap().as_slice()) { bail!( "program code does not start with Stylus prefix {}", @@ -94,7 +80,13 @@ pub async fn cache_program(cfg: &CacheConfig) -> Result<()> { use CacheManager::CacheManagerErrors as C; match error { C::AsmTooLarge(_) => bail!("program too large"), - _ => bail!("unexpected CacheManager error: {msg}"), + C::AlreadyCached(_) => bail!("program already cached"), + C::BidsArePaused(_) => { + bail!("bidding is currently paused for the Stylus cache manager") + } + C::BidTooSmall(_) => { + bail!("bid amount {} (wei) too small", cfg.bid.unwrap_or_default()) + } } } let verbose = cfg.common_cfg.verbose; @@ -105,11 +97,11 @@ pub async fn cache_program(cfg: &CacheConfig) -> Result<()> { if verbose { let gas = format_gas(receipt.gas_used.unwrap_or_default()); greyln!( - "Deployed code at address: {address} {} {gas}", + "Successfully cached program at address: {address} {} {gas}", "with".grey() ); } else { - greyln!("Deployed code at address: {address}"); + greyln!("Successfully cached program at address: {address}"); } let tx_hash = receipt.transaction_hash.debug_lavender(); greyln!("Sent Stylus cache tx with hash: {tx_hash}"); From 14c9ea5205b7795a00b7f1748c3e17264def3b1d Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Mon, 1 Jul 2024 09:01:49 -0500 Subject: [PATCH 16/23] all done --- check/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/src/main.rs b/check/src/main.rs index 6081119..ff2eb7d 100644 --- a/check/src/main.rs +++ b/check/src/main.rs @@ -105,7 +105,7 @@ pub struct CacheConfig { /// Deployed and activated program address to cache. #[arg(long)] program_address: H160, - /// Bid, in wei, to place on the program cache. + /// Bid, in wei, to place on the desired program to cache #[arg(short, long, hide(true))] bid: Option, } From c3739111c4e924c1ac4e1b0b86392569252c3ec5 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Mon, 1 Jul 2024 09:05:19 -0500 Subject: [PATCH 17/23] rev --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 62b90f1..682f003 100755 --- a/install.sh +++ b/install.sh @@ -2,4 +2,4 @@ cargo fmt cargo clippy --package cargo-stylus --package cargo-stylus-cgen --package cargo-stylus-check cargo install --path main cargo install --path cgen -cargo install --path check \ No newline at end of file +cargo install --path check From b332288bde15eb0bdc754075090cd15cf8de9027 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 2 Jul 2024 09:25:39 -0500 Subject: [PATCH 18/23] clippy --- check/src/check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check/src/check.rs b/check/src/check.rs index b742dc3..fe3fa0d 100644 --- a/check/src/check.rs +++ b/check/src/check.rs @@ -3,7 +3,7 @@ use crate::{ check::ArbWasm::ArbWasmErrors, - constants::{ARB_WASM_ADDRESS, ARB_WASM_H160, ONE_ETH}, + constants::{ARB_WASM_H160, ONE_ETH}, macros::*, project::{self, BuildConfig}, CheckConfig, From 2946dad1c7697ef27d3bc72fe168bbdd1f9acaec Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 2 Jul 2024 09:27:03 -0500 Subject: [PATCH 19/23] lint --- replay/src/trace.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/replay/src/trace.rs b/replay/src/trace.rs index 8fe63b3..9f39069 100644 --- a/replay/src/trace.rs +++ b/replay/src/trace.rs @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; use sneks::SimpleSnakeNames; use std::{collections::VecDeque, mem}; +#[allow(dead_code)] #[derive(Debug)] pub struct Trace { pub top_frame: TraceFrame, @@ -399,6 +400,7 @@ pub struct Hostio { pub end_ink: u64, } +#[allow(dead_code)] #[derive(Clone, Debug, SimpleSnakeNames)] pub enum HostioKind { UserEntrypoint { From e985d3ce09cdc0b2fff87650ee4018a961fd4670 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 2 Jul 2024 09:29:55 -0500 Subject: [PATCH 20/23] deadcode --- check/src/project.rs | 1 - check/src/verify.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/check/src/project.rs b/check/src/project.rs index 7a06dab..774499a 100644 --- a/check/src/project.rs +++ b/check/src/project.rs @@ -30,7 +30,6 @@ pub enum OptLevel { pub struct BuildConfig { pub opt_level: OptLevel, pub stable: bool, - pub rebuild: bool, } impl BuildConfig { diff --git a/check/src/verify.rs b/check/src/verify.rs index aa73713..9cb4faf 100644 --- a/check/src/verify.rs +++ b/check/src/verify.rs @@ -52,7 +52,6 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> { let build_cfg = project::BuildConfig { opt_level: project::OptLevel::default(), stable: cfg.common_cfg.rust_stable, - rebuild: false, }; let wasm_file: PathBuf = project::build_dylib(build_cfg.clone()) .map_err(|e| eyre!("could not build project to WASM: {e}"))?; From 6d44ee472e5b2a954dea2737ad43f06701c6b8bd Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 2 Jul 2024 09:30:38 -0500 Subject: [PATCH 21/23] edit --- replay/src/trace.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/replay/src/trace.rs b/replay/src/trace.rs index 9f39069..4548773 100644 --- a/replay/src/trace.rs +++ b/replay/src/trace.rs @@ -21,7 +21,6 @@ use std::{collections::VecDeque, mem}; #[derive(Debug)] pub struct Trace { pub top_frame: TraceFrame, - pub receipt: TransactionReceipt, pub tx: Transaction, pub json: Value, } @@ -62,7 +61,6 @@ impl Trace { Ok(Self { top_frame, - receipt, tx, json, }) From ee646cc3a831e020c048ba4cca357e93085df8f3 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 2 Jul 2024 09:34:29 -0500 Subject: [PATCH 22/23] import --- replay/src/trace.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/replay/src/trace.rs b/replay/src/trace.rs index 4548773..ec83655 100644 --- a/replay/src/trace.rs +++ b/replay/src/trace.rs @@ -7,9 +7,7 @@ use alloy_primitives::{Address, FixedBytes, TxHash, B256, U256}; use cargo_stylus_util::color::{Color, DebugColor}; use ethers::{ providers::{JsonRpcClient, Middleware, Provider}, - types::{ - GethDebugTracerType, GethDebugTracingOptions, GethTrace, Transaction, TransactionReceipt, - }, + types::{GethDebugTracerType, GethDebugTracingOptions, GethTrace, Transaction}, utils::__serde_json::{from_value, Value}, }; use eyre::{bail, Result}; From d629f10a3a004319544ec6fe5f9113856688d9b4 Mon Sep 17 00:00:00 2001 From: Raul Jordan Date: Tue, 2 Jul 2024 09:36:34 -0500 Subject: [PATCH 23/23] no dead --- replay/src/trace.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/replay/src/trace.rs b/replay/src/trace.rs index ec83655..fecd5c6 100644 --- a/replay/src/trace.rs +++ b/replay/src/trace.rs @@ -15,7 +15,6 @@ use serde::{Deserialize, Serialize}; use sneks::SimpleSnakeNames; use std::{collections::VecDeque, mem}; -#[allow(dead_code)] #[derive(Debug)] pub struct Trace { pub top_frame: TraceFrame,