Risc0 docker compilation (#58)

This commit is contained in:
Han
2025-07-23 18:50:58 +08:00
committed by GitHub
parent bc3d99fa1b
commit 21e2c161de
29 changed files with 894 additions and 836 deletions

View File

@@ -1 +1 @@
/target
**/target

View File

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

View File

@@ -1,33 +0,0 @@
name: Test Risc0 (Docker)
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test-risc0-via-docker-build:
name: Build Risc0 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-risc0 image
run: |
docker build \
--tag ere-builder-risc0:latest \
--file docker/risc0/Dockerfile .

31
Cargo.lock generated
View File

@@ -2336,18 +2336,21 @@ dependencies = [
]
[[package]]
name = "ere-risczero"
name = "ere-risc0"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"borsh",
"build-utils",
"bytemuck",
"hex",
"risc0-zkvm",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.12",
"tracing",
"zkvm-interface",
]
@@ -4052,9 +4055,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
dependencies = [
"libc",
]
@@ -7804,6 +7807,22 @@ 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"
@@ -8105,7 +8124,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -9186,9 +9205,9 @@ dependencies = [
[[package]]
name = "sppark"
version = "0.1.11"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16bf457036c0a778140ce4c3bcf9ff30c5c70a9d9c0bb04fe513af025b647b2c"
checksum = "6bdc4f02f557e3037bbe2a379cac8be6e014a67beb7bf0996b536979392f6361"
dependencies = [
"cc",
"which",

View File

@@ -6,7 +6,7 @@ members = [
"crates/ere-nexus",
"crates/ere-openvm",
"crates/ere-pico",
"crates/ere-risczero",
"crates/ere-risc0",
"crates/ere-sp1",
"crates/ere-zisk",
# zkVM interface
@@ -14,6 +14,7 @@ members = [
# Guest compilers
"docker/sp1",
"docker/risc0",
]
resolver = "2"
@@ -31,6 +32,7 @@ tempfile = "3.3"
toml = "0.8"
clap = { version = "4.5.41", features = ["derive"] }
anyhow = "1.0"
hex = "0.4.3"
# local dependencies
zkvm-interface = { path = "crates/zkvm-interface" }

View File

@@ -1,29 +0,0 @@
# Heavily inspired by Reth: https://github.com/paradigmxyz/reth/blob/4c39b98b621c53524c6533a9c7b52fc42c25abd6/Makefile
.DEFAULT_GOAL := help
##@ Help
.PHONY: help
help: # Display this help.
@awk 'BEGIN {FS = ":.*#"; printf "Usage:\n make \033[34m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?#/ { printf " \033[34m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
##@ Build
.PHONY: build
build: # Build the Ream binary into `target` directory.
@cargo build --verbose --release
##@ Lint
.PHONY: clean
clean: # Run `cargo clean`.
@cargo clean
.PHONY: lint pr
lint: # Run `clippy` and `rustfmt`.
cargo +nightly fmt --all
cargo clippy --all --all-targets --no-deps -- --deny warnings
# clippy for bls with supranational feature
cargo clippy --all-targets --no-deps -- --deny warnings
# cargo sort
cargo sort --grouped

View File

@@ -6,6 +6,18 @@ use std::{
use thiserror::Error;
use tracing::info;
#[derive(Debug, Error)]
pub enum Error {
#[error("Invalid Dockerfile path: {0}")]
InvalidDockerfilePath(PathBuf),
#[error("Docker image build failed: {0}")]
DockerBuildFailed(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Docker image build failed")]
ImageBuildFailed,
#[error("Docker is not available. Please ensure Docker is installed and running.")]
DockerIsNotAvailable,
}
pub fn build_image(compiler_dockerfile: &Path, tag: &str) -> Result<(), Error> {
// Check that Docker is installed and available
if Command::new("docker")
@@ -72,14 +84,62 @@ pub fn build_image(compiler_dockerfile: &Path, tag: &str) -> Result<(), Error> {
Ok(())
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Invalid Dockerfile path: {0}")]
InvalidDockerfilePath(PathBuf),
#[error("Docker image build failed: {0}")]
DockerBuildFailed(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Docker image build failed")]
ImageBuildFailed,
#[error("Docker is not available. Please ensure Docker is installed and running.")]
DockerIsNotAvailable,
#[derive(Debug)]
pub struct DockerRunCommand {
image: String,
volumes: Vec<(String, String)>, // (host_path, container_path)
command: Vec<String>,
// remove image after running
remove_after: bool,
}
impl DockerRunCommand {
pub fn new(image: impl Into<String>) -> Self {
Self {
image: image.into(),
volumes: Vec::new(),
command: Vec::new(),
remove_after: false,
}
}
pub fn with_volume(
mut self,
host_path: impl Into<String>,
container_path: impl Into<String>,
) -> Self {
self.volumes.push((host_path.into(), container_path.into()));
self
}
pub fn with_command(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.command.extend(args.into_iter().map(|s| s.into()));
self
}
pub fn remove_after_run(mut self) -> Self {
self.remove_after = true;
self
}
pub fn to_args(&self) -> Vec<String> {
let mut args = vec!["run".to_string()];
if self.remove_after {
args.push("--rm".to_string());
}
for (host_path, container_path) in &self.volumes {
args.extend(["-v".to_string(), format!("{host_path}:{container_path}")]);
}
args.push(self.image.clone());
args.extend(self.command.iter().cloned());
args
}
pub fn run(&self) -> Result<std::process::ExitStatus, std::io::Error> {
Command::new("docker").args(self.to_args()).status()
}
}

View File

@@ -1,5 +1,5 @@
[package]
name = "ere-risczero"
name = "ere-risc0"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
@@ -7,9 +7,9 @@ license.workspace = true
[dependencies]
zkvm-interface = { workspace = true }
anyhow = "1.0" #TODO: remove only needed in tests
#toml = "0.8"
risc0-zkvm = { version = "2.3.0", features = ["unstable"] }
build-utils = { workspace = true }
anyhow = "1.0"
risc0-zkvm = { version = "^2.3.0", features = ["unstable"] }
borsh = "1.5.7"
hex = "*"
@@ -17,6 +17,9 @@ tempfile = "3.3"
serde_json = "1.0"
thiserror = "2"
serde = { version = "1.0.219", features = ["derive", "rc"] }
tracing = "0.1"
bytemuck = "1.13"
bincode = "1.3"
[build-dependencies]
build-utils = { workspace = true }

View File

@@ -0,0 +1,111 @@
use crate::error::CompileError;
use build_utils::docker;
use risc0_zkvm::Digest;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use tempfile::TempDir;
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)))?;
// 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()
// 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)?;
Ok(Risc0Program { elf, image_id })
}
#[cfg(test)]
mod tests {
mod compile {
use crate::compile::compile_risc0_program;
use std::path::{Path, PathBuf};
fn get_test_risc0_methods_crate_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("risc0")
.join("compile")
.join("basic")
.canonicalize()
.expect("Failed to find or canonicalize test Risc0 methods crate")
}
#[test]
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");
assert!(
!program.elf.is_empty(),
"Risc0 ELF bytes should not be empty."
);
}
}
}

View File

@@ -0,0 +1,32 @@
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Risc0Error {
#[error(transparent)]
Compile(#[from] CompileError),
}
#[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),
}

313
crates/ere-risc0/src/lib.rs Normal file
View File

@@ -0,0 +1,313 @@
use build_utils::docker;
use compile::compile_risc0_program;
use risc0_zkvm::Receipt;
use std::{
fs,
path::{Path, PathBuf},
};
use tempfile::TempDir;
use zkvm_interface::{
Compiler, Input, InputItem, ProgramExecutionReport, ProgramProvingReport, ProverResourceType,
zkVM, zkVMError,
};
include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
mod compile;
pub use compile::Risc0Program;
mod error;
use error::Risc0Error;
#[allow(non_camel_case_types)]
pub struct RV32_IM_RISC0_ZKVM_ELF;
impl Compiler for RV32_IM_RISC0_ZKVM_ELF {
type Error = Risc0Error;
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)
}
}
impl EreRisc0 {
pub fn new(
program: <RV32_IM_RISC0_ZKVM_ELF as Compiler>::Program,
resource_type: ProverResourceType,
) -> Self {
match resource_type {
ProverResourceType::Cpu => {
#[cfg(any(feature = "cuda", feature = "metal"))]
panic!("CPU mode requires both 'cuda' and 'metal' features to be disabled");
}
ProverResourceType::Gpu => {
#[cfg(not(any(feature = "cuda", feature = "metal")))]
panic!("GPU selected but neither 'cuda' nor 'metal' feature is enabled");
}
ProverResourceType::Network(_) => {
panic!(
"Network proving not yet implemented for RISC Zero. Use CPU or GPU resource type."
);
}
}
Self {
program,
resource_type,
}
}
}
pub struct EreRisc0 {
program: <RV32_IM_RISC0_ZKVM_ELF as Compiler>::Program,
#[allow(dead_code)]
resource_type: ProverResourceType,
}
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()));
}
// 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)
}
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()));
}
// 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)))?;
Ok((proof, report))
}
fn verify(&self, proof: &[u8]) -> Result<(), zkVMError> {
let decoded: Receipt =
borsh::from_slice(proof).map_err(|err| zkVMError::Other(Box::new(err)))?;
decoded
.verify(self.program.image_id)
.map_err(|err| zkVMError::Other(Box::new(err)))
}
fn name(&self) -> &'static str {
NAME
}
fn sdk_version(&self) -> &'static str {
SDK_VERSION
}
}
// 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;
use super::*;
use zkvm_interface::Input;
fn get_prove_test_guest_program_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("risc0")
.join("compile")
.join("basic")
.canonicalize()
.expect("Failed to find or canonicalize test Risc0 methods crate")
}
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(""))
}
#[test]
fn test_prove_r0_dummy_input() {
let program = get_compiled_test_r0_elf_for_prove().unwrap();
let mut input_builder = Input::new();
let n: u32 = 42;
let a: u16 = 42;
input_builder.write(n);
input_builder.write(a);
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu);
let (proof_bytes, _) = zkvm
.prove(&input_builder)
.unwrap_or_else(|err| panic!("Proving error in test: {err:?}"));
assert!(!proof_bytes.is_empty(), "Proof bytes should not be empty.");
let verify_results = zkvm.verify(&proof_bytes).is_ok();
assert!(verify_results);
// TODO: Check public inputs
}
#[test]
fn test_prove_r0_fails_on_bad_input_causing_execution_failure() {
let elf_bytes = get_compiled_test_r0_elf_for_prove().unwrap();
let empty_input = Input::new();
let zkvm = EreRisc0::new(elf_bytes, ProverResourceType::Cpu);
let prove_result = zkvm.prove(&empty_input);
assert!(prove_result.is_err());
}
}
#[cfg(test)]
mod execute_tests {
use std::path::PathBuf;
use super::*;
use zkvm_interface::Input;
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(""))
}
fn get_execute_test_guest_program_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("risc0")
.join("compile")
.join("basic")
.canonicalize()
.expect("Failed to find or canonicalize test Risc0 methods crate")
}
#[test]
fn test_execute_r0_dummy_input() {
let program = get_compiled_test_r0_elf().unwrap();
let mut input_builder = Input::new();
let n: u32 = 42;
let a: u16 = 42;
input_builder.write(n);
input_builder.write(a);
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu);
zkvm.execute(&input_builder)
.unwrap_or_else(|err| panic!("Execution error: {err:?}"));
}
#[test]
fn test_execute_r0_no_input_for_guest_expecting_input() {
let program = get_compiled_test_r0_elf().unwrap();
let empty_input = Input::new();
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu);
let result = zkvm.execute(&empty_input);
assert!(
result.is_err(),
"execute should fail if guest expects input but none is provided."
);
}
}

