From 3da61c14cbefc2d15d50b982632af261277d1357 Mon Sep 17 00:00:00 2001 From: Ignacio Hagopian Date: Wed, 16 Jul 2025 13:54:15 +0200 Subject: [PATCH] sp1: use Docker for guest program compilation (#54) Signed-off-by: Ignacio Hagopian --- .github/workflows/rust-checks.yml | 59 +++++++++++ .github/workflows/test-sp1-docker.yml | 33 ------ Cargo.toml | 9 ++ README.md | 10 +- crates/build-utils/Cargo.toml | 2 + crates/build-utils/src/docker.rs | 85 +++++++++++++++ crates/build-utils/src/lib.rs | 2 + crates/ere-sp1/Cargo.toml | 6 +- crates/ere-sp1/src/compile.rs | 123 +++++++--------------- crates/ere-sp1/src/error.rs | 51 +++------ crates/ere-sp1/src/lib.rs | 3 +- docker/sp1/Cargo.toml | 16 +++ docker/sp1/Dockerfile | 21 ++-- docker/sp1/src/main.rs | 103 ++++++++++++++++++ scripts/sdk_installers/install_sp1_sdk.sh | 4 +- 15 files changed, 359 insertions(+), 168 deletions(-) create mode 100644 .github/workflows/rust-checks.yml delete mode 100644 .github/workflows/test-sp1-docker.yml create mode 100644 crates/build-utils/src/docker.rs create mode 100644 docker/sp1/Cargo.toml create mode 100644 docker/sp1/src/main.rs diff --git a/.github/workflows/rust-checks.yml b/.github/workflows/rust-checks.yml new file mode 100644 index 0000000..7f2fea4 --- /dev/null +++ b/.github/workflows/rust-checks.yml @@ -0,0 +1,59 @@ +name: Rust Checks +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check-fmt: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Check formatting + run: cargo fmt --check --all + + check-tests: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + crate: [ere-sp1] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: clippy + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Check clippy + run: cargo clippy --bins --lib --examples --tests --benches --all-features -p ${{ matrix.crate }} + + - name: Run tests + run: cargo test --release -p ${{ matrix.crate }} diff --git a/.github/workflows/test-sp1-docker.yml b/.github/workflows/test-sp1-docker.yml deleted file mode 100644 index 31d270f..0000000 --- a/.github/workflows/test-sp1-docker.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Test SP1 (Docker) - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - test-sp1-via-docker-build: - name: Build SP1 Docker Image - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build ere-base image - run: | - docker build \ - --tag ere-base:latest \ - --file docker/base/Dockerfile.base . - - - name: Build ere-builder-sp1 image - run: | - docker build \ - --tag ere-builder-sp1:latest \ - --file docker/sp1/Dockerfile . diff --git a/Cargo.toml b/Cargo.toml index 6d0fed7..d7e1a5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ members = [ "crates/ere-pico", "crates/ere-jolt", "crates/ere-zisk", + + # Guest compilers + "docker/sp1", ] resolver = "2" @@ -22,6 +25,12 @@ license = "MIT OR Apache-2.0" [workspace.lints] [workspace.dependencies] +tracing = "0.1.41" +tempfile = "3.3" +toml = "0.8" +clap = { version = "4.5.41", features = ["derive"] } +anyhow = "1.0" + # local dependencies zkvm-interface = { path = "crates/zkvm-interface" } build-utils = { path = "crates/build-utils" } diff --git a/README.md b/README.md index 28c8a27..ccb3ea1 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,19 @@ ## Quick Start -### 1. Install SDKs +This guide assumes you have Rust and Cargo installed. If not, please refer to the [Rust installation guide](https://www.rust-lang.org/tools/install). +Also, you must have Docker installed since some of the SDKs require it. +### 1. Install SDKs (if required) + +All zkVMs but SP1 require you to install their SDKs, for example: ```bash -bash scripts/sdk_installers/install_sp1_sdk.sh bash scripts/sdk_installers/install_jolt_sdk.sh ``` +For SP1, guest program compilation uses Docker. With time more zkVMs will follow this patterns so installing SDKs +in the host machine isn't necessary. + ### 2. Add Dependencies ```toml diff --git a/crates/build-utils/Cargo.toml b/crates/build-utils/Cargo.toml index dfe2f1e..a76b1ad 100644 --- a/crates/build-utils/Cargo.toml +++ b/crates/build-utils/Cargo.toml @@ -6,7 +6,9 @@ rust-version.workspace = true license.workspace = true [dependencies] +tracing.workspace = true cargo_metadata = "0.20.0" +thiserror = "2.0.12" [lints] workspace = true diff --git a/crates/build-utils/src/docker.rs b/crates/build-utils/src/docker.rs new file mode 100644 index 0000000..5fdc079 --- /dev/null +++ b/crates/build-utils/src/docker.rs @@ -0,0 +1,85 @@ +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use thiserror::Error; +use tracing::info; + +pub fn build_image(compiler_dockerfile: &Path, tag: &str) -> Result<(), Error> { + // Check that Docker is installed and available + if Command::new("docker") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_err() + { + return Err(Error::DockerIsNotAvailable); + } + + info!( + "Building Docker image in {} with tag {}", + compiler_dockerfile.display(), + tag + ); + + let cargo_workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize() + .unwrap(); + + // Build base image + info!("Building base Docker image..."); + let dockerfile_base_path = cargo_workspace_dir.join("docker/base/Dockerfile.base"); + let status = Command::new("docker") + .args([ + "build", + "-t", + "ere-base:latest", + "-f", + dockerfile_base_path + .to_str() + .ok_or_else(|| Error::InvalidDockerfilePath(dockerfile_base_path.clone()))?, + cargo_workspace_dir.to_str().unwrap(), + ]) + .status() + .map_err(|e| Error::DockerBuildFailed(e.into()))?; + if !status.success() { + return Err(Error::ImageBuildFailed); + } + + info!("Building guest compiler image..."); + let dockerfile_path = cargo_workspace_dir.join(compiler_dockerfile); + let status = Command::new("docker") + .args([ + "build", + "-t", + tag, + "-f", + dockerfile_path + .to_str() + .ok_or_else(|| Error::InvalidDockerfilePath(dockerfile_path.clone()))?, + cargo_workspace_dir.to_str().unwrap(), + ]) + .status() + .map_err(|e| Error::DockerBuildFailed(e.into()))?; + + if !status.success() { + return Err(Error::ImageBuildFailed); + } + + Ok(()) +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Invalid Dockerfile path: {0}")] + InvalidDockerfilePath(PathBuf), + #[error("Docker image build failed: {0}")] + DockerBuildFailed(#[source] Box), + #[error("Docker image build failed")] + ImageBuildFailed, + #[error("Docker is not available. Please ensure Docker is installed and running.")] + DockerIsNotAvailable, +} diff --git a/crates/build-utils/src/lib.rs b/crates/build-utils/src/lib.rs index b19afa3..b2ec435 100644 --- a/crates/build-utils/src/lib.rs +++ b/crates/build-utils/src/lib.rs @@ -1,5 +1,7 @@ use std::{env, fs, path::Path}; +pub mod docker; + // Detect and generate a Rust source file that contains the name and version of the SDK. pub fn detect_and_generate_name_and_sdk_version(name: &str, sdk_dep_name: &str) { let meta = cargo_metadata::MetadataCommand::new() diff --git a/crates/ere-sp1/Cargo.toml b/crates/ere-sp1/Cargo.toml index 23888a3..6cd7b6b 100644 --- a/crates/ere-sp1/Cargo.toml +++ b/crates/ere-sp1/Cargo.toml @@ -6,16 +6,16 @@ rust-version.workspace = true license.workspace = true [dependencies] -sp1-sdk = "5.0.5" zkvm-interface = { workspace = true } -toml = "0.8" +build-utils.workspace = true +sp1-sdk = "5.0.5" tempfile = "3.3" bincode = "1.3" thiserror = "2" tracing = "0.1" [build-dependencies] -build-utils = { workspace = true } +build-utils.workspace = true [lib] name = "ere_succinct" diff --git a/crates/ere-sp1/src/compile.rs b/crates/ere-sp1/src/compile.rs index 9563019..1ce17fb 100644 --- a/crates/ere-sp1/src/compile.rs +++ b/crates/ere-sp1/src/compile.rs @@ -1,102 +1,59 @@ -use std::{fs, path::Path, process::Command}; +use std::{ + path::{Path, PathBuf}, + process::Command, +}; +use build_utils::docker; use tempfile::TempDir; -use toml::Value as TomlValue; use tracing::info; use crate::error::CompileError; -/// Compile the guest crate and return raw ELF bytes. -pub fn compile_sp1_program(program_crate_path: &Path) -> Result, CompileError> { - info!("Compiling SP1 program at {}", program_crate_path.display()); +pub fn compile(guest_program_full_path: &Path) -> Result, CompileError> { + // Build the SP1 docker image + let tag = "ere-build-sp1:latest"; + docker::build_image(&PathBuf::from("docker/sp1/Dockerfile"), tag) + .map_err(|e| CompileError::DockerImageBuildFailed(Box::new(e)))?; - if !program_crate_path.exists() || !program_crate_path.is_dir() { - return Err(CompileError::InvalidProgramPath( - program_crate_path.to_path_buf(), - )); - } + // Compile the guest program using the SP1 docker image + let guest_program_path_str = guest_program_full_path + .to_str() + .ok_or_else(|| CompileError::InvalidGuestPath(guest_program_full_path.to_path_buf()))?; + let elf_output_dir = TempDir::new().map_err(CompileError::CreatingTempOutputDirectoryFailed)?; + let elf_output_dir_str = elf_output_dir + .path() + .to_str() + .ok_or_else(|| CompileError::InvalidTempOutputPath(elf_output_dir.path().to_path_buf()))?; - let guest_manifest_path = program_crate_path.join("Cargo.toml"); - if !guest_manifest_path.exists() { - return Err(CompileError::CargoTomlMissing { - program_dir: program_crate_path.to_path_buf(), - manifest_path: guest_manifest_path.clone(), - }); - } + info!("Compiling program: {}", guest_program_path_str); - // ── read + parse Cargo.toml ─────────────────────────────────────────── - let manifest_content = - fs::read_to_string(&guest_manifest_path).map_err(|e| CompileError::ReadFile { - path: guest_manifest_path.clone(), - source: e, - })?; - - let manifest_toml: TomlValue = - manifest_content - .parse::() - .map_err(|e| CompileError::ParseCargoToml { - path: guest_manifest_path.clone(), - source: e, - })?; - - let program_name = manifest_toml - .get("package") - .and_then(|p| p.get("name")) - .and_then(|n| n.as_str()) - .ok_or_else(|| CompileError::MissingPackageName { - path: guest_manifest_path.clone(), - })?; - - info!("Parsed program name: {program_name}"); - - // ── build into a temp dir ───────────────────────────────────────────── - let temp_output_dir = TempDir::new_in(program_crate_path)?; - let temp_output_dir_path = temp_output_dir.path(); - let elf_name = format!("{program_name}.elf"); - - info!( - "Running `cargo prove build` → dir: {}, ELF: {}", - temp_output_dir_path.display(), - elf_name - ); - - let status = Command::new("cargo") - .current_dir(program_crate_path) + let status = Command::new("docker") .args([ - "prove", - "build", - "--output-directory", - temp_output_dir_path.to_str().unwrap(), - "--elf-name", - &elf_name, + "run", + "--rm", + // Mount volumes + "-v", + &format!("{guest_program_path_str}:/guest-program"), + "-v", + &format!("{elf_output_dir_str}:/output"), + tag, + // Guest compiler execution + "./guest-compiler", + "/guest-program", + "/output", ]) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::inherit()) .status() - .map_err(|e| CompileError::CargoProveBuild { - cwd: program_crate_path.to_path_buf(), - source: e, - })?; + .map_err(CompileError::DockerCommandFailed)?; if !status.success() { - return Err(CompileError::CargoBuildFailed { - status, - path: program_crate_path.to_path_buf(), - }); + return Err(CompileError::DockerContainerRunFailed(status)); } - let elf_path = temp_output_dir_path.join(&elf_name); - if !elf_path.exists() { - return Err(CompileError::ElfNotFound(elf_path)); - } + // Read the compiled ELF program from the output directory + let elf = std::fs::read(elf_output_dir.path().join("guest.elf")) + .map_err(CompileError::ReadCompiledELFProgram)?; - let elf_bytes = fs::read(&elf_path).map_err(|e| CompileError::ReadFile { - path: elf_path, - source: e, - })?; - - info!("SP1 program compiled OK – {} bytes", elf_bytes.len()); - Ok(elf_bytes) + Ok(elf) } #[cfg(test)] @@ -125,7 +82,7 @@ mod tests { fn test_compile_sp1_program() { let test_guest_path = get_compile_test_guest_program_path(); - match compile_sp1_program(&test_guest_path) { + match compile(&test_guest_path) { Ok(elf_bytes) => { assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); } diff --git a/crates/ere-sp1/src/error.rs b/crates/ere-sp1/src/error.rs index b69c9d8..3c6a690 100644 --- a/crates/ere-sp1/src/error.rs +++ b/crates/ere-sp1/src/error.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, process::ExitStatus}; +use std::path::PathBuf; use thiserror::Error; use zkvm_interface::zkVMError; @@ -27,41 +27,20 @@ pub enum SP1Error { /// Errors that can be encountered while compiling a SP1 program #[derive(Debug, Error)] pub enum CompileError { - #[error("Program path does not exist or is not a directory: {0}")] - InvalidProgramPath(PathBuf), - #[error( - "Cargo.toml not found in program directory: {program_dir}. Expected at: {manifest_path}" - )] - CargoTomlMissing { - program_dir: PathBuf, - manifest_path: PathBuf, - }, - #[error("Could not find `[package].name` in guest Cargo.toml at {path}")] - MissingPackageName { path: PathBuf }, - #[error("Compiled ELF not found at expected path: {0}")] - ElfNotFound(PathBuf), - #[error("`cargo prove build` failed with status: {status} for program at {path}")] - CargoBuildFailed { status: ExitStatus, path: PathBuf }, - #[error("Failed to read file at {path}: {source}")] - ReadFile { - path: PathBuf, - #[source] - source: std::io::Error, - }, - #[error("Failed to parse guest Cargo.toml at {path}: {source}")] - ParseCargoToml { - path: PathBuf, - #[source] - source: toml::de::Error, - }, - #[error("Failed to execute `cargo prove build` in {cwd}: {source}")] - CargoProveBuild { - cwd: PathBuf, - #[source] - source: std::io::Error, - }, - #[error("Failed to create temporary output directory: {0}")] - TempDir(#[from] std::io::Error), + #[error("Failed to build Docker image: {0}")] + DockerImageBuildFailed(#[source] Box), + #[error("Docker command failed to execute: {0}")] + DockerCommandFailed(#[source] std::io::Error), + #[error("Docker container run failed with status: {0}")] + DockerContainerRunFailed(std::process::ExitStatus), + #[error("Invalid guest program path: {0}")] + InvalidGuestPath(PathBuf), + #[error("Failed to create temporary directory: {0}")] + CreatingTempOutputDirectoryFailed(#[source] std::io::Error), + #[error("Failed to create temporary output path: {0}")] + InvalidTempOutputPath(PathBuf), + #[error("Failed to read compiled ELF program: {0}")] + ReadCompiledELFProgram(#[source] std::io::Error), } #[derive(Debug, Error)] diff --git a/crates/ere-sp1/src/lib.rs b/crates/ere-sp1/src/lib.rs index 58c5d70..3500d0d 100644 --- a/crates/ere-sp1/src/lib.rs +++ b/crates/ere-sp1/src/lib.rs @@ -2,7 +2,6 @@ use std::time::Instant; -use compile::compile_sp1_program; use sp1_sdk::{ CpuProver, CudaProver, NetworkProver, Prover, ProverClient, SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey, @@ -108,7 +107,7 @@ impl Compiler for RV32_IM_SUCCINCT_ZKVM_ELF { type Program = Vec; fn compile(path_to_program: &std::path::Path) -> Result { - compile_sp1_program(path_to_program).map_err(SP1Error::from) + compile::compile(path_to_program).map_err(SP1Error::from) } } diff --git a/docker/sp1/Cargo.toml b/docker/sp1/Cargo.toml new file mode 100644 index 0000000..2fd7149 --- /dev/null +++ b/docker/sp1/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sp1-guest-compiler" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +tempfile.workspace = true +toml.workspace = true +tracing.workspace = true +clap.workspace = true +anyhow.workspace = true + +[lints] +workspace = true diff --git a/docker/sp1/Dockerfile b/docker/sp1/Dockerfile index cc0bfa8..fd4bafa 100644 --- a/docker/sp1/Dockerfile +++ b/docker/sp1/Dockerfile @@ -1,4 +1,13 @@ ARG BASE_IMAGE_TAG=latest + +# Build guest-compiler binary +FROM rust:1.85 AS builder +RUN apt-get update && apt-get install -y build-essential libclang-dev +WORKDIR /guest-compiler +COPY . . +RUN cargo build --release -p sp1-guest-compiler + +# Build zkVM builder image FROM ere-base:${BASE_IMAGE_TAG} ARG USERNAME=ere_user @@ -18,6 +27,7 @@ RUN chmod +x /tmp/install_sp1_sdk.sh # The install_sp1_sdk.sh script will respect these ENV variables. # TODO: we are hardcoding /root which may not work for other users ENV SP1UP_HOME="/root/.sp1up" \ + SP1UP_SDK_INSTALL_VERSION="v5.0.8" \ SP1_HOME="/root/.sp1" # Run the SP1 SDK installation script @@ -31,14 +41,9 @@ ENV PATH="${SP1UP_HOME}/bin:${SP1_HOME}/bin:$PATH" # Verify SP1 installation (optional here, as script does it, but good for sanity) RUN cargo prove --version -# Copy the entire ere project context -# The WORKDIR is /app from the base image -WORKDIR /app -COPY . . - -# Run tests -RUN echo "Running tests for ere-sp1 library..." && \ - cargo test --release -p ere-sp1 --lib -- --color always +# Copy guest compiler binary +COPY --from=builder /guest-compiler/target/release/sp1-guest-compiler /guest-compiler/guest-compiler +WORKDIR /guest-compiler CMD ["/bin/bash"] diff --git a/docker/sp1/src/main.rs b/docker/sp1/src/main.rs new file mode 100644 index 0000000..3c5b6fb --- /dev/null +++ b/docker/sp1/src/main.rs @@ -0,0 +1,103 @@ +use std::{fs, path::PathBuf, process::Command}; + +use anyhow::Context; +use clap::Parser; +use toml::Value as TomlValue; +use tracing::info; + +#[derive(Parser)] +#[command(author, version)] +struct Cli { + /// Path to the guest program crate directory. + guest_folder: PathBuf, + + /// Compiled ELF output folder where guest.elf will be placed. + elf_output_folder: PathBuf, +} + +pub fn main() -> anyhow::Result<()> { + let args = Cli::parse(); + + let dir = args.guest_folder; + + info!("Compiling SP1 program at {}", dir.display()); + + if !dir.exists() || !dir.is_dir() { + anyhow::bail!( + "Program path does not exist or is not a directory: {}", + dir.display() + ); + } + + let guest_manifest_path = dir.join("Cargo.toml"); + if !guest_manifest_path.exists() { + anyhow::bail!( + "Cargo.toml not found in program directory: {}. Expected at: {}", + dir.display(), + guest_manifest_path.display() + ); + } + + // ── read + parse Cargo.toml ─────────────────────────────────────────── + let manifest_content = fs::read_to_string(&guest_manifest_path) + .with_context(|| format!("Failed to read file at {}", guest_manifest_path.display()))?; + + let manifest_toml: TomlValue = manifest_content.parse::().with_context(|| { + format!( + "Failed to parse guest Cargo.toml at {}", + guest_manifest_path.display() + ) + })?; + + let program_name = manifest_toml + .get("package") + .and_then(|p| p.get("name")) + .and_then(|n| n.as_str()) + .with_context(|| { + format!( + "Could not find `[package].name` in guest Cargo.toml at {}", + guest_manifest_path.display() + ) + })?; + + info!("Parsed program name: {program_name}"); + + // ── build into a temp dir ───────────────────────────────────────────── + info!( + "Running `cargo prove build` → dir: {}", + args.elf_output_folder.display() + ); + + let status = Command::new("cargo") + .current_dir(&dir) + .args([ + "prove", + "build", + "--output-directory", + args.elf_output_folder.to_str().unwrap(), + "--elf-name", + "guest.elf", + ]) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .status() + .with_context(|| format!("Failed to execute `cargo prove build` in {}", dir.display()))?; + + if !status.success() { + anyhow::bail!("Failed to execute `cargo prove build` in {}", dir.display()) + } + + let elf_path = args.elf_output_folder.join("guest.elf"); + if !elf_path.exists() { + anyhow::bail!( + "Compiled ELF not found at expected path: {}", + elf_path.display() + ); + } + + let elf_bytes = fs::read(&elf_path) + .with_context(|| format!("Failed to read file at {}", elf_path.display()))?; + info!("SP1 program compiled OK - {} bytes", elf_bytes.len()); + + Ok(()) +} diff --git a/scripts/sdk_installers/install_sp1_sdk.sh b/scripts/sdk_installers/install_sp1_sdk.sh index c34b3f9..7b5c7a6 100755 --- a/scripts/sdk_installers/install_sp1_sdk.sh +++ b/scripts/sdk_installers/install_sp1_sdk.sh @@ -29,12 +29,14 @@ curl -L https://sp1up.succinct.xyz | bash -s -- --yes # and for subsequent commands if this script is sourced. export PATH="${SP1UP_HOME}/bin:${SP1_HOME}/bin:$PATH" +export SDK_VERSION="${SP1UP_SDK_INSTALL_VERSION:-latest}" + # Run sp1up to install/update the toolchain if ! command -v sp1up &> /dev/null; then echo "Error: sp1up command not found after installation script. Check PATH or installation." >&2 exit 1 fi -sp1up # Installs the toolchain and cargo-prove +sp1up -v ${SDK_VERSION} # Installs the toolchain and cargo-prove echo "Verifying SP1 installation..." if ! command -v cargo &> /dev/null; then