Skip to content

Commit

Permalink
Major Changes to Reproducible Builds (#53)
Browse files Browse the repository at this point in the history
* include project hash as custom section in wasm

* proper project hash inclusion in wasm

* add more details about what went wrong

* co

* edits

* edit

* verification and include toolchain

* edit

* edit

* toolchain

* patch up

* update version

* sanitize version
  • Loading branch information
rauljordan authored Jul 16, 2024
1 parent 884f8d8 commit be51b58
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 101 deletions.
196 changes: 151 additions & 45 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ resolver = "2"

[workspace.package]
authors = ["Offchain Labs"]
version = "0.4.0"
version = "0.4.1"
edition = "2021"
homepage = "https://arbitrum.io"
license = "MIT OR Apache-2.0"
repository = "https://github.com/OffchainLabs/cargo-stylus"

[workspace.dependencies]
alloy-primitives = "0.7.2"
alloy-json-abi = "0.7.2"
alloy-sol-macro = "0.7.2"
alloy-sol-types = "0.7.2"
alloy-primitives = "0.7.6"
alloy-json-abi = "0.7.6"
alloy-sol-macro = "0.7.6"
alloy-sol-types = "0.7.6"
alloy-ethers-typecast = "0.2.0"
clap = { version = "4.5.4", features = [ "derive", "color" ] }
ethers = "2.0.10"
Expand All @@ -34,4 +34,4 @@ parking_lot = "0.12.1"
sneks = "0.1.2"

# members
cargo-stylus-util = { path = "util", version = "0.4.0" }
cargo-stylus-util = { path = "util", version = "0.4.1" }
4 changes: 4 additions & 0 deletions check/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ tokio.workspace = true
wasmer = "3.1.0"
glob = "0.3.1"
tempfile = "3.10.1"
wasmparser = "0.213.0"
wasm-encoder = "0.213.0"
wasm-gen = "0.1.4"
toml = "0.8.14"
49 changes: 19 additions & 30 deletions check/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,22 @@ pub async fn check(cfg: &CheckConfig) -> Result<ProgramCheck> {
greyln!("reading wasm file at {}", wasm.to_string_lossy().lavender());
}

let (wasm, code) = project::compress_wasm(&wasm).wrap_err("failed to compress WASM")?;
// Next, we include the project's hash as a custom section
// in the user's WASM so it can be verified by Cargo stylus'
// reproducible verification. This hash is added as a section that is
// ignored by WASM runtimes, so it will only exist in the file
// for metadata purposes.
// add_project_hash_to_wasm_file(wasm, project_hash)
let (wasm_file_bytes, code) =
project::compress_wasm(&wasm, project_hash).wrap_err("failed to compress WASM")?;

greyln!("contract size: {}", format_file_size(code.len(), 16, 24));

if verbose {
greyln!("wasm size: {}", format_file_size(wasm.len(), 96, 128));
greyln!(
"wasm size: {}",
format_file_size(wasm_file_bytes.len(), 96, 128)
);
greyln!("connecting to RPC: {}", &cfg.common_cfg.endpoint.lavender());
}

Expand All @@ -73,34 +83,23 @@ pub async fn check(cfg: &CheckConfig) -> Result<ProgramCheck> {
let codehash = alloy_primitives::keccak256(&code);

if program_exists(codehash, &provider).await? {
return Ok(ProgramCheck::Active { code, project_hash });
return Ok(ProgramCheck::Active { code });
}

let address = cfg.program_address.unwrap_or(H160::random());
let fee = check_activate(code.clone().into(), address, &provider).await?;
let visual_fee = format_data_fee(fee).unwrap_or("???".red());
greyln!("wasm data fee: {visual_fee}");
Ok(ProgramCheck::Ready {
code,
fee,
project_hash,
})
Ok(ProgramCheck::Ready { code, fee })
}

/// Whether a program is active, or needs activation.
#[derive(PartialEq)]
pub enum ProgramCheck {
/// Program already exists onchain.
Active {
code: Vec<u8>,
project_hash: [u8; 32],
},
Active { code: Vec<u8> },
/// Program can be activated with the given data fee.
Ready {
code: Vec<u8>,
fee: U256,
project_hash: [u8; 32],
},
Ready { code: Vec<u8>, fee: U256 },
}

impl ProgramCheck {
Expand All @@ -110,14 +109,6 @@ impl ProgramCheck {
Self::Ready { code, .. } => code,
}
}

pub fn project_hash(&self) -> &[u8; 32] {
match self {
Self::Active { project_hash, .. } => project_hash,
Self::Ready { project_hash, .. } => project_hash,
}
}

pub fn suggest_fee(&self) -> U256 {
match self {
Self::Active { .. } => U256::default(),
Expand All @@ -132,11 +123,9 @@ impl CheckConfig {
return Ok((wasm, [0u8; 32]));
}
let cfg = BuildConfig::new(self.common_cfg.rust_stable);
let project_hash = project::hash_files(
self.common_cfg.source_files_for_project_hash.clone(),
cfg.clone(),
)?;
let wasm = project::build_dylib(cfg)?;
let wasm = project::build_dylib(cfg.clone())?;
let project_hash =
project::hash_files(self.common_cfg.source_files_for_project_hash.clone(), cfg)?;
Ok((wasm, project_hash))
}
}
Expand Down
7 changes: 7 additions & 0 deletions check/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ pub const GITHUB_TEMPLATE_REPO_MINIMAL: &str =

/// One ether in wei.
pub const ONE_ETH: U256 = U256([1000000000000000000, 0, 0, 0]);

/// Name of the custom wasm section that is added to contracts deployed with cargo stylus
/// to include a hash of the Rust project's source files for reproducible verification of builds.
pub const PROJECT_HASH_SECTION_NAME: &str = "project_hash";

/// Name of the toolchain file used to specify the Rust toolchain version for a project.
pub const TOOLCHAIN_FILE_NAME: &str = "rust-toolchain.toml";
26 changes: 18 additions & 8 deletions check/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ pub async fn deploy(cfg: DeployConfig) -> Result<()> {
}
}