View File

@@ -1,62 +0,0 @@
// This is ere-risczero/build_script_template.rs
// This script will be temporarily copied as build.rs into the target methods crate.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
#[derive(Debug)]
struct GuestMethodInfo {
name: String,
elf_path: String, // Path to the ELF in OUT_DIR, as determined by risc0_build
image_id_hex: String,
}
fn main() {
let guest_entries = risc0_build::embed_methods();
if guest_entries.is_empty() {
eprintln!("ere Risc0 Template Build: risc0_build::embed_methods() found no guest methods.");
return;
}
let entry = &guest_entries[0]; // For simplicity, take the first guest
let info = GuestMethodInfo {
name: entry.name.to_string(),
elf_path: entry.path.to_string(), // This path is to the ELF in OUT_DIR
image_id_hex: entry
.image_id
.as_bytes()
.iter()
.map(|b| format!("{:02x}", b))
.collect(),
};
// Output the info to a known file directly in the methods crate directory.
let manifest_dir =
env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set for template build.rs");
let info_file_path = Path::new(&manifest_dir).join("ere_guest_info.json");
let json_output = format!(
r#"{{
"name": "{}",
"elf_path": "{}",
"image_id_hex": "{}"
}}"#,
info.name.replace('\\', "\\\\").replace('"', "\\\""),
info.elf_path.replace('\\', "\\\\").replace('"', "\\\""),
info.image_id_hex
);
let mut file = File::create(&info_file_path)
.expect("Template build.rs: Failed to create ere_guest_info.json in manifest dir");
file.write_all(json_output.as_bytes())
.expect("Template build.rs: Failed to write to ere_guest_info.json in manifest dir");
println!("cargo:rerun-if-changed=build.rs");
eprintln!(
"ere Risc0 Template Build: Guest info written to {:?}",
info_file_path
);
}

