Add ere-dockerized (#75)

This commit is contained in:
Han
2025-08-02 19:21:52 +08:00
committed by GitHub
parent 1585a77405
commit 42e7c6c416
42 changed files with 1365 additions and 941 deletions

View File

@@ -33,7 +33,7 @@ jobs:
strategy:
fail-fast: false
matrix:
crate: [ere-sp1, ere-risc0]
zkvm: [sp1, risc0]
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -53,7 +53,7 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Check clippy
run: cargo clippy --bins --lib --examples --tests --benches -p ${{ matrix.crate }}
run: cargo clippy --bins --lib --examples --tests --benches -p ere-${{ matrix.zkvm }}
- name: Run tests
run: cargo test --release -p ${{ matrix.crate }}
run: cargo test --release -p ere-dockerized -- ${{ matrix.zkvm }}

View File

@@ -30,4 +30,5 @@ jobs:
run: |
docker build \
--tag ere-builder-zisk:latest \
--file docker/zisk/Dockerfile .
--file docker/zisk/Dockerfile \
--build-arg CI=1 .

44
Cargo.lock generated
View File

@@ -2359,6 +2359,20 @@ dependencies = [
"zkvm-interface",
]
[[package]]
name = "ere-dockerized"
version = "0.1.0"
dependencies = [
"bincode",
"build-utils",
"bytemuck",
"risc0-zkvm",
"serde",
"tempfile",
"thiserror 2.0.12",
"zkvm-interface",
]
[[package]]
name = "ere-jolt"
version = "0.1.0"
@@ -2425,7 +2439,9 @@ dependencies = [
"borsh",
"build-utils",
"bytemuck",
"cargo_metadata 0.19.2",
"hex",
"risc0-build",
"risc0-zkvm",
"serde",
"serde_json",
@@ -2444,6 +2460,7 @@ dependencies = [
"sp1-sdk",
"tempfile",
"thiserror 2.0.12",
"toml 0.8.22",
"tracing",
"zkvm-interface",
]
@@ -7886,22 +7903,6 @@ dependencies = [
"sppark",
]
[[package]]
name = "risc0-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"borsh",
"clap",
"hex",
"risc0-zkvm",
"tempfile",
"toml 0.8.22",
"tracing",
"zkvm-interface",
]
[[package]]
name = "risc0-core"
version = "2.0.0"
@@ -8961,17 +8962,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "sp1-guest-compiler"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"tempfile",
"toml 0.8.22",
"tracing",
]
[[package]]
name = "sp1-primitives"
version = "5.1.0"

View File

@@ -11,11 +11,9 @@ members = [
"crates/ere-zisk",
# zkVM interface
"crates/zkvm-interface",
# CLI
# CLI and dockerized zkVM
"crates/ere-cli",
# Guest compilers
"docker/sp1",
"docker/risc0",
"crates/ere-dockerized",
]
resolver = "2"
@@ -39,6 +37,7 @@ hex = "0.4.3"
zkvm-interface = { path = "crates/zkvm-interface" }
build-utils = { path = "crates/build-utils" }
ere-cli = { path = "crates/ere-cli" }
ere-dockerized = { path = "crates/ere-dockerized" }
ere-jolt = { path = "crates/ere-jolt" }
ere-nexus = { path = "crates/ere-nexus" }
ere-openvm = { path = "crates/ere-openvm" }

View File

@@ -78,9 +78,8 @@ ere-sp1 = { path = "crates/ere-sp1" }
use zkvm_interface::{Compiler, zkVM, Input, ProverResourceType};
use ere_sp1::{EreSP1, RV32_IM_SUCCINCT_ZKVM_ELF};
let mount_directory = std::path::Path::new(".");
let guest_relative = std::path::Path::new("guest/hello");
let elf = RV32_IM_SUCCINCT_ZKVM_ELF::compile(mount_directory, guest_relative)?; // compile
let guest_directory = std::path::Path::new("guest/hello");
let elf = RV32_IM_SUCCINCT_ZKVM_ELF.compile(guest_directory)?; // compile
let mut io = Input::new();
io.write(&42u32)?;
let zkvm = EreSP1::new(elf, ProverResourceType::Cpu); // create zkVM instance

View File

@@ -1,23 +1,28 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use cargo_metadata::MetadataCommand;
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()
gen_name_and_sdk_version(name, &detect_sdk_version(sdk_dep_name));
}
// Detect version of the SDK.
pub fn detect_sdk_version(sdk_dep_name: &str) -> String {
let meta = MetadataCommand::new()
.exec()
.expect("Failed to get cargo metadata");
let version = meta
.packages
meta.packages
.iter()
.find(|pkg| pkg.name.eq_ignore_ascii_case(sdk_dep_name))
.map(|pkg| pkg.version.to_string())
.unwrap_or_else(|| {
panic!("Dependency {sdk_dep_name} not found in Cargo.toml");
});
gen_name_and_sdk_version(name, &version);
})
}
// Generate a Rust source file that contains the provided name and version of the SDK.
@@ -31,3 +36,13 @@ pub fn gen_name_and_sdk_version(name: &str, version: &str) {
.unwrap();
println!("cargo:rerun-if-changed=Cargo.lock");
}
/// Detects version of the crate of the `build.rs` that being ran.
pub fn detect_self_crate_version() -> String {
let meta = MetadataCommand::new()
.exec()
.expect("Failed to get cargo metadata");
// `root_package` returns the crate of the `build.rs` that being ran.
meta.root_package().unwrap().version.to_string()
}

View File

@@ -1,3 +1,5 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use anyhow::{Context, Error};
use clap::{Parser, Subcommand};
use std::{fs, path::PathBuf};
@@ -31,12 +33,9 @@ struct Cli {
enum Commands {
/// Compile a guest program
Compile {
/// Path to the base directory (workspace root)
/// Path to the guest program
#[arg(long)]
mount_directory: PathBuf,
/// Relative path from `mount_directory` to the guest program
#[arg(long)]
guest_relative: PathBuf,
guest_path: PathBuf,
/// Path where the compiled program will be written
#[arg(long)]
program_path: PathBuf,
@@ -90,10 +89,9 @@ fn main() -> Result<(), Error> {
match args.command {
Commands::Compile {
mount_directory,
guest_relative,
guest_path,
program_path,
} => compile(mount_directory, guest_relative, program_path),
} => compile(guest_path, program_path),
Commands::Prove {
program_path,
input_path,
@@ -114,31 +112,27 @@ fn main() -> Result<(), Error> {
}
}
fn compile(
mount_directory: PathBuf,
guest_relative: PathBuf,
program_path: PathBuf,
) -> Result<(), Error> {
fn compile(guest_path: PathBuf, program_path: PathBuf) -> Result<(), Error> {
#[cfg(feature = "jolt")]
let program = ere_jolt::JOLT_TARGET::compile(&mount_directory, &guest_relative);
let program = ere_jolt::JOLT_TARGET.compile(&guest_path);
#[cfg(feature = "nexus")]
let program = ere_nexus::NEXUS_TARGET::compile(&mount_directory, &guest_relative);
let program = ere_nexus::NEXUS_TARGET.compile(&guest_path);
#[cfg(feature = "openvm")]
let program = ere_openvm::OPENVM_TARGET::compile(&mount_directory, &guest_relative);
let program = ere_openvm::OPENVM_TARGET.compile(&guest_path);
#[cfg(feature = "pico")]
let program = ere_pico::PICO_TARGET::compile(&mount_directory, &guest_relative);
let program = ere_pico::PICO_TARGET.compile(&guest_path);
#[cfg(feature = "risc0")]
let program = ere_risc0::RV32_IM_RISC0_ZKVM_ELF::compile(&mount_directory, &guest_relative);
let program = ere_risc0::RV32_IM_RISC0_ZKVM_ELF.compile(&guest_path);
#[cfg(feature = "sp1")]
let program = ere_sp1::RV32_IM_SUCCINCT_ZKVM_ELF::compile(&mount_directory, &guest_relative);
let program = ere_sp1::RV32_IM_SUCCINCT_ZKVM_ELF.compile(&guest_path);
#[cfg(feature = "zisk")]
let program = ere_zisk::RV64_IMA_ZISK_ZKVM_ELF::compile(&mount_directory, &guest_relative);
let program = ere_zisk::RV64_IMA_ZISK_ZKVM_ELF.compile(&guest_path);
serde::write(
&program_path,

View File

@@ -0,0 +1,26 @@
[package]
name = "ere-dockerized"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
bincode = "1.3"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3"
thiserror = "2"
zkvm-interface = { workspace = true, features = ["clap"] }
# Needed for Risc0 and OpenVM input serialization.
bytemuck = "1.13"
risc0-zkvm = { version = "^2.3.0", default-features = false }
[dev-dependencies]
[build-dependencies]
build-utils = { workspace = true }
[lints]
workspace = true

View File

@@ -0,0 +1,65 @@
use build_utils::{detect_sdk_version, detect_self_crate_version};
use std::{env, fs, path::Path};
fn main() {
generate_crate_version();
generate_zkvm_sdk_version_impl();
println!("cargo:rerun-if-changed=Cargo.lock");
}
fn generate_crate_version() {
let crate_version = format!(
"const CRATE_VERSION: &str = \"{}\";",
detect_self_crate_version()
);
let out_dir = env::var("OUT_DIR").unwrap();
let dst = Path::new(&out_dir).join("crate_version.rs");
fs::write(dst, crate_version).unwrap();
}
fn generate_zkvm_sdk_version_impl() {
let [
jolt_version,
nexus_version,
openvm_version,
pico_version,
risc0_version,
sp1_version,
] = [
"jolt-sdk",
"nexus-sdk",
"openvm-sdk",
"pico-sdk",
"risc0-zkvm",
"sp1-sdk",
]
.map(detect_sdk_version);
// FIXME: ZisK doens't depend on SDK yet, so we hardcode the version here,
// same as the one in `scripts/sdk_installers/install_zisk_sdk.sh`.
// Once ZisK's SDK is ready, we should update this to detect the SDK
// version.
// The issue for tracking https://github.com/eth-act/ere/issues/73.
let zisk_version = "0.9.0";
let zkvm_sdk_version_impl = format!(
r#"impl crate::ErezkVM {{
pub fn sdk_version(&self) -> &'static str {{
match self {{
Self::Jolt => "{jolt_version}",
Self::Nexus => "{nexus_version}",
Self::OpenVM => "{openvm_version}",
Self::Pico => "{pico_version}",
Self::Risc0 => "{risc0_version}",
Self::SP1 => "{sp1_version}",
Self::Zisk => "{zisk_version}",
}}
}}
}}"#,
);
let out_dir = env::var("OUT_DIR").unwrap();
let dst = Path::new(&out_dir).join("zkvm_sdk_version_impl.rs");
fs::write(dst, zkvm_sdk_version_impl).unwrap();
}

View File

@@ -0,0 +1,158 @@
use crate::error::CommonError;
use std::{
fmt::{self, Display, Formatter},
io,
path::Path,
process::Command,
};
#[derive(Clone)]
pub struct CmdOption(String, Option<String>);
impl CmdOption {
pub fn new(key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
Self(to_string(key), Some(to_string(value)))
}
pub fn flag(key: impl AsRef<str>) -> Self {
Self(to_string(key), None)
}
pub fn to_args(&self) -> Vec<String> {
let Self(key, value) = self;
match value {
Some(value) => vec![format!("--{key}"), format!("{value}")],
None => vec![format!("--{key}")],
}
}
}
impl Display for CmdOption {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let Self(key, value) = self;
match value {
Some(value) => write!(f, "--{key} {value}"),
None => write!(f, "--{key}"),
}
}
}
#[derive(Default)]
pub struct DockerBuildCmd {
options: Vec<CmdOption>,
}
impl DockerBuildCmd {
pub fn new() -> Self {
Self::default()
}
pub fn file(mut self, file: impl AsRef<Path>) -> Self {
self.options
.push(CmdOption::new("file", file.as_ref().to_string_lossy()));
self
}
pub fn tag(mut self, tag: impl AsRef<str>) -> Self {
self.options.push(CmdOption::new("tag", tag));
self
}
pub fn bulid_arg(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
self.options.push(CmdOption::new(
"build-arg",
format!("{}={}", to_string(key), to_string(value)),
));
self
}
pub fn exec(self, context: impl AsRef<Path>) -> Result<(), io::Error> {
let mut cmd = Command::new("docker");
cmd.arg("build");
for flag in self.options {
cmd.args(flag.to_args());
}
cmd.arg(context.as_ref().to_string_lossy().to_string());
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"Command {cmd:?} failed with status: {status}",
)));
}
Ok(())
}
}
pub struct DockerRunCmd {
options: Vec<CmdOption>,
image: String,
}
impl DockerRunCmd {
pub fn new(image: String) -> Self {
Self {
options: Vec::new(),
image,
}
}
pub fn volume(mut self, host: impl AsRef<Path>, container: impl AsRef<Path>) -> Self {
self.options.push(CmdOption::new(
"volume",
format!(
"{}:{}",
host.as_ref().display(),
container.as_ref().display(),
),
));
self
}
pub fn gpus(mut self, devices: impl AsRef<str>) -> Self {
self.options.push(CmdOption::new("gpus", devices));
self
}
pub fn rm(mut self) -> Self {
self.options.push(CmdOption::flag("rm"));
self
}
pub fn exec(self, commands: impl IntoIterator<Item: AsRef<str>>) -> Result<(), io::Error> {
let mut cmd = Command::new("docker");
cmd.arg("run");
for flag in self.options {
cmd.args(flag.to_args());
}
cmd.arg(self.image);
for command in commands {
cmd.arg(command.as_ref());
}
let status = cmd.status()?;
if !status.success() {
return Err(io::Error::other(format!(
"Command {cmd:?} failed with status: {status}",
)));
}
Ok(())
}
}
pub fn docker_image_exists(image: impl AsRef<str>) -> Result<bool, CommonError> {
let output = Command::new("docker")
.args(["images", "--quiet", image.as_ref()])
.output()
.map_err(CommonError::DockerImageCmd)?;
// If image exists, image id will be printed hence stdout will be non-empty.
Ok(!output.stdout.is_empty())
}
fn to_string(s: impl AsRef<str>) -> String {
s.as_ref().to_string()
}

