Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions cargo-stylus/src/commands/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@

use alloy::primitives::TxHash;
use eyre::eyre;
use stylus_tools::ops;
use itertools::izip;
use stylus_tools::core::build::reproducible::run_reproducible;

use crate::{
common_args::{ProviderArgs, VerificationArgs},
common_args::{ProjectArgs, ProviderArgs, VerificationArgs},
error::CargoStylusResult,
utils::decode0x,
};

#[derive(Debug, clap::Args)]
pub struct Args {
/// Cargo stylus version when deploying reproducibly to downloads the corresponding cargo-stylus-base Docker image.
/// If not set, uses the default version of the local cargo stylus binary.
#[arg(long)]
cargo_stylus_version: Option<String>,
/// Hash of the deployment transaction.
#[arg(long)]
deployment_tx: String,
deployment_tx: Vec<String>,

#[command(flatten)]
project: ProjectArgs,
#[command(flatten)]
provider: ProviderArgs,
#[command(flatten)]
Expand All @@ -25,11 +32,27 @@ pub struct Args {

pub async fn exec(args: Args) -> CargoStylusResult {
let provider = args.provider.build_provider().await?;
let hash = decode0x(args.deployment_tx)?;
if hash.len() != 32 {
return Err(eyre!("Invalid hash").into());

for (contract, deployment_tx) in izip!(args.project.contracts()?, args.deployment_tx) {
if args.verification.no_verify {
let hash = decode0x(&deployment_tx)?;
if hash.len() != 32 {
return Err(eyre!("Invalid hash").into());
}
let hash = TxHash::from_slice(&hash);
contract.verify(hash, &provider).await?;
} else {
println!("Running in a Docker container for reproducibility, this may take a while",);
let mut cli_args: Vec<String> =
vec![String::from("verify"), String::from("--no-verify")];
cli_args.push(deployment_tx);
run_reproducible(
&contract.package,
args.cargo_stylus_version.clone(),
cli_args,
)?;
}
}
let hash = TxHash::from_slice(&hash);
ops::verify(hash, &provider).await?;

Ok(())
}
2 changes: 1 addition & 1 deletion cargo-stylus/src/common_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,5 @@ pub struct VerificationArgs {
/// Useful for local builds, but at the risk of not having a reproducible contract for
/// verification purposes.
#[arg(long)]
no_verify: bool,
pub no_verify: bool,
}
9 changes: 9 additions & 0 deletions cargo-stylus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ impl From<stylus_tools::core::build::BuildError> for CargoStylusError {
}
}

impl From<stylus_tools::core::build::reproducible::ReproducibleBuildError> for CargoStylusError {
fn from(err: stylus_tools::core::build::reproducible::ReproducibleBuildError) -> Self {
Self {
error: err.into(),
exit_code: ExitCode::FAILURE,
}
}
}

impl From<stylus_tools::core::check::CheckError> for CargoStylusError {
fn from(err: stylus_tools::core::check::CheckError) -> Self {
Self {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2025, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md

pub mod reproducible;

use std::{path::PathBuf, process::Stdio};

use cargo_metadata::MetadataCommand;
Expand Down
72 changes: 72 additions & 0 deletions stylus-tools/src/core/build/reproducible.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2025, Offchain Labs, Inc.
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md

use std::io::Write;

use cargo_metadata::Package;

use crate::utils::{
docker::{self, image_exists, validate_host, DockerError},
toolchain::{get_toolchain_channel, ToolchainError},
};

pub fn run_reproducible(
package: &Package,
cargo_stylus_version: Option<String>,
command_line: impl IntoIterator<Item = String>,
) -> Result<(), ReproducibleBuildError> {
validate_host()?;
let toolchain_channel = get_toolchain_channel(package)?;
greyln!(
"Running reproducible Stylus command with toolchain {}",
toolchain_channel.mint()
);
let cargo_stylus_version =
cargo_stylus_version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
let image_name = create_image(&cargo_stylus_version, &toolchain_channel)?;

let mut args = vec!["cargo".to_string(), "stylus".to_string()];
for arg in command_line.into_iter() {
args.push(arg);
}
docker::run_in_container(&image_name, package.source.clone().unwrap().repr, args)?;
Ok(())
}

/// Returns the image name
fn create_image(
cargo_stylus_version: &str,
toolchain_version: &str,
) -> Result<String, DockerError> {
let name = image_name(cargo_stylus_version, toolchain_version);
if image_exists(&name)? {
return Ok(name);
}
println!("Building Docker image for Rust toolchain {toolchain_version}");
let mut build = docker::cmd::build(&name)?;
write!(
build.file(),
"\
ARG BUILD_PLATFORM=linux/amd64
FROM --platform=${{BUILD_PLATFORM}} offchainlabs/cargo-stylus-base:{cargo_stylus_version} AS base
RUN rustup toolchain install {toolchain_version}-x86_64-unknown-linux-gnu
RUN rustup default {toolchain_version}-x86_64-unknown-linux-gnu
RUN rustup target add wasm32-unknown-unknown
RUN rustup component add rust-src --toolchain {toolchain_version}-x86_64-unknown-linux-gnu
",
)?;
build.wait()?;
Ok(name)
}

fn image_name(cargo_stylus_version: &str, toolchain_version: &str) -> String {
format!("cargo-stylus-base-{cargo_stylus_version}-toolchain-{toolchain_version}")
}

#[derive(Debug, thiserror::Error)]
pub enum ReproducibleBuildError {
#[error("docker error: {0}")]
Docker(#[from] DockerError),
#[error("toolchain error: {0}")]
Toolchain(#[from] ToolchainError),
}
12 changes: 11 additions & 1 deletion stylus-tools/src/core/project/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use std::path::PathBuf;

use alloy::{
primitives::{Address, B256, U256},
primitives::{Address, TxHash, B256, U256},
providers::{Provider, WalletProvider},
};
use cargo_metadata::{semver::Version, Package, TargetKind};
Expand All @@ -16,6 +16,7 @@ use crate::{
deployment::{deploy, DeploymentConfig, DeploymentError},
manifest,
reflection::ReflectionConfig,
verification::{self, VerificationStatus},
},
error::decode_contract_error,
ops,
Expand Down Expand Up @@ -104,6 +105,15 @@ impl Contract {
ops::export_abi(self.package.name.as_ref(), config)
}

pub async fn verify(
&self,
tx_hash: TxHash,
provider: &impl Provider,
) -> eyre::Result<VerificationStatus> {
let status = verification::verify(self, tx_hash, provider).await?;
Ok(status)
}

pub fn print_constructor(&self, config: &ReflectionConfig) -> eyre::Result<()> {
ops::print_constructor(self.package.name.as_ref(), config)
}
Expand Down
32 changes: 26 additions & 6 deletions stylus-tools/src/utils/docker/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@

//! Run commands from the Docker CLI.

#![allow(dead_code)]

use std::{
ffi::OsStr,
process::{Command, Stdio},
str,
};
Expand All @@ -14,15 +13,33 @@ use super::{error::DockerError, json};

const DOCKER_PROGRAM: &str = "docker";

#[derive(Debug)]
pub struct DockerBuild {
pub child: std::process::Child,
}

impl DockerBuild {
pub fn file(&mut self) -> impl std::io::Write + '_ {
self.child.stdin.as_mut().unwrap()
}

pub fn wait(mut self) -> Result<(), DockerError> {
self.child.wait().map_err(DockerError::WaitFailure)?;
Ok(())
}
}

/// Start a Docker build.
pub fn build(tag: &str) {
let _child = Command::new(DOCKER_PROGRAM)
pub fn build(tag: &str) -> Result<DockerBuild, DockerError> {
let child = Command::new(DOCKER_PROGRAM)
.arg("build")
.args(["--tag", tag])
.arg(".")
.args(["--file", "-"])
.stdin(Stdio::piped())
.spawn();
.spawn()
.map_err(DockerError::CommandExecution)?;
Ok(DockerBuild { child })
}

/// List local Docker images.
Expand Down Expand Up @@ -53,12 +70,13 @@ pub fn run(
network: Option<&str>,
volumes: &[(&str, &str)],
workdir: Option<&str>,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<(), DockerError> {
// TODO: builder pattern
// TODO: --mount instead of --volume
// TODO: check return code
let mut cmd = Command::new(DOCKER_PROGRAM);
cmd.args(["run", image]);
cmd.arg("run");
if let Some(network) = network {
cmd.args(["--network", network]);
}
Expand All @@ -68,6 +86,8 @@ pub fn run(
for (host_path, container_path) in volumes {
cmd.args(["--volume", &format!("{host_path}:{container_path}")]);
}
cmd.arg(image);
cmd.args(args);
cmd.spawn()
.map_err(DockerError::CommandExecution)?
.wait()
Expand Down
15 changes: 15 additions & 0 deletions stylus-tools/src/utils/docker/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const CANNOT_CONNECT: &str = "Cannot connect to the Docker daemon";

#[derive(Debug, thiserror::Error)]
pub enum DockerError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),

#[error("Failed to execute Docker command: {0}")]
CommandExecution(io::Error),
#[error("Wait failed: {0}")]
Expand All @@ -21,6 +24,18 @@ pub enum DockerError {
CannotConnect(String),
#[error("Docker error: {0}")]
Docker(String),

#[error("unable to determine host OS type")]
UnableToDetermineOsType,
#[error("unable to determine kernel version")]
UnableToDetermineKernelVersion,
#[error(
"Reproducible cargo stylus commands on Windows are only supported \
in Windows Linux Subsystem (WSL). Please install within WSL. \
To instead opt out of reproducible builds, add the --no-verify \
flag to your commands."
)]
WSLRequired,
}

impl DockerError {
Expand Down
40 changes: 36 additions & 4 deletions stylus-tools/src/utils/docker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,45 @@

//! Utilities for working with docker.

// TODO: pub here?
use error::DockerError;
use std::{ffi::OsStr, path::Path};

pub use error::DockerError;

pub mod cmd;
pub mod error;
pub mod json;

pub fn _image_exists(image_name: &str) -> Result<bool, DockerError> {
mod error;

pub fn validate_host() -> Result<(), DockerError> {
let os_type = sys_info::os_type().map_err(|_| DockerError::UnableToDetermineOsType)?;
if os_type == "Windows" {
// Check for WSL environment
let kernel_version =
sys_info::os_release().map_err(|_| DockerError::UnableToDetermineKernelVersion)?;
if kernel_version.contains("microsoft") || kernel_version.contains("WSL") {
greyln!("Detected Windows Linux Subsystem host");
} else {
return Err(DockerError::WSLRequired);
}
}
Ok(())
}

pub fn image_exists(image_name: &str) -> Result<bool, DockerError> {
Ok(!cmd::images(image_name)?.is_empty())
}

pub fn run_in_container(
image_name: &str,
dir: impl AsRef<Path>,
args: impl IntoIterator<Item = impl AsRef<OsStr>>,
) -> Result<(), DockerError> {
cmd::run(
image_name,
Some("host"),
&[(dir.as_ref().to_str().unwrap(), "/source")],
Some("/source"),
args,
)?;
Ok(())
}
Loading