View File

@@ -1,110 +0,0 @@
mod file_utils;
use file_utils::FileRestorer;
use risc0_zkvm::Digest;
use crate::error::CompileError;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
#[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,
}
/// BUILD_SCRIPT_TEMPLATE that we will use to fetch the elf-path
/// TODO: We might be able to deterministically get the elf path
/// TODO: But note we also probably want the image id too, so not sure
/// TODO: we can remove this hack sometime soon.
const BUILD_SCRIPT_TEMPLATE: &str = include_str!("../build_script_template.rs");
pub(crate) fn compile_risczero_program(path: &Path) -> Result<Risc0Program, CompileError> {
if !path.exists() || !path.is_dir() {
return Err(CompileError::InvalidMethodsPath(path.to_path_buf()));
}
// Inject `build.rs`
let build_rs_path = path.join("build.rs");
let _restorer = FileRestorer::new(&build_rs_path)?;
fs::write(&build_rs_path, BUILD_SCRIPT_TEMPLATE)
.map_err(|e| CompileError::io(e, "writing template build.rs"))?;
// Run `cargo build`
let output = Command::new("cargo")
.current_dir(path)
.arg("build")
.arg("--release")
.output()
.map_err(|e| CompileError::io(e, "spawning cargo build"))?;
if !output.status.success() {
return Err(CompileError::CargoBuildFailure {
crate_path: path.to_path_buf(),
status: output.status,
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
});
}
// Read guest info JSON
let info_file = path.join("ere_guest_info.json");
let info_text = fs::read_to_string(&info_file)
.map_err(|e| CompileError::io(e, "reading ere_guest_info.json"))?;
let info_json: JsonValue = serde_json::from_str(&info_text)
.map_err(|e| CompileError::serde(e, "parsing ere_guest_info.json"))?;
let elf_path = info_json["elf_path"]
.as_str()
.map(PathBuf::from)
.ok_or_else(|| CompileError::MissingJsonField {
field: "elf_path",
file: info_file.clone(),
})?;
let image_id_hex_str = info_json["image_id_hex"].as_str().unwrap();
let image_id = hex::decode(image_id_hex_str).unwrap();
let image_id = image_id.try_into().unwrap();
// Return Program
fs::read(&elf_path)
.map_err(|e| CompileError::io(e, "reading ELF file"))
.map(|elf| Risc0Program { elf, image_id })
}
#[cfg(test)]
mod tests {
mod compile {
use crate::compile::compile_risczero_program;
use std::path::PathBuf;
fn get_test_risczero_methods_crate_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("risczero")
.join("compile")
.join("project_structure_build")
.canonicalize()
.expect("Failed to find or canonicalize test Risc0 methods crate")
}
#[test]
fn test_compile_risczero_method_with_custom_build_rs() {
let test_methods_path = get_test_risczero_methods_crate_path();
let program =
compile_risczero_program(&test_methods_path).expect("risc0 compilation failed");
assert!(
!program.elf.is_empty(),
"Risc0 ELF bytes should not be empty."
);
}
}
}