View File

@@ -0,0 +1,102 @@
use std::{io, path::PathBuf};
use thiserror::Error;
use zkvm_interface::zkVMError;
impl From<DockerizedError> for zkVMError {
fn from(value: DockerizedError) -> Self {
zkVMError::Other(Box::new(value))
}
}
impl From<CommonError> for zkVMError {
fn from(value: CommonError) -> Self {
zkVMError::Other(Box::new(value))
}
}
#[derive(Debug, Error)]
pub enum DockerizedError {
#[error(transparent)]
Compile(#[from] CompileError),
#[error(transparent)]
Execute(#[from] ExecuteError),
#[error(transparent)]
Prove(#[from] ProveError),
#[error(transparent)]
Verify(#[from] VerifyError),
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error(
"Guest directory must be in mounting directory, mounting_directory: {mounting_directory}, guest_directory: {guest_directory}"
)]
GuestNotInMountingDirecty {
mounting_directory: PathBuf,
guest_directory: PathBuf,
},
#[error(transparent)]
Common(#[from] CommonError),
}
#[derive(Debug, Error)]
pub enum ExecuteError {
#[error(transparent)]
Common(#[from] CommonError),
}
#[derive(Debug, Error)]
pub enum ProveError {
#[error(transparent)]
Common(#[from] CommonError),
}
#[derive(Debug, Error)]
pub enum VerifyError {
#[error(transparent)]
Common(#[from] CommonError),
}
#[derive(Debug, Error)]
pub enum CommonError {
#[error("{context}: {source}")]
Io {
#[source]
source: io::Error,
context: String,
},
#[error("Failed to execute `docker image`: {0}")]
DockerImageCmd(io::Error),
#[error("Failed to execute `docker build`: {0}")]
DockerBuildCmd(io::Error),
#[error("Failed to execute `docker run`: {0}")]
DockerRunCmd(io::Error),
#[error("{context}: {source}")]
Serialization {
#[source]
source: Box<dyn std::error::Error + Send + Sync + 'static>,
context: String,
},
}
impl CommonError {
pub fn io(source: io::Error, context: impl ToString) -> Self {
Self::Io {
source,
context: context.to_string(),
}
}
pub fn serilization(
source: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
context: impl ToString,
) -> Self {
Self::Serialization {
source: source.into(),
context: context.to_string(),
}
}
}

View File

@@ -0,0 +1,64 @@
use crate::{ErezkVM, error::CommonError};
use serde::Serialize;
use zkvm_interface::{Input, InputItem};
impl ErezkVM {
pub fn serialize_object(
&self,
obj: &(impl Serialize + ?Sized),
) -> Result<Vec<u8>, CommonError> {
match self {
// Issue for tracking: https://github.com/eth-act/ere/issues/4.
Self::Jolt => unimplemented!(),
// Issue for tracking: https://github.com/eth-act/ere/issues/63.
Self::Nexus => unimplemented!(),
// FIXME: Instead of using `openvm::serde::to_vec`, we use Risc0's
// serializer, because OpenVM uses the same one, to avoid the
// duplicated extern symbol they export.
// It'd be better to have each zkvm provides their
// lightweight serde crate.
Self::OpenVM => risc0_zkvm::serde::to_vec(obj)
.map(|words| words.into_iter().flat_map(|w| w.to_le_bytes()).collect())
.map_err(|err| {
CommonError::serilization(
err,
"Failed to serialize object with `risc0_zkvm::serde::to_vec`",
)
}),
Self::Pico => bincode::serialize(obj).map_err(|err| {
CommonError::serilization(err, "Failed to serialize object with `bincode`")
}),
Self::Risc0 => risc0_zkvm::serde::to_vec(obj)
.map(|vec| bytemuck::cast_slice(&vec).to_vec())
.map_err(|err| {
CommonError::serilization(
err,
"Failed to serialize object with `risc0_zkvm::serde::to_vec`",
)
}),
Self::SP1 => bincode::serialize(obj).map_err(|err| {
CommonError::serilization(err, "Failed to serialize object with `bincode`")
}),
Self::Zisk => bincode::serialize(obj).map_err(|err| {
CommonError::serilization(err, "Failed to serialize object with `bincode`")
}),
}
}
pub fn serialize_inputs(&self, inputs: &Input) -> Result<Vec<u8>, CommonError> {
bincode::serialize(
&inputs
.iter()
.map(|input| {
Ok(match input {
InputItem::Object(obj) => self.serialize_object(&**obj)?,
InputItem::Bytes(bytes) => bytes.clone(),
})
})
.collect::<Result<Vec<Vec<u8>>, CommonError>>()?,
)
.map_err(|err| {
CommonError::serilization(err, "Failed to serialize sequence of bytes with `bincode`")
})
}
}

View File

@@ -0,0 +1,525 @@
//! # Ere Dockerized
//!
//! A Docker-based wrapper for other zkVM crates `ere-{zkvm}`.
//!
//! This crate provides a unified interface to dockerize the `Compiler` and
//! `zkVM` implementation of other zkVM crates `ere-{zkvm}`, it requires only
//! `docker` to be installed, but no zkVM specific SDK.
//!
//! ## Example
//!
//! ```rust,no_run
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use ere_dockerized::{EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM};
//! use zkvm_interface::{Compiler, Input, ProverResourceType, zkVM};
//! use std::path::Path;
//!
//! // Compile a guest program
//! let compiler = EreDockerizedCompiler::new(ErezkVM::SP1, "mounting/directory");
//! let guest_path = Path::new("relative/path/to/guest/program");
//! let program = compiler.compile(&guest_path)?;
//!
//! // Create zkVM instance
//! let zkvm = EreDockerizedzkVM::new(
//! ErezkVM::SP1,
//! program,
//! ProverResourceType::Cpu
//! )?;
//!
//! // Prepare inputs
//! let mut inputs = Input::new();
//! inputs.write(42u32);
//! inputs.write(100u16);
//!
//! // Execute program
//! let execution_report = zkvm.execute(&inputs)?;
//! println!("Execution cycles: {}", execution_report.total_num_cycles);
//!
//! // Generate proof
//! let (proof, proving_report) = zkvm.prove(&inputs)?;
//! println!("Proof generated in: {:?}", proving_report.proving_time);
//!
//! // Verify proof
//! zkvm.verify(&proof)?;
//! println!("Proof verified successfully!");
//! # Ok(())
//! # }
//! ```
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use crate::{
docker::{DockerBuildCmd, DockerRunCmd, docker_image_exists},
error::{CommonError, CompileError, DockerizedError, ExecuteError, ProveError, VerifyError},
};
use serde::{Deserialize, Serialize};
use std::{
env,
fmt::{self, Display, Formatter},
fs, iter,
path::{Path, PathBuf},
str::FromStr,
};
use tempfile::TempDir;
use zkvm_interface::{
Compiler, Input, ProgramExecutionReport, ProgramProvingReport, ProverResourceType, zkVM,
zkVMError,
};
include!(concat!(env!("OUT_DIR"), "/crate_version.rs"));
include!(concat!(env!("OUT_DIR"), "/zkvm_sdk_version_impl.rs"));
pub mod docker;
pub mod error;
pub mod input;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ErezkVM {
Jolt,
Nexus,
OpenVM,
Pico,
Risc0,
SP1,
Zisk,
}
impl ErezkVM {
pub fn as_str(&self) -> &'static str {
match self {
Self::Jolt => "jolt",
Self::Nexus => "nexus",
Self::OpenVM => "openvm",
Self::Pico => "pico",
Self::Risc0 => "risc0",
Self::SP1 => "sp1",
Self::Zisk => "zisk",
}
}
pub fn base_tag(&self, version: &str) -> String {
format!("ere-base:{version}")
}
pub fn base_zkvm_tag(&self, version: &str) -> String {
format!("ere-base-{self}:{version}")
}
pub fn cli_zkvm_tag(&self, version: &str) -> String {
format!("ere-cli-{self}:{version}")
}
/// This method builds 3 Docker images in sequence:
/// 1. `ere-base:latest`: Base image with common dependencies
/// 2. `ere-base-{zkvm}:latest`: zkVM-specific base image with the zkVM SDK
/// 3. `ere-cli-{zkvm}:latest`: CLI image with the `ere-cli` binary built with feature `{zkvm}`
///
/// Images are cached and only rebuilt if they don't exist or if the
/// `ERE_FORCE_REBUILD_DOCKER_IMAGE=true` environment variable is set.
pub fn build_docker_image(&self) -> Result<(), CommonError> {
let workspace_dir = workspace_dir();
let force_rebuild = env::var("ERE_FORCE_REBUILD_DOCKER_IMAGE")
.ok()
.and_then(|s| s.parse::<bool>().ok())
.unwrap_or_default();
if force_rebuild || !docker_image_exists(self.base_tag(CRATE_VERSION))? {
DockerBuildCmd::new()
.file(
workspace_dir
.join("docker")
.join("base")
.join("Dockerfile.base"),
)
.tag(self.base_tag(CRATE_VERSION))
.tag(self.base_tag("latest"))
.exec(&workspace_dir)
.map_err(CommonError::DockerBuildCmd)?;
}
if force_rebuild || !docker_image_exists(self.base_zkvm_tag(CRATE_VERSION))? {
DockerBuildCmd::new()
.file(
workspace_dir
.join("docker")
.join(self.as_str())
.join("Dockerfile"),
)
.tag(self.base_zkvm_tag(CRATE_VERSION))
.tag(self.base_zkvm_tag("latest"))
.bulid_arg("BASE_IMAGE_TAG", self.base_tag(CRATE_VERSION))
.exec(&workspace_dir)
.map_err(CommonError::DockerBuildCmd)?;
}
if force_rebuild || !docker_image_exists(self.cli_zkvm_tag(CRATE_VERSION))? {
DockerBuildCmd::new()
.file(workspace_dir.join("docker").join("cli").join("Dockerfile"))
.tag(self.cli_zkvm_tag(CRATE_VERSION))
.tag(self.cli_zkvm_tag("latest"))
.bulid_arg("BASE_ZKVM_IMAGE_TAG", self.base_zkvm_tag(CRATE_VERSION))
.bulid_arg("ZKVM", self.as_str())
.exec(&workspace_dir)
.map_err(CommonError::DockerBuildCmd)?;
}
Ok(())
}
}
impl FromStr for ErezkVM {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"jolt" => Self::Jolt,
"nexus" => Self::Nexus,
"openvm" => Self::OpenVM,
"pico" => Self::Pico,
"risc0" => Self::Risc0,
"sp1" => Self::SP1,
"zisk" => Self::Zisk,
_ => return Err(format!("Unsupported zkvm {s}")),
})
}
}
impl Display for ErezkVM {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub struct EreDockerizedCompiler {
zkvm: ErezkVM,
mount_directory: PathBuf,
}
impl EreDockerizedCompiler {
pub fn new(zkvm: ErezkVM, mount_directory: impl AsRef<Path>) -> Self {
Self {
zkvm,
mount_directory: mount_directory.as_ref().to_path_buf(),
}
}
}
/// Wrapper for serialized program.
#[derive(Clone, Serialize, Deserialize)]
pub struct SerializedProgram(Vec<u8>);
impl Compiler for EreDockerizedCompiler {
type Error = CompileError;
type Program = SerializedProgram;
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
self.zkvm.build_docker_image()?;
let guest_relative_path = guest_directory
.strip_prefix(&self.mount_directory)
.map_err(|_| CompileError::GuestNotInMountingDirecty {
mounting_directory: self.mount_directory.to_path_buf(),
guest_directory: guest_directory.to_path_buf(),
})?;
let guest_path_in_docker = PathBuf::from("/guest").join(guest_relative_path);
let tempdir = TempDir::new()
.map_err(|err| CommonError::io(err, "Failed to create temporary directory"))?;
DockerRunCmd::new(self.zkvm.cli_zkvm_tag(CRATE_VERSION))
.rm()
.volume(&self.mount_directory, "/guest")
.volume(tempdir.path(), "/guest-output")
.exec([
"compile",
"--guest-path",
guest_path_in_docker.to_string_lossy().as_ref(),
"--program-path",
"/guest-output/program",
])
.map_err(CommonError::DockerRunCmd)?;
let program_path = tempdir.path().join("program");
let program = fs::read(&program_path).map_err(|err| {
CommonError::io(
err,
format!(
"Failed to read compiled program at {}",
program_path.display()
),
)
})?;
Ok(SerializedProgram(program))
}
}
pub struct EreDockerizedzkVM {
zkvm: ErezkVM,
program: SerializedProgram,
resource: ProverResourceType,
}
impl EreDockerizedzkVM {
pub fn new(
zkvm: ErezkVM,
program: SerializedProgram,
resource: ProverResourceType,
) -> Result<Self, zkVMError> {
zkvm.build_docker_image()?;
Ok(Self {
zkvm,
program,
resource,
})
}
}
impl zkVM for EreDockerizedzkVM {
fn execute(&self, inputs: &Input) -> Result<ProgramExecutionReport, zkVMError> {
let tempdir = TempDir::new()
.map_err(|err| CommonError::io(err, "Failed to create temporary directory"))
.map_err(|err| DockerizedError::Execute(ExecuteError::Common(err)))?;
fs::write(tempdir.path().join("program"), &self.program.0)
.map_err(|err| CommonError::io(err, "Failed to write program"))
.map_err(|err| DockerizedError::Execute(ExecuteError::Common(err)))?;
fs::write(
tempdir.path().join("input"),
self.zkvm
.serialize_inputs(inputs)
.map_err(|err| DockerizedError::Execute(ExecuteError::Common(err)))?,
)
.map_err(|err| CommonError::io(err, "Failed to write input"))
.map_err(|err| DockerizedError::Execute(ExecuteError::Common(err)))?;
let mut cmd = DockerRunCmd::new(self.zkvm.cli_zkvm_tag(CRATE_VERSION))
.rm()
.volume(tempdir.path(), "/workspace");
if matches!(self.resource, ProverResourceType::Gpu) {
cmd = cmd.gpus("all")
}
cmd.exec(
iter::empty()
.chain([
"execute",
"--program-path",
"/workspace/program",
"--input-path",
"/workspace/input",
"--report-path",
"/workspace/report",
])
.chain(self.resource.to_args()),
)
.map_err(CommonError::DockerRunCmd)
.map_err(|err| DockerizedError::Execute(ExecuteError::Common(err)))?;
let report_bytes = fs::read(tempdir.path().join("report"))
.map_err(|err| CommonError::io(err, "Failed to read report"))
.map_err(|err| DockerizedError::Execute(ExecuteError::Common(err)))?;
let report = bincode::deserialize(&report_bytes)
.map_err(|err| CommonError::serilization(err, "Failed to deserialize report"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
Ok(report)
}
fn prove(&self, inputs: &Input) -> Result<(Vec<u8>, ProgramProvingReport), zkVMError> {
let tempdir = TempDir::new()
.map_err(|err| CommonError::io(err, "Failed to create temporary directory"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
fs::write(tempdir.path().join("program"), &self.program.0)
.map_err(|err| CommonError::io(err, "Failed to write program"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
fs::write(
tempdir.path().join("input"),
self.zkvm
.serialize_inputs(inputs)
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?,
)
.map_err(|err| CommonError::io(err, "Failed to write input"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
let mut cmd = DockerRunCmd::new(self.zkvm.cli_zkvm_tag(CRATE_VERSION))
.rm()
.volume(tempdir.path(), "/workspace");
if matches!(self.resource, ProverResourceType::Gpu) {
cmd = cmd.gpus("all")
}
cmd.exec(
iter::empty()
.chain([
"prove",
"--program-path",
"/workspace/program",
"--input-path",
"/workspace/input",
"--proof-path",
"/workspace/proof",
"--report-path",
"/workspace/report",
])
.chain(self.resource.to_args()),
)
.map_err(CommonError::DockerRunCmd)
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
let proof = fs::read(tempdir.path().join("proof"))
.map_err(|err| CommonError::io(err, "Failed to read proof"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
let report_bytes = fs::read(tempdir.path().join("report"))
.map_err(|err| CommonError::io(err, "Failed to read report"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
let report = bincode::deserialize(&report_bytes)
.map_err(|err| CommonError::serilization(err, "Failed to deserialize report"))
.map_err(|err| DockerizedError::Prove(ProveError::Common(err)))?;
Ok((proof, report))
}
fn verify(&self, proof: &[u8]) -> Result<(), zkVMError> {
let tempdir = TempDir::new()
.map_err(|err| CommonError::io(err, "Failed to create temporary directory"))
.map_err(|err| DockerizedError::Verify(VerifyError::Common(err)))?;
fs::write(tempdir.path().join("program"), &self.program.0)
.map_err(|err| CommonError::io(err, "Failed to write program"))
.map_err(|err| DockerizedError::Verify(VerifyError::Common(err)))?;
fs::write(tempdir.path().join("proof"), proof)
.map_err(|err| CommonError::io(err, "Failed to write proof"))
.map_err(|err| DockerizedError::Verify(VerifyError::Common(err)))?;
DockerRunCmd::new(self.zkvm.cli_zkvm_tag(CRATE_VERSION))
.rm()
.volume(tempdir.path(), "/workspace")
.exec([
"verify",
"--program-path",
"/workspace/program",
"--proof-path",
"/workspace/proof",
])
.map_err(CommonError::DockerRunCmd)
.map_err(|err| DockerizedError::Verify(VerifyError::Common(err)))?;
Ok(())
}
fn name(&self) -> &'static str {
self.zkvm.as_str()
}
fn sdk_version(&self) -> &'static str {
self.zkvm.sdk_version()
}
}
fn workspace_dir() -> PathBuf {
let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
dir.pop();
dir.pop();
dir.canonicalize().unwrap()
}
#[cfg(test)]
mod test {
use crate::{EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM, workspace_dir};
use zkvm_interface::{Compiler, Input, ProverResourceType, zkVM};
// TODO: Test other ere-{zkvm} when they are end-to-end ready:
// - ere-jolt
// - ere-nexus
// - ere-pico
#[test]
fn dockerized_openvm() {
let zkvm = ErezkVM::OpenVM;
let guest_directory = workspace_dir().join(format!("tests/{zkvm}/compile/basic"));
let program = EreDockerizedCompiler::new(zkvm, workspace_dir())
.compile(&guest_directory)
.unwrap();
let zkvm = EreDockerizedzkVM::new(zkvm, program, ProverResourceType::Cpu).unwrap();
let mut inputs = Input::new();
inputs.write(42u64);
let _report = zkvm.execute(&inputs).unwrap();
let (proof, _report) = zkvm.prove(&inputs).unwrap();
zkvm.verify(&proof).unwrap();
}
#[test]
fn dockerized_risc0() {
let zkvm = ErezkVM::Risc0;
let guest_directory = workspace_dir().join(format!("tests/{zkvm}/compile/basic"));
let program = EreDockerizedCompiler::new(zkvm, workspace_dir())
.compile(&guest_directory)
.unwrap();
let zkvm = EreDockerizedzkVM::new(zkvm, program, ProverResourceType::Cpu).unwrap();
let mut inputs = Input::new();
inputs.write(42u32);
let _report = zkvm.execute(&inputs).unwrap();
let (proof, _report) = zkvm.prove(&inputs).unwrap();
zkvm.verify(&proof).unwrap();
}
#[test]
fn dockerized_sp1() {
let zkvm = ErezkVM::SP1;
let guest_directory = workspace_dir().join(format!("tests/{zkvm}/prove/basic"));
let program = EreDockerizedCompiler::new(zkvm, workspace_dir())
.compile(&guest_directory)
.unwrap();
let zkvm = EreDockerizedzkVM::new(zkvm, program, ProverResourceType::Cpu).unwrap();
let mut inputs = Input::new();
inputs.write(42u32);
inputs.write(42u16);
let _report = zkvm.execute(&inputs).unwrap();
let (proof, _report) = zkvm.prove(&inputs).unwrap();
zkvm.verify(&proof).unwrap();
}
#[test]
fn dockerized_zisk() {
let zkvm = ErezkVM::Zisk;
let guest_directory = workspace_dir().join(format!("tests/{zkvm}/prove/basic"));
let program = EreDockerizedCompiler::new(zkvm, workspace_dir())
.compile(&guest_directory)
.unwrap();
let zkvm = EreDockerizedzkVM::new(zkvm, program, ProverResourceType::Cpu).unwrap();
let mut inputs = Input::new();
inputs.write(42u32);
inputs.write(42u16);
let _report = zkvm.execute(&inputs).unwrap();
let (proof, _report) = zkvm.prove(&inputs).unwrap();
zkvm.verify(&proof).unwrap();
}
}

View File

@@ -27,14 +27,9 @@ impl Compiler for JOLT_TARGET {
type Program = Vec<u8>;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
let guest_dir = workspace_directory.join(guest_relative);
fn compile(&self, guest_dir: &Path) -> Result<Self::Program, Self::Error> {
// Change current directory for `Program::build` to build guest program.
set_current_dir(&guest_dir).map_err(|source| CompileError::SetCurrentDirFailed {
set_current_dir(guest_dir).map_err(|source| CompileError::SetCurrentDirFailed {
source,
path: guest_dir.to_path_buf(),
})?;
@@ -153,7 +148,7 @@ pub fn program(elf: &[u8]) -> Result<(TempDir, jolt::host::Program), zkVMError>
#[cfg(test)]
mod tests {
use crate::{EreJolt, JOLT_TARGET};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use zkvm_interface::{Compiler, Input, ProverResourceType, zkVM};
// TODO: for now, we just get one test file
@@ -172,29 +167,29 @@ mod tests {
#[test]
fn test_compile_trait() {
let test_guest_path = get_compile_test_guest_program_path();
let elf = JOLT_TARGET::compile(&test_guest_path, Path::new("")).unwrap();
let elf = JOLT_TARGET.compile(&test_guest_path).unwrap();
assert!(!elf.is_empty(), "elf has not been compiled");
}
#[test]
fn test_execute() {
let test_guest_path = get_compile_test_guest_program_path();
let program = JOLT_TARGET::compile(&test_guest_path, Path::new("")).unwrap();
let elf = JOLT_TARGET.compile(&test_guest_path).unwrap();
let mut inputs = Input::new();
inputs.write(1_u32);
let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap();
let zkvm = EreJolt::new(elf, ProverResourceType::Cpu).unwrap();
zkvm.execute(&inputs).unwrap();
}
#[test]
fn test_prove_verify() {
let test_guest_path = get_compile_test_guest_program_path();
let program = JOLT_TARGET::compile(&test_guest_path, Path::new("")).unwrap();
let elf = JOLT_TARGET.compile(&test_guest_path).unwrap();
let mut inputs = Input::new();
inputs.write(1_u32);
let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap();
let zkvm = EreJolt::new(elf, ProverResourceType::Cpu).unwrap();
let (proof, _) = zkvm.prove(&inputs).unwrap();
zkvm.verify(&proof).unwrap();
}

View File

@@ -29,19 +29,14 @@ impl Compiler for NEXUS_TARGET {
type Program = PathBuf;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
let guest_path = workspace_directory.join(guest_relative);
fn compile(&self, guest_path: &Path) -> Result<Self::Program, Self::Error> {
// 1. Check guest path
if !guest_path.exists() {
return Err(NexusError::PathNotFound(guest_path.to_path_buf()));
}
std::env::set_current_dir(&guest_path).map_err(|e| CompileError::Client(e.into()))?;
std::env::set_current_dir(guest_path).map_err(|e| CompileError::Client(e.into()))?;
let package_name = get_cargo_package_name(&guest_path)
let package_name = get_cargo_package_name(guest_path)
.ok_or(CompileError::Client(Box::from(format!(
"Failed to get guest package name, where guest path: {:?}",
guest_path
@@ -182,7 +177,7 @@ mod tests {
#[test]
fn test_compile() -> anyhow::Result<()> {
let test_guest_path = get_test_guest_program_path();
let elf_path = NEXUS_TARGET::compile(&test_guest_path, Path::new(""))?;
let elf_path = NEXUS_TARGET.compile(&test_guest_path)?;
let prover: Stwo<Local> = Stwo::new_from_file(&elf_path.to_string_lossy().to_string())?;
let elf = prover.elf.clone();
assert!(
@@ -195,8 +190,9 @@ mod tests {
#[test]
fn test_execute() {
let test_guest_path = get_test_guest_program_path();
let elf =
NEXUS_TARGET::compile(&test_guest_path, Path::new("")).expect("compilation failed");
let elf = NEXUS_TARGET
.compile(&test_guest_path)
.expect("compilation failed");
let mut input = Input::new();
input.write(10u64);
@@ -207,7 +203,7 @@ mod tests {
#[test]
fn test_prove_verify() -> anyhow::Result<()> {
let test_guest_path = get_test_guest_program_path();
let elf = NEXUS_TARGET::compile(&test_guest_path, Path::new(""))?;
let elf = NEXUS_TARGET.compile(&test_guest_path)?;
let mut input = Input::new();
input.write(10u64);

View File

@@ -37,9 +37,8 @@ impl Compiler for OPENVM_TARGET {
type Program = OpenVMProgram;
// Inlining `openvm_sdk::Sdk::build` in order to get raw elf bytes.
fn compile(workspace_path: &Path, guest_relative: &Path) -> Result<Self::Program, Self::Error> {
let guest_directory = workspace_path.join(guest_relative);
let pkg = openvm_build::get_package(&guest_directory);
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
let pkg = openvm_build::get_package(guest_directory);
let guest_opts = GuestOptions::default().with_profile("release".to_string());
let target_dir = match openvm_build::build_guest_package(&pkg, &guest_opts, None, &None) {
Ok(target_dir) => target_dir,
@@ -47,7 +46,7 @@ impl Compiler for OPENVM_TARGET {
Err(None) => return Err(CompileError::BuildSkipped.into()),
};
let elf_path = openvm_build::find_unique_executable(&guest_directory, target_dir, &None)
let elf_path = openvm_build::find_unique_executable(guest_directory, target_dir, &None)
.map_err(|e| CompileError::UniqueElfNotFound(e.into()))?;
let elf = fs::read(&elf_path).map_err(|source| CompileError::ReadElfFailed {
source,
@@ -225,8 +224,7 @@ mod tests {
#[test]
fn test_compile() {
let test_guest_path = get_compile_test_guest_program_path();
let program =
OPENVM_TARGET::compile(&test_guest_path, Path::new("")).expect("compilation failed");
let program = OPENVM_TARGET.compile(&test_guest_path).unwrap();
assert!(!program.elf.is_empty(), "ELF bytes should not be empty.");
}
@@ -235,8 +233,7 @@ mod tests {
fn test_execute_empty_input_panic() {
// Panics because the program expects input arguments, but we supply none
let test_guest_path = get_compile_test_guest_program_path();
let program =
OPENVM_TARGET::compile(&test_guest_path, Path::new("")).expect("compilation failed");
let program = OPENVM_TARGET.compile(&test_guest_path).unwrap();
let empty_input = Input::new();
let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap();
@@ -246,8 +243,7 @@ mod tests {
#[test]
fn test_execute() {
let test_guest_path = get_compile_test_guest_program_path();
let program =
OPENVM_TARGET::compile(&test_guest_path, Path::new("")).expect("compilation failed");
let program = OPENVM_TARGET.compile(&test_guest_path).unwrap();
let mut input = Input::new();
input.write(10u64);
@@ -258,8 +254,7 @@ mod tests {
#[test]
fn test_prove_verify() {
let test_guest_path = get_compile_test_guest_program_path();
let program =
OPENVM_TARGET::compile(&test_guest_path, Path::new("")).expect("compilation failed");
let program = OPENVM_TARGET.compile(&test_guest_path).unwrap();
let mut input = Input::new();
input.write(10u64);

View File

@@ -17,12 +17,7 @@ impl Compiler for PICO_TARGET {
type Program = Vec<u8>;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
let guest_path = workspace_directory.join(guest_relative);
fn compile(&self, guest_path: &Path) -> Result<Self::Program, Self::Error> {
// 1. Check guest path
if !guest_path.exists() {
return Err(PicoError::PathNotFound(guest_path.to_path_buf()));
@@ -30,7 +25,7 @@ impl Compiler for PICO_TARGET {
// 2. Run `cargo pico build`
let status = Command::new("cargo")
.current_dir(&guest_path)
.current_dir(guest_path)
.env("RUST_LOG", "info")
.args(["pico", "build"])
.status()?; // From<io::Error> → Spawn
@@ -145,7 +140,7 @@ impl zkVM for ErePico {
#[cfg(test)]
mod tests {
use crate::PICO_TARGET;
use std::{path::Path, path::PathBuf};
use std::path::PathBuf;
use zkvm_interface::Compiler;
fn get_compile_test_guest_program_path() -> PathBuf {
@@ -172,7 +167,7 @@ mod tests {
let test_guest_path = get_compile_test_guest_program_path();
println!("Using test guest path: {}", test_guest_path.display());
match PICO_TARGET::compile(&test_guest_path, Path::new("")) {
match PICO_TARGET.compile(&test_guest_path) {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}

View File

@@ -7,8 +7,9 @@ license.workspace = true
[dependencies]
zkvm-interface = { workspace = true }
build-utils = { workspace = true }
anyhow = "1.0"
# Use `risc0-build` to build package instead of `cargo-risczero` for `unstable` features
risc0-build = { version = "^2.3.0", features = ["unstable"] }
risc0-zkvm = { version = "^2.3.0", features = ["unstable"] }
borsh = "1.5.7"
hex = "*"
@@ -20,6 +21,7 @@ serde = { version = "1.0.219", features = ["derive", "rc"] }
tracing = "0.1"
bytemuck = "1.13"
bincode = "1.3"
cargo_metadata = "0.19"
[build-dependencies]
build-utils = { workspace = true }

View File

@@ -1,80 +1,42 @@
use crate::error::CompileError;
use build_utils::docker;
use cargo_metadata::MetadataCommand;
use risc0_build::GuestOptions;
use risc0_zkvm::Digest;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use tempfile::TempDir;
use std::path::Path;
use tracing::info;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Risc0Program {
// TODO: Seems like the risc0 compilation is also compiling
// TODO: the analogous prover and verifying key
pub(crate) elf: Vec<u8>,
pub(crate) image_id: Digest,
}
pub fn compile_risc0_program(
workspace_directory: &Path,
guest_program_relative: &Path,
) -> Result<Risc0Program, CompileError> {
// Build the SP1 docker image
let tag = "ere-risc0-cli:latest";
docker::build_image(&PathBuf::from("docker/risc0/Dockerfile"), tag)
.map_err(|e| CompileError::DockerImageBuildFailed(Box::new(e)))?;
pub fn compile_risc0_program(guest_directory: &Path) -> Result<Risc0Program, CompileError> {
info!("Compiling Risc0 program at {}", guest_directory.display());
// Prepare paths for compilation
let mount_directory_str = workspace_directory
.to_str()
.ok_or_else(|| CompileError::InvalidMountPath(workspace_directory.to_path_buf()))?;
let metadata = MetadataCommand::new().current_dir(guest_directory).exec()?;
let package = metadata
.root_package()
.ok_or_else(|| CompileError::MissingPackageName {
path: guest_directory.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 =
risc0_build::build_package(package, &metadata.target_directory, GuestOptions::default())
.map_err(|source| CompileError::Risc0BuildFailure {
source,
crate_path: guest_directory.to_path_buf(),
})?
.into_iter()
.next()
.ok_or(CompileError::Risc0BuildMissingGuest)?;
let container_mount_directory = PathBuf::from_str("/guest-workspace").unwrap();
let container_guest_program_path = container_mount_directory.join(guest_program_relative);
let container_guest_program_str = container_guest_program_path
.to_str()
.ok_or_else(|| CompileError::InvalidGuestPath(guest_program_relative.to_path_buf()))?;
let elf = guest.elf.to_vec();
let image_id = guest.image_id;
info!(
"Compiling program: mount_directory={} guest_program={}",
mount_directory_str, container_guest_program_str
);
// Build and run Docker command
let docker_cmd = docker::DockerRunCommand::new(tag)
.remove_after_run()
// Needed by `cargo risczero build` which uses docker in docker.
.with_volume("/var/run/docker.sock", "/var/run/docker.sock")
.with_volume(mount_directory_str, "/guest-workspace")
.with_volume(elf_output_dir_str, "/output")
.with_command(["compile", container_guest_program_str, "/output"]);
let status = docker_cmd
.run()
.map_err(CompileError::DockerCommandFailed)?;
if !status.success() {
return Err(CompileError::DockerContainerRunFailed(status));
}
// 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 image_id = std::fs::read(elf_output_dir.path().join("image_id"))
.and_then(|image_id| {
Digest::try_from(image_id)
.map_err(|image_id| format!("Invalid image id: {image_id:?}"))
.map_err(std::io::Error::other)
})
.map_err(CompileError::ReadImageId)?;
info!("Risc0 program compiled OK - {} bytes", elf.len());
info!("Image ID - {image_id}");
Ok(Risc0Program { elf, image_id })
}
@@ -83,7 +45,7 @@ pub fn compile_risc0_program(
mod tests {
mod compile {
use crate::compile::compile_risc0_program;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
fn get_test_risc0_methods_crate_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
@@ -100,8 +62,8 @@ mod tests {
fn test_compile_risc0_method() {
let test_methods_path = get_test_risc0_methods_crate_path();
let program = compile_risc0_program(&test_methods_path, Path::new(""))
.expect("risc0 compilation failed");
let program =
compile_risc0_program(&test_methods_path).expect("risc0 compilation failed");
assert!(
!program.elf.is_empty(),
"Risc0 ELF bytes should not be empty."

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -9,24 +9,28 @@ pub enum Risc0Error {
#[derive(Debug, Error)]
pub enum CompileError {
#[error("Failed to build Docker image: {0}")]
DockerImageBuildFailed(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[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 mount path: {0}")]
InvalidMountPath(PathBuf),
#[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),
#[error("Failed to read image id: {0}")]
ReadImageId(#[source] std::io::Error),
#[error("Failed to compute image id: {0}")]
ComputeImaegIdFailed(#[source] anyhow::Error),
#[error("{context}: {source}")]
Io {
#[source]
source: io::Error,
context: &'static str,
},
#[error("`cargo metadata` failed: {0}")]
MetadataCommand(#[from] cargo_metadata::Error),
#[error("Could not find `[package].name` in guest Cargo.toml at {path}")]
MissingPackageName { path: PathBuf },
#[error("`risc0_build::build_package` for {crate_path} failed: {source}")]
Risc0BuildFailure {
#[source]
source: anyhow::Error,
crate_path: PathBuf,
},
#[error("`risc0_build::build_package` succeeded but failed to find guest")]
Risc0BuildMissingGuest,
}
impl CompileError {
pub fn io(e: io::Error, context: &'static str) -> Self {
Self::Io { source: e, context }
}
}

View File

@@ -1,11 +1,7 @@
use build_utils::docker;
use crate::error::Risc0Error;
use compile::compile_risc0_program;
use risc0_zkvm::Receipt;
use std::{
fs,
path::{Path, PathBuf},
};
use tempfile::TempDir;
use risc0_zkvm::{ExecutorEnv, ProverOpts, Receipt, default_executor, default_prover};
use std::{path::Path, time::Instant};
use zkvm_interface::{
Compiler, Input, InputItem, ProgramExecutionReport, ProgramProvingReport, ProverResourceType,
zkVM, zkVMError,
@@ -14,10 +10,9 @@ use zkvm_interface::{
include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
mod compile;
pub use compile::Risc0Program;
mod error;
use error::Risc0Error;
pub use compile::Risc0Program;
#[allow(non_camel_case_types)]
pub struct RV32_IM_RISC0_ZKVM_ELF;
@@ -27,11 +22,8 @@ impl Compiler for RV32_IM_RISC0_ZKVM_ELF {
type Program = Risc0Program;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
compile_risc0_program(workspace_directory, guest_relative).map_err(Risc0Error::from)
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
compile_risc0_program(guest_directory).map_err(Risc0Error::from)
}
}
@@ -71,94 +63,55 @@ pub struct EreRisc0 {
impl zkVM for EreRisc0 {
fn execute(&self, inputs: &Input) -> Result<ProgramExecutionReport, zkVMError> {
// Build the Docker image
let tag = "ere-risc0-cli:latest";
docker::build_image(&PathBuf::from("docker/risc0/Dockerfile"), tag)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
// Create temporary directory for file exchange
let temp_dir = TempDir::new().map_err(|e| zkVMError::Other(Box::new(e)))?;
let elf_path = temp_dir.path().join("guest.elf");
let input_path = temp_dir.path().join("input");
let report_path = temp_dir.path().join("report");
// Write ELF file to temp directory
fs::write(&elf_path, &self.program.elf).map_err(|e| zkVMError::Other(Box::new(e)))?;
// Write input bytes to temp directory
fs::write(&input_path, &serialize_input(inputs)?)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
// Run Docker command for execution
let status = docker::DockerRunCommand::new(tag)
.remove_after_run()
.with_volume(temp_dir.path().to_string_lossy().to_string(), "/workspace")
.with_command([
"execute",
"/workspace/guest.elf",
"/workspace/input",
"/workspace/report",
])
.run()
.map_err(|e| zkVMError::Other(Box::new(e)))?;
if !status.success() {
return Err(zkVMError::Other("Docker execution command failed".into()));
let executor = default_executor();
let mut env = ExecutorEnv::builder();
for input in inputs.iter() {
match input {
InputItem::Object(serialize) => {
env.write(serialize).unwrap();
}
InputItem::Bytes(items) => {
env.write_frame(items);
}
}
}
let env = env.build().map_err(|err| zkVMError::Other(err.into()))?;
// Read the execution report from the output file
let report: ProgramExecutionReport = bincode::deserialize(
&fs::read(report_path).map_err(|e| zkVMError::Other(Box::new(e)))?,
)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
Ok(report)
let start = Instant::now();
let session_info = executor
.execute(env, &self.program.elf)
.map_err(|err| zkVMError::Other(err.into()))?;
Ok(ProgramExecutionReport {
total_num_cycles: session_info.cycles() as u64,
execution_duration: start.elapsed(),
..Default::default()
})
}
fn prove(&self, inputs: &Input) -> Result<(Vec<u8>, ProgramProvingReport), zkVMError> {
// Build the Docker image
let tag = "ere-risc0-cli:latest";
docker::build_image(&PathBuf::from("docker/risc0/Dockerfile"), tag)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
// Create temporary directory for file exchange
let temp_dir = TempDir::new().map_err(|e| zkVMError::Other(Box::new(e)))?;
let elf_path = temp_dir.path().join("guest.elf");
let input_path = temp_dir.path().join("input");
let proof_path = temp_dir.path().join("proof");
let report_path = temp_dir.path().join("report");
// Write ELF file to temp directory
fs::write(&elf_path, &self.program.elf).map_err(|e| zkVMError::Other(Box::new(e)))?;
// Write input bytes to temp directory
fs::write(&input_path, &serialize_input(inputs)?)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
// Run Docker command for proving
let status = docker::DockerRunCommand::new(tag)
.remove_after_run()
.with_volume(temp_dir.path().to_string_lossy().to_string(), "/workspace")
.with_command([
"prove",
"/workspace/guest.elf",
"/workspace/input",
"/workspace/proof",
"/workspace/report",
])
.run()
.map_err(|e| zkVMError::Other(Box::new(e)))?;
if !status.success() {
return Err(zkVMError::Other("Docker proving command failed".into()));
let prover = default_prover();
let mut env = ExecutorEnv::builder();
for input in inputs.iter() {
match input {
InputItem::Object(serialize) => {
env.write(serialize).unwrap();
}
InputItem::Bytes(items) => {
env.write_frame(items);
}
}
}
let env = env.build().map_err(|err| zkVMError::Other(err.into()))?;
// Read the proof from the output file
let proof = fs::read(proof_path).map_err(|e| zkVMError::Other(Box::new(e)))?;
let report = bincode::deserialize(
&fs::read(report_path).map_err(|e| zkVMError::Other(Box::new(e)))?,
)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
let now = std::time::Instant::now();
let prove_info = prover
.prove_with_opts(env, &self.program.elf, &ProverOpts::succinct())
.map_err(|err| zkVMError::Other(err.into()))?;
let proving_time = now.elapsed();
Ok((proof, report))
let encoded =
borsh::to_vec(&prove_info.receipt).map_err(|err| zkVMError::Other(Box::new(err)))?;
Ok((encoded, ProgramProvingReport::new(proving_time)))
}
fn verify(&self, proof: &[u8]) -> Result<(), zkVMError> {
@@ -179,25 +132,6 @@ impl zkVM for EreRisc0 {
}
}
// Serialize input bytes in the same way as the `ExecutorEnvBuilder`.
fn serialize_input(inputs: &Input) -> Result<Vec<u8>, zkVMError> {
let mut input_bytes = Vec::new();
for input in inputs.iter() {
match input {
InputItem::Object(serialize) => {
let vec = risc0_zkvm::serde::to_vec(serialize)
.map_err(|e| zkVMError::Other(Box::new(e)))?;
input_bytes.extend_from_slice(bytemuck::cast_slice(&vec));
}
InputItem::Bytes(items) => {
input_bytes.extend_from_slice(&(items.len() as u32).to_le_bytes());
input_bytes.extend_from_slice(items);
}
}
}
Ok(input_bytes)
}
#[cfg(test)]
mod prove_tests {
use std::path::PathBuf;
@@ -218,7 +152,7 @@ mod prove_tests {
fn get_compiled_test_r0_elf_for_prove() -> Result<Risc0Program, Risc0Error> {
let test_guest_path = get_prove_test_guest_program_path();
RV32_IM_RISC0_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
RV32_IM_RISC0_ZKVM_ELF.compile(&test_guest_path)
}
#[test]
@@ -266,7 +200,7 @@ mod execute_tests {
fn get_compiled_test_r0_elf() -> Result<Risc0Program, Risc0Error> {
let test_guest_path = get_execute_test_guest_program_path();
RV32_IM_RISC0_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
RV32_IM_RISC0_ZKVM_ELF.compile(&test_guest_path)
}
fn get_execute_test_guest_program_path() -> PathBuf {

View File

@@ -7,12 +7,12 @@ license.workspace = true
[dependencies]
zkvm-interface = { workspace = true }
build-utils.workspace = true
sp1-sdk = "5.1.0"
tempfile = "3.3"
bincode = "1.3"
thiserror = "2"
tracing = "0.1"
toml.workspace = true
[build-dependencies]
build-utils.workspace = true

View File

@@ -1,65 +1,96 @@
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use build_utils::docker;
use crate::error::CompileError;
use std::{fs, path::Path, process::Command};
use tempfile::TempDir;
use tracing::info;
use crate::error::CompileError;
pub fn compile(guest_directory: &Path) -> Result<Vec<u8>, CompileError> {
info!("Compiling SP1 program at {}", guest_directory.display());
pub fn compile(
workspace_directory: &Path,
guest_program_relative: &Path,
) -> Result<Vec<u8>, 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)))?;
// Prepare paths for compilation
let mount_directory_str = workspace_directory
.to_str()
.ok_or_else(|| CompileError::InvalidMountPath(workspace_directory.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 container_mount_directory = PathBuf::from_str("/guest-workspace").unwrap();
let container_guest_program_path = container_mount_directory.join(guest_program_relative);
let container_guest_program_str = container_guest_program_path
.to_str()
.ok_or_else(|| CompileError::InvalidGuestPath(guest_program_relative.to_path_buf()))?;
info!(
"Compiling program: mount_directory={} guest_program={}",
mount_directory_str, container_guest_program_str
);
// Build and run Docker command
let docker_cmd = docker::DockerRunCommand::new(tag)
.remove_after_run()
.with_volume(mount_directory_str, "/guest-workspace")
.with_volume(elf_output_dir_str, "/output")
.with_command(["./guest-compiler", container_guest_program_str, "/output"]);
let status = docker_cmd
.run()
.map_err(CompileError::DockerCommandFailed)?;
if !status.success() {
return Err(CompileError::DockerContainerRunFailed(status));
if !guest_directory.exists() || !guest_directory.is_dir() {
return Err(CompileError::InvalidProgramPath(
guest_directory.to_path_buf(),
));
}
// 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 guest_manifest_path = guest_directory.join("Cargo.toml");
if !guest_manifest_path.exists() {
return Err(CompileError::CargoTomlMissing {
program_dir: guest_directory.to_path_buf(),
manifest_path: guest_manifest_path.clone(),
});
}
Ok(elf)
// ── 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: toml::Value =
manifest_content
.parse::<toml::Value>()
.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(guest_directory)?;
let temp_output_dir_path = temp_output_dir.path();
info!(
"Running `cargo prove build` → dir: {}",
temp_output_dir_path.display(),
);
let status = Command::new("cargo")
.current_dir(guest_directory)
.args([
"prove",
"build",
"--output-directory",
temp_output_dir_path.to_str().unwrap(),
"--elf-name",
"guest.elf",
])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map_err(|e| CompileError::CargoProveBuild {
cwd: guest_directory.to_path_buf(),
source: e,
})?;
if !status.success() {
return Err(CompileError::CargoBuildFailed {
status,
path: guest_directory.to_path_buf(),
});
}
let elf_path = temp_output_dir_path.join("guest.elf");
if !elf_path.exists() {
return Err(CompileError::ElfNotFound(elf_path));
}
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)
}
#[cfg(test)]
@@ -88,7 +119,7 @@ mod tests {
fn test_compile_sp1_program() {
let test_guest_path = get_compile_test_guest_program_path();
match compile(&test_guest_path, Path::new("")) {
match compile(&test_guest_path) {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}
@@ -101,7 +132,7 @@ mod tests {
#[test]
fn test_compile_trait() {
let test_guest_path = get_compile_test_guest_program_path();
match RV32_IM_SUCCINCT_ZKVM_ELF::compile(&test_guest_path, Path::new("")) {
match RV32_IM_SUCCINCT_ZKVM_ELF.compile(&test_guest_path) {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{path::PathBuf, process::ExitStatus};
use thiserror::Error;
use zkvm_interface::zkVMError;
@@ -27,22 +27,41 @@ pub enum SP1Error {
/// Errors that can be encountered while compiling a SP1 program
#[derive(Debug, Error)]
pub enum CompileError {
#[error("Failed to build Docker image: {0}")]
DockerImageBuildFailed(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[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 mount path: {0}")]
InvalidMountPath(PathBuf),
#[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),
#[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),
}
#[derive(Debug, Error)]

View File

@@ -106,11 +106,8 @@ impl Compiler for RV32_IM_SUCCINCT_ZKVM_ELF {
type Program = Vec<u8>;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
compile::compile(workspace_directory, guest_relative).map_err(SP1Error::from)
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
compile::compile(guest_directory).map_err(SP1Error::from)
}
}
@@ -235,7 +232,7 @@ mod execute_tests {
fn get_compiled_test_sp1_elf() -> Result<Vec<u8>, SP1Error> {
let test_guest_path = get_execute_test_guest_program_path();
RV32_IM_SUCCINCT_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
RV32_IM_SUCCINCT_ZKVM_ELF.compile(&test_guest_path)
}
fn get_execute_test_guest_program_path() -> PathBuf {
@@ -306,7 +303,7 @@ mod prove_tests {
fn get_compiled_test_sp1_elf_for_prove() -> Result<Vec<u8>, SP1Error> {
let test_guest_path = get_prove_test_guest_program_path();
RV32_IM_SUCCINCT_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
RV32_IM_SUCCINCT_ZKVM_ELF.compile(&test_guest_path)
}
#[test]

View File

@@ -154,7 +154,7 @@ mod tests {
#[test]
fn test_compile_trait() {
let test_guest_path = get_compile_test_guest_program_path();
match RV64_IMA_ZISK_ZKVM_ELF::compile(&test_guest_path, Path::new("")) {
match RV64_IMA_ZISK_ZKVM_ELF.compile(&test_guest_path) {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}

View File

@@ -30,11 +30,8 @@ impl Compiler for RV64_IMA_ZISK_ZKVM_ELF {
type Program = Vec<u8>;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
compile_zisk_program(&workspace_directory.join(guest_relative)).map_err(ZiskError::Compile)
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
compile_zisk_program(guest_directory).map_err(ZiskError::Compile)
}
}
@@ -448,7 +445,7 @@ mod execute_tests {
fn get_compiled_test_zisk_elf() -> Result<Vec<u8>, ZiskError> {
let test_guest_path = get_execute_test_guest_program_path();
RV64_IMA_ZISK_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
RV64_IMA_ZISK_ZKVM_ELF.compile(&test_guest_path)
}
fn get_execute_test_guest_program_path() -> PathBuf {
@@ -514,7 +511,7 @@ mod prove_tests {
fn get_compiled_test_zisk_elf_for_prove() -> Result<Vec<u8>, ZiskError> {
let test_guest_path = get_prove_test_guest_program_path();
RV64_IMA_ZISK_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
RV64_IMA_ZISK_ZKVM_ELF.compile(&test_guest_path)
}
#[test]

View File

@@ -20,10 +20,8 @@ pub trait Compiler {
/// Compiles the program and returns the program
///
/// # Arguments
/// * `mount_directory` - The base directory (workspace root)
/// * `guest_relative` - The relative path from mount_directory to the guest program
fn compile(mount_directory: &Path, guest_relative: &Path)
-> Result<Self::Program, Self::Error>;
/// * `guest_directory` - The path to the guest program directory
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error>;
}
/// ResourceType specifies what resource will be used to create the proofs.
@@ -37,6 +35,19 @@ pub enum ProverResourceType {
Network(NetworkProverConfig),
}
#[cfg(feature = "clap")]
impl ProverResourceType {
pub fn to_args(&self) -> Vec<&str> {
match self {
Self::Cpu => vec!["cpu"],
Self::Gpu => vec!["gpu"],
Self::Network(config) => core::iter::once("network")
.chain(config.to_args())
.collect(),
}
}
}
/// An error that can occur during prove, execute or verification
/// of a zkVM.
///

View File

@@ -12,3 +12,13 @@ pub struct NetworkProverConfig {
/// Optional API key for authentication
pub api_key: Option<String>,
}
#[cfg(feature = "clap")]
impl NetworkProverConfig {
pub fn to_args(&self) -> Vec<&str> {
core::iter::once(["--endpoint", self.endpoint.as_str()])
.chain(self.api_key.as_deref().map(|val| ["--api-key", val]))
.flatten()
.collect()
}
}

16
docker/cli/Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
ARG BASE_ZKVM_IMAGE_TAG=ere-base-zkvm:latest
FROM ${BASE_ZKVM_IMAGE_TAG}
COPY . /ere
WORKDIR /ere
ARG ZKVM
RUN cargo build --release --package ere-cli --bin ere-cli --features ${ZKVM} && \
cp /ere/target/release/ere-cli /ere/ere-cli && \
cargo clean && \
rm -rf $CARGO_HOME/registry/src $CARGO_HOME/registry/cache
ENTRYPOINT ["/ere/ere-cli"]

View File

@@ -1,5 +1,6 @@
ARG BASE_IMAGE_TAG=latest
FROM ere-base:${BASE_IMAGE_TAG}
ARG BASE_IMAGE_TAG=ere-base:latest
FROM ${BASE_IMAGE_TAG}
# The ere-base image provides Rust, Cargo (with a default nightly), and common tools.
# We operate as root for SDK installation.
@@ -17,4 +18,4 @@ RUN /tmp/install_jolt_sdk.sh && rm /tmp/install_jolt_sdk.sh # Clean up the scrip
# Verify jolt CLI is accessible.
RUN jolt --version
CMD ["/bin/bash"]
CMD ["/bin/bash"]

View File

@@ -1,5 +1,6 @@
ARG BASE_IMAGE_TAG=latest
FROM ere-base:${BASE_IMAGE_TAG}
ARG BASE_IMAGE_TAG=ere-base:latest
FROM ${BASE_IMAGE_TAG}
# The ere-base image provides Rust, Cargo, and common tools.
# We operate as root for SDK installation.
@@ -20,21 +21,10 @@ RUN /tmp/install_nexus_sdk.sh && rm /tmp/install_nexus_sdk.sh # Clean up the scr
# Define the Nexus toolchain for convenience in subsequent commands if needed, though cargo-nexus should use it.
ENV NEXUS_TOOLCHAIN_VERSION="nightly-2025-06-05"
# Set default toolchain
RUN rustup default "$NEXUS_TOOLCHAIN_VERSION"
# Verify Nexus installation
RUN echo "Verifying Nexus installation in Dockerfile (post-script)..." && cargo-nexus --version
# Copy the entire ere project context
# The WORKDIR is /app from the base image
WORKDIR /app
COPY . .
# Build
RUN echo "Build tests for ere-nexus library..." && \
cargo build --tests --release -p ere-nexus
# Run tests
RUN echo "Running tests for ere-nexus library..." && \
cargo test --release -p ere-nexus --lib -- --color always && \
echo "Running Nexus tests Success..."
CMD ["/bin/bash"]
CMD ["/bin/bash"]

View File

@@ -1,5 +1,6 @@
ARG BASE_IMAGE_TAG=latest
FROM ere-base:${BASE_IMAGE_TAG}
ARG BASE_IMAGE_TAG=ere-base:latest
FROM ${BASE_IMAGE_TAG}
# The ere-base image provides Rust, Cargo, and common tools.
# We operate as root for SDK installation.
@@ -20,13 +21,4 @@ ENV OPENVM_TOOLCHAIN_VERSION="nightly-2025-02-14"
# Verify cargo-openvm is accessible with the correct toolchain
RUN cargo "+${OPENVM_TOOLCHAIN_VERSION}" openvm --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-openvm library..." && \
cargo test --release -p ere-openvm --lib -- --color always
CMD ["/bin/bash"]
CMD ["/bin/bash"]

View File

@@ -1,5 +1,6 @@
ARG BASE_IMAGE_TAG=latest
FROM ere-base:${BASE_IMAGE_TAG}
ARG BASE_IMAGE_TAG=ere-base:latest
FROM ${BASE_IMAGE_TAG}
# The ere-base image provides Rust, Cargo, and common tools.
# We operate as root for SDK installation.
@@ -19,16 +20,10 @@ RUN /tmp/install_pico_sdk.sh && rm /tmp/install_pico_sdk.sh # Clean up the scrip
# Define the Pico toolchain for convenience in subsequent commands if needed, though cargo pico should use it.
ENV PICO_TOOLCHAIN_VERSION="nightly-2024-11-27"
# Set default toolchain
RUN rustup default "$PICO_TOOLCHAIN_VERSION"
# Verify Pico installation
RUN echo "Verifying Pico installation in Dockerfile (post-script)..." && cargo "+${PICO_TOOLCHAIN_VERSION}" pico --version
RUN echo "Verifying Pico installation in Dockerfile (post-script)..." && cargo pico --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-pico library..." && \
cargo "+${PICO_TOOLCHAIN_VERSION}" test --release -p ere-pico --lib -- --color always
CMD ["/bin/bash"]
CMD ["/bin/bash"]

View File

@@ -1,21 +0,0 @@
[package]
name = "risc0-cli"
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
hex.workspace = true
zkvm-interface.workspace = true
risc0-zkvm = { version = "^2.3.0", features = ["unstable"] }
borsh = "1.5"
bincode = "1.3"
[lints]
workspace = true

View File

@@ -1,14 +1,6 @@
ARG BASE_IMAGE_TAG=latest
ARG BASE_IMAGE_TAG=ere-base:latest
FROM rust:1.85 AS builder
WORKDIR /risc0-cli
# Build `risc0-cli`
COPY . .
RUN cargo build --release -p risc0-cli
FROM ere-base:${BASE_IMAGE_TAG}
FROM ${BASE_IMAGE_TAG}
# Copy and run the Risc0 SDK installer script
COPY scripts/sdk_installers/install_risc0_sdk.sh /tmp/install_risc0_sdk.sh
@@ -22,8 +14,4 @@ RUN echo "Verifying Risc0 installation in Dockerfile (post-script)..." && cargo
# Get docker for `cargo risczero build`
RUN curl -fsSL https://get.docker.com | sh
# Copy guest compiler binary
COPY --from=builder /risc0-cli/target/release/risc0-cli /risc0-cli/risc0-cli
# Set entrypoint to `risc0-cli`
ENTRYPOINT ["/risc0-cli/risc0-cli"]
CMD ["/bin/bash"]

View File

@@ -1,289 +0,0 @@
use anyhow::Context;
use clap::{Parser, Subcommand};
use risc0_zkvm::{ExecutorEnv, ProverOpts, default_executor, default_prover};
use std::{fs, path::PathBuf, process::Command};
use toml::Value as TomlValue;
use tracing::info;
use zkvm_interface::{ProgramExecutionReport, ProgramProvingReport};
#[derive(Parser)]
#[command(author, version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Compile a guest program
Compile {
/// Path to the guest program crate directory.
guest_folder: PathBuf,
/// Output folder where compiled `guest.elf` and `image_id` will be placed.
output_folder: PathBuf,
},
/// Execute a compiled program
Execute {
/// Path to the compiled ELF file
elf_path: PathBuf,
/// Path to the serialized input bytes file
input_path: PathBuf,
/// Path where the execution report will be written
report_path: PathBuf,
},
/// Prove execution of a compiled program
Prove {
/// Path to the compiled ELF file
elf_path: PathBuf,
/// Path to the serialized input bytes file
input_path: PathBuf,
/// Path where the proof will be written
proof_path: PathBuf,
/// Path where the report will be written
report_path: PathBuf,
},
}
pub fn main() -> anyhow::Result<()> {
let args = Cli::parse();
match args.command {
Commands::Compile {
guest_folder,
output_folder,
} => compile(guest_folder, output_folder),
Commands::Prove {
elf_path,
input_path,
proof_path,
report_path,
} => prove(elf_path, input_path, proof_path, report_path),
Commands::Execute {
elf_path,
input_path,
report_path,
} => execute(elf_path, input_path, report_path),
}
}
fn compile(guest_folder: PathBuf, output_folder: PathBuf) -> anyhow::Result<()> {
let dir = guest_folder;
info!("Compiling Risc0 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::<TomlValue>().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 risczero build` → dir: {}",
output_folder.display()
);
let output = Command::new("cargo")
.current_dir(&dir)
.args(["risczero", "build"])
.stderr(std::process::Stdio::inherit())
.output()
.with_context(|| {
format!(
"Failed to execute `cargo risczer build` in {}",
dir.display()
)
})?;
if !output.status.success() {
anyhow::bail!(
"Failed to execute `cargo risczero build` in {}",
dir.display()
)
}
let (image_id, elf_path) = {
let stdout = String::from_utf8_lossy(&output.stdout);
let line = stdout
.lines()
.find(|line| line.starts_with("ImageID: "))
.unwrap();
let (image_id, elf_path) = line
.trim_start_matches("ImageID: ")
.split_once(" - ")
.unwrap();
(image_id.to_string(), PathBuf::from(elf_path))
};
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!("Risc0 program compiled OK - {} bytes", elf_bytes.len());
info!("Image ID - {image_id}");
fs::copy(&elf_path, output_folder.join("guest.elf")).with_context(|| {
format!(
"Failed to copy elf file from {} to {}",
elf_path.display(),
output_folder.join("guest.elf").display()
)
})?;
fs::write(output_folder.join("image_id"), hex::decode(image_id)?).with_context(|| {
format!(
"Failed to write image id to {}",
output_folder.join("image_id").display()
)
})?;
Ok(())
}
fn execute(elf_path: PathBuf, input_path: PathBuf, report_path: PathBuf) -> anyhow::Result<()> {
info!("Starting execution for ELF at {}", elf_path.display());
// Read the ELF file
let elf = fs::read(&elf_path)
.with_context(|| format!("Failed to read ELF file at {}", elf_path.display()))?;
// Read the serialized input bytes
let input_bytes = fs::read(&input_path)
.with_context(|| format!("Failed to read input bytes at {}", input_path.display()))?;
info!("ELF size: {} bytes", elf.len());
info!("Input size: {} bytes", input_bytes.len());
// Create executor environment using write_slice to write the serialized input bytes directly
let executor = default_executor();
let env = ExecutorEnv::builder()
.write_slice(&input_bytes)
.build()
.context("Failed to build executor environment")?;
info!("Starting execution...");
let start = std::time::Instant::now();
// Execute the program
let session_info = executor
.execute(env, &elf)
.context("Failed to execute program")?;
let execution_duration = start.elapsed();
info!("Execution completed in {:?}", execution_duration);
info!("Total cycles: {}", session_info.cycles());
// Create execution report
let report = ProgramExecutionReport {
total_num_cycles: session_info.cycles() as u64,
execution_duration,
..Default::default()
};
// Serialize and write the report
let report_bytes =
bincode::serialize(&report).context("Failed to serialize execution report")?;
fs::write(&report_path, report_bytes)
.with_context(|| format!("Failed to write report to {}", report_path.display()))?;
info!("Execution report written to {}", report_path.display());
Ok(())
}
fn prove(
elf_path: PathBuf,
input_path: PathBuf,
proof_path: PathBuf,
report_path: PathBuf,
) -> anyhow::Result<()> {
info!(
"Starting proof generation for ELF at {}",
elf_path.display()
);
// Read the ELF file
let elf = fs::read(&elf_path)
.with_context(|| format!("Failed to read ELF file at {}", elf_path.display()))?;
// Read the serialized input bytes
let input_bytes = fs::read(&input_path)
.with_context(|| format!("Failed to read input bytes at {}", input_path.display()))?;
info!("ELF size: {} bytes", elf.len());
info!("Input size: {} bytes", input_bytes.len());
// Create prover environment using write_slice to write the serialized input bytes directly
let prover = default_prover();
let env = ExecutorEnv::builder()
.write_slice(&input_bytes)
.build()
.context("Failed to build executor environment")?;
info!("Starting proof generation...");
let now = std::time::Instant::now();
// Generate proof
let prove_info = prover
.prove_with_opts(env, &elf, &ProverOpts::succinct())
.context("Failed to generate proof")?;
let proving_time = now.elapsed();
info!("Proof generation completed in {:?}", proving_time);
// Serialize and write the proof
let proof_bytes = borsh::to_vec(&prove_info.receipt).context("Failed to serialize proof")?;
fs::write(&proof_path, proof_bytes)
.with_context(|| format!("Failed to write proof to {}", proof_path.display()))?;
let report_bytes = bincode::serialize(&ProgramProvingReport::new(proving_time))
.context("Failed to serialize report")?;
fs::write(&report_path, report_bytes)
.with_context(|| format!("Failed to write report to {}", report_path.display()))?;
info!("Proof written to {}", proof_path.display());
info!("Report written to {}", report_path.display());
Ok(())
}

View File

@@ -1,16 +0,0 @@
[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

View File

@@ -1,23 +1,9 @@
ARG BASE_IMAGE_TAG=latest
ARG BASE_IMAGE_TAG=ere-base: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}
FROM ${BASE_IMAGE_TAG}
ARG USERNAME=ere_user
# Ensure Cargo/Rustup environment variables are set from the base image for SDK script
# TODO: These should be inherited from ere-base.
ENV RUSTUP_HOME=${RUSTUP_HOME:-/usr/local/rustup} \
CARGO_HOME=${CARGO_HOME:-/usr/local/cargo} \
PATH=${PATH:-/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin}
# Copy the SP1 SDK installer script
COPY scripts/sdk_installers/install_sp1_sdk.sh /tmp/install_sp1_sdk.sh
RUN chmod +x /tmp/install_sp1_sdk.sh
@@ -41,11 +27,7 @@ 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 guest compiler binary
COPY --from=builder /guest-compiler/target/release/sp1-guest-compiler /guest-compiler/guest-compiler
WORKDIR /guest-compiler
CMD ["/bin/bash"]
CMD ["/bin/bash"]
# TODO: Maybe we use root to install it in ere_user and then switch back to ere_user for security
# USER ${USERNAME} # Switch to non-root user again
# USER ${USERNAME} # Switch to non-root user again

View File

@@ -1,103 +0,0 @@
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::<TomlValue>().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(())
}

View File

@@ -1,18 +1,18 @@
ARG BASE_IMAGE_TAG=latest
FROM ere-base:${BASE_IMAGE_TAG}
ARG BASE_IMAGE_TAG=ere-base:latest
FROM ${BASE_IMAGE_TAG}
# The ere-base image provides Rust, Cargo, and common tools.
# ZisK requires Ubuntu 22.04 or higher (ere-base uses 22.04 by default).
# ZisK requires Ubuntu 22.04 or higher (ere-base uses 24.04 by default).
# We operate as root for SDK and dependency installation.
# Install ZisK system dependencies (for Ubuntu)
# Taken from https://0xpolygonhermez.github.io/zisk/getting_started/installation.html
RUN apt-get update && apt-get install -y --no-install-recommends \
xz-utils \
jq \
# build-essential is in ere-base
# jq is in ere-base
# curl is in ere-base
# git is in ere-base
# build-essential is in ere-base
qemu-system \
libomp-dev \
libgmp-dev \
@@ -28,7 +28,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
openmpi-bin \
openmpi-common \
libclang-dev \
clang
clang && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN wget https://developer.download.nvidia.com/compute/cuda/repos/$(. /etc/os-release && echo "${ID}${VERSION_ID}" | tr -d '.')/$(uname -i)/cuda-keyring_1.1-1_all.deb && \
dpkg -i cuda-keyring_1.1-1_all.deb && \
@@ -37,14 +38,21 @@ RUN wget https://developer.download.nvidia.com/compute/cuda/repos/$(. /etc/os-re
apt install -y cuda-toolkit && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# If current environment is in CI or not.
ARG CI
# Copy the ZisK SDK installer script from the workspace context
COPY scripts/sdk_installers/install_zisk_sdk.sh /tmp/install_zisk_sdk.sh
RUN chmod +x /tmp/install_zisk_sdk.sh
# Run the ZisK SDK installation script using ziskup.
# This script installs the 'zisk' Rust toolchain and cargo-zisk.
# TODO: Download the proving key if the CI runner has enough disk space.
RUN SETUP_KEY=verify /tmp/install_zisk_sdk.sh && rm /tmp/install_zisk_sdk.sh # Clean up the script
# This script installs the 'zisk' Rust toolchain and `cargo-zisk`
#
# If argument `CI` is set, we only install verifying key, this is used by github
# CI runner which only has small disk space (proving key requires ~35 GB).
RUN if [ -n "$CI" ]; then export SETUP_KEY=verify; fi && \
/tmp/install_zisk_sdk.sh && \
rm /tmp/install_zisk_sdk.sh # Clean up the script
# The 'zisk' Rust toolchain is now installed.
# cargo-zisk is installed in /root/.zisk/bin.
@@ -56,16 +64,4 @@ ENV PATH="${PATH}:${ZISK_BIN_DIR}"
# Verify cargo-zisk is accessible
RUN echo "Verifying Zisk installation in Dockerfile ..." && cargo-zisk --version
# Copy the entire ere project context
# The WORKDIR is /app from the base image
WORKDIR /app
COPY . .
# Run only compile and execution test, because proving requires ~31 GiB disk
# space for the provingKey.
# TODO: Run all tests if the CI runner has enough disk space to install the proving key.
RUN echo "Running tests for ere-zisk library..." && \
rm -rf ~/.zisk/provingKey && \
cargo test --release -p ere-zisk --lib -- --color always compile::tests execute_tests
CMD ["/bin/bash"]
CMD ["/bin/bash"]

View File

@@ -77,6 +77,13 @@ else
fi
# Step 4: Make sure `lib-c`'s build script is ran.
#
# `ziskos` provides guest program runtime, and `lib-c` is a dependency of `ziskos`,
# when we need to compile guest, the `build.rs` of `lib-c` will need to be ran once,
# but if there are multiple `build.rs` running at the same time, it will panic.
# So here we make sure it's already ran, and the built thing will be stored in
# `$CARGO_HOME/git/checkouts/zisk-{hash}/{rev}/lib-c/c/build`, so could be
# re-used as long as the `ziskos` has the same version.
TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"
cargo init . --name build-lib-c