let contract = cfg
.deploy_contract(program.code(), program.project_hash(), sender, &client)
.await?;
let contract = cfg.deploy_contract(program.code(), sender, &client).await?;

match program {
ProgramCheck::Ready { .. } => cfg.activate(sender, contract, data_fee, &client).await?,
Expand All @@ -98,11 +96,10 @@ impl DeployConfig {
async fn deploy_contract(
&self,
code: &[u8],
project_hash: &[u8; 32],
sender: H160,
client: &SignerClient,
) -> Result<H160> {
let init_code = program_deployment_calldata(code, project_hash);
let init_code = program_deployment_calldata(code);

let tx = Eip1559TransactionRequest::new()
.from(sender)
Expand Down Expand Up @@ -231,27 +228,40 @@ pub async fn run_tx(
}

/// Prepares an EVM bytecode prelude for contract creation.
pub fn program_deployment_calldata(code: &[u8], hash: &[u8; 32]) -> Vec<u8> {
pub fn program_deployment_calldata(code: &[u8]) -> Vec<u8> {
let mut code_len = [0u8; 32];
U256::from(code.len()).to_big_endian(&mut code_len);
let mut deploy: Vec<u8> = 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(42 + 1); // prelude + version
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
}

pub fn extract_program_evm_deployment_prelude(calldata: &[u8]) -> Vec<u8> {
// The length of the prelude, version part is 42 + 1 as per the code
let metadata_length = 42 + 1;
// Extract and return the metadata part
calldata[0..metadata_length].to_vec()
}

pub fn extract_compressed_wasm(calldata: &[u8]) -> Vec<u8> {
// The length of the prelude, version part is 42 + 1 as per the code
let metadata_length = 42 + 1;
// Extract and return the metadata part
calldata[metadata_length..].to_vec()
}

pub fn format_gas(gas: U256) -> String {
let gas: u64 = gas.try_into().unwrap_or(u64::MAX);
let text = format!("{gas} gas");
Expand Down
20 changes: 17 additions & 3 deletions check/src/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/main/licenses/COPYRIGHT.md

use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};

use eyre::{bail, eyre, Result};

use crate::constants::TOOLCHAIN_FILE_NAME;
use crate::project::extract_toolchain_channel;

fn version_to_image_name(version: &str) -> String {
format!("cargo-stylus-{}", version)
}
Expand All @@ -24,6 +28,8 @@ fn create_image(version: &str) -> Result<()> {
if image_exists(&name)? {
return Ok(());
}
let toolchain_file_path = PathBuf::from(".").as_path().join(TOOLCHAIN_FILE_NAME);
let toolchain_channel = extract_toolchain_channel(&toolchain_file_path)?;
let mut child = Command::new("docker")
.arg("build")
.arg("-t")
Expand All @@ -37,15 +43,19 @@ fn create_image(version: &str) -> Result<()> {
child.stdin.as_mut().unwrap(),
"\
FROM rust:{} as builder\n\
RUN rustup toolchain install {} && rustup default {}
RUN rustup target add wasm32-unknown-unknown
RUN rustup target add wasm32-wasi
RUN rustup target add aarch64-unknown-linux-gnu
RUN rustup target add x86_64-unknown-linux-gnu
RUN cargo install cargo-stylus
RUN cargo install --force cargo-stylus-check
RUN cargo install --force cargo-stylus-replay
RUN cargo install --force cargo-stylus-cgen
",
version
version,
toolchain_channel,
toolchain_channel,
)?;
child.wait().map_err(|e| eyre!("wait failed: {e}"))?;
Ok(())
Expand Down Expand Up @@ -76,10 +86,14 @@ fn run_in_docker_container(version: &str, command_line: &[&str]) -> Result<()> {
}

pub fn run_reproducible(version: &str, command_line: &[String]) -> Result<()> {
let version: String = version
.chars()
.filter(|c| c.is_alphanumeric() || *c == '.')
.collect();
let mut command = vec!["cargo", "stylus"];
for s in command_line.iter() {
command.push(s);
}
create_image(version)?;
run_in_docker_container(version, &command)
create_image(&version)?;
run_in_docker_container(&version, &command)
}
Loading

0 comments on commit be51b58

Please sign in to comment.