View File

@@ -1,156 +0,0 @@
use crate::error::CompileError;
use std::{
fs,
path::{Path, PathBuf},
};
// NOTE: We can remove this if we can deterministically always knows where the risc0 artifacts
// will be.
/// RAII guard for backing up a file and ensuring its original state is restored
/// when the guard goes out of scope, or that a temporarily created file is deleted.
#[derive(Debug)]
pub struct FileRestorer {
path: PathBuf,
original_content: Option<Vec<u8>>,
was_originally_present: bool,
}
impl FileRestorer {
/// Creates a new FileRestorer for the given path.
/// It reads and stores the original content if the file exists.
pub fn new(path_to_manage: &Path) -> Result<Self, CompileError> {
let was_originally_present = path_to_manage.exists();
let original_content =
if was_originally_present {
if path_to_manage.is_dir() {
return Err(CompileError::InvalidMethodsPath(path_to_manage.into()));
}
Some(fs::read(path_to_manage).map_err(|e| {
CompileError::io(e, "FileRestorer: could not read original file")
})?)
} else {
None
};
Ok(Self {
path: path_to_manage.to_path_buf(),
original_content,
was_originally_present,
})
}
}
impl Drop for FileRestorer {
fn drop(&mut self) {
if let Some(content) = &self.original_content {
// Original file existed, restore its content.
if let Err(e) = fs::write(&self.path, content) {
eprintln!(
"ERROR (FileRestorer): Failed to restore original content to file {}: {}. Manual restoration may be needed.",
self.path.display(),
e
);
}
} else if self.was_originally_present {
// This case (original file existed, but no content backed up) should ideally not be reached
// if `new()` successfully read it or errored out. This implies an issue in `new()` logic or state.
eprintln!(
"ERROR (FileRestorer): Original file {} was present but no backup content was stored. Cannot restore properly.",
self.path.display()
);
} else {
// File was not originally present, so the file at `self.path` was created by the user of FileRestorer.
// We should delete it.
if self.path.exists() && !self.path.is_dir() {
// Extra check for is_dir before remove_file
if let Err(e) = fs::remove_file(&self.path) {
eprintln!(
"ERROR (FileRestorer): Failed to remove temporary file {}: {}. Manual removal may be needed.",
self.path.display(),
e
);
}
} else if self.path.exists() && self.path.is_dir() {
eprintln!(
"ERROR (FileRestorer): Path {} was expected to be a file created by the operation, but it's a directory. Will not remove.",
self.path.display()
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result;
use std::{fs::File, io::Read};
use tempfile::NamedTempFile;
#[test]
fn test_file_restorer_restores_existing_file() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let initial_content = b"initial content";
fs::write(temp_file.path(), initial_content)?;
let file_path = temp_file.path().to_path_buf();
{
let _restorer = FileRestorer::new(&file_path)?;
// Modify the file while restorer is in scope
fs::write(&file_path, b"modified content")?;
let mut current_content = Vec::new();
File::open(&file_path)?.read_to_end(&mut current_content)?;
assert_eq!(current_content, b"modified content");
} // _restorer goes out of scope here, Drop is called
let mut final_content = Vec::new();
File::open(&file_path)?.read_to_end(&mut final_content)?;
assert_eq!(
final_content, initial_content,
"File content was not restored."
);
Ok(())
}
#[test]
fn test_file_restorer_removes_created_file() -> Result<()> {
let temp_file = NamedTempFile::new()?; // Creates a file
let file_path = temp_file.path().to_path_buf();
// Ensure it's deleted before the test so FileRestorer sees it as new
drop(temp_file); // This deletes the file created by NamedTempFile
assert!(
!file_path.exists(),
"Temp file should be deleted before FileRestorer test for creation."
);
{
let _restorer = FileRestorer::new(&file_path)?;
assert!(
!file_path.exists(),
"File should not exist yet if it was not originally present."
);
// Create the file while restorer is in scope
fs::write(&file_path, b"newly created content")?;
assert!(file_path.exists(), "File should exist after being written.");
} // _restorer goes out of scope here, Drop is called
assert!(
!file_path.exists(),
"Newly created file was not removed by FileRestorer."
);
Ok(())
}
#[test]
fn test_file_restorer_handles_path_is_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let result = FileRestorer::new(temp_dir.path());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
CompileError::InvalidMethodsPath(_)
));
}
}

