diff --git a/.dockerignore b/.dockerignore index c41cc9e..f2a4093 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1 @@ -/target \ No newline at end of file +**/target \ No newline at end of file diff --git a/.github/workflows/rust-checks.yml b/.github/workflows/rust-checks.yml index 75f9b67..fbb9a37 100644 --- a/.github/workflows/rust-checks.yml +++ b/.github/workflows/rust-checks.yml @@ -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 }} diff --git a/.github/workflows/test-risc0-docker.yml b/.github/workflows/test-risc0-docker.yml deleted file mode 100644 index 6697d5e..0000000 --- a/.github/workflows/test-risc0-docker.yml +++ /dev/null @@ -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 . \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fc0a325..89ee58e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index fb02bab..db16d9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/Makefile b/Makefile deleted file mode 100644 index 0c5c6d9..0000000 --- a/Makefile +++ /dev/null @@ -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\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 diff --git a/crates/build-utils/src/docker.rs b/crates/build-utils/src/docker.rs index 5fdc079..857f842 100644 --- a/crates/build-utils/src/docker.rs +++ b/crates/build-utils/src/docker.rs @@ -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), + #[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), - #[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, + // remove image after running + remove_after: bool, +} + +impl DockerRunCommand { + pub fn new(image: impl Into) -> Self { + Self { + image: image.into(), + volumes: Vec::new(), + command: Vec::new(), + remove_after: false, + } + } + + pub fn with_volume( + mut self, + host_path: impl Into, + container_path: impl Into, + ) -> Self { + self.volumes.push((host_path.into(), container_path.into())); + self + } + + pub fn with_command(mut self, args: impl IntoIterator>) -> 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 { + 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 { + Command::new("docker").args(self.to_args()).status() + } } diff --git a/crates/ere-risczero/Cargo.toml b/crates/ere-risc0/Cargo.toml similarity index 70% rename from crates/ere-risczero/Cargo.toml rename to crates/ere-risc0/Cargo.toml index d3f2485..279573e 100644 --- a/crates/ere-risczero/Cargo.toml +++ b/crates/ere-risc0/Cargo.toml @@ -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 } diff --git a/crates/ere-risczero/build.rs b/crates/ere-risc0/build.rs similarity index 100% rename from crates/ere-risczero/build.rs rename to crates/ere-risc0/build.rs diff --git a/crates/ere-risc0/src/compile.rs b/crates/ere-risc0/src/compile.rs new file mode 100644 index 0000000..4927947 --- /dev/null +++ b/crates/ere-risc0/src/compile.rs @@ -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, + pub(crate) image_id: Digest, +} + +pub fn compile_risc0_program( + workspace_directory: &Path, + guest_program_relative: &Path, +) -> Result { + // 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." + ); + } + } +} diff --git a/crates/ere-risc0/src/error.rs b/crates/ere-risc0/src/error.rs new file mode 100644 index 0000000..48f9eaf --- /dev/null +++ b/crates/ere-risc0/src/error.rs @@ -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), + #[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), +} diff --git a/crates/ere-risc0/src/lib.rs b/crates/ere-risc0/src/lib.rs new file mode 100644 index 0000000..13754cc --- /dev/null +++ b/crates/ere-risc0/src/lib.rs @@ -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 { + compile_risc0_program(workspace_directory, guest_relative).map_err(Risc0Error::from) + } +} + +impl EreRisc0 { + pub fn new( + program: ::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: ::Program, + #[allow(dead_code)] + resource_type: ProverResourceType, +} + +impl zkVM for EreRisc0 { + fn execute(&self, inputs: &Input) -> Result { + // 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, 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, 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 { + 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 { + 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." + ); + } +} diff --git a/crates/ere-risczero/build_script_template.rs b/crates/ere-risczero/build_script_template.rs deleted file mode 100644 index 0f4ae54..0000000 --- a/crates/ere-risczero/build_script_template.rs +++ /dev/null @@ -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 - ); -} diff --git a/crates/ere-risczero/src/compile.rs b/crates/ere-risczero/src/compile.rs deleted file mode 100644 index e84730d..0000000 --- a/crates/ere-risczero/src/compile.rs +++ /dev/null @@ -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, - 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 { - 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." - ); - } - } -} diff --git a/crates/ere-risczero/src/compile/file_utils.rs b/crates/ere-risczero/src/compile/file_utils.rs deleted file mode 100644 index 86511d5..0000000 --- a/crates/ere-risczero/src/compile/file_utils.rs +++ /dev/null @@ -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>, - 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 { - 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(_) - )); - } -} diff --git a/crates/ere-risczero/src/error.rs b/crates/ere-risczero/src/error.rs deleted file mode 100644 index a2c7e98..0000000 --- a/crates/ere-risczero/src/error.rs +++ /dev/null @@ -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 } - } -} diff --git a/crates/ere-risczero/src/lib.rs b/crates/ere-risczero/src/lib.rs deleted file mode 100644 index be31558..0000000 --- a/crates/ere-risczero/src/lib.rs +++ /dev/null @@ -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 { - compile_risczero_program(&workspace_directory.join(guest_relative)) - .map_err(RiscZeroError::from) - } -} - -impl EreRisc0 { - pub fn new( - program: ::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: ::Program, - #[allow(dead_code)] - resource_type: ProverResourceType, -} - -impl zkVM for EreRisc0 { - fn execute(&self, inputs: &Input) -> Result { - 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, 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 { - 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 { - 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." - ); - } -} diff --git a/crates/ere-sp1/Cargo.toml b/crates/ere-sp1/Cargo.toml index 8d798fe..1449b67 100644 --- a/crates/ere-sp1/Cargo.toml +++ b/crates/ere-sp1/Cargo.toml @@ -16,7 +16,3 @@ tracing = "0.1" [build-dependencies] build-utils.workspace = true - -[lib] -name = "ere_succinct" -path = "src/lib.rs" diff --git a/crates/ere-sp1/src/compile.rs b/crates/ere-sp1/src/compile.rs index 55c64bd..cd3fdee 100644 --- a/crates/ere-sp1/src/compile.rs +++ b/crates/ere-sp1/src/compile.rs @@ -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, - // remove image after running - remove_after: bool, -} - -impl DockerRunCommand { - fn new(image: impl Into) -> Self { - Self { - image: image.into(), - volumes: Vec::new(), - command: Vec::new(), - remove_after: false, - } - } - - fn with_volume( - mut self, - host_path: impl Into, - container_path: impl Into, - ) -> Self { - self.volumes.push((host_path.into(), container_path.into())); - self - } - - fn with_command(mut self, args: impl IntoIterator>) -> 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 { - 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 { - Command::new("docker").args(self.to_args()).status() - } -} diff --git a/docker/risc0/Cargo.toml b/docker/risc0/Cargo.toml new file mode 100644 index 0000000..b4271fe --- /dev/null +++ b/docker/risc0/Cargo.toml @@ -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 diff --git a/docker/risc0/Dockerfile b/docker/risc0/Dockerfile index cc43479..fdd678e 100644 --- a/docker/risc0/Dockerfile +++ b/docker/risc0/Dockerfile @@ -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"] \ No newline at end of file +# Set entrypoint to `risc0-cli` +ENTRYPOINT ["/risc0-cli/risc0-cli"] diff --git a/docker/risc0/src/main.rs b/docker/risc0/src/main.rs new file mode 100644 index 0000000..8db47ee --- /dev/null +++ b/docker/risc0/src/main.rs @@ -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::().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(()) +} diff --git a/tests/risczero/compile/project_structure_build/guest/Cargo.toml b/tests/risc0/compile/basic/Cargo.toml similarity index 74% rename from tests/risczero/compile/project_structure_build/guest/Cargo.toml rename to tests/risc0/compile/basic/Cargo.toml index b6d189f..7e40b33 100644 --- a/tests/risczero/compile/project_structure_build/guest/Cargo.toml +++ b/tests/risc0/compile/basic/Cargo.toml @@ -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'] } diff --git a/tests/risczero/compile/project_structure_build/guest/src/main.rs b/tests/risc0/compile/basic/src/main.rs similarity index 100% rename from tests/risczero/compile/project_structure_build/guest/src/main.rs rename to tests/risc0/compile/basic/src/main.rs diff --git a/tests/risczero/compile/project_structure_build/Cargo.toml b/tests/risczero/compile/project_structure_build/Cargo.toml deleted file mode 100644 index f7824df..0000000 --- a/tests/risczero/compile/project_structure_build/Cargo.toml +++ /dev/null @@ -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"] diff --git a/tests/risczero/compile/project_structure_build/build.rs b/tests/risczero/compile/project_structure_build/build.rs deleted file mode 100644 index 08a8a4e..0000000 --- a/tests/risczero/compile/project_structure_build/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - risc0_build::embed_methods(); -} diff --git a/tests/risczero/compile/project_structure_build/ere_guest_info.json b/tests/risczero/compile/project_structure_build/ere_guest_info.json deleted file mode 100644 index 260a549..0000000 --- a/tests/risczero/compile/project_structure_build/ere_guest_info.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/tests/risczero/compile/project_structure_build/rust-toolchain.toml b/tests/risczero/compile/project_structure_build/rust-toolchain.toml deleted file mode 100644 index 36614c3..0000000 --- a/tests/risczero/compile/project_structure_build/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "stable" -components = ["rustfmt", "rust-src"] -profile = "minimal" diff --git a/tests/risczero/compile/project_structure_build/src/lib.rs b/tests/risczero/compile/project_structure_build/src/lib.rs deleted file mode 100644 index 1bdb308..0000000 --- a/tests/risczero/compile/project_structure_build/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -include!(concat!(env!("OUT_DIR"), "/methods.rs"));