View File

@@ -1,46 +0,0 @@
use std::{io, path::PathBuf, process::ExitStatus};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RiscZeroError {
#[error(transparent)]
Compile(#[from] CompileError),
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error("{context}: {source}")]
Io {
#[source]
source: io::Error,
context: &'static str,
},
#[error("{context}: {source}")]
SerdeJson {
#[source]
source: serde_json::Error,
context: &'static str,
},
#[error("Methods crate path does not exist or is not a directory: {0}")]
InvalidMethodsPath(PathBuf),
#[error(
"`cargo build` for {crate_path} failed with status {status}\nstdout:\n{stdout}\nstderr:\n{stderr}"
)]
CargoBuildFailure {
crate_path: PathBuf,
status: ExitStatus,
stdout: String,
stderr: String,
},
#[error("Could not find field `{field}` in JSON file `{file}`")]
MissingJsonField { field: &'static str, file: PathBuf },
}
impl CompileError {
pub fn io(e: io::Error, context: &'static str) -> Self {
Self::Io { source: e, context }
}
pub fn serde(e: serde_json::Error, context: &'static str) -> Self {
Self::SerdeJson { source: e, context }
}
}

View File

@@ -1,260 +0,0 @@
use std::{path::Path, time::Instant};
use compile::compile_risczero_program;
use risc0_zkvm::{ExecutorEnv, ProverOpts, Receipt, default_executor, default_prover};
use zkvm_interface::{
Compiler, Input, InputItem, ProgramExecutionReport, ProgramProvingReport, ProverResourceType,
zkVM, zkVMError,
};
include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
mod compile;
pub use compile::Risc0Program;
mod error;
use error::RiscZeroError;
#[allow(non_camel_case_types)]
pub struct RV32_IM_RISCZERO_ZKVM_ELF;
impl Compiler for RV32_IM_RISCZERO_ZKVM_ELF {
type Error = RiscZeroError;
type Program = Risc0Program;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
compile_risczero_program(&workspace_directory.join(guest_relative))
.map_err(RiscZeroError::from)
}
}
impl EreRisc0 {
pub fn new(
program: <RV32_IM_RISCZERO_ZKVM_ELF as Compiler>::Program,
resource_type: ProverResourceType,
) -> Self {
match resource_type {
ProverResourceType::Cpu => {
#[cfg(any(feature = "cuda", feature = "metal"))]
panic!("CPU mode requires both 'cuda' and 'metal' features to be disabled");
}
ProverResourceType::Gpu => {
#[cfg(not(any(feature = "cuda", feature = "metal")))]
panic!("GPU selected but neither 'cuda' nor 'metal' feature is enabled");
}
ProverResourceType::Network(_) => {
panic!(
"Network proving not yet implemented for RISC Zero. Use CPU or GPU resource type."
);
}
}
Self {
program,
resource_type,
}
}
}
pub struct EreRisc0 {
program: <RV32_IM_RISCZERO_ZKVM_ELF as Compiler>::Program,
#[allow(dead_code)]
resource_type: ProverResourceType,
}
impl zkVM for EreRisc0 {
fn execute(&self, inputs: &Input) -> Result<ProgramExecutionReport, zkVMError> {
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()))?;
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> {
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()))?;
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();
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> {
let decoded: Receipt =
borsh::from_slice(proof).map_err(|err| zkVMError::Other(Box::new(err)))?;
decoded
.verify(self.program.image_id)
.map_err(|err| zkVMError::Other(Box::new(err)))
}
fn name(&self) -> &'static str {
NAME
}
fn sdk_version(&self) -> &'static str {
SDK_VERSION
}
}
#[cfg(test)]
mod prove_tests {
use std::path::PathBuf;
use super::*;
use zkvm_interface::Input;
fn get_prove_test_guest_program_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("risczero")
.join("compile")
.join("project_structure_build")
.canonicalize()
.expect("Failed to find or canonicalize test Risc0 methods crate")
}
fn get_compiled_test_r0_elf_for_prove() -> Result<Risc0Program, RiscZeroError> {
let test_guest_path = get_prove_test_guest_program_path();
RV32_IM_RISCZERO_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
}
#[test]
fn test_prove_r0_dummy_input() {
let program = get_compiled_test_r0_elf_for_prove().unwrap();
let mut input_builder = Input::new();
let n: u32 = 42;
let a: u16 = 42;
input_builder.write(n);
input_builder.write(a);
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu);
let proof_bytes = match zkvm.prove(&input_builder) {
Ok((prove_result, _)) => prove_result,
Err(err) => {
panic!("Proving error in test: {err}",);
}
};
assert!(!proof_bytes.is_empty(), "Proof bytes should not be empty.");
let verify_results = zkvm.verify(&proof_bytes).is_ok();
assert!(verify_results);
// TODO: Check public inputs
}
#[test]
// TODO: Note: SP1 will panic here
// #[should_panic]
fn test_prove_r0_fails_on_bad_input_causing_execution_failure() {
let elf_bytes = get_compiled_test_r0_elf_for_prove().unwrap();
let empty_input = Input::new();
let zkvm = EreRisc0::new(elf_bytes, ProverResourceType::Cpu);
let prove_result = zkvm.prove(&empty_input);
assert!(prove_result.is_err());
}
}
#[cfg(test)]
mod execute_tests {
use std::path::PathBuf;
use super::*;
use zkvm_interface::Input;
fn get_compiled_test_r0_elf() -> Result<Risc0Program, RiscZeroError> {
let test_guest_path = get_execute_test_guest_program_path();
RV32_IM_RISCZERO_ZKVM_ELF::compile(&test_guest_path, Path::new(""))
}
fn get_execute_test_guest_program_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("risczero")
.join("compile")
.join("project_structure_build")
.canonicalize()
.expect("Failed to find or canonicalize test Risc0 methods crate")
}
#[test]
fn test_execute_r0_dummy_input() {
let program = get_compiled_test_r0_elf().unwrap();
let mut input_builder = Input::new();
let n: u32 = 42;
let a: u16 = 42;
input_builder.write(n);
input_builder.write(a);
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu);
let result = zkvm.execute(&input_builder);
if let Err(err) = &result {
panic!("Execution error: {err}");
}
}
#[test]
fn test_execute_r0_no_input_for_guest_expecting_input() {
let program = get_compiled_test_r0_elf().unwrap();
let empty_input = Input::new();
let zkvm = EreRisc0::new(program, ProverResourceType::Cpu);
let result = zkvm.execute(&empty_input);
assert!(
result.is_err(),
"execute should fail if guest expects input but none is provided."
);
}
}

View File

@@ -16,7 +16,3 @@ tracing = "0.1"
[build-dependencies]
build-utils.workspace = true
[lib]
name = "ere_succinct"
path = "src/lib.rs"

View File

@@ -1,6 +1,5 @@
use std::{
path::{Path, PathBuf},
process::Command,
str::FromStr,
};
@@ -42,7 +41,7 @@ pub fn compile(
);
// Build and run Docker command
let docker_cmd = DockerRunCommand::new(tag)
let docker_cmd = docker::DockerRunCommand::new(tag)
.remove_after_run()
.with_volume(mount_directory_str, "/guest-workspace")
.with_volume(elf_output_dir_str, "/output")
@@ -112,63 +111,3 @@ mod tests {
}
}
}
#[derive(Debug)]
struct DockerRunCommand {
image: String,
volumes: Vec<(String, String)>, // (host_path, container_path)
command: Vec<String>,
// remove image after running
remove_after: bool,
}
impl DockerRunCommand {
fn new(image: impl Into<String>) -> Self {
Self {
image: image.into(),
volumes: Vec::new(),
command: Vec::new(),
remove_after: false,
}
}
fn with_volume(
mut self,
host_path: impl Into<String>,
container_path: impl Into<String>,
) -> Self {
self.volumes.push((host_path.into(), container_path.into()));
self
}
fn with_command(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.command.extend(args.into_iter().map(|s| s.into()));
self
}
fn remove_after_run(mut self) -> Self {
self.remove_after = true;
self
}
fn to_args(&self) -> Vec<String> {
let mut args = vec!["run".to_string()];
if self.remove_after {
args.push("--rm".to_string());
}
for (host_path, container_path) in &self.volumes {
args.extend(["-v".to_string(), format!("{host_path}:{container_path}")]);
}
args.push(self.image.clone());
args.extend(self.command.iter().cloned());
args
}
fn run(&self) -> Result<std::process::ExitStatus, std::io::Error> {
Command::new("docker").args(self.to_args()).status()
}
}

21
docker/risc0/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[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,33 +1,29 @@
ARG BASE_IMAGE_TAG=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}
ARG USERNAME=ere_user
USER root
# Ensure Cargo/Rustup environment variables are set from the base image for SDK script
ENV RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH
# Copy and run the Risc0 SDK installer script
COPY scripts/sdk_installers/install_risc0_sdk.sh /tmp/install_risc0_sdk.sh
RUN chmod +x /tmp/install_risc0_sdk.sh
# Run the script without version arguments to install latest
# TODO: We need to change this in all scripts so that we can fix the version in CI
RUN /tmp/install_risc0_sdk.sh
RUN chmod +x /tmp/install_risc0_sdk.sh && /tmp/install_risc0_sdk.sh
# Verify Risc0 installation (script also does this, but good for Dockerfile sanity)
RUN echo "Verifying Risc0 installation in Dockerfile (post-script)..." && cargo risczero --version
# Copy the entire ere project context
# The WORKDIR is /app from the base image
WORKDIR /app
COPY . .
# Get docker for `cargo risczero build`
RUN curl -fsSL https://get.docker.com | sh
# Run tests
RUN echo "Running tests for ere-risczero library..." && \
cargo test --release -p ere-risczero --lib -- --color always
# Copy guest compiler binary
COPY --from=builder /risc0-cli/target/release/risc0-cli /risc0-cli/risc0-cli
CMD ["/bin/bash"]
# Set entrypoint to `risc0-cli`
ENTRYPOINT ["/risc0-cli/risc0-cli"]

289
docker/risc0/src/main.rs Normal file
View File

@@ -0,0 +1,289 @@
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,11 +1,9 @@
[package]
name = "risc0guest"
name = "ere-test-risc0-guest"
version = "0.1.0"
edition = "2021"
[workspace]
[dependencies]
risc0-zkvm = { version = "^2.3.0", default-features = false, features = [
'std',
] }
risc0-zkvm = { version = "^2.3.0", default-features = false, features = ['std'] }

View File

@@ -1,12 +0,0 @@
[package]
name = "methods"
version = "0.1.0"
edition = "2021"
[workspace]
[build-dependencies]
risc0-build = { version = "^2.3.0" }
[package.metadata.risc0]
methods = ["guest"]

View File

@@ -1,3 +0,0 @@
fn main() {
risc0_build::embed_methods();
}

View File

@@ -1,5 +0,0 @@
{
"name": "risc0guest",
"elf_path": "/Users/kev/work/ere/tests/risczero/project_structure_build/target/riscv-guest/methods/risc0guest/riscv32im-risc0-zkvm-elf/release/risc0guest.bin",
"image_id_hex": "6a0d2e9f10ded46c571e644cdf8776e8f96be1844df36386fdee6c0a0e90084c"
}

View File

@@ -1,4 +0,0 @@
[toolchain]
channel = "stable"
components = ["rustfmt", "rust-src"]
profile = "minimal"

View File

@@ -1 +0,0 @@
include!(concat!(env!("OUT_DIR"), "/methods.rs"));