diff --git a/Cargo.lock b/Cargo.lock index fe75ad7..f235c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3737,6 +3737,7 @@ dependencies = [ "ere-test-utils", "ere-zkvm-interface", "execution_utils", + "serde", "serde_json", "tempfile", "thiserror 2.0.12", @@ -3820,6 +3821,7 @@ dependencies = [ "ere-zkvm-interface", "jolt-core", "jolt-sdk", + "serde", "tempfile", "thiserror 2.0.12", ] @@ -3960,6 +3962,7 @@ dependencies = [ "ere-compile-utils", "ere-test-utils", "ere-zkvm-interface", + "serde", "sp1-sdk", "tempfile", "thiserror 2.0.12", @@ -3987,6 +3990,7 @@ dependencies = [ "ere-compile-utils", "ere-test-utils", "ere-zkvm-interface", + "serde", "thiserror 2.0.12", "tracing", "zkm-sdk", @@ -4003,6 +4007,7 @@ dependencies = [ "ere-compile-utils", "ere-test-utils", "ere-zkvm-interface", + "serde", "strum 0.27.2", "tempfile", "thiserror 2.0.12", diff --git a/README.md b/README.md index 17350cb..ee0849d 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,10 @@ ere-sp1 = { git = "https://github.com/eth-act/ere.git", tag = "v0.0.12" } ```rust // main.rs use ere_sp1::{EreSP1, RV32_IM_SUCCINCT_ZKVM_ELF}; -use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; +use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, +}; fn main() -> Result<(), Box> { let guest_directory = std::path::Path::new("workspace/guest"); @@ -128,7 +131,10 @@ ere-dockerized = { git = "https://github.com/eth-act/ere.git", tag = "v0.0.12" } ```rust // main.rs use ere_dockerized::{EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM}; -use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; +use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, +}; fn main() -> Result<(), Box> { let guest_directory = std::path::Path::new("workspace/guest"); diff --git a/crates/dockerized/compiler/Cargo.toml b/crates/dockerized/compiler/Cargo.toml index c693200..33a4514 100644 --- a/crates/dockerized/compiler/Cargo.toml +++ b/crates/dockerized/compiler/Cargo.toml @@ -13,16 +13,16 @@ serde.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } # Local dependencies -ere-airbender = { workspace = true, optional = true } -ere-jolt = { workspace = true, optional = true } -ere-miden = { workspace = true, optional = true } -ere-nexus = { workspace = true, optional = true } -ere-openvm = { workspace = true, optional = true } -ere-pico = { workspace = true, optional = true } -ere-risc0 = { workspace = true, optional = true } -ere-sp1 = { workspace = true, optional = true } -ere-ziren = { workspace = true, optional = true } -ere-zisk = { workspace = true, optional = true } +ere-airbender = { workspace = true, features = ["compiler"], optional = true } +ere-jolt = { workspace = true, features = ["compiler"], optional = true } +ere-miden = { workspace = true, features = ["compiler"], optional = true } +ere-nexus = { workspace = true, features = ["compiler"], optional = true } +ere-openvm = { workspace = true, features = ["compiler"], optional = true } +ere-pico = { workspace = true, features = ["compiler"], optional = true } +ere-risc0 = { workspace = true, features = ["compiler"], optional = true } +ere-sp1 = { workspace = true, features = ["compiler"], optional = true } +ere-ziren = { workspace = true, features = ["compiler"], optional = true } +ere-zisk = { workspace = true, features = ["compiler"], optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] diff --git a/crates/dockerized/compiler/src/main.rs b/crates/dockerized/compiler/src/main.rs index eebc313..d8fae5f 100644 --- a/crates/dockerized/compiler/src/main.rs +++ b/crates/dockerized/compiler/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error}; use clap::Parser; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use serde::Serialize; use std::{env, fs::File, path::PathBuf}; use tracing_subscriber::EnvFilter; diff --git a/crates/dockerized/dockerized/src/docker.rs b/crates/dockerized/dockerized/src/docker.rs index afc6e97..f4a70d9 100644 --- a/crates/dockerized/dockerized/src/docker.rs +++ b/crates/dockerized/dockerized/src/docker.rs @@ -1,4 +1,4 @@ -use crate::error::DockerizedError; +use crate::error::Error; use std::{ env, fmt::{self, Display, Formatter}, @@ -213,26 +213,27 @@ impl DockerRunCmd { } } -pub fn stop_docker_container(container_name: impl AsRef) -> Result<(), DockerizedError> { +pub fn stop_docker_container(container_name: impl AsRef) -> Result<(), Error> { let output = Command::new("docker") .args(["container", "stop", container_name.as_ref()]) .output() - .map_err(DockerizedError::DockerContainerCmd)?; + .map_err(Error::DockerContainerCmd)?; if String::from_utf8_lossy(&output.stdout).starts_with("Error") { - return Err(DockerizedError::DockerContainerCmd(io::Error::other( - format!("Failed to stop container {}", container_name.as_ref()), - ))); + return Err(Error::DockerContainerCmd(io::Error::other(format!( + "Failed to stop container {}", + container_name.as_ref() + )))); } Ok(()) } -pub fn docker_image_exists(image: impl AsRef) -> Result { +pub fn docker_image_exists(image: impl AsRef) -> Result { let output = Command::new("docker") .args(["images", "--quiet", image.as_ref()]) .output() - .map_err(DockerizedError::DockerImageCmd)?; + .map_err(Error::DockerImageCmd)?; // If image exists, image id will be printed hence stdout will be non-empty. Ok(!output.stdout.is_empty()) } diff --git a/crates/dockerized/dockerized/src/error.rs b/crates/dockerized/dockerized/src/error.rs index 95ae4e5..ea359df 100644 --- a/crates/dockerized/dockerized/src/error.rs +++ b/crates/dockerized/dockerized/src/error.rs @@ -1,20 +1,20 @@ -use ere_server::client::{TwirpErrorResponse, zkVMClientError}; +use ere_server::client::{self, TwirpErrorResponse}; use std::{io, path::PathBuf}; use thiserror::Error; -impl From for DockerizedError { - fn from(value: zkVMClientError) -> Self { +impl From for Error { + fn from(value: client::Error) -> Self { match value { - zkVMClientError::zkVM(err) => DockerizedError::zkVM(err), - zkVMClientError::ConnectionTimeout => DockerizedError::ConnectionTimeout, - zkVMClientError::Rpc(err) => DockerizedError::Rpc(err), + client::Error::zkVM(err) => Self::zkVM(err), + client::Error::ConnectionTimeout => Self::ConnectionTimeout, + client::Error::Rpc(err) => Self::Rpc(err), } } } #[derive(Debug, Error)] #[allow(non_camel_case_types)] -pub enum DockerizedError { +pub enum Error { #[error( "Guest directory must be in mounting directory, mounting_directory: {mounting_directory}, guest_directory: {guest_directory}" )] @@ -44,7 +44,7 @@ pub enum DockerizedError { Rpc(TwirpErrorResponse), } -impl DockerizedError { +impl Error { pub fn io(source: io::Error, context: impl ToString) -> Self { Self::Io { source, diff --git a/crates/dockerized/dockerized/src/lib.rs b/crates/dockerized/dockerized/src/lib.rs index 2b5eeb6..3334ce6 100644 --- a/crates/dockerized/dockerized/src/lib.rs +++ b/crates/dockerized/dockerized/src/lib.rs @@ -27,7 +27,10 @@ //! ```rust,no_run //! # fn main() -> Result<(), Box> { //! use ere_dockerized::{EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM}; -//! use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; +//! use ere_zkvm_interface::{ +//! compiler::Compiler, +//! zkvm::{ProofKind, ProverResourceType, zkVM}, +//! }; //! use std::path::Path; //! //! // The zkVM we plan to use @@ -65,12 +68,14 @@ use crate::{ cuda::cuda_arch, docker::{DockerBuildCmd, DockerRunCmd, docker_image_exists, stop_docker_container}, - error::DockerizedError, }; use ere_server::client::{Url, zkVMClient}; use ere_zkvm_interface::{ - Compiler, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, ProverResourceType, - PublicValues, zkVM, + compiler::Compiler, + zkvm::{ + ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, ProverResourceType, + PublicValues, zkVM, + }, }; use serde::{Deserialize, Serialize}; use std::{ @@ -83,13 +88,15 @@ use std::{ use tempfile::TempDir; use tracing::error; +mod cuda; +mod docker; +mod error; + +pub use error::Error; + include!(concat!(env!("OUT_DIR"), "/crate_version.rs")); include!(concat!(env!("OUT_DIR"), "/zkvm_sdk_version_impl.rs")); -pub mod cuda; -pub mod docker; -pub mod error; - /// Offset of port used for `ere-server` for [`ErezkVM`]s. const ERE_SERVER_PORT_OFFSET: u16 = 4174; @@ -163,7 +170,7 @@ impl ErezkVM { /// /// Images are cached and only rebuilt if they don't exist or if the /// `ERE_FORCE_REBUILD_DOCKER_IMAGE` environment variable is set. - pub fn build_docker_image(&self, gpu: bool) -> Result<(), DockerizedError> { + pub fn build_docker_image(&self, gpu: bool) -> Result<(), Error> { let workspace_dir = workspace_dir(); let docker_dir = workspace_dir.join("docker"); @@ -180,8 +187,7 @@ impl ErezkVM { cmd = cmd.build_arg("CUDA", "1"); } - cmd.exec(&workspace_dir) - .map_err(DockerizedError::DockerBuildCmd)?; + cmd.exec(&workspace_dir).map_err(Error::DockerBuildCmd)?; } // Build `ere-base-{zkvm}` @@ -206,8 +212,7 @@ impl ErezkVM { } } - cmd.exec(&workspace_dir) - .map_err(DockerizedError::DockerBuildCmd)?; + cmd.exec(&workspace_dir).map_err(Error::DockerBuildCmd)?; } // Build `ere-compiler-{zkvm}` @@ -218,7 +223,7 @@ impl ErezkVM { .tag(self.compiler_zkvm_image("latest")) .build_arg("BASE_ZKVM_IMAGE", self.base_zkvm_image(CRATE_VERSION, gpu)) .exec(&workspace_dir) - .map_err(DockerizedError::DockerBuildCmd)?; + .map_err(Error::DockerBuildCmd)?; } // Build `ere-server-{zkvm}` @@ -233,8 +238,7 @@ impl ErezkVM { cmd = cmd.build_arg("CUDA", "1"); } - cmd.exec(&workspace_dir) - .map_err(DockerizedError::DockerBuildCmd)?; + cmd.exec(&workspace_dir).map_err(Error::DockerBuildCmd)?; } Ok(()) @@ -248,7 +252,7 @@ impl ErezkVM { &self, program: &SerializedProgram, resource: &ProverResourceType, - ) -> Result { + ) -> Result { let port = self.server_port().to_string(); let name = format!("ere-server-{self}-{port}"); let gpu = matches!(resource, ProverResourceType::Gpu); @@ -296,8 +300,8 @@ impl ErezkVM { } } - let tempdir = TempDir::new() - .map_err(|err| DockerizedError::io(err, "Failed to create temporary directory"))?; + let tempdir = + TempDir::new().map_err(|err| Error::io(err, "Failed to create temporary directory"))?; // zkVM specific options needed for proving Groth16 proof. cmd = match self { @@ -327,8 +331,7 @@ impl ErezkVM { let args = iter::empty() .chain(["--port", &port]) .chain(resource.to_args()); - cmd.spawn(args, &program.0) - .map_err(DockerizedError::DockerRunCmd)?; + cmd.spawn(args, &program.0).map_err(Error::DockerRunCmd)?; Ok(ServerContainer { name, tempdir }) } @@ -366,7 +369,7 @@ pub struct EreDockerizedCompiler { } impl EreDockerizedCompiler { - pub fn new(zkvm: ErezkVM, mount_directory: impl AsRef) -> Result { + pub fn new(zkvm: ErezkVM, mount_directory: impl AsRef) -> Result { zkvm.build_docker_image(false)?; Ok(Self { zkvm, @@ -384,20 +387,20 @@ impl EreDockerizedCompiler { pub struct SerializedProgram(Vec); impl Compiler for EreDockerizedCompiler { - type Error = DockerizedError; + type Error = Error; type Program = SerializedProgram; fn compile(&self, guest_directory: &Path) -> Result { let guest_relative_path = guest_directory .strip_prefix(&self.mount_directory) - .map_err(|_| DockerizedError::GuestNotInMountingDirecty { + .map_err(|_| Error::GuestNotInMountingDirecty { mounting_directory: self.mount_directory.to_path_buf(), guest_directory: guest_directory.to_path_buf(), })?; let guest_path_in_docker = PathBuf::from("/guest").join(guest_relative_path); - let tempdir = TempDir::new() - .map_err(|err| DockerizedError::io(err, "Failed to create temporary directory"))?; + let tempdir = + TempDir::new().map_err(|err| Error::io(err, "Failed to create temporary directory"))?; let mut cmd = DockerRunCmd::new(self.zkvm.compiler_zkvm_image(CRATE_VERSION)) .rm() @@ -419,11 +422,11 @@ impl Compiler for EreDockerizedCompiler { "--output-path", "/output/program", ]) - .map_err(DockerizedError::DockerRunCmd)?; + .map_err(Error::DockerRunCmd)?; let program_path = tempdir.path().join("program"); let program = fs::read(&program_path).map_err(|err| { - DockerizedError::io( + Error::io( err, format!( "Failed to read compiled program at {}", @@ -463,7 +466,7 @@ impl EreDockerizedzkVM { zkvm: ErezkVM, program: SerializedProgram, resource: ProverResourceType, - ) -> Result { + ) -> Result { zkvm.build_docker_image(matches!(resource, ProverResourceType::Gpu))?; let server_container = zkvm.spawn_server(&program, &resource)?; @@ -496,7 +499,7 @@ impl EreDockerizedzkVM { impl zkVM for EreDockerizedzkVM { fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { let (public_values, report) = - block_on(self.client.execute(input.to_vec())).map_err(DockerizedError::from)?; + block_on(self.client.execute(input.to_vec())).map_err(Error::from)?; Ok((public_values, report)) } @@ -507,14 +510,13 @@ impl zkVM for EreDockerizedzkVM { proof_kind: ProofKind, ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { let (public_values, proof, report) = - block_on(self.client.prove(input.to_vec(), proof_kind)) - .map_err(DockerizedError::from)?; + block_on(self.client.prove(input.to_vec(), proof_kind)).map_err(Error::from)?; Ok((public_values, proof, report)) } fn verify(&self, proof: &Proof) -> anyhow::Result { - let public_values = block_on(self.client.verify(proof)).map_err(DockerizedError::from)?; + let public_values = block_on(self.client.verify(proof)).map_err(Error::from)?; Ok(public_values) } @@ -550,11 +552,13 @@ fn home_dir() -> PathBuf { #[cfg(test)] mod test { use crate::{ - DockerizedError, EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM, SerializedProgram, - workspace_dir, + EreDockerizedCompiler, EreDockerizedzkVM, ErezkVM, Error, SerializedProgram, workspace_dir, }; use ere_test_utils::{host::*, program::basic::BasicProgramInput}; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; use std::sync::{Mutex, MutexGuard, OnceLock}; macro_rules! test_compile { @@ -604,10 +608,7 @@ mod test { // Invalid test cases for input in $invalid_test_cases { let err = zkvm.execute(&input).unwrap_err(); - assert!(matches!( - err.downcast::().unwrap(), - DockerizedError::zkVM(_) - ),); + assert!(matches!(err.downcast::().unwrap(), Error::zkVM(_)),); } drop(zkvm); @@ -627,10 +628,7 @@ mod test { // Invalid test cases for input in $invalid_test_cases { let err = zkvm.prove(&input, ProofKind::default()).unwrap_err(); - assert!(matches!( - err.downcast::().unwrap(), - DockerizedError::zkVM(_) - ),); + assert!(matches!(err.downcast::().unwrap(), Error::zkVM(_)),); } drop(zkvm); diff --git a/crates/dockerized/server/Cargo.toml b/crates/dockerized/server/Cargo.toml index d5ca2bc..ca3ad7c 100644 --- a/crates/dockerized/server/Cargo.toml +++ b/crates/dockerized/server/Cargo.toml @@ -21,16 +21,16 @@ tracing = { workspace = true, optional = true } tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } # Local dependencies -ere-airbender = { workspace = true, optional = true } -ere-jolt = { workspace = true, optional = true } -ere-miden = { workspace = true, optional = true } -ere-nexus = { workspace = true, optional = true } -ere-openvm = { workspace = true, optional = true } -ere-pico = { workspace = true, optional = true } -ere-risc0 = { workspace = true, optional = true } -ere-sp1 = { workspace = true, optional = true } -ere-ziren = { workspace = true, optional = true } -ere-zisk = { workspace = true, optional = true } +ere-airbender = { workspace = true, features = ["zkvm"], optional = true } +ere-jolt = { workspace = true, features = ["zkvm"], optional = true } +ere-miden = { workspace = true, features = ["zkvm"], optional = true } +ere-nexus = { workspace = true, features = ["zkvm"], optional = true } +ere-openvm = { workspace = true, features = ["zkvm"], optional = true } +ere-pico = { workspace = true, features = ["zkvm"], optional = true } +ere-risc0 = { workspace = true, features = ["zkvm"], optional = true } +ere-sp1 = { workspace = true, features = ["zkvm"], optional = true } +ere-ziren = { workspace = true, features = ["zkvm"], optional = true } +ere-zisk = { workspace = true, features = ["zkvm"], optional = true } ere-zkvm-interface = { workspace = true, features = ["clap"] } [dev-dependencies] diff --git a/crates/dockerized/server/src/client.rs b/crates/dockerized/server/src/client.rs index 2f4061c..0577633 100644 --- a/crates/dockerized/server/src/client.rs +++ b/crates/dockerized/server/src/client.rs @@ -3,7 +3,7 @@ use crate::api::{ execute_response::Result as ExecuteResult, prove_response::Result as ProveResult, verify_response::Result as VerifyResult, }; -use ere_zkvm_interface::{ +use ere_zkvm_interface::zkvm::{ ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, PublicValues, }; use std::time::{Duration, Instant}; @@ -15,7 +15,7 @@ pub use twirp::{TwirpErrorResponse, url::Url}; #[derive(Debug, Error)] #[allow(non_camel_case_types)] -pub enum zkVMClientError { +pub enum Error { #[error("zkVM method error: {0}")] zkVM(String), #[error("Connection to zkVM server timeout after 5 minutes")] @@ -31,7 +31,7 @@ pub struct zkVMClient { } impl zkVMClient { - pub async fn new(url: Url) -> Result { + pub async fn new(url: Url) -> Result { const TIMEOUT: Duration = Duration::from_secs(300); // 5mins const INTERVAL: Duration = Duration::from_millis(500); @@ -40,7 +40,7 @@ impl zkVMClient { let start = Instant::now(); loop { if start.elapsed() > TIMEOUT { - return Err(zkVMClientError::ConnectionTimeout); + return Err(Error::ConnectionTimeout); } match http_client.get(url.join("health").unwrap()).send().await { @@ -57,7 +57,7 @@ impl zkVMClient { pub async fn execute( &self, input: Vec, - ) -> Result<(PublicValues, ProgramExecutionReport), zkVMClientError> { + ) -> Result<(PublicValues, ProgramExecutionReport), Error> { let request = Request::new(ExecuteRequest { input }); let response = self.client.execute(request).await?; @@ -69,7 +69,7 @@ impl zkVMClient { .map_err(deserialize_report_err)? .0, )), - ExecuteResult::Err(err) => Err(zkVMClientError::zkVM(err)), + ExecuteResult::Err(err) => Err(Error::zkVM(err)), } } @@ -77,7 +77,7 @@ impl zkVMClient { &self, input: Vec, proof_kind: ProofKind, - ) -> Result<(PublicValues, Proof, ProgramProvingReport), zkVMClientError> { + ) -> Result<(PublicValues, Proof, ProgramProvingReport), Error> { let request = Request::new(ProveRequest { input, proof_kind: proof_kind as i32, @@ -93,11 +93,11 @@ impl zkVMClient { .map_err(deserialize_report_err)? .0, )), - ProveResult::Err(err) => Err(zkVMClientError::zkVM(err)), + ProveResult::Err(err) => Err(Error::zkVM(err)), } } - pub async fn verify(&self, proof: &Proof) -> Result { + pub async fn verify(&self, proof: &Proof) -> Result { let request = Request::new(VerifyRequest { proof: proof.as_bytes().to_vec(), proof_kind: proof.kind() as i32, @@ -107,7 +107,7 @@ impl zkVMClient { match response.into_body().result.ok_or_else(result_none_err)? { VerifyResult::Ok(result) => Ok(result.public_values), - VerifyResult::Err(err) => Err(zkVMClientError::zkVM(err)), + VerifyResult::Err(err) => Err(Error::zkVM(err)), } } } diff --git a/crates/dockerized/server/src/main.rs b/crates/dockerized/server/src/main.rs index ab7fb54..b1a7e4f 100644 --- a/crates/dockerized/server/src/main.rs +++ b/crates/dockerized/server/src/main.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Error}; use clap::Parser; use ere_server::server::{router, zkVMServer}; -use ere_zkvm_interface::{ProverResourceType, zkVM}; +use ere_zkvm_interface::zkvm::{ProverResourceType, zkVM}; use std::{ io::{self, Read}, net::{Ipv4Addr, SocketAddr}, @@ -113,34 +113,34 @@ fn construct_zkvm(program: Vec, resource: ProverResourceType) -> Result Result; +} diff --git a/crates/zkvm-interface/src/lib.rs b/crates/zkvm-interface/src/lib.rs index 8ef2a35..8a52b8f 100644 --- a/crates/zkvm-interface/src/lib.rs +++ b/crates/zkvm-interface/src/lib.rs @@ -1,134 +1,7 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] -#![allow(clippy::double_parens)] -#![allow(non_camel_case_types)] -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use std::path::Path; -use strum::{EnumDiscriminants, EnumIs, EnumTryAs, FromRepr}; +pub mod compiler; +pub mod zkvm; -mod error; -pub use error::CommonError; - -mod reports; -pub use reports::{ProgramExecutionReport, ProgramProvingReport}; - -mod network; -pub use network::NetworkProverConfig; - -/// Compiler trait for compiling programs into an opaque sequence of bytes. -pub trait Compiler { - type Error: std::error::Error + Send + Sync + 'static; - type Program: Clone + Send + Sync + Serialize + DeserializeOwned; - - /// Compiles the program and returns the program - /// - /// # Arguments - /// * `guest_directory` - The path to the guest program directory - fn compile(&self, guest_directory: &Path) -> Result; -} - -/// ResourceType specifies what resource will be used to create the proofs. -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "clap", derive(clap::Subcommand))] -pub enum ProverResourceType { - #[default] - Cpu, - Gpu, - /// Use a remote prover network - Network(NetworkProverConfig), -} - -#[cfg(feature = "clap")] -impl ProverResourceType { - pub fn to_args(&self) -> Vec<&str> { - match self { - Self::Cpu => vec!["cpu"], - Self::Gpu => vec!["gpu"], - Self::Network(config) => core::iter::once("network") - .chain(config.to_args()) - .collect(), - } - } -} - -/// Public values committed/revealed by guest program. -/// -/// Use [`zkVM::deserialize_from`] to deserialize object from the bytes. -pub type PublicValues = Vec; - -/// Proof generated by [`zkVM::prove`], that also includes the [`PublicValues`] -/// for [`zkVM::verify`] to work. -#[derive(Clone, Debug, Serialize, Deserialize, EnumDiscriminants, EnumIs, EnumTryAs)] -#[strum_discriminants(derive(Default, FromRepr))] -#[strum_discriminants(name(ProofKind))] -pub enum Proof { - /// Compressed proof in contant size regardless of the cycle count. - #[strum_discriminants(default)] - Compressed(Vec), - /// Groth16 proof that internally verifies a Compressed proof. - Groth16(Vec), -} - -impl Proof { - /// Construct [`Proof`] from [`ProofKind`] and bytes. - pub fn new(proof_kind: ProofKind, bytes: Vec) -> Self { - match proof_kind { - ProofKind::Compressed => Self::Compressed(bytes), - ProofKind::Groth16 => Self::Groth16(bytes), - } - } - - /// Returns [`ProofKind`]. - pub fn kind(&self) -> ProofKind { - ProofKind::from(self) - } - - /// Returns inner proof as bytes. - pub fn as_bytes(&self) -> &[u8] { - match self { - Self::Compressed(bytes) => bytes, - Self::Groth16(bytes) => bytes, - } - } -} - -#[auto_impl::auto_impl(&, Arc, Box)] -/// zkVM trait to abstract away the differences between each zkVM. -/// -/// This trait provides a unified interface, the workflow is: -/// 1. Compile a guest program using the corresponding `Compiler`. -/// 2. Create a zkVM instance with the compiled program and prover resource. -/// 3. Execute, prove, and verify using the zkVM instance methods. -/// -/// Note that a zkVM instance is created for specific program, each zkVM -/// implementation will have their own construction function. -pub trait zkVM { - /// Executes the program with the given input. - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)>; - - /// Creates a proof of the program execution with given input. - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)>; - - /// Verifies a proof of the program used to create this zkVM instance, then - /// returns the public values extracted from the proof. - #[must_use = "Public values must be used"] - fn verify(&self, proof: &Proof) -> anyhow::Result; - - /// Returns the name of the zkVM - fn name(&self) -> &'static str; - - /// Returns the version of the zkVM SDK (e.g. 0.1.0) - fn sdk_version(&self) -> &'static str; -} - -pub trait zkVMProgramDigest { - /// Digest of specific compiled guest program used when verify a proof. - type ProgramDigest: Clone + Serialize + DeserializeOwned; - - /// Returns [`zkVMProgramDigest::ProgramDigest`]. - fn program_digest(&self) -> anyhow::Result; -} +pub use compiler::*; +pub use zkvm::*; diff --git a/crates/zkvm-interface/src/zkvm.rs b/crates/zkvm-interface/src/zkvm.rs new file mode 100644 index 0000000..c5e3028 --- /dev/null +++ b/crates/zkvm-interface/src/zkvm.rs @@ -0,0 +1,59 @@ +#![allow(non_camel_case_types)] + +use serde::{Serialize, de::DeserializeOwned}; + +mod error; +mod proof; +mod report; +mod resource; + +pub use error::CommonError; +pub use proof::{Proof, ProofKind}; +pub use report::{ProgramExecutionReport, ProgramProvingReport}; +pub use resource::{NetworkProverConfig, ProverResourceType}; + +/// Public values committed/revealed by guest program. +/// +/// Use [`zkVM::deserialize_from`] to deserialize object from the bytes. +pub type PublicValues = Vec; + +/// zkVM trait to abstract away the differences between each zkVM. +/// +/// This trait provides a unified interface, the workflow is: +/// 1. Compile a guest program using the corresponding `Compiler`. +/// 2. Create a zkVM instance with the compiled program and prover resource. +/// 3. Execute, prove, and verify using the zkVM instance methods. +/// +/// Note that a zkVM instance is created for specific program, each zkVM +/// implementation will have their own construction function. +#[auto_impl::auto_impl(&, Arc, Box)] +pub trait zkVM { + /// Executes the program with the given input. + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)>; + + /// Creates a proof of the program execution with given input. + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)>; + + /// Verifies a proof of the program used to create this zkVM instance, then + /// returns the public values extracted from the proof. + #[must_use = "Public values must be used"] + fn verify(&self, proof: &Proof) -> anyhow::Result; + + /// Returns the name of the zkVM + fn name(&self) -> &'static str; + + /// Returns the version of the zkVM SDK (e.g. 0.1.0) + fn sdk_version(&self) -> &'static str; +} + +pub trait zkVMProgramDigest { + /// Digest of specific compiled guest program used when verify a proof. + type ProgramDigest: Clone + Serialize + DeserializeOwned; + + /// Returns [`zkVMProgramDigest::ProgramDigest`]. + fn program_digest(&self) -> anyhow::Result; +} diff --git a/crates/zkvm-interface/src/error.rs b/crates/zkvm-interface/src/zkvm/error.rs similarity index 99% rename from crates/zkvm-interface/src/error.rs rename to crates/zkvm-interface/src/zkvm/error.rs index 960ac2d..ab24134 100644 --- a/crates/zkvm-interface/src/error.rs +++ b/crates/zkvm-interface/src/zkvm/error.rs @@ -1,4 +1,4 @@ -use crate::ProofKind; +use crate::zkvm::ProofKind; use std::{ io, path::Path, diff --git a/crates/zkvm-interface/src/zkvm/proof.rs b/crates/zkvm-interface/src/zkvm/proof.rs new file mode 100644 index 0000000..757be3a --- /dev/null +++ b/crates/zkvm-interface/src/zkvm/proof.rs @@ -0,0 +1,40 @@ +#![allow(clippy::double_parens)] + +use serde::{Deserialize, Serialize}; +use strum::{EnumDiscriminants, EnumIs, EnumTryAs, FromRepr}; + +/// Proof generated by [`zkVM::prove`], that also includes the [`PublicValues`] +/// for [`zkVM::verify`] to work. +#[derive(Clone, Debug, Serialize, Deserialize, EnumDiscriminants, EnumIs, EnumTryAs)] +#[strum_discriminants(derive(Default, FromRepr))] +#[strum_discriminants(name(ProofKind))] +pub enum Proof { + /// Compressed proof in contant size regardless of the cycle count. + #[strum_discriminants(default)] + Compressed(Vec), + /// Groth16 proof that internally verifies a Compressed proof. + Groth16(Vec), +} + +impl Proof { + /// Construct [`Proof`] from [`ProofKind`] and bytes. + pub fn new(proof_kind: ProofKind, bytes: Vec) -> Self { + match proof_kind { + ProofKind::Compressed => Self::Compressed(bytes), + ProofKind::Groth16 => Self::Groth16(bytes), + } + } + + /// Returns [`ProofKind`]. + pub fn kind(&self) -> ProofKind { + ProofKind::from(self) + } + + /// Returns inner proof as bytes. + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Compressed(bytes) => bytes, + Self::Groth16(bytes) => bytes, + } + } +} diff --git a/crates/zkvm-interface/src/reports.rs b/crates/zkvm-interface/src/zkvm/report.rs similarity index 100% rename from crates/zkvm-interface/src/reports.rs rename to crates/zkvm-interface/src/zkvm/report.rs diff --git a/crates/zkvm-interface/src/network.rs b/crates/zkvm-interface/src/zkvm/resource.rs similarity index 54% rename from crates/zkvm-interface/src/network.rs rename to crates/zkvm-interface/src/zkvm/resource.rs index 328a101..85684f8 100644 --- a/crates/zkvm-interface/src/network.rs +++ b/crates/zkvm-interface/src/zkvm/resource.rs @@ -22,3 +22,27 @@ impl NetworkProverConfig { .collect() } } + +/// ResourceType specifies what resource will be used to create the proofs. +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "clap", derive(clap::Subcommand))] +pub enum ProverResourceType { + #[default] + Cpu, + Gpu, + /// Use a remote prover network + Network(NetworkProverConfig), +} + +#[cfg(feature = "clap")] +impl ProverResourceType { + pub fn to_args(&self) -> Vec<&str> { + match self { + Self::Cpu => vec!["cpu"], + Self::Gpu => vec!["gpu"], + Self::Network(config) => core::iter::once("network") + .chain(config.to_args()) + .collect(), + } + } +} diff --git a/crates/zkvm/airbender/Cargo.toml b/crates/zkvm/airbender/Cargo.toml index 99a4941..fe681ee 100644 --- a/crates/zkvm/airbender/Cargo.toml +++ b/crates/zkvm/airbender/Cargo.toml @@ -8,15 +8,16 @@ license.workspace = true [dependencies] anyhow.workspace = true bincode = { workspace = true, features = ["alloc", "serde"] } +serde.workspace = true serde_json.workspace = true tempfile.workspace = true thiserror.workspace = true # Airbender dependencies -airbender_execution_utils.workspace = true +airbender_execution_utils = { workspace = true, optional = true } # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -25,5 +26,10 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:airbender_execution_utils"] + [lints] workspace = true diff --git a/crates/zkvm/airbender/src/compiler.rs b/crates/zkvm/airbender/src/compiler.rs index b79589a..7f55511 100644 --- a/crates/zkvm/airbender/src/compiler.rs +++ b/crates/zkvm/airbender/src/compiler.rs @@ -1,5 +1,5 @@ +mod error; mod rust_rv32ima; +pub use error::Error; pub use rust_rv32ima::RustRv32ima; - -pub type AirbenderProgram = Vec; diff --git a/crates/zkvm/airbender/src/compiler/error.rs b/crates/zkvm/airbender/src/compiler/error.rs new file mode 100644 index 0000000..477e200 --- /dev/null +++ b/crates/zkvm/airbender/src/compiler/error.rs @@ -0,0 +1,8 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), +} diff --git a/crates/zkvm/airbender/src/compiler/rust_rv32ima.rs b/crates/zkvm/airbender/src/compiler/rust_rv32ima.rs index d78c778..e7a0427 100644 --- a/crates/zkvm/airbender/src/compiler/rust_rv32ima.rs +++ b/crates/zkvm/airbender/src/compiler/rust_rv32ima.rs @@ -1,6 +1,6 @@ -use crate::{compiler::AirbenderProgram, error::CompileError}; +use crate::{compiler::Error, program::AirbenderProgram}; use ere_compile_utils::{CargoBuildCmd, CommonError}; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{ env, io::Write, @@ -35,7 +35,7 @@ const LINKER_SCRIPT: &str = concat!( pub struct RustRv32ima; impl Compiler for RustRv32ima { - type Error = CompileError; + type Error = Error; type Program = AirbenderProgram; @@ -48,11 +48,11 @@ impl Compiler for RustRv32ima { .rustflags(RUSTFLAGS) .exec(guest_directory, TARGET_TRIPLE)?; let bin = objcopy_binary(&elf)?; - Ok(bin) + Ok(AirbenderProgram { bin }) } } -fn objcopy_binary(elf: &[u8]) -> Result, CompileError> { +fn objcopy_binary(elf: &[u8]) -> Result, Error> { let mut cmd = Command::new("rust-objcopy"); let mut child = cmd .args(["-O", "binary", "-", "-"]) @@ -88,12 +88,12 @@ fn objcopy_binary(elf: &[u8]) -> Result, CompileError> { mod tests { use crate::compiler::RustRv32ima; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("airbender", "basic"); - let bin = RustRv32ima.compile(&guest_directory).unwrap(); - assert!(!bin.is_empty(), "Binary should not be empty."); + let program = RustRv32ima.compile(&guest_directory).unwrap(); + assert!(!program.bin.is_empty(), "Binary should not be empty."); } } diff --git a/crates/zkvm/airbender/src/lib.rs b/crates/zkvm/airbender/src/lib.rs index 0df663b..a53c4ec 100644 --- a/crates/zkvm/airbender/src/lib.rs +++ b/crates/zkvm/airbender/src/lib.rs @@ -1,173 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{ - client::{AirbenderSdk, VkHashChain}, - compiler::AirbenderProgram, - error::AirbenderError, -}; -use airbender_execution_utils::ProgramProof; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use std::time::Instant; +pub mod program; -mod client; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +#[cfg(feature = "zkvm")] +pub mod zkvm; -pub struct EreAirbender { - sdk: AirbenderSdk, -} - -impl EreAirbender { - pub fn new( - bin: AirbenderProgram, - resource: ProverResourceType, - ) -> Result { - let gpu = matches!(resource, ProverResourceType::Gpu); - let sdk = AirbenderSdk::new(&bin, gpu); - Ok(Self { sdk }) - } -} - -impl zkVM for EreAirbender { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let start = Instant::now(); - let (public_values, cycles) = self.sdk.execute(input)?; - let execution_duration = start.elapsed(); - - Ok(( - public_values, - ProgramExecutionReport { - total_num_cycles: cycles, - execution_duration, - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - let start = Instant::now(); - let (public_values, proof) = self.sdk.prove(input)?; - let proving_time = start.elapsed(); - - let proof_bytes = bincode::serde::encode_to_vec(&proof, bincode::config::legacy()) - .map_err(|err| CommonError::serialize("proof", "bincode", err))?; - - Ok(( - public_values, - Proof::Compressed(proof_bytes), - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - let (proof, _): (ProgramProof, _) = - bincode::serde::decode_from_slice(proof, bincode::config::legacy()) - .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; - - let public_values = self.sdk.verify(&proof)?; - - Ok(public_values) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreAirbender { - type ProgramDigest = VkHashChain; - - fn program_digest(&self) -> anyhow::Result { - Ok(*self.sdk.vk_chain_hash()) - } -} - -#[cfg(test)] -mod tests { - use crate::{ - EreAirbender, - compiler::{AirbenderProgram, RustRv32ima}, - }; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - fn basic_program() -> AirbenderProgram { - static PROGRAM: OnceLock = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustRv32ima - .compile(&testing_guest_directory("airbender", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid().into_output_sha256(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid().into_output_sha256(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/airbender/src/program.rs b/crates/zkvm/airbender/src/program.rs new file mode 100644 index 0000000..09091f3 --- /dev/null +++ b/crates/zkvm/airbender/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Airbender program that contains binary format of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct AirbenderProgram { + pub(crate) bin: Vec, +} + +impl AirbenderProgram { + pub fn bin(&self) -> &[u8] { + &self.bin + } +} diff --git a/crates/zkvm/airbender/src/zkvm.rs b/crates/zkvm/airbender/src/zkvm.rs new file mode 100644 index 0000000..10d257d --- /dev/null +++ b/crates/zkvm/airbender/src/zkvm.rs @@ -0,0 +1,166 @@ +use crate::{program::AirbenderProgram, zkvm::sdk::AirbenderSdk}; +use airbender_execution_utils::ProgramProof; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use std::time::Instant; + +mod error; +mod sdk; + +pub use error::Error; +pub use sdk::VkHashChain; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +pub struct EreAirbender { + sdk: AirbenderSdk, +} + +impl EreAirbender { + pub fn new(program: AirbenderProgram, resource: ProverResourceType) -> Result { + let gpu = matches!(resource, ProverResourceType::Gpu); + let sdk = AirbenderSdk::new(program.bin(), gpu); + Ok(Self { sdk }) + } +} + +impl zkVM for EreAirbender { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let start = Instant::now(); + let (public_values, cycles) = self.sdk.execute(input)?; + let execution_duration = start.elapsed(); + + Ok(( + public_values, + ProgramExecutionReport { + total_num_cycles: cycles, + execution_duration, + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + let start = Instant::now(); + let (public_values, proof) = self.sdk.prove(input)?; + let proving_time = start.elapsed(); + + let proof_bytes = bincode::serde::encode_to_vec(&proof, bincode::config::legacy()) + .map_err(|err| CommonError::serialize("proof", "bincode", err))?; + + Ok(( + public_values, + Proof::Compressed(proof_bytes), + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + let (proof, _): (ProgramProof, _) = + bincode::serde::decode_from_slice(proof, bincode::config::legacy()) + .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; + + let public_values = self.sdk.verify(&proof)?; + + Ok(public_values) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreAirbender { + type ProgramDigest = VkHashChain; + + fn program_digest(&self) -> anyhow::Result { + Ok(*self.sdk.vk_chain_hash()) + } +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv32ima, program::AirbenderProgram, zkvm::EreAirbender}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> AirbenderProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv32ima + .compile(&testing_guest_directory("airbender", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid().into_output_sha256(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid().into_output_sha256(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreAirbender::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/airbender/src/error.rs b/crates/zkvm/airbender/src/zkvm/error.rs similarity index 75% rename from crates/zkvm/airbender/src/error.rs rename to crates/zkvm/airbender/src/zkvm/error.rs index 6e9a0d0..69abc65 100644 --- a/crates/zkvm/airbender/src/error.rs +++ b/crates/zkvm/airbender/src/zkvm/error.rs @@ -1,17 +1,11 @@ -use crate::client::VkHashChain; -use ere_compile_utils::CommonError; +use crate::zkvm::sdk::VkHashChain; +use ere_zkvm_interface::zkvm::CommonError; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] CommonError(#[from] CommonError), -} - -#[derive(Debug, Error)] -pub enum AirbenderError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), // Execution #[error("Failed to parse public value from stdout: {0}")] diff --git a/crates/zkvm/airbender/src/client.rs b/crates/zkvm/airbender/src/zkvm/sdk.rs similarity index 92% rename from crates/zkvm/airbender/src/client.rs rename to crates/zkvm/airbender/src/zkvm/sdk.rs index 533ece8..d14d202 100644 --- a/crates/zkvm/airbender/src/client.rs +++ b/crates/zkvm/airbender/src/zkvm/sdk.rs @@ -1,9 +1,9 @@ -use crate::error::AirbenderError; +use crate::zkvm::error::Error; use airbender_execution_utils::{ Machine, ProgramProof, compute_chain_encoding, generate_params_for_binary, universal_circuit_verifier_vk, verify_recursion_log_23_layer, }; -use ere_zkvm_interface::{CommonError, PublicValues}; +use ere_zkvm_interface::zkvm::{CommonError, PublicValues}; use std::{array, fs, io::BufRead, iter, process::Command}; use tempfile::tempdir; @@ -46,7 +46,7 @@ impl AirbenderSdk { &self.vk_hash_chain } - pub fn execute(&self, input: &[u8]) -> Result<(PublicValues, u64), AirbenderError> { + pub fn execute(&self, input: &[u8]) -> Result<(PublicValues, u64), Error> { let tempdir = tempdir().map_err(CommonError::tempdir)?; let bin_path = tempdir.path().join("guest.bin"); @@ -92,9 +92,7 @@ impl AirbenderSdk { Some(bytes) }) .ok_or_else(|| { - AirbenderError::ParsePublicValue( - String::from_utf8_lossy(&output.stdout).to_string(), - ) + Error::ParsePublicValue(String::from_utf8_lossy(&output.stdout).to_string()) })?; // Parse cycles from stdout in format of: @@ -109,13 +107,13 @@ impl AirbenderSdk { cycle.parse().ok() }) .ok_or_else(|| { - AirbenderError::ParseCycles(String::from_utf8_lossy(&output.stdout).to_string()) + Error::ParseCycles(String::from_utf8_lossy(&output.stdout).to_string()) })?; Ok((public_values, cycles)) } - pub fn prove(&self, input: &[u8]) -> Result<(PublicValues, ProgramProof), AirbenderError> { + pub fn prove(&self, input: &[u8]) -> Result<(PublicValues, ProgramProof), Error> { let tempdir = tempdir().map_err(CommonError::tempdir)?; let bin_path = tempdir.path().join("guest.bin"); @@ -189,7 +187,7 @@ impl AirbenderSdk { let (public_values, vk_hash_chain) = extract_public_values_and_vk_hash_chain(&proof)?; if self.vk_hash_chain != vk_hash_chain { - return Err(AirbenderError::UnexpectedVkHashChain { + return Err(Error::UnexpectedVkHashChain { preprocessed: self.vk_hash_chain, proved: vk_hash_chain, }); @@ -198,16 +196,16 @@ impl AirbenderSdk { Ok((public_values, proof)) } - pub fn verify(&self, proof: &ProgramProof) -> Result { + pub fn verify(&self, proof: &ProgramProof) -> Result { let is_valid = verify_recursion_log_23_layer(proof); if !is_valid { - return Err(AirbenderError::ProofVerificationFailed); + return Err(Error::ProofVerificationFailed); } let (public_values, vk_hash_chain) = extract_public_values_and_vk_hash_chain(proof)?; if self.vk_hash_chain != vk_hash_chain { - return Err(AirbenderError::UnexpectedVkHashChain { + return Err(Error::UnexpectedVkHashChain { preprocessed: self.vk_hash_chain, proved: vk_hash_chain, }); @@ -232,9 +230,9 @@ fn encode_input(input: &[u8]) -> String { // Extract public values and VK hash chain from register values. fn extract_public_values_and_vk_hash_chain( proof: &ProgramProof, -) -> Result<(PublicValues, VkHashChain), AirbenderError> { +) -> Result<(PublicValues, VkHashChain), Error> { if proof.register_final_values.len() != 32 { - return Err(AirbenderError::InvalidRegisterCount( + return Err(Error::InvalidRegisterCount( proof.register_final_values.len(), )); } diff --git a/crates/zkvm/jolt/Cargo.toml b/crates/zkvm/jolt/Cargo.toml index 37828b6..fe5e5c0 100644 --- a/crates/zkvm/jolt/Cargo.toml +++ b/crates/zkvm/jolt/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] anyhow.workspace = true +serde.workspace = true tempfile.workspace = true thiserror.workspace = true @@ -14,10 +15,10 @@ thiserror.workspace = true jolt-ark-serialize = { workspace = true, features = ["derive"] } jolt-common.workspace = true jolt-core.workspace = true -jolt-sdk = { workspace = true, features = ["host"] } +jolt-sdk = { workspace = true, features = ["host"], optional = true } # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -26,5 +27,10 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:jolt-sdk"] + [lints] workspace = true diff --git a/crates/zkvm/jolt/src/compiler.rs b/crates/zkvm/jolt/src/compiler.rs index f24c7ed..eff4ad4 100644 --- a/crates/zkvm/jolt/src/compiler.rs +++ b/crates/zkvm/jolt/src/compiler.rs @@ -1,7 +1,7 @@ +mod error; mod rust_rv64imac; mod rust_rv64imac_customized; +pub use error::Error; pub use rust_rv64imac::RustRv64imac; pub use rust_rv64imac_customized::RustRv64imacCustomized; - -pub type JoltProgram = Vec; diff --git a/crates/zkvm/jolt/src/error.rs b/crates/zkvm/jolt/src/compiler/error.rs similarity index 51% rename from crates/zkvm/jolt/src/error.rs rename to crates/zkvm/jolt/src/compiler/error.rs index 4d5858f..628fbca 100644 --- a/crates/zkvm/jolt/src/error.rs +++ b/crates/zkvm/jolt/src/compiler/error.rs @@ -1,10 +1,9 @@ use ere_compile_utils::CommonError; -use jolt_core::utils::errors::ProofVerifyError; use std::{io, path::PathBuf}; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] CommonError(#[from] CommonError), @@ -18,17 +17,3 @@ pub enum CompileError { #[error("Failed to build guest")] BuildFailed, } - -#[derive(Debug, Error)] -pub enum JoltError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), - - // Execute - #[error("Execution panics")] - ExecutionPanic, - - // Verify - #[error("Failed to verify proof: {0}")] - VerifyProofFailed(#[from] ProofVerifyError), -} diff --git a/crates/zkvm/jolt/src/compiler/rust_rv64imac.rs b/crates/zkvm/jolt/src/compiler/rust_rv64imac.rs index 1833ac0..a271dd0 100644 --- a/crates/zkvm/jolt/src/compiler/rust_rv64imac.rs +++ b/crates/zkvm/jolt/src/compiler/rust_rv64imac.rs @@ -1,6 +1,6 @@ -use crate::{compiler::JoltProgram, error::CompileError}; +use crate::{compiler::Error, program::JoltProgram}; use ere_compile_utils::CargoBuildCmd; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use jolt_common::constants::{ DEFAULT_MEMORY_SIZE, DEFAULT_STACK_SIZE, EMULATOR_MEMORY_CAPACITY, STACK_CANARY_SIZE, }; @@ -41,7 +41,7 @@ fn make_linker_script() -> String { pub struct RustRv64imac; impl Compiler for RustRv64imac { - type Error = CompileError; + type Error = Error; type Program = JoltProgram; @@ -53,21 +53,24 @@ impl Compiler for RustRv64imac { .build_options(CARGO_BUILD_OPTIONS) .rustflags(RUSTFLAGS) .exec(guest_directory, TARGET_TRIPLE)?; - Ok(elf) + Ok(JoltProgram { elf }) } } #[cfg(test)] mod tests { - use crate::{EreJolt, compiler::RustRv64imac}; + use crate::{compiler::RustRv64imac, zkvm::EreJolt}; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::{Compiler, ProverResourceType, zkVM}; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProverResourceType, zkVM}, + }; #[test] fn test_compile() { let guest_directory = testing_guest_directory("jolt", "stock_nightly_no_std"); - let elf = RustRv64imac.compile(&guest_directory).unwrap(); - assert!(!elf.is_empty(), "ELF bytes should not be empty."); + let program = RustRv64imac.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } #[test] diff --git a/crates/zkvm/jolt/src/compiler/rust_rv64imac_customized.rs b/crates/zkvm/jolt/src/compiler/rust_rv64imac_customized.rs index 98b8b03..d2421ef 100644 --- a/crates/zkvm/jolt/src/compiler/rust_rv64imac_customized.rs +++ b/crates/zkvm/jolt/src/compiler/rust_rv64imac_customized.rs @@ -1,7 +1,7 @@ -use crate::{compiler::JoltProgram, error::CompileError}; +use crate::{compiler::Error, program::JoltProgram}; use ere_compile_utils::{CommonError, cargo_metadata}; -use ere_zkvm_interface::Compiler; -use jolt_sdk::host::Program; +use ere_zkvm_interface::compiler::Compiler; +use jolt_core::host::Program; use std::{env::set_current_dir, fs, path::Path}; use tempfile::tempdir; @@ -10,13 +10,13 @@ use tempfile::tempdir; pub struct RustRv64imacCustomized; impl Compiler for RustRv64imacCustomized { - type Error = CompileError; + type Error = Error; type Program = JoltProgram; fn compile(&self, guest_directory: &Path) -> Result { // Change current directory for `Program::build` to build guest program. - set_current_dir(guest_directory).map_err(|err| CompileError::SetCurrentDirFailed { + set_current_dir(guest_directory).map_err(|err| Error::SetCurrentDirFailed { err, path: guest_directory.to_path_buf(), })?; @@ -33,12 +33,12 @@ impl Compiler for RustRv64imacCustomized { program.build(&tempdir.path().to_string_lossy()); program.elf.unwrap() }) - .map_err(|_| CompileError::BuildFailed)?; + .map_err(|_| Error::BuildFailed)?; let elf = fs::read(&elf_path).map_err(|err| CommonError::read_file("elf", &elf_path, err))?; - Ok(elf) + Ok(JoltProgram { elf }) } } @@ -46,12 +46,12 @@ impl Compiler for RustRv64imacCustomized { mod tests { use crate::compiler::RustRv64imacCustomized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("jolt", "basic"); - let elf = RustRv64imacCustomized.compile(&guest_directory).unwrap(); - assert!(!elf.is_empty(), "ELF bytes should not be empty."); + let program = RustRv64imacCustomized.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/jolt/src/lib.rs b/crates/zkvm/jolt/src/lib.rs index fedb8f0..a53c4ec 100644 --- a/crates/zkvm/jolt/src/lib.rs +++ b/crates/zkvm/jolt/src/lib.rs @@ -1,178 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{ - client::{JoltProof, JoltSdk}, - compiler::JoltProgram, - error::JoltError, -}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, -}; -use jolt_ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use std::{env, io::Cursor, time::Instant}; +pub mod program; -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); - -mod client; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -pub struct EreJolt { - sdk: JoltSdk, - _resource: ProverResourceType, -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -impl EreJolt { - pub fn new(elf: JoltProgram, resource: ProverResourceType) -> Result { - if !matches!(resource, ProverResourceType::Cpu) { - panic!("Network or GPU proving not yet implemented for Miden. Use CPU resource type."); - } - let sdk = JoltSdk::new(&elf); - Ok(EreJolt { - sdk, - _resource: resource, - }) - } -} - -impl zkVM for EreJolt { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let start = Instant::now(); - let (public_values, total_num_cycles) = self.sdk.execute(input)?; - let execution_duration = start.elapsed(); - - Ok(( - public_values, - ProgramExecutionReport { - total_num_cycles, - execution_duration, - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - let start = Instant::now(); - let (public_values, proof) = self.sdk.prove(input)?; - let proving_time = start.elapsed(); - - let mut proof_bytes = Vec::new(); - proof - .serialize_compressed(&mut proof_bytes) - .map_err(|err| CommonError::serialize("proof", "jolt", err))?; - - Ok(( - public_values, - Proof::Compressed(proof_bytes), - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - let proof = JoltProof::deserialize_compressed(&mut Cursor::new(proof)) - .map_err(|err| CommonError::deserialize("proof", "jolt", err))?; - - let public_values = self.sdk.verify(proof)?; - - Ok(public_values) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -#[cfg(test)] -mod tests { - use crate::{ - EreJolt, - compiler::{JoltProgram, RustRv64imacCustomized}, - }; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::{Mutex, OnceLock}; - - /// While proving, Jolt uses global static variables to store some - /// parameters, that might cause panics if we prove concurrently, so we put - /// a lock here for the test to work without the need to set test threads. - static PROVE_LOCK: Mutex<()> = Mutex::new(()); - - fn basic_program() -> JoltProgram { - static PROGRAM: OnceLock = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustRv64imacCustomized - .compile(&testing_guest_directory("jolt", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); - - let _guard = PROVE_LOCK.lock().unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); - - let _guard = PROVE_LOCK.lock().unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/jolt/src/program.rs b/crates/zkvm/jolt/src/program.rs new file mode 100644 index 0000000..a63e350 --- /dev/null +++ b/crates/zkvm/jolt/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Jolt program that contains ELF of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct JoltProgram { + pub(crate) elf: Vec, +} + +impl JoltProgram { + pub fn elf(&self) -> &[u8] { + &self.elf + } +} diff --git a/crates/zkvm/jolt/src/zkvm.rs b/crates/zkvm/jolt/src/zkvm.rs new file mode 100644 index 0000000..b85b25e --- /dev/null +++ b/crates/zkvm/jolt/src/zkvm.rs @@ -0,0 +1,172 @@ +use crate::{ + program::JoltProgram, + zkvm::sdk::{JoltProof, JoltSdk}, +}; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, +}; +use jolt_ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use std::{env, io::Cursor, time::Instant}; + +mod error; +mod sdk; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +pub struct EreJolt { + sdk: JoltSdk, +} + +impl EreJolt { + pub fn new(program: JoltProgram, resource: ProverResourceType) -> Result { + if !matches!(resource, ProverResourceType::Cpu) { + panic!("Network or GPU proving not yet implemented for Miden. Use CPU resource type."); + } + let sdk = JoltSdk::new(program.elf()); + Ok(EreJolt { sdk }) + } +} + +impl zkVM for EreJolt { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let start = Instant::now(); + let (public_values, total_num_cycles) = self.sdk.execute(input)?; + let execution_duration = start.elapsed(); + + Ok(( + public_values, + ProgramExecutionReport { + total_num_cycles, + execution_duration, + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + let start = Instant::now(); + let (public_values, proof) = self.sdk.prove(input)?; + let proving_time = start.elapsed(); + + let mut proof_bytes = Vec::new(); + proof + .serialize_compressed(&mut proof_bytes) + .map_err(|err| CommonError::serialize("proof", "jolt", err))?; + + Ok(( + public_values, + Proof::Compressed(proof_bytes), + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + let proof = JoltProof::deserialize_compressed(&mut Cursor::new(proof)) + .map_err(|err| CommonError::deserialize("proof", "jolt", err))?; + + let public_values = self.sdk.verify(proof)?; + + Ok(public_values) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv64imacCustomized, program::JoltProgram, zkvm::EreJolt}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::{Mutex, OnceLock}; + + /// While proving, Jolt uses global static variables to store some + /// parameters, that might cause panics if we prove concurrently, so we put + /// a lock here for the test to work without the need to set test threads. + static PROVE_LOCK: Mutex<()> = Mutex::new(()); + + fn basic_program() -> JoltProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv64imacCustomized + .compile(&testing_guest_directory("jolt", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); + + let _guard = PROVE_LOCK.lock().unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreJolt::new(program, ProverResourceType::Cpu).unwrap(); + + let _guard = PROVE_LOCK.lock().unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/jolt/src/zkvm/error.rs b/crates/zkvm/jolt/src/zkvm/error.rs new file mode 100644 index 0000000..f6a7f3e --- /dev/null +++ b/crates/zkvm/jolt/src/zkvm/error.rs @@ -0,0 +1,17 @@ +use ere_zkvm_interface::zkvm::CommonError; +use jolt_core::utils::errors::ProofVerifyError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), + + // Execute + #[error("Execution panics")] + ExecutionPanic, + + // Verify + #[error("Failed to verify proof: {0}")] + VerifyProofFailed(#[from] ProofVerifyError), +} diff --git a/crates/zkvm/jolt/src/client.rs b/crates/zkvm/jolt/src/zkvm/sdk.rs similarity index 91% rename from crates/zkvm/jolt/src/client.rs rename to crates/zkvm/jolt/src/zkvm/sdk.rs index 4bb3496..e67488f 100644 --- a/crates/zkvm/jolt/src/client.rs +++ b/crates/zkvm/jolt/src/zkvm/sdk.rs @@ -1,5 +1,5 @@ -use crate::error::JoltError; -use ere_zkvm_interface::{CommonError, PublicValues}; +use crate::zkvm::Error; +use ere_zkvm_interface::zkvm::{CommonError, PublicValues}; use jolt_ark_serialize::{self as ark_serialize, CanonicalDeserialize, CanonicalSerialize}; use jolt_common::constants::{ DEFAULT_MAX_INPUT_SIZE, DEFAULT_MAX_OUTPUT_SIZE, DEFAULT_MAX_TRACE_LENGTH, DEFAULT_MEMORY_SIZE, @@ -62,7 +62,7 @@ impl JoltSdk { } } - pub fn execute(&self, input: &[u8]) -> Result<(PublicValues, u64), JoltError> { + pub fn execute(&self, input: &[u8]) -> Result<(PublicValues, u64), Error> { let (cycles, _, io) = trace( &self.elf, None, @@ -70,16 +70,16 @@ impl JoltSdk { &self.memory_config, ); if io.panic { - return Err(JoltError::ExecutionPanic); + return Err(Error::ExecutionPanic); } let public_values = deserialize_output(&io.outputs)?; Ok((public_values, cycles.len() as _)) } - pub fn prove(&self, input: &[u8]) -> Result<(PublicValues, JoltProof), JoltError> { + pub fn prove(&self, input: &[u8]) -> Result<(PublicValues, JoltProof), Error> { let (proof, io, _) = JoltRV64IMAC::prove(&self.pk, &self.elf, &serialize_input(input)?); if io.panic { - return Err(JoltError::ExecutionPanic); + return Err(Error::ExecutionPanic); } let public_values = deserialize_output(&io.outputs)?; let proof = JoltProof { @@ -90,7 +90,7 @@ impl JoltSdk { Ok((public_values, proof)) } - pub fn verify(&self, proof: JoltProof) -> Result { + pub fn verify(&self, proof: JoltProof) -> Result { JoltRV64IMAC::verify( &self.vk, proof.proof, @@ -107,12 +107,12 @@ impl JoltSdk { } } -fn serialize_input(bytes: &[u8]) -> Result, JoltError> { +fn serialize_input(bytes: &[u8]) -> Result, Error> { Ok(postcard::to_stdvec(bytes) .map_err(|err| CommonError::serialize("input", "postcard", err))?) } -fn deserialize_output(output: &[u8]) -> Result, JoltError> { +fn deserialize_output(output: &[u8]) -> Result, Error> { Ok(if output.is_empty() { Vec::new() } else { diff --git a/crates/zkvm/miden/Cargo.toml b/crates/zkvm/miden/Cargo.toml index 39f376f..bd91e70 100644 --- a/crates/zkvm/miden/Cargo.toml +++ b/crates/zkvm/miden/Cargo.toml @@ -13,10 +13,10 @@ thiserror.workspace = true # Miden miden-assembly = { workspace = true, features = ["std"] } miden-core = { workspace = true, features = ["std"] } -miden-processor = { workspace = true, features = ["std"] } -miden-prover = { workspace = true, features = ["std"] } +miden-processor = { workspace = true, features = ["std"], optional = true } +miden-prover = { workspace = true, features = ["std"], optional = true } miden-stdlib = { workspace = true, features = ["std"] } -miden-verifier.workspace = true +miden-verifier = { workspace = true, optional = true } # Local dependencies ere-zkvm-interface.workspace = true @@ -27,5 +27,10 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true +[features] +default = ["compiler", "zkvm"] +compiler = [] +zkvm = ["dep:miden-processor", "dep:miden-prover", "dep:miden-verifier"] + [lints] workspace = true diff --git a/crates/zkvm/miden/src/compiler.rs b/crates/zkvm/miden/src/compiler.rs index 9b2bce3..29a47c7 100644 --- a/crates/zkvm/miden/src/compiler.rs +++ b/crates/zkvm/miden/src/compiler.rs @@ -1,46 +1,5 @@ -use miden_core::{ - Program, ProgramInfo, - utils::{Deserializable, Serializable}, -}; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; -use std::ops::Deref; - +mod error; mod miden_asm; +pub use error::Error; pub use miden_asm::MidenAsm; - -pub type MidenProgram = MidenSerdeWrapper; -pub type MidenProgramInfo = MidenSerdeWrapper; - -/// Wrapper that implements `serde` for Miden structures. -#[derive(Clone)] -pub struct MidenSerdeWrapper(pub T); - -impl Deref for MidenSerdeWrapper { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Serialize for MidenSerdeWrapper { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_bytes(&self.0.to_bytes()) - } -} - -impl<'de, T: Deserializable> Deserialize<'de> for MidenSerdeWrapper { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let bytes = Vec::::deserialize(deserializer)?; - T::read_from_bytes(&bytes) - .map(Self) - .map_err(D::Error::custom) - } -} diff --git a/crates/zkvm/miden/src/error.rs b/crates/zkvm/miden/src/compiler/error.rs similarity index 57% rename from crates/zkvm/miden/src/error.rs rename to crates/zkvm/miden/src/compiler/error.rs index 4b2b8b4..df0c244 100644 --- a/crates/zkvm/miden/src/error.rs +++ b/crates/zkvm/miden/src/compiler/error.rs @@ -1,11 +1,9 @@ use miden_assembly::Report; -use miden_processor::ExecutionError; -use miden_verifier::VerificationError; use std::path::PathBuf; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error("Invalid program directory name")] InvalidProgramPath, @@ -28,21 +26,3 @@ pub enum CompileError { #[error("Miden assembly compilation failed: {0}")] AssemblyCompilation(Report), } - -#[derive(Debug, Error)] -pub enum MidenError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), - - // Execute - #[error("Miden execution failed")] - Execute(#[from] ExecutionError), - - // Prove - #[error("Miden proving failed: {0}")] - Prove(#[source] ExecutionError), - - // Verify - #[error("Miden verification failed")] - Verify(#[from] VerificationError), -} diff --git a/crates/zkvm/miden/src/compiler/miden_asm.rs b/crates/zkvm/miden/src/compiler/miden_asm.rs index 08c399c..876ac56 100644 --- a/crates/zkvm/miden/src/compiler/miden_asm.rs +++ b/crates/zkvm/miden/src/compiler/miden_asm.rs @@ -1,8 +1,8 @@ use crate::{ - compiler::{MidenProgram, MidenSerdeWrapper}, - error::CompileError, + compiler::Error, + program::{MidenProgram, MidenSerdeWrapper}, }; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use miden_assembly::Assembler; use miden_stdlib::StdLibrary; use std::{env, fs, path::Path}; @@ -11,39 +11,38 @@ use std::{env, fs, path::Path}; pub struct MidenAsm; impl Compiler for MidenAsm { - type Error = CompileError; + type Error = Error; type Program = MidenProgram; fn compile(&self, guest_directory: &Path) -> Result { let dir_name = guest_directory .file_name() .and_then(|name| name.to_str()) - .ok_or(CompileError::InvalidProgramPath)?; + .ok_or(Error::InvalidProgramPath)?; let entrypoint = format!("{dir_name}.masm"); let entrypoint_path = guest_directory.join(&entrypoint); if !entrypoint_path.exists() { - return Err(CompileError::MissingEntrypoint { + return Err(Error::MissingEntrypoint { program_dir: guest_directory.display().to_string(), entrypoint, }); } - let source = - fs::read_to_string(&entrypoint_path).map_err(|err| CompileError::ReadEntrypoint { - entrypoint_path, - err, - })?; + let source = fs::read_to_string(&entrypoint_path).map_err(|err| Error::ReadEntrypoint { + entrypoint_path, + err, + })?; // Compile using Miden assembler let mut assembler = Assembler::default().with_debug_mode(env::var_os("MIDEN_DEBUG").is_some()); assembler .link_dynamic_library(StdLibrary::default()) - .map_err(CompileError::LoadStdLibrary)?; + .map_err(Error::LoadStdLibrary)?; let program = assembler .assemble_program(&source) - .map_err(CompileError::AssemblyCompilation)?; + .map_err(Error::AssemblyCompilation)?; Ok(MidenSerdeWrapper(program)) } @@ -53,7 +52,7 @@ impl Compiler for MidenAsm { mod tests { use crate::compiler::MidenAsm; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { diff --git a/crates/zkvm/miden/src/lib.rs b/crates/zkvm/miden/src/lib.rs index ae4883d..a53c4ec 100644 --- a/crates/zkvm/miden/src/lib.rs +++ b/crates/zkvm/miden/src/lib.rs @@ -1,265 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{ - compiler::{MidenProgram, MidenProgramInfo, MidenSerdeWrapper}, - error::MidenError, -}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use miden_core::{ - Program, - utils::{Deserializable, Serializable}, -}; -use miden_processor::{ - DefaultHost, ExecutionOptions, ProgramInfo, StackInputs, StackOutputs, execute as miden_execute, -}; -use miden_prover::{AdviceInputs, ExecutionProof, ProvingOptions, prove as miden_prove}; -use miden_stdlib::StdLibrary; -use miden_verifier::verify as miden_verify; -use std::{env, time::Instant}; - -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +pub mod program; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -pub use miden_core::{Felt, FieldElement}; +#[cfg(feature = "zkvm")] +pub mod zkvm; -/// [`zkVM`] implementation for Miden. -/// -/// Miden VM takes list of field elements as input instead of bytes, so in -/// [`zkVM::execute`] and [`zkVM::prove`] we require the given `input` is built -/// from [`felts_to_bytes`]. -/// Similarly, the output values of Miden is also list of field elements, to -/// be compatible with [`zkVM`], we convert it into [`PublicValues`] by -/// [`felts_to_bytes`] as well. -pub struct EreMiden { - program: Program, - _resource: ProverResourceType, -} - -impl EreMiden { - pub fn new(program: MidenProgram, resource: ProverResourceType) -> Result { - if !matches!(resource, ProverResourceType::Cpu) { - panic!("Network or GPU proving not yet implemented for Miden. Use CPU resource type."); - } - Ok(Self { - program: program.0, - _resource: resource, - }) - } - - fn setup_host() -> Result { - let mut host = DefaultHost::default(); - - host.load_library(&StdLibrary::default()) - .map_err(MidenError::Execute)?; - - Ok(host) - } -} - -impl zkVM for EreMiden { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let stack_inputs = StackInputs::default(); - let advice_inputs = AdviceInputs::default().with_stack(bytes_to_felts(input)?); - let mut host = Self::setup_host()?; - - let start = Instant::now(); - let trace = miden_execute( - &self.program, - stack_inputs, - advice_inputs, - &mut host, - ExecutionOptions::default(), - ) - .map_err(MidenError::Execute)?; - - let public_values = felts_to_bytes(trace.stack_outputs().as_slice()); - - let report = ProgramExecutionReport { - total_num_cycles: trace.trace_len_summary().main_trace_len() as u64, - execution_duration: start.elapsed(), - ..Default::default() - }; - - Ok((public_values, report)) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - - let stack_inputs = StackInputs::default(); - let advice_inputs = AdviceInputs::default().with_stack(bytes_to_felts(input)?); - let mut host = Self::setup_host()?; - - let start = Instant::now(); - let proving_options = - ProvingOptions::with_96_bit_security(env::var_os("MIDEN_DEBUG").is_some()); - - let (stack_outputs, proof) = miden_prove( - &self.program, - stack_inputs.clone(), - advice_inputs, - &mut host, - proving_options, - ) - .map_err(MidenError::Prove)?; - - let public_values = felts_to_bytes(stack_outputs.as_slice()); - let proof_bytes = (stack_outputs, proof).to_bytes(); - - Ok(( - public_values, - Proof::Compressed(proof_bytes), - ProgramProvingReport::new(start.elapsed()), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - let program_info: ProgramInfo = self.program.clone().into(); - - let stack_inputs = StackInputs::default(); - let (stack_outputs, proof): (StackOutputs, ExecutionProof) = - Deserializable::read_from_bytes(proof) - .map_err(|err| CommonError::deserialize("proof", "miden", err))?; - - miden_verify(program_info, stack_inputs, stack_outputs.clone(), proof) - .map_err(MidenError::Verify)?; - - Ok(felts_to_bytes(stack_outputs.as_slice())) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreMiden { - type ProgramDigest = MidenProgramInfo; - - fn program_digest(&self) -> anyhow::Result { - Ok(MidenSerdeWrapper(self.program.clone().into())) - } -} - -/// Convert Miden field elements into bytes -pub fn felts_to_bytes(felts: &[Felt]) -> Vec { - felts - .iter() - .flat_map(|felt| felt.as_int().to_le_bytes()) - .collect() -} - -/// Convert bytes into Miden field elements. -pub fn bytes_to_felts(bytes: &[u8]) -> Result, MidenError> { - if bytes.len() % 8 != 0 { - let err = anyhow::anyhow!( - "Invalid bytes length {}, expected multiple of 8", - bytes.len() - ); - Err(CommonError::serialize("input", "miden", err))?; - } - Ok(bytes - .chunks(8) - .map(|bytes| Felt::try_from(u64::from_le_bytes(bytes.try_into().unwrap()))) - .collect::, _>>() - .map_err(|err| CommonError::serialize("input", "miden", anyhow::anyhow!(err)))?) -} - -#[cfg(test)] -mod tests { - use crate::{ - EreMiden, Felt, FieldElement, bytes_to_felts, - compiler::{MidenAsm, MidenProgram}, - felts_to_bytes, - }; - use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - - fn load_miden_program(guest_name: &str) -> MidenProgram { - MidenAsm - .compile(&testing_guest_directory("miden", guest_name)) - .unwrap() - } - - #[test] - fn test_prove_and_verify_add() { - let program = load_miden_program("add"); - let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap(); - - let const_a = -Felt::ONE; - let const_b = Felt::ONE / Felt::ONE.double(); - let expected_sum = const_a + const_b; - - let input = felts_to_bytes(&[const_a, const_b]); - - // Prove - let (prover_public_values, proof, _) = zkvm.prove(&input, ProofKind::default()).unwrap(); - - // Verify - let verifier_public_values = zkvm.verify(&proof).unwrap(); - assert_eq!(prover_public_values, verifier_public_values); - - // Assert output - let output = bytes_to_felts(&verifier_public_values).unwrap(); - assert_eq!(output[0], expected_sum); - } - - #[test] - fn test_prove_and_verify_fib() { - let program = load_miden_program("fib"); - let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap(); - - let n_iterations = 50u32; - let expected_fib = Felt::try_from(12_586_269_025u64).unwrap(); - - let input = felts_to_bytes(&[Felt::from(0u32), Felt::from(1u32), Felt::from(n_iterations)]); - - // Prove - let (prover_public_values, proof, _) = zkvm.prove(&input, ProofKind::default()).unwrap(); - - // Verify - let verifier_public_values = zkvm.verify(&proof).unwrap(); - assert_eq!(prover_public_values, verifier_public_values); - - // Assert output - let output = bytes_to_felts(&verifier_public_values).unwrap(); - assert_eq!(output[0], expected_fib); - } - - #[test] - fn test_invalid_input() { - let program = load_miden_program("add"); - let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap(); - - let empty_inputs = Vec::new(); - assert!(zkvm.execute(&empty_inputs).is_err()); - - let insufficient_inputs = felts_to_bytes(&[Felt::from(5u32)]); - assert!(zkvm.execute(&insufficient_inputs).is_err()); - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/miden/src/program.rs b/crates/zkvm/miden/src/program.rs new file mode 100644 index 0000000..a2d8cde --- /dev/null +++ b/crates/zkvm/miden/src/program.rs @@ -0,0 +1,42 @@ +use miden_core::{ + Program, ProgramInfo, + utils::{Deserializable, Serializable}, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; +use std::ops::Deref; + +pub type MidenProgram = MidenSerdeWrapper; +pub type MidenProgramInfo = MidenSerdeWrapper; + +/// Wrapper that implements `serde` for Miden structures. +#[derive(Clone)] +pub struct MidenSerdeWrapper(pub T); + +impl Deref for MidenSerdeWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Serialize for MidenSerdeWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.0.to_bytes()) + } +} + +impl<'de, T: Deserializable> Deserialize<'de> for MidenSerdeWrapper { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let bytes = Vec::::deserialize(deserializer)?; + T::read_from_bytes(&bytes) + .map(Self) + .map_err(D::Error::custom) + } +} diff --git a/crates/zkvm/miden/src/zkvm.rs b/crates/zkvm/miden/src/zkvm.rs new file mode 100644 index 0000000..ac2ef14 --- /dev/null +++ b/crates/zkvm/miden/src/zkvm.rs @@ -0,0 +1,259 @@ +use crate::program::{MidenProgram, MidenProgramInfo, MidenSerdeWrapper}; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use miden_core::{ + Program, + utils::{Deserializable, Serializable}, +}; +use miden_processor::{ + DefaultHost, ExecutionOptions, ProgramInfo, StackInputs, StackOutputs, execute as miden_execute, +}; +use miden_prover::{AdviceInputs, ExecutionProof, ProvingOptions, prove as miden_prove}; +use miden_stdlib::StdLibrary; +use miden_verifier::verify as miden_verify; +use std::{env, time::Instant}; + +mod error; + +pub use error::Error; +pub use miden_core::{Felt, FieldElement}; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +/// [`zkVM`] implementation for Miden. +/// +/// Miden VM takes list of field elements as input instead of bytes, so in +/// [`zkVM::execute`] and [`zkVM::prove`] we require the given `input` is built +/// from [`felts_to_bytes`]. +/// Similarly, the output values of Miden is also list of field elements, to +/// be compatible with [`zkVM`], we convert it into [`PublicValues`] by +/// [`felts_to_bytes`] as well. +pub struct EreMiden { + program: Program, +} + +impl EreMiden { + pub fn new(program: MidenProgram, resource: ProverResourceType) -> Result { + if !matches!(resource, ProverResourceType::Cpu) { + panic!("Network or GPU proving not yet implemented for Miden. Use CPU resource type."); + } + Ok(Self { program: program.0 }) + } + + fn setup_host() -> Result { + let mut host = DefaultHost::default(); + + host.load_library(&StdLibrary::default()) + .map_err(Error::Execute)?; + + Ok(host) + } +} + +impl zkVM for EreMiden { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let stack_inputs = StackInputs::default(); + let advice_inputs = AdviceInputs::default().with_stack(bytes_to_felts(input)?); + let mut host = Self::setup_host()?; + + let start = Instant::now(); + let trace = miden_execute( + &self.program, + stack_inputs, + advice_inputs, + &mut host, + ExecutionOptions::default(), + ) + .map_err(Error::Execute)?; + + let public_values = felts_to_bytes(trace.stack_outputs().as_slice()); + + let report = ProgramExecutionReport { + total_num_cycles: trace.trace_len_summary().main_trace_len() as u64, + execution_duration: start.elapsed(), + ..Default::default() + }; + + Ok((public_values, report)) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + + let stack_inputs = StackInputs::default(); + let advice_inputs = AdviceInputs::default().with_stack(bytes_to_felts(input)?); + let mut host = Self::setup_host()?; + + let start = Instant::now(); + let proving_options = + ProvingOptions::with_96_bit_security(env::var_os("MIDEN_DEBUG").is_some()); + + let (stack_outputs, proof) = miden_prove( + &self.program, + stack_inputs.clone(), + advice_inputs, + &mut host, + proving_options, + ) + .map_err(Error::Prove)?; + + let public_values = felts_to_bytes(stack_outputs.as_slice()); + let proof_bytes = (stack_outputs, proof).to_bytes(); + + Ok(( + public_values, + Proof::Compressed(proof_bytes), + ProgramProvingReport::new(start.elapsed()), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + let program_info: ProgramInfo = self.program.clone().into(); + + let stack_inputs = StackInputs::default(); + let (stack_outputs, proof): (StackOutputs, ExecutionProof) = + Deserializable::read_from_bytes(proof) + .map_err(|err| CommonError::deserialize("proof", "miden", err))?; + + miden_verify(program_info, stack_inputs, stack_outputs.clone(), proof) + .map_err(Error::Verify)?; + + Ok(felts_to_bytes(stack_outputs.as_slice())) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreMiden { + type ProgramDigest = MidenProgramInfo; + + fn program_digest(&self) -> anyhow::Result { + Ok(MidenSerdeWrapper(self.program.clone().into())) + } +} + +/// Convert Miden field elements into bytes +pub fn felts_to_bytes(felts: &[Felt]) -> Vec { + felts + .iter() + .flat_map(|felt| felt.as_int().to_le_bytes()) + .collect() +} + +/// Convert bytes into Miden field elements. +pub fn bytes_to_felts(bytes: &[u8]) -> Result, Error> { + if bytes.len() % 8 != 0 { + let err = anyhow::anyhow!( + "Invalid bytes length {}, expected multiple of 8", + bytes.len() + ); + Err(CommonError::serialize("input", "miden", err))?; + } + Ok(bytes + .chunks(8) + .map(|bytes| Felt::try_from(u64::from_le_bytes(bytes.try_into().unwrap()))) + .collect::, _>>() + .map_err(|err| CommonError::serialize("input", "miden", anyhow::anyhow!(err)))?) +} + +#[cfg(test)] +mod tests { + use crate::{ + compiler::MidenAsm, + program::MidenProgram, + zkvm::{EreMiden, Felt, FieldElement, bytes_to_felts, felts_to_bytes}, + }; + use ere_test_utils::host::testing_guest_directory; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + + fn load_miden_program(guest_name: &str) -> MidenProgram { + MidenAsm + .compile(&testing_guest_directory("miden", guest_name)) + .unwrap() + } + + #[test] + fn test_prove_and_verify_add() { + let program = load_miden_program("add"); + let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap(); + + let const_a = -Felt::ONE; + let const_b = Felt::ONE / Felt::ONE.double(); + let expected_sum = const_a + const_b; + + let input = felts_to_bytes(&[const_a, const_b]); + + // Prove + let (prover_public_values, proof, _) = zkvm.prove(&input, ProofKind::default()).unwrap(); + + // Verify + let verifier_public_values = zkvm.verify(&proof).unwrap(); + assert_eq!(prover_public_values, verifier_public_values); + + // Assert output + let output = bytes_to_felts(&verifier_public_values).unwrap(); + assert_eq!(output[0], expected_sum); + } + + #[test] + fn test_prove_and_verify_fib() { + let program = load_miden_program("fib"); + let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap(); + + let n_iterations = 50u32; + let expected_fib = Felt::try_from(12_586_269_025u64).unwrap(); + + let input = felts_to_bytes(&[Felt::from(0u32), Felt::from(1u32), Felt::from(n_iterations)]); + + // Prove + let (prover_public_values, proof, _) = zkvm.prove(&input, ProofKind::default()).unwrap(); + + // Verify + let verifier_public_values = zkvm.verify(&proof).unwrap(); + assert_eq!(prover_public_values, verifier_public_values); + + // Assert output + let output = bytes_to_felts(&verifier_public_values).unwrap(); + assert_eq!(output[0], expected_fib); + } + + #[test] + fn test_invalid_input() { + let program = load_miden_program("add"); + let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap(); + + let empty_inputs = Vec::new(); + assert!(zkvm.execute(&empty_inputs).is_err()); + + let insufficient_inputs = felts_to_bytes(&[Felt::from(5u32)]); + assert!(zkvm.execute(&insufficient_inputs).is_err()); + } +} diff --git a/crates/zkvm/miden/src/zkvm/error.rs b/crates/zkvm/miden/src/zkvm/error.rs new file mode 100644 index 0000000..779e47c --- /dev/null +++ b/crates/zkvm/miden/src/zkvm/error.rs @@ -0,0 +1,22 @@ +use ere_zkvm_interface::zkvm::CommonError; +use miden_processor::ExecutionError; +use miden_verifier::VerificationError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), + + // Execute + #[error("Miden execution failed")] + Execute(#[from] ExecutionError), + + // Prove + #[error("Miden proving failed: {0}")] + Prove(#[source] ExecutionError), + + // Verify + #[error("Miden verification failed")] + Verify(#[from] VerificationError), +} diff --git a/crates/zkvm/nexus/Cargo.toml b/crates/zkvm/nexus/Cargo.toml index 9ce99eb..a967269 100644 --- a/crates/zkvm/nexus/Cargo.toml +++ b/crates/zkvm/nexus/Cargo.toml @@ -14,12 +14,12 @@ thiserror.workspace = true tracing.workspace = true # Nexus dependencies -nexus-core.workspace = true -nexus-sdk.workspace = true -nexus-vm.workspace = true +nexus-core = { workspace = true, optional = true } +nexus-sdk = { workspace = true, optional = true } +nexus-vm = { workspace = true, optional = true } # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -28,6 +28,10 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:nexus-core", "dep:nexus-sdk", "dep:nexus-vm"] [lints] workspace = true diff --git a/crates/zkvm/nexus/src/compiler.rs b/crates/zkvm/nexus/src/compiler.rs index 925698e..41992ec 100644 --- a/crates/zkvm/nexus/src/compiler.rs +++ b/crates/zkvm/nexus/src/compiler.rs @@ -1,5 +1,5 @@ +mod error; mod rust_rv32i; +pub use error::Error; pub use rust_rv32i::RustRv32i; - -pub type NexusProgram = Vec; diff --git a/crates/zkvm/nexus/src/compiler/error.rs b/crates/zkvm/nexus/src/compiler/error.rs new file mode 100644 index 0000000..477e200 --- /dev/null +++ b/crates/zkvm/nexus/src/compiler/error.rs @@ -0,0 +1,8 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), +} diff --git a/crates/zkvm/nexus/src/compiler/rust_rv32i.rs b/crates/zkvm/nexus/src/compiler/rust_rv32i.rs index dda0fcd..7b1ad25 100644 --- a/crates/zkvm/nexus/src/compiler/rust_rv32i.rs +++ b/crates/zkvm/nexus/src/compiler/rust_rv32i.rs @@ -1,6 +1,6 @@ -use crate::{compiler::NexusProgram, error::CompileError}; +use crate::{compiler::Error, program::NexusProgram}; use ere_compile_utils::CargoBuildCmd; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::path::Path; const TARGET_TRIPLE: &str = "riscv32i-unknown-none-elf"; @@ -17,7 +17,7 @@ const CARGO_BUILD_OPTIONS: &[&str] = &[ pub struct RustRv32i; impl Compiler for RustRv32i { - type Error = CompileError; + type Error = Error; type Program = NexusProgram; @@ -30,7 +30,7 @@ impl Compiler for RustRv32i { .build_options(CARGO_BUILD_OPTIONS) .rustflags(RUSTFLAGS) .exec(guest_directory, TARGET_TRIPLE)?; - Ok(elf) + Ok(NexusProgram { elf }) } } @@ -38,12 +38,12 @@ impl Compiler for RustRv32i { mod tests { use crate::compiler::RustRv32i; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("nexus", "basic"); - let elf = RustRv32i.compile(&guest_directory).unwrap(); - assert!(!elf.is_empty(), "ELF bytes should not be empty."); + let program = RustRv32i.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/nexus/src/lib.rs b/crates/zkvm/nexus/src/lib.rs index 74cc881..a53c4ec 100644 --- a/crates/zkvm/nexus/src/lib.rs +++ b/crates/zkvm/nexus/src/lib.rs @@ -1,219 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{compiler::NexusProgram, error::NexusError}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, -}; -use nexus_core::nvm::{self, ElfFile}; -use nexus_sdk::{ - KnownExitCodes, Prover, Verifiable, Viewable, - stwo::seq::{Proof as NexusProof, Stwo}, -}; -use nexus_vm::trace::Trace; -use serde::{Deserialize, Serialize}; -use std::time::Instant; -use tracing::info; - -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +pub mod program; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -#[derive(Serialize, Deserialize)] -pub struct NexusProofBundle { - proof: NexusProof, - public_values: Vec, -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -pub struct EreNexus { - elf: NexusProgram, -} - -impl EreNexus { - pub fn new(elf: NexusProgram, resource: ProverResourceType) -> Result { - if !matches!(resource, ProverResourceType::Cpu) { - panic!("Network or GPU proving not yet implemented for Nexus. Use CPU resource type."); - } - Ok(Self { elf }) - } -} - -impl zkVM for EreNexus { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let elf = ElfFile::from_bytes(&self.elf).map_err(NexusError::ParseElf)?; - - // Nexus sdk does not provide a trace, so we need to use core `nvm` - // Encoding is copied directly from `prove_with_input` - let mut private_encoded = if input.is_empty() { - Vec::new() - } else { - postcard::to_stdvec_cobs(&input) - .map_err(|err| CommonError::serialize("input", "postcard", err))? - }; - - if !private_encoded.is_empty() { - let private_padded_len = (private_encoded.len() + 3) & !3; - assert!(private_padded_len >= private_encoded.len()); - private_encoded.resize(private_padded_len, 0x00); - } - - let start = Instant::now(); - let (view, trace) = nvm::k_trace(elf, &[], &[], private_encoded.as_slice(), 1) - .map_err(NexusError::Execute)?; - let execution_duration = start.elapsed(); - - let public_values = view - .public_output() - .map_err(|err| CommonError::deserialize("public_values", "postcard", err))?; - - Ok(( - public_values, - ProgramExecutionReport { - total_num_cycles: trace.get_num_steps() as u64, - execution_duration, - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - - let elf = ElfFile::from_bytes(&self.elf).map_err(NexusError::ParseElf)?; - - let prover = Stwo::new(&elf).map_err(NexusError::Prove)?; - - let start = Instant::now(); - let (view, proof) = prover - .prove_with_input(&input, &()) - .map_err(NexusError::Prove)?; - let proving_time = start.elapsed(); - - let public_values = view - .public_output() - .map_err(|err| CommonError::deserialize("public_values", "postcard", err))?; - - let proof_bundle = NexusProofBundle { - proof, - public_values, - }; - - let proof_bytes = bincode::serde::encode_to_vec(&proof_bundle, bincode::config::legacy()) - .map_err(|err| CommonError::serialize("proof", "bincode", err))?; - - Ok(( - proof_bundle.public_values, - Proof::Compressed(proof_bytes), - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - info!("Verifying proof..."); - - let (proof_bundle, _): (NexusProofBundle, _) = - bincode::serde::decode_from_slice(proof, bincode::config::legacy()) - .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; - - proof_bundle - .proof - .verify_expected_from_program_bytes::<(), Vec>( - &(), - KnownExitCodes::ExitSuccess as u32, - &proof_bundle.public_values, - &self.elf, - &[], - ) - .map_err(NexusError::Verify)?; - - info!("Verify Succeeded!"); - - Ok(proof_bundle.public_values) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -#[cfg(test)] -mod tests { - use crate::{EreNexus, NexusProgram, compiler::RustRv32i}; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - fn basic_program() -> NexusProgram { - static PROGRAM: OnceLock = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustRv32i - .compile(&testing_guest_directory("nexus", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/nexus/src/program.rs b/crates/zkvm/nexus/src/program.rs new file mode 100644 index 0000000..5c61ff7 --- /dev/null +++ b/crates/zkvm/nexus/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Nexus program that contains ELF of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct NexusProgram { + pub(crate) elf: Vec, +} + +impl NexusProgram { + pub fn elf(&self) -> &[u8] { + &self.elf + } +} diff --git a/crates/zkvm/nexus/src/zkvm.rs b/crates/zkvm/nexus/src/zkvm.rs new file mode 100644 index 0000000..dbeff8f --- /dev/null +++ b/crates/zkvm/nexus/src/zkvm.rs @@ -0,0 +1,219 @@ +use crate::program::NexusProgram; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, +}; +use nexus_core::nvm::{self, ElfFile}; +use nexus_sdk::{ + KnownExitCodes, Prover, Verifiable, Viewable, + stwo::seq::{Proof as NexusProof, Stwo}, +}; +use nexus_vm::trace::Trace; +use serde::{Deserialize, Serialize}; +use std::time::Instant; +use tracing::info; + +mod error; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +#[derive(Serialize, Deserialize)] +pub struct NexusProofBundle { + proof: NexusProof, + public_values: Vec, +} + +pub struct EreNexus { + program: NexusProgram, +} + +impl EreNexus { + pub fn new(program: NexusProgram, resource: ProverResourceType) -> Result { + if !matches!(resource, ProverResourceType::Cpu) { + panic!("Network or GPU proving not yet implemented for Nexus. Use CPU resource type."); + } + Ok(Self { program }) + } +} + +impl zkVM for EreNexus { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let elf = ElfFile::from_bytes(self.program.elf()).map_err(Error::ParseElf)?; + + // Nexus sdk does not provide a trace, so we need to use core `nvm` + // Encoding is copied directly from `prove_with_input` + let mut private_encoded = if input.is_empty() { + Vec::new() + } else { + postcard::to_stdvec_cobs(&input) + .map_err(|err| CommonError::serialize("input", "postcard", err))? + }; + + if !private_encoded.is_empty() { + let private_padded_len = (private_encoded.len() + 3) & !3; + assert!(private_padded_len >= private_encoded.len()); + private_encoded.resize(private_padded_len, 0x00); + } + + let start = Instant::now(); + let (view, trace) = + nvm::k_trace(elf, &[], &[], private_encoded.as_slice(), 1).map_err(Error::Execute)?; + let execution_duration = start.elapsed(); + + let public_values = view + .public_output() + .map_err(|err| CommonError::deserialize("public_values", "postcard", err))?; + + Ok(( + public_values, + ProgramExecutionReport { + total_num_cycles: trace.get_num_steps() as u64, + execution_duration, + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + + let elf = ElfFile::from_bytes(self.program.elf()).map_err(Error::ParseElf)?; + + let prover = Stwo::new(&elf).map_err(Error::Prove)?; + + let start = Instant::now(); + let (view, proof) = prover.prove_with_input(&input, &()).map_err(Error::Prove)?; + let proving_time = start.elapsed(); + + let public_values = view + .public_output() + .map_err(|err| CommonError::deserialize("public_values", "postcard", err))?; + + let proof_bundle = NexusProofBundle { + proof, + public_values, + }; + + let proof_bytes = bincode::serde::encode_to_vec(&proof_bundle, bincode::config::legacy()) + .map_err(|err| CommonError::serialize("proof", "bincode", err))?; + + Ok(( + proof_bundle.public_values, + Proof::Compressed(proof_bytes), + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + info!("Verifying proof..."); + + let (proof_bundle, _): (NexusProofBundle, _) = + bincode::serde::decode_from_slice(proof, bincode::config::legacy()) + .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; + + proof_bundle + .proof + .verify_expected_from_program_bytes::<(), Vec>( + &(), + KnownExitCodes::ExitSuccess as u32, + &proof_bundle.public_values, + self.program.elf(), + &[], + ) + .map_err(Error::Verify)?; + + info!("Verify Succeeded!"); + + Ok(proof_bundle.public_values) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv32i, program::NexusProgram, zkvm::EreNexus}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> NexusProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv32i + .compile(&testing_guest_directory("nexus", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreNexus::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/nexus/src/error.rs b/crates/zkvm/nexus/src/zkvm/error.rs similarity index 69% rename from crates/zkvm/nexus/src/error.rs rename to crates/zkvm/nexus/src/zkvm/error.rs index 0865a06..13b98f3 100644 --- a/crates/zkvm/nexus/src/error.rs +++ b/crates/zkvm/nexus/src/zkvm/error.rs @@ -1,17 +1,12 @@ +use ere_zkvm_interface::zkvm::CommonError; use nexus_sdk::stwo::seq::Error as StwoError; use nexus_vm::error::VMError; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), -} - -#[derive(Debug, Error)] -pub enum NexusError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), + CommonError(#[from] CommonError), #[error("Parse ELF failed: {0}")] ParseElf(#[source] VMError), diff --git a/crates/zkvm/openvm/Cargo.toml b/crates/zkvm/openvm/Cargo.toml index fc3be78..c91598f 100644 --- a/crates/zkvm/openvm/Cargo.toml +++ b/crates/zkvm/openvm/Cargo.toml @@ -21,7 +21,7 @@ openvm-stark-sdk.workspace = true openvm-transpiler.workspace = true # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -31,7 +31,9 @@ ere-test-utils = { workspace = true, features = ["host"] } ere-build-utils.workspace = true [features] -default = [] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = [] cuda = ["openvm-sdk/cuda"] [lints] diff --git a/crates/zkvm/openvm/src/compiler.rs b/crates/zkvm/openvm/src/compiler.rs index 6278f9e..9bbde7d 100644 --- a/crates/zkvm/openvm/src/compiler.rs +++ b/crates/zkvm/openvm/src/compiler.rs @@ -1,54 +1,40 @@ -use crate::error::CompileError; use ere_compile_utils::CommonError; use openvm_sdk::config::{AppConfig, DEFAULT_APP_LOG_BLOWUP, DEFAULT_LEAF_LOG_BLOWUP, SdkVmConfig}; use openvm_stark_sdk::config::FriParameters; -use serde::{Deserialize, Serialize}; use std::{fs, path::Path}; +mod error; mod rust_rv32ima; mod rust_rv32ima_customized; +pub use error::Error; pub use rust_rv32ima::RustRv32ima; pub use rust_rv32ima_customized::RustRv32imaCustomized; -#[derive(Clone, Serialize, Deserialize)] -pub struct OpenVMProgram { - pub elf: Vec, - pub app_config: AppConfig, -} - -impl OpenVMProgram { - fn from_elf_and_app_config_path( - elf: Vec, - app_config_path: impl AsRef, - ) -> Result { - let app_config = if app_config_path.as_ref().exists() { - let toml = fs::read_to_string(app_config_path.as_ref()) - .map_err(|err| CommonError::read_file("app_config", &app_config_path, err))?; - toml::from_str(&toml) - .map_err(|err| CommonError::deserialize("app_config", "toml", err))? - } else { - // The default `AppConfig` copied from https://github.com/openvm-org/openvm/blob/v1.4.0/crates/cli/src/default.rs#L35. - AppConfig { - app_fri_params: FriParameters::standard_with_100_bits_conjectured_security( - DEFAULT_APP_LOG_BLOWUP, - ) - .into(), - // By default it supports RISCV32IM with IO but no precompiles. - app_vm_config: SdkVmConfig::builder() - .system(Default::default()) - .rv32i(Default::default()) - .rv32m(Default::default()) - .io(Default::default()) - .build(), - leaf_fri_params: FriParameters::standard_with_100_bits_conjectured_security( - DEFAULT_LEAF_LOG_BLOWUP, - ) - .into(), - compiler_options: Default::default(), - } - }; - - Ok(Self { elf, app_config }) - } +fn read_app_config(app_config_path: impl AsRef) -> Result, Error> { + Ok(if app_config_path.as_ref().exists() { + let toml = fs::read_to_string(app_config_path.as_ref()) + .map_err(|err| CommonError::read_file("app_config", &app_config_path, err))?; + toml::from_str(&toml).map_err(|err| CommonError::deserialize("app_config", "toml", err))? + } else { + // The default `AppConfig` copied from https://github.com/openvm-org/openvm/blob/v1.4.0/crates/cli/src/default.rs#L35. + AppConfig { + app_fri_params: FriParameters::standard_with_100_bits_conjectured_security( + DEFAULT_APP_LOG_BLOWUP, + ) + .into(), + // By default it supports RISCV32IM with IO but no precompiles. + app_vm_config: SdkVmConfig::builder() + .system(Default::default()) + .rv32i(Default::default()) + .rv32m(Default::default()) + .io(Default::default()) + .build(), + leaf_fri_params: FriParameters::standard_with_100_bits_conjectured_security( + DEFAULT_LEAF_LOG_BLOWUP, + ) + .into(), + compiler_options: Default::default(), + } + }) } diff --git a/crates/zkvm/openvm/src/compiler/error.rs b/crates/zkvm/openvm/src/compiler/error.rs new file mode 100644 index 0000000..7229ba2 --- /dev/null +++ b/crates/zkvm/openvm/src/compiler/error.rs @@ -0,0 +1,17 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), + + #[error("Failed to build guest, code: {0}")] + BuildFailed(i32), + + #[error("Guest building skipped (OPENVM_SKIP_BUILD is set)")] + BuildSkipped, + + #[error("Missing to find unique elf: {0}")] + UniqueElfNotFound(eyre::Error), +} diff --git a/crates/zkvm/openvm/src/compiler/rust_rv32ima.rs b/crates/zkvm/openvm/src/compiler/rust_rv32ima.rs index 378e7ea..c840c8b 100644 --- a/crates/zkvm/openvm/src/compiler/rust_rv32ima.rs +++ b/crates/zkvm/openvm/src/compiler/rust_rv32ima.rs @@ -1,6 +1,9 @@ -use crate::{compiler::OpenVMProgram, error::CompileError}; +use crate::{ + compiler::{Error, read_app_config}, + program::OpenVMProgram, +}; use ere_compile_utils::CargoBuildCmd; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{env, path::Path}; const TARGET_TRIPLE: &str = "riscv32ima-unknown-none-elf"; @@ -35,7 +38,7 @@ const CARGO_BUILD_OPTIONS: &[&str] = &[ pub struct RustRv32ima; impl Compiler for RustRv32ima { - type Error = CompileError; + type Error = Error; type Program = OpenVMProgram; @@ -46,21 +49,27 @@ impl Compiler for RustRv32ima { .build_options(CARGO_BUILD_OPTIONS) .rustflags(RUSTFLAGS) .exec(guest_directory, TARGET_TRIPLE)?; - OpenVMProgram::from_elf_and_app_config_path(elf, guest_directory.join("openvm.toml")) + Ok(OpenVMProgram { + elf, + app_config: read_app_config(guest_directory.join("openvm.toml"))?, + }) } } #[cfg(test)] mod tests { - use crate::{EreOpenVM, compiler::RustRv32ima}; + use crate::{compiler::RustRv32ima, zkvm::EreOpenVM}; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::{Compiler, ProverResourceType, zkVM}; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProverResourceType, zkVM}, + }; #[test] fn test_compile() { let guest_directory = testing_guest_directory("openvm", "stock_nightly_no_std"); let program = RustRv32ima.compile(&guest_directory).unwrap(); - assert!(!program.elf.is_empty(), "ELF bytes should not be empty."); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } #[test] diff --git a/crates/zkvm/openvm/src/compiler/rust_rv32ima_customized.rs b/crates/zkvm/openvm/src/compiler/rust_rv32ima_customized.rs index 20f81af..f52a33c 100644 --- a/crates/zkvm/openvm/src/compiler/rust_rv32ima_customized.rs +++ b/crates/zkvm/openvm/src/compiler/rust_rv32ima_customized.rs @@ -1,6 +1,9 @@ -use crate::{compiler::OpenVMProgram, error::CompileError}; +use crate::{ + compiler::{Error, read_app_config}, + program::OpenVMProgram, +}; use ere_compile_utils::CommonError; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use openvm_build::GuestOptions; use std::{fs, path::Path}; @@ -9,7 +12,7 @@ use std::{fs, path::Path}; pub struct RustRv32imaCustomized; impl Compiler for RustRv32imaCustomized { - type Error = CompileError; + type Error = Error; type Program = OpenVMProgram; @@ -19,16 +22,19 @@ impl Compiler for RustRv32imaCustomized { let guest_opts = GuestOptions::default().with_profile("release".to_string()); let target_dir = match openvm_build::build_guest_package(&pkg, &guest_opts, None, &None) { Ok(target_dir) => target_dir, - Err(Some(code)) => return Err(CompileError::BuildFailed(code))?, - Err(None) => return Err(CompileError::BuildSkipped)?, + Err(Some(code)) => return Err(Error::BuildFailed(code))?, + Err(None) => return Err(Error::BuildSkipped)?, }; let elf_path = openvm_build::find_unique_executable(guest_directory, target_dir, &None) - .map_err(CompileError::UniqueElfNotFound)?; + .map_err(Error::UniqueElfNotFound)?; let elf = fs::read(&elf_path).map_err(|err| CommonError::read_file("elf", &elf_path, err))?; - OpenVMProgram::from_elf_and_app_config_path(elf, guest_directory.join("openvm.toml")) + Ok(OpenVMProgram { + elf, + app_config: read_app_config(guest_directory.join("openvm.toml"))?, + }) } } @@ -36,12 +42,12 @@ impl Compiler for RustRv32imaCustomized { mod tests { use crate::compiler::RustRv32imaCustomized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("openvm", "basic"); let program = RustRv32imaCustomized.compile(&guest_directory).unwrap(); - assert!(!program.elf.is_empty(), "ELF bytes should not be empty."); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/openvm/src/lib.rs b/crates/zkvm/openvm/src/lib.rs index 30eb13c..a53c4ec 100644 --- a/crates/zkvm/openvm/src/lib.rs +++ b/crates/zkvm/openvm/src/lib.rs @@ -1,286 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{compiler::OpenVMProgram, error::OpenVMError}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use openvm_circuit::arch::instructions::exe::VmExe; -use openvm_continuations::verifier::internal::types::VmStarkProof; -use openvm_sdk::{ - CpuSdk, F, SC, StdIn, - codec::{Decode, Encode}, - commit::AppExecutionCommit, - config::{AppConfig, SdkVmConfig}, - fs::read_object_from_file, - keygen::{AggProvingKey, AggVerifyingKey, AppProvingKey}, -}; -use openvm_stark_sdk::openvm_stark_backend::p3_field::PrimeField32; -use openvm_transpiler::{elf::Elf, openvm_platform::memory::MEM_SIZE}; -use std::{env, path::PathBuf, sync::Arc, time::Instant}; - -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +pub mod program; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -pub struct EreOpenVM { - app_config: AppConfig, - app_exe: Arc>, - app_pk: AppProvingKey, - agg_pk: AggProvingKey, - agg_vk: AggVerifyingKey, - app_commit: AppExecutionCommit, - resource: ProverResourceType, -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -impl EreOpenVM { - pub fn new(program: OpenVMProgram, resource: ProverResourceType) -> Result { - match resource { - #[cfg(not(feature = "cuda"))] - ProverResourceType::Gpu => { - panic!("Feature `cuda` is disabled. Enable `cuda` to use GPU resource type") - } - ProverResourceType::Network(_) => { - panic!( - "Network proving not yet implemented for OpenVM. Use CPU or GPU resource type." - ); - } - _ => {} - } - - let sdk = CpuSdk::new(program.app_config.clone()).map_err(OpenVMError::SdkInit)?; - - let elf = Elf::decode(&program.elf, MEM_SIZE as u32).map_err(OpenVMError::ElfDecode)?; - - let app_exe = sdk.convert_to_exe(elf).map_err(OpenVMError::Transpile)?; - - let (app_pk, _) = sdk.app_keygen(); - - let agg_pk = read_object_from_file::(agg_pk_path()) - .map_err(OpenVMError::ReadAggKeyFailed)?; - let agg_vk = agg_pk.get_agg_vk(); - - let _ = sdk.set_agg_pk(agg_pk.clone()); - - let app_commit = sdk - .prover(app_exe.clone()) - .map_err(OpenVMError::ProverInit)? - .app_commit(); - - Ok(Self { - app_config: program.app_config, - app_exe, - app_pk, - agg_pk, - agg_vk, - app_commit, - resource, - }) - } - - fn cpu_sdk(&self) -> Result { - let sdk = CpuSdk::new_without_transpiler(self.app_config.clone()) - .map_err(OpenVMError::SdkInit)?; - let _ = sdk.set_app_pk(self.app_pk.clone()); - let _ = sdk.set_agg_pk(self.agg_pk.clone()); - Ok(sdk) - } - - #[cfg(feature = "cuda")] - fn gpu_sdk(&self) -> Result { - let sdk = openvm_sdk::GpuSdk::new_without_transpiler(self.app_config.clone()) - .map_err(OpenVMError::SdkInit)?; - let _ = sdk.set_app_pk(self.app_pk.clone()); - let _ = sdk.set_agg_pk(self.agg_pk.clone()); - Ok(sdk) - } -} - -impl zkVM for EreOpenVM { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let mut stdin = StdIn::default(); - stdin.write_bytes(input); - - let start = Instant::now(); - let public_values = self - .cpu_sdk()? - .execute(self.app_exe.clone(), stdin) - .map_err(OpenVMError::Execute)?; - - Ok(( - public_values, - ProgramExecutionReport { - execution_duration: start.elapsed(), - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - - let mut stdin = StdIn::default(); - stdin.write_bytes(input); - - let now = std::time::Instant::now(); - let (proof, app_commit) = match self.resource { - ProverResourceType::Cpu => self.cpu_sdk()?.prove(self.app_exe.clone(), stdin), - #[cfg(feature = "cuda")] - ProverResourceType::Gpu => self.gpu_sdk()?.prove(self.app_exe.clone(), stdin), - #[cfg(not(feature = "cuda"))] - ProverResourceType::Gpu => { - panic!("Feature `cuda` is disabled. Enable `cuda` to use GPU resource type") - } - ProverResourceType::Network(_) => { - panic!( - "Network proving not yet implemented for OpenVM. Use CPU or GPU resource type." - ); - } - } - .map_err(OpenVMError::Prove)?; - let elapsed = now.elapsed(); - - if app_commit != self.app_commit { - bail!(OpenVMError::UnexpectedAppCommit { - preprocessed: self.app_commit.into(), - proved: app_commit.into(), - }); - } - - let public_values = extract_public_values(&proof.user_public_values)?; - let proof_bytes = proof - .encode_to_vec() - .map_err(|err| CommonError::serialize("proof", "openvm_sdk", err))?; - - Ok(( - public_values, - Proof::Compressed(proof_bytes), - ProgramProvingReport::new(elapsed), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - let proof = VmStarkProof::::decode(&mut proof.as_slice()) - .map_err(|err| CommonError::deserialize("proof", "openvm_sdk", err))?; - - CpuSdk::verify_proof(&self.agg_vk, self.app_commit, &proof).map_err(OpenVMError::Verify)?; - - let public_values = extract_public_values(&proof.user_public_values)?; - - Ok(public_values) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreOpenVM { - type ProgramDigest = AppExecutionCommit; - - fn program_digest(&self) -> anyhow::Result { - Ok(self.app_commit) - } -} - -/// Extract public values in bytes from field elements. -/// -/// The public values revealed in guest program will be flatten into `Vec` -/// then converted to field elements `Vec`, so here we try to downcast it. -fn extract_public_values(user_public_values: &[F]) -> Result, OpenVMError> { - user_public_values - .iter() - .map(|v| u8::try_from(v.as_canonical_u32()).ok()) - .collect::>() - .ok_or(OpenVMError::InvalidPublicValue) -} - -fn agg_pk_path() -> PathBuf { - PathBuf::from(std::env::var("HOME").expect("env `$HOME` should be set")) - .join(".openvm/agg_stark.pk") -} - -#[cfg(test)] -mod tests { - use crate::{ - EreOpenVM, - compiler::{OpenVMProgram, RustRv32imaCustomized}, - }; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - fn basic_program() -> OpenVMProgram { - static PROGRAM: OnceLock = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustRv32imaCustomized - .compile(&testing_guest_directory("openvm", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid().into_output_sha256(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid().into_output_sha256(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/openvm/src/program.rs b/crates/zkvm/openvm/src/program.rs new file mode 100644 index 0000000..b86827f --- /dev/null +++ b/crates/zkvm/openvm/src/program.rs @@ -0,0 +1,19 @@ +use openvm_sdk::config::{AppConfig, SdkVmConfig}; +use serde::{Deserialize, Serialize}; + +/// OpenVM program that contains ELF of compiled guest and app config. +#[derive(Clone, Serialize, Deserialize)] +pub struct OpenVMProgram { + pub(crate) elf: Vec, + pub(crate) app_config: AppConfig, +} + +impl OpenVMProgram { + pub fn elf(&self) -> &[u8] { + &self.elf + } + + pub fn app_config(&self) -> &AppConfig { + &self.app_config + } +} diff --git a/crates/zkvm/openvm/src/zkvm.rs b/crates/zkvm/openvm/src/zkvm.rs new file mode 100644 index 0000000..f694950 --- /dev/null +++ b/crates/zkvm/openvm/src/zkvm.rs @@ -0,0 +1,285 @@ +use crate::program::OpenVMProgram; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use openvm_circuit::arch::instructions::exe::VmExe; +use openvm_continuations::verifier::internal::types::VmStarkProof; +use openvm_sdk::{ + CpuSdk, F, SC, StdIn, + codec::{Decode, Encode}, + commit::AppExecutionCommit, + config::{AppConfig, SdkVmConfig}, + fs::read_object_from_file, + keygen::{AggProvingKey, AggVerifyingKey, AppProvingKey}, +}; +use openvm_stark_sdk::openvm_stark_backend::p3_field::PrimeField32; +use openvm_transpiler::{elf::Elf, openvm_platform::memory::MEM_SIZE}; +use std::{env, path::PathBuf, sync::Arc, time::Instant}; + +mod error; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +pub struct EreOpenVM { + app_config: AppConfig, + app_exe: Arc>, + app_pk: AppProvingKey, + agg_pk: AggProvingKey, + agg_vk: AggVerifyingKey, + app_commit: AppExecutionCommit, + resource: ProverResourceType, +} + +impl EreOpenVM { + pub fn new(program: OpenVMProgram, resource: ProverResourceType) -> Result { + match resource { + #[cfg(not(feature = "cuda"))] + ProverResourceType::Gpu => { + panic!("Feature `cuda` is disabled. Enable `cuda` to use GPU resource type") + } + ProverResourceType::Network(_) => { + panic!( + "Network proving not yet implemented for OpenVM. Use CPU or GPU resource type." + ); + } + _ => {} + } + + let sdk = CpuSdk::new(program.app_config().clone()).map_err(Error::SdkInit)?; + + let elf = Elf::decode(program.elf(), MEM_SIZE as u32).map_err(Error::ElfDecode)?; + + let app_exe = sdk.convert_to_exe(elf).map_err(Error::Transpile)?; + + let (app_pk, _) = sdk.app_keygen(); + + let agg_pk = read_object_from_file::(agg_pk_path()) + .map_err(Error::ReadAggKeyFailed)?; + let agg_vk = agg_pk.get_agg_vk(); + + let _ = sdk.set_agg_pk(agg_pk.clone()); + + let app_commit = sdk + .prover(app_exe.clone()) + .map_err(Error::ProverInit)? + .app_commit(); + + Ok(Self { + app_config: program.app_config, + app_exe, + app_pk, + agg_pk, + agg_vk, + app_commit, + resource, + }) + } + + fn cpu_sdk(&self) -> Result { + let sdk = + CpuSdk::new_without_transpiler(self.app_config.clone()).map_err(Error::SdkInit)?; + let _ = sdk.set_app_pk(self.app_pk.clone()); + let _ = sdk.set_agg_pk(self.agg_pk.clone()); + Ok(sdk) + } + + #[cfg(feature = "cuda")] + fn gpu_sdk(&self) -> Result { + let sdk = openvm_sdk::GpuSdk::new_without_transpiler(self.app_config.clone()) + .map_err(Error::SdkInit)?; + let _ = sdk.set_app_pk(self.app_pk.clone()); + let _ = sdk.set_agg_pk(self.agg_pk.clone()); + Ok(sdk) + } +} + +impl zkVM for EreOpenVM { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let mut stdin = StdIn::default(); + stdin.write_bytes(input); + + let start = Instant::now(); + let public_values = self + .cpu_sdk()? + .execute(self.app_exe.clone(), stdin) + .map_err(Error::Execute)?; + + Ok(( + public_values, + ProgramExecutionReport { + execution_duration: start.elapsed(), + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + + let mut stdin = StdIn::default(); + stdin.write_bytes(input); + + let now = std::time::Instant::now(); + let (proof, app_commit) = match self.resource { + ProverResourceType::Cpu => self.cpu_sdk()?.prove(self.app_exe.clone(), stdin), + #[cfg(feature = "cuda")] + ProverResourceType::Gpu => self.gpu_sdk()?.prove(self.app_exe.clone(), stdin), + #[cfg(not(feature = "cuda"))] + ProverResourceType::Gpu => { + panic!("Feature `cuda` is disabled. Enable `cuda` to use GPU resource type") + } + ProverResourceType::Network(_) => { + panic!( + "Network proving not yet implemented for OpenVM. Use CPU or GPU resource type." + ); + } + } + .map_err(Error::Prove)?; + let elapsed = now.elapsed(); + + if app_commit != self.app_commit { + bail!(Error::UnexpectedAppCommit { + preprocessed: self.app_commit.into(), + proved: app_commit.into(), + }); + } + + let public_values = extract_public_values(&proof.user_public_values)?; + let proof_bytes = proof + .encode_to_vec() + .map_err(|err| CommonError::serialize("proof", "openvm_sdk", err))?; + + Ok(( + public_values, + Proof::Compressed(proof_bytes), + ProgramProvingReport::new(elapsed), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + let proof = VmStarkProof::::decode(&mut proof.as_slice()) + .map_err(|err| CommonError::deserialize("proof", "openvm_sdk", err))?; + + CpuSdk::verify_proof(&self.agg_vk, self.app_commit, &proof).map_err(Error::Verify)?; + + let public_values = extract_public_values(&proof.user_public_values)?; + + Ok(public_values) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreOpenVM { + type ProgramDigest = AppExecutionCommit; + + fn program_digest(&self) -> anyhow::Result { + Ok(self.app_commit) + } +} + +/// Extract public values in bytes from field elements. +/// +/// The public values revealed in guest program will be flatten into `Vec` +/// then converted to field elements `Vec`, so here we try to downcast it. +fn extract_public_values(user_public_values: &[F]) -> Result, Error> { + user_public_values + .iter() + .map(|v| u8::try_from(v.as_canonical_u32()).ok()) + .collect::>() + .ok_or(Error::InvalidPublicValue) +} + +fn agg_pk_path() -> PathBuf { + PathBuf::from(std::env::var("HOME").expect("env `$HOME` should be set")) + .join(".openvm/agg_stark.pk") +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv32imaCustomized, program::OpenVMProgram, zkvm::EreOpenVM}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> OpenVMProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv32imaCustomized + .compile(&testing_guest_directory("openvm", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid().into_output_sha256(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid().into_output_sha256(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreOpenVM::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/openvm/src/error.rs b/crates/zkvm/openvm/src/zkvm/error.rs similarity index 69% rename from crates/zkvm/openvm/src/error.rs rename to crates/zkvm/openvm/src/zkvm/error.rs index 6610f89..1bcdd0b 100644 --- a/crates/zkvm/openvm/src/error.rs +++ b/crates/zkvm/openvm/src/zkvm/error.rs @@ -1,25 +1,11 @@ +use ere_zkvm_interface::zkvm::CommonError; use openvm_sdk::{SdkError, commit::AppExecutionCommit}; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), - - #[error("Failed to build guest, code: {0}")] - BuildFailed(i32), - - #[error("Guest building skipped (OPENVM_SKIP_BUILD is set)")] - BuildSkipped, - - #[error("Missing to find unique elf: {0}")] - UniqueElfNotFound(eyre::Error), -} - -#[derive(Debug, Error)] -pub enum OpenVMError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), + CommonError(#[from] CommonError), // Common #[error("Initialize SDK failed: {0}")] diff --git a/crates/zkvm/pico/Cargo.toml b/crates/zkvm/pico/Cargo.toml index 50ef31b..e2bb35c 100644 --- a/crates/zkvm/pico/Cargo.toml +++ b/crates/zkvm/pico/Cargo.toml @@ -14,11 +14,11 @@ tempfile.workspace = true thiserror.workspace = true # Pico dependencies -pico-p3-field.workspace = true -pico-vm.workspace = true +pico-p3-field = { workspace = true, optional = true } +pico-vm = { workspace = true, optional = true } # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -27,5 +27,10 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:pico-p3-field", "dep:pico-vm"] + [lints] workspace = true diff --git a/crates/zkvm/pico/src/compiler.rs b/crates/zkvm/pico/src/compiler.rs index a4e54e1..888b55b 100644 --- a/crates/zkvm/pico/src/compiler.rs +++ b/crates/zkvm/pico/src/compiler.rs @@ -1,7 +1,7 @@ +mod error; mod rust_rv32ima; mod rust_rv32ima_customized; +pub use error::Error; pub use rust_rv32ima::RustRv32ima; pub use rust_rv32ima_customized::RustRv32imaCustomized; - -pub type PicoProgram = Vec; diff --git a/crates/zkvm/pico/src/compiler/error.rs b/crates/zkvm/pico/src/compiler/error.rs new file mode 100644 index 0000000..477e200 --- /dev/null +++ b/crates/zkvm/pico/src/compiler/error.rs @@ -0,0 +1,8 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), +} diff --git a/crates/zkvm/pico/src/compiler/rust_rv32ima.rs b/crates/zkvm/pico/src/compiler/rust_rv32ima.rs index 51e2a11..e39f047 100644 --- a/crates/zkvm/pico/src/compiler/rust_rv32ima.rs +++ b/crates/zkvm/pico/src/compiler/rust_rv32ima.rs @@ -1,6 +1,6 @@ -use crate::{compiler::PicoProgram, error::CompileError}; +use crate::{compiler::Error, program::PicoProgram}; use ere_compile_utils::CargoBuildCmd; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{env, path::Path}; const TARGET_TRIPLE: &str = "riscv32ima-unknown-none-elf"; @@ -32,7 +32,7 @@ const CARGO_BUILD_OPTIONS: &[&str] = &[ pub struct RustRv32ima; impl Compiler for RustRv32ima { - type Error = CompileError; + type Error = Error; type Program = PicoProgram; @@ -43,21 +43,24 @@ impl Compiler for RustRv32ima { .build_options(CARGO_BUILD_OPTIONS) .rustflags(RUSTFLAGS) .exec(guest_directory, TARGET_TRIPLE)?; - Ok(elf) + Ok(PicoProgram { elf }) } } #[cfg(test)] mod tests { - use crate::{ErePico, compiler::RustRv32ima}; + use crate::{compiler::RustRv32ima, zkvm::ErePico}; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::{Compiler, ProverResourceType, zkVM}; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProverResourceType, zkVM}, + }; #[test] fn test_compile() { let guest_directory = testing_guest_directory("pico", "stock_nightly_no_std"); - let elf = RustRv32ima.compile(&guest_directory).unwrap(); - assert!(!elf.is_empty(), "ELF bytes should not be empty."); + let program = RustRv32ima.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } #[test] diff --git a/crates/zkvm/pico/src/compiler/rust_rv32ima_customized.rs b/crates/zkvm/pico/src/compiler/rust_rv32ima_customized.rs index 59573db..5900e55 100644 --- a/crates/zkvm/pico/src/compiler/rust_rv32ima_customized.rs +++ b/crates/zkvm/pico/src/compiler/rust_rv32ima_customized.rs @@ -1,6 +1,6 @@ -use crate::error::CompileError; +use crate::{compiler::Error, program::PicoProgram}; use ere_compile_utils::{CommonError, cargo_metadata}; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{fs, path::Path, process::Command}; use tempfile::tempdir; @@ -9,9 +9,9 @@ use tempfile::tempdir; pub struct RustRv32imaCustomized; impl Compiler for RustRv32imaCustomized { - type Error = CompileError; + type Error = Error; - type Program = Vec; + type Program = PicoProgram; fn compile(&self, guest_directory: &Path) -> Result { let tempdir = tempdir().map_err(CommonError::tempdir)?; @@ -32,10 +32,10 @@ impl Compiler for RustRv32imaCustomized { } let elf_path = tempdir.path().join("riscv32im-pico-zkvm-elf"); - let elf_bytes = + let elf = fs::read(&elf_path).map_err(|err| CommonError::read_file("elf", &elf_path, err))?; - Ok(elf_bytes) + Ok(PicoProgram { elf }) } } @@ -43,12 +43,12 @@ impl Compiler for RustRv32imaCustomized { mod tests { use crate::compiler::RustRv32imaCustomized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("pico", "basic"); - let elf = RustRv32imaCustomized.compile(&guest_directory).unwrap(); - assert!(!elf.is_empty(), "ELF bytes should not be empty."); + let program = RustRv32imaCustomized.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/pico/src/lib.rs b/crates/zkvm/pico/src/lib.rs index 453a13f..a53c4ec 100644 --- a/crates/zkvm/pico/src/lib.rs +++ b/crates/zkvm/pico/src/lib.rs @@ -1,244 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{ - client::{BaseVerifyingKey, MetaProof, ProverClient}, - compiler::PicoProgram, - error::PicoError, -}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use pico_p3_field::PrimeField32; -use pico_vm::emulator::stdin::EmulatorStdinBuilder; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::{env, panic, time::Instant}; +pub mod program; -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); - -pub mod client; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -#[derive(Serialize, Deserialize)] -pub struct PicoProofWithPublicValues { - proof: MetaProof, - public_values: Vec, -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -pub struct ErePico { - program: PicoProgram, -} - -impl ErePico { - pub fn new(program: PicoProgram, resource: ProverResourceType) -> Result { - if !matches!(resource, ProverResourceType::Cpu) { - panic!("Network or GPU proving not yet implemented for Pico. Use CPU resource type."); - } - Ok(ErePico { program }) - } - - pub fn client(&self) -> ProverClient { - ProverClient::new(&self.program) - } -} - -impl zkVM for ErePico { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let mut stdin = EmulatorStdinBuilder::default(); - stdin.write_slice(input); - - let ((total_num_cycles, public_values), execution_duration) = panic::catch_unwind(|| { - let client = self.client(); - let start = Instant::now(); - let result = client.execute(stdin); - (result, start.elapsed()) - }) - .map_err(|err| PicoError::ExecutePanic(panic_msg(err)))?; - - Ok(( - public_values, - ProgramExecutionReport { - total_num_cycles, - execution_duration, - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - - let mut stdin = EmulatorStdinBuilder::default(); - stdin.write_slice(input); - - let ((public_values, proof), proving_time) = panic::catch_unwind(|| { - let client = self.client(); - let start = Instant::now(); - let result = client.prove(stdin)?; - Ok((result, start.elapsed())) - }) - .map_err(|err| PicoError::ProvePanic(panic_msg(err)))? - .map_err(PicoError::Prove)?; - - let proof_bytes = bincode::serde::encode_to_vec( - &PicoProofWithPublicValues { - proof, - public_values: public_values.clone(), - }, - bincode::config::legacy(), - ) - .map_err(|err| CommonError::serialize("proof", "bincode", err))?; - - Ok(( - public_values, - Proof::Compressed(proof_bytes), - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - let client = self.client(); - - let (proof, _): (PicoProofWithPublicValues, _) = - bincode::serde::decode_from_slice(proof, bincode::config::legacy()) - .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; - - client.verify(&proof.proof).map_err(PicoError::Verify)?; - - let claimed = <[u8; 32]>::from(Sha256::digest(&proof.public_values)); - let proved = extract_public_values_sha256_digest(&proof.proof)?; - if claimed != proved { - bail!(PicoError::UnexpectedPublicValuesDigest { claimed, proved }); - } - - Ok(proof.public_values) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for ErePico { - type ProgramDigest = BaseVerifyingKey; - - fn program_digest(&self) -> anyhow::Result { - Ok(self.client().vk().clone()) - } -} - -/// Extract public values sha256 digest from base proof of compressed proof. -/// The sha256 digest will be placed at the first 32 field elements of the -/// public values of the only base proof. -fn extract_public_values_sha256_digest(proof: &MetaProof) -> Result<[u8; 32], PicoError> { - if proof.proofs().len() != 1 { - return Err(PicoError::InvalidBaseProofLength(proof.proofs().len())); - } - - if proof.proofs()[0].public_values.len() < 32 { - return Err(PicoError::InvalidPublicValuesLength( - proof.proofs()[0].public_values.len(), - )); - } - - Ok(proof.proofs()[0].public_values[..32] - .iter() - .map(|value| u8::try_from(value.as_canonical_u32())) - .collect::, _>>() - .map_err(|_| PicoError::InvalidPublicValues)? - .try_into() - .unwrap()) -} - -fn panic_msg(err: Box) -> String { - None.or_else(|| err.downcast_ref::().cloned()) - .or_else(|| err.downcast_ref::<&'static str>().map(ToString::to_string)) - .unwrap_or_else(|| "unknown panic msg".to_string()) -} - -#[cfg(test)] -mod tests { - use crate::{ - ErePico, - compiler::{PicoProgram, RustRv32imaCustomized}, - }; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - static BASIC_PROGRAM: OnceLock = OnceLock::new(); - - fn basic_program() -> PicoProgram { - BASIC_PROGRAM - .get_or_init(|| { - RustRv32imaCustomized - .compile(&testing_guest_directory("pico", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/pico/src/program.rs b/crates/zkvm/pico/src/program.rs new file mode 100644 index 0000000..f8c863d --- /dev/null +++ b/crates/zkvm/pico/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Pico program that contains ELF of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct PicoProgram { + pub(crate) elf: Vec, +} + +impl PicoProgram { + pub fn elf(&self) -> &[u8] { + &self.elf + } +} diff --git a/crates/zkvm/pico/src/zkvm.rs b/crates/zkvm/pico/src/zkvm.rs new file mode 100644 index 0000000..0117de2 --- /dev/null +++ b/crates/zkvm/pico/src/zkvm.rs @@ -0,0 +1,241 @@ +use crate::{ + program::PicoProgram, + zkvm::sdk::{BaseVerifyingKey, MetaProof, ProverClient}, +}; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use pico_p3_field::PrimeField32; +use pico_vm::emulator::stdin::EmulatorStdinBuilder; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::{env, panic, time::Instant}; + +mod error; +mod sdk; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +#[derive(Serialize, Deserialize)] +pub struct PicoProofWithPublicValues { + proof: MetaProof, + public_values: Vec, +} + +pub struct ErePico { + program: PicoProgram, +} + +impl ErePico { + pub fn new(program: PicoProgram, resource: ProverResourceType) -> Result { + if !matches!(resource, ProverResourceType::Cpu) { + panic!("Network or GPU proving not yet implemented for Pico. Use CPU resource type."); + } + Ok(ErePico { program }) + } + + pub fn client(&self) -> ProverClient { + ProverClient::new(self.program.elf()) + } +} + +impl zkVM for ErePico { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let mut stdin = EmulatorStdinBuilder::default(); + stdin.write_slice(input); + + let ((total_num_cycles, public_values), execution_duration) = panic::catch_unwind(|| { + let client = self.client(); + let start = Instant::now(); + let result = client.execute(stdin); + (result, start.elapsed()) + }) + .map_err(|err| Error::ExecutePanic(panic_msg(err)))?; + + Ok(( + public_values, + ProgramExecutionReport { + total_num_cycles, + execution_duration, + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + + let mut stdin = EmulatorStdinBuilder::default(); + stdin.write_slice(input); + + let ((public_values, proof), proving_time) = panic::catch_unwind(|| { + let client = self.client(); + let start = Instant::now(); + let result = client.prove(stdin)?; + Ok((result, start.elapsed())) + }) + .map_err(|err| Error::ProvePanic(panic_msg(err)))? + .map_err(Error::Prove)?; + + let proof_bytes = bincode::serde::encode_to_vec( + &PicoProofWithPublicValues { + proof, + public_values: public_values.clone(), + }, + bincode::config::legacy(), + ) + .map_err(|err| CommonError::serialize("proof", "bincode", err))?; + + Ok(( + public_values, + Proof::Compressed(proof_bytes), + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + let client = self.client(); + + let (proof, _): (PicoProofWithPublicValues, _) = + bincode::serde::decode_from_slice(proof, bincode::config::legacy()) + .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; + + client.verify(&proof.proof).map_err(Error::Verify)?; + + let claimed = <[u8; 32]>::from(Sha256::digest(&proof.public_values)); + let proved = extract_public_values_sha256_digest(&proof.proof)?; + if claimed != proved { + bail!(Error::UnexpectedPublicValuesDigest { claimed, proved }); + } + + Ok(proof.public_values) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for ErePico { + type ProgramDigest = BaseVerifyingKey; + + fn program_digest(&self) -> anyhow::Result { + Ok(self.client().vk().clone()) + } +} + +/// Extract public values sha256 digest from base proof of compressed proof. +/// The sha256 digest will be placed at the first 32 field elements of the +/// public values of the only base proof. +fn extract_public_values_sha256_digest(proof: &MetaProof) -> Result<[u8; 32], Error> { + if proof.proofs().len() != 1 { + return Err(Error::InvalidBaseProofLength(proof.proofs().len())); + } + + if proof.proofs()[0].public_values.len() < 32 { + return Err(Error::InvalidPublicValuesLength( + proof.proofs()[0].public_values.len(), + )); + } + + Ok(proof.proofs()[0].public_values[..32] + .iter() + .map(|value| u8::try_from(value.as_canonical_u32())) + .collect::, _>>() + .map_err(|_| Error::InvalidPublicValues)? + .try_into() + .unwrap()) +} + +fn panic_msg(err: Box) -> String { + None.or_else(|| err.downcast_ref::().cloned()) + .or_else(|| err.downcast_ref::<&'static str>().map(ToString::to_string)) + .unwrap_or_else(|| "unknown panic msg".to_string()) +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv32imaCustomized, program::PicoProgram, zkvm::ErePico}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> PicoProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv32imaCustomized + .compile(&testing_guest_directory("pico", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = ErePico::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/pico/src/error.rs b/crates/zkvm/pico/src/zkvm/error.rs similarity index 80% rename from crates/zkvm/pico/src/error.rs rename to crates/zkvm/pico/src/zkvm/error.rs index 9d207c4..8d4bbe8 100644 --- a/crates/zkvm/pico/src/error.rs +++ b/crates/zkvm/pico/src/zkvm/error.rs @@ -1,15 +1,10 @@ +use ere_zkvm_interface::zkvm::CommonError; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), -} - -#[derive(Debug, Error)] -pub enum PicoError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), + CommonError(#[from] CommonError), // Execute #[error("Pico execution panicked: {0}")] diff --git a/crates/zkvm/pico/src/client.rs b/crates/zkvm/pico/src/zkvm/sdk.rs similarity index 98% rename from crates/zkvm/pico/src/client.rs rename to crates/zkvm/pico/src/zkvm/sdk.rs index 21accbc..fd7c340 100644 --- a/crates/zkvm/pico/src/client.rs +++ b/crates/zkvm/pico/src/zkvm/sdk.rs @@ -3,7 +3,7 @@ // on chain. Issue for tracking: https://github.com/eth-act/ere/issues/140. use anyhow::{Error, Ok, Result}; -use ere_zkvm_interface::PublicValues; +use ere_zkvm_interface::zkvm::PublicValues; use pico_vm::{ compiler::riscv::program::Program, configs::{config::StarkGenericConfig, stark_config::KoalaBearPoseidon2}, diff --git a/crates/zkvm/risc0/Cargo.toml b/crates/zkvm/risc0/Cargo.toml index 7cb7698..6f9e358 100644 --- a/crates/zkvm/risc0/Cargo.toml +++ b/crates/zkvm/risc0/Cargo.toml @@ -15,11 +15,11 @@ tracing.workspace = true # Risc0 dependencies risc0-build = { workspace = true, features = ["unstable"] } risc0-zkp.workspace = true -risc0-zkvm = { workspace = true, features = ["client", "unstable"] } +risc0-zkvm = { workspace = true, features = ["client", "unstable"], optional = true } risc0-binfmt.workspace = true # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -30,6 +30,9 @@ ere-test-utils = { workspace = true, features = ["host"] } ere-build-utils.workspace = true [features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:risc0-zkvm"] metal = ["risc0-zkvm/metal"] [lints] diff --git a/crates/zkvm/risc0/src/compiler.rs b/crates/zkvm/risc0/src/compiler.rs index e387252..888b55b 100644 --- a/crates/zkvm/risc0/src/compiler.rs +++ b/crates/zkvm/risc0/src/compiler.rs @@ -1,14 +1,7 @@ -use risc0_zkvm::Digest; -use serde::{Deserialize, Serialize}; - +mod error; mod rust_rv32ima; mod rust_rv32ima_customized; +pub use error::Error; pub use rust_rv32ima::RustRv32ima; pub use rust_rv32ima_customized::RustRv32imaCustomized; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Risc0Program { - pub(crate) elf: Vec, - pub(crate) image_id: Digest, -} diff --git a/crates/zkvm/risc0/src/compiler/error.rs b/crates/zkvm/risc0/src/compiler/error.rs new file mode 100644 index 0000000..adfc5c0 --- /dev/null +++ b/crates/zkvm/risc0/src/compiler/error.rs @@ -0,0 +1,22 @@ +use ere_compile_utils::CommonError; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), + + #[error("`risc0_build::build_package` for {guest_path} failed: {err}")] + BuildFailure { + #[source] + err: anyhow::Error, + guest_path: PathBuf, + }, + + #[error("`risc0_build::build_package` succeeded but failed to find guest")] + Risc0BuildMissingGuest, + + #[error("ELF binary image calculation failure: {0}")] + ImageIDCalculationFailure(anyhow::Error), +} diff --git a/crates/zkvm/risc0/src/compiler/rust_rv32ima.rs b/crates/zkvm/risc0/src/compiler/rust_rv32ima.rs index 633c8bc..a27aaf9 100644 --- a/crates/zkvm/risc0/src/compiler/rust_rv32ima.rs +++ b/crates/zkvm/risc0/src/compiler/rust_rv32ima.rs @@ -1,6 +1,6 @@ -use crate::{compiler::Risc0Program, error::CompileError}; +use crate::{compiler::Error, program::Risc0Program}; use ere_compile_utils::CargoBuildCmd; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use risc0_binfmt::ProgramBinary; use std::{env, path::Path}; use tracing::info; @@ -32,7 +32,7 @@ const CARGO_BUILD_OPTIONS: &[&str] = &[ pub struct RustRv32ima; impl Compiler for RustRv32ima { - type Error = CompileError; + type Error = Error; type Program = Risc0Program; @@ -47,7 +47,7 @@ impl Compiler for RustRv32ima { let program = ProgramBinary::new(elf.as_slice(), V1COMPAT_ELF); let image_id = program .compute_image_id() - .map_err(CompileError::ImageIDCalculationFailure)?; + .map_err(Error::ImageIDCalculationFailure)?; info!("Risc0 program compiled OK - {} bytes", elf.len()); info!("Image ID - {image_id}"); @@ -61,9 +61,12 @@ impl Compiler for RustRv32ima { #[cfg(test)] mod tests { - use crate::{EreRisc0, compiler::RustRv32ima}; + use crate::{compiler::RustRv32ima, zkvm::EreRisc0}; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::{Compiler, ProverResourceType, zkVM}; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProverResourceType, zkVM}, + }; #[test] fn test_compile() { diff --git a/crates/zkvm/risc0/src/compiler/rust_rv32ima_customized.rs b/crates/zkvm/risc0/src/compiler/rust_rv32ima_customized.rs index 145d73b..84f33d0 100644 --- a/crates/zkvm/risc0/src/compiler/rust_rv32ima_customized.rs +++ b/crates/zkvm/risc0/src/compiler/rust_rv32ima_customized.rs @@ -1,6 +1,6 @@ -use crate::{compiler::Risc0Program, error::CompileError}; +use crate::{compiler::Error, program::Risc0Program}; use ere_compile_utils::cargo_metadata; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use risc0_build::GuestOptions; use std::path::Path; use tracing::info; @@ -10,7 +10,7 @@ use tracing::info; pub struct RustRv32imaCustomized; impl Compiler for RustRv32imaCustomized { - type Error = CompileError; + type Error = Error; type Program = Risc0Program; @@ -27,13 +27,13 @@ impl Compiler for RustRv32imaCustomized { &metadata.target_directory, GuestOptions::default(), ) - .map_err(|err| CompileError::BuildFailure { + .map_err(|err| Error::BuildFailure { err, guest_path: guest_directory.to_path_buf(), })? .into_iter() .next() - .ok_or(CompileError::Risc0BuildMissingGuest)?; + .ok_or(Error::Risc0BuildMissingGuest)?; let elf = guest.elf.to_vec(); let image_id = guest.image_id; @@ -49,7 +49,7 @@ impl Compiler for RustRv32imaCustomized { mod tests { use crate::compiler::RustRv32imaCustomized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { diff --git a/crates/zkvm/risc0/src/lib.rs b/crates/zkvm/risc0/src/lib.rs index 4b96352..a53c4ec 100644 --- a/crates/zkvm/risc0/src/lib.rs +++ b/crates/zkvm/risc0/src/lib.rs @@ -1,303 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{compiler::Risc0Program, error::Risc0Error}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use risc0_zkvm::{ - DEFAULT_MAX_PO2, DefaultProver, Digest, ExecutorEnv, ExternalProver, InnerReceipt, ProverOpts, - Receipt, default_executor, default_prover, -}; -use std::{env, ops::RangeInclusive, rc::Rc, time::Instant}; - -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +pub mod program; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -/// Default logarithmic segment size from [`DEFAULT_SEGMENT_LIMIT_PO2`]. -/// -/// [`DEFAULT_SEGMENT_LIMIT_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/rv32im/src/execute/mod.rs#L39. -const DEFAULT_SEGMENT_PO2: usize = 20; +#[cfg(feature = "zkvm")] +pub mod zkvm; -/// Supported range of logarithmic segment size. -/// -/// The minimum is by [`MIN_LIFT_PO2`] to be lifted. -/// -/// The maximum is by [`DEFAULT_MAX_PO2`], although the real maximum is `24`, -/// but it requires us to set the `control_ids` manually in the `ProverOpts`. -/// -/// [`MIN_LIFT_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/recursion/src/control_id.rs#L19 -/// [`DEFAULT_MAX_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/zkvm/src/receipt.rs#L884 -const SEGMENT_PO2_RANGE: RangeInclusive = 14..=DEFAULT_MAX_PO2; - -/// Default logarithmic keccak size from [`KECCAK_DEFAULT_PO2`]. -/// -/// [`KECCAK_DEFAULT_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/keccak/src/lib.rs#L27. -const DEFAULT_KECCAK_PO2: usize = 17; - -/// Supported range of logarithmic keccak size from [`KECCAK_PO2_RANGE`]. -/// -/// [`KECCAK_PO2_RANGE`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/keccak/src/lib.rs#L29. -const KECCAK_PO2_RANGE: RangeInclusive = 14..=18; - -pub struct EreRisc0 { - program: Risc0Program, - resource: ProverResourceType, - segment_po2: usize, - keccak_po2: usize, -} - -impl EreRisc0 { - pub fn new(program: Risc0Program, resource: ProverResourceType) -> Result { - if matches!(resource, ProverResourceType::Network(_)) { - panic!( - "Network proving not yet implemented for RISC Zero. Use CPU or GPU resource type." - ); - } - - let [segment_po2, keccak_po2] = [ - ("RISC0_SEGMENT_PO2", DEFAULT_SEGMENT_PO2, SEGMENT_PO2_RANGE), - ("RISC0_KECCAK_PO2", DEFAULT_KECCAK_PO2, KECCAK_PO2_RANGE), - ] - .map(|(key, default, range)| { - let val = env::var(key) - .ok() - .and_then(|po2| po2.parse::().ok()) - .unwrap_or(default); - if !range.contains(&val) { - panic!("Unsupported po2 value {val} of {key}, expected in range {range:?}") - } - val - }); - - Ok(Self { - program, - resource, - segment_po2, - keccak_po2, - }) - } -} - -impl zkVM for EreRisc0 { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let executor = default_executor(); - let env = ExecutorEnv::builder() - .write_slice(input) - .build() - .map_err(Risc0Error::BuildExecutorEnv)?; - - let start = Instant::now(); - let session_info = executor - .execute(env, &self.program.elf) - .map_err(Risc0Error::Execute)?; - - let public_values = session_info.journal.bytes.clone(); - - Ok(( - public_values, - ProgramExecutionReport { - total_num_cycles: session_info.cycles() as u64, - execution_duration: start.elapsed(), - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - let prover = match self.resource { - ProverResourceType::Cpu => Rc::new(ExternalProver::new("ipc", "r0vm")), - ProverResourceType::Gpu => { - if cfg!(feature = "metal") { - // When `metal` is enabled, we use the `LocalProver` to do - // proving. but it's not public so we use `default_prover` - // to instantiate it. - default_prover() - } else { - // The `DefaultProver` uses `r0vm-cuda` to spawn multiple - // workers to do multi-gpu proving. - // It uses env `RISC0_DEFAULT_PROVER_NUM_GPUS` to determine - // how many available GPUs there are. - Rc::new( - DefaultProver::new("r0vm-cuda") - .map_err(Risc0Error::InitializeCudaProver)?, - ) - } - } - ProverResourceType::Network(_) => { - panic!( - "Network proving not yet implemented for RISC Zero. Use CPU or GPU resource type." - ); - } - }; - - let env = ExecutorEnv::builder() - .write_slice(input) - .segment_limit_po2(self.segment_po2 as _) - .keccak_max_po2(self.keccak_po2 as _) - .and_then(|builder| builder.build()) - .map_err(Risc0Error::BuildExecutorEnv)?; - - let opts = match proof_kind { - ProofKind::Compressed => ProverOpts::succinct(), - ProofKind::Groth16 => ProverOpts::groth16(), - }; - - let now = Instant::now(); - let prove_info = prover - .prove_with_opts(env, &self.program.elf, &opts) - .map_err(Risc0Error::Prove)?; - let proving_time = now.elapsed(); - - let public_values = prove_info.receipt.journal.bytes.clone(); - let proof = Proof::new( - proof_kind, - borsh::to_vec(&prove_info.receipt) - .map_err(|err| CommonError::serialize("proof", "borsh", err))?, - ); - - Ok(( - public_values, - proof, - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let proof_kind = proof.kind(); - - let receipt: Receipt = borsh::from_slice(proof.as_bytes()) - .map_err(|err| CommonError::deserialize("proof", "borsh", err))?; - - if !matches!( - (proof_kind, &receipt.inner), - (ProofKind::Compressed, InnerReceipt::Succinct(_)) - | (ProofKind::Groth16, InnerReceipt::Groth16(_)) - ) { - let got = match &receipt.inner { - InnerReceipt::Composite(_) => "Composite", - InnerReceipt::Succinct(_) => "Succinct", - InnerReceipt::Groth16(_) => "Groth16", - InnerReceipt::Fake(_) => "Fake", - _ => "Unknown", - }; - bail!(Risc0Error::InvalidProofKind(proof_kind, got.to_string())); - } - - receipt - .verify(self.program.image_id) - .map_err(Risc0Error::Verify)?; - - let public_values = receipt.journal.bytes.clone(); - - Ok(public_values) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreRisc0 { - type ProgramDigest = Digest; - - fn program_digest(&self) -> anyhow::Result { - Ok(self.program.image_id) - } -} - -#[cfg(test)] -mod tests { - use crate::{ - EreRisc0, - compiler::{Risc0Program, RustRv32imaCustomized}, - }; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - static BASIC_PROGRAM: OnceLock = OnceLock::new(); - - fn basic_program() -> Risc0Program { - BASIC_PROGRAM - .get_or_init(|| { - RustRv32imaCustomized - .compile(&testing_guest_directory("risc0", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } - - #[test] - fn test_aligned_allocs() { - let program = RustRv32imaCustomized - .compile(&testing_guest_directory("risc0", "allocs_alignment")) - .unwrap(); - - for i in 1..=16_u32 { - let zkvm = EreRisc0::new(program.clone(), ProverResourceType::Cpu).unwrap(); - - let input = i.to_le_bytes(); - - if i.is_power_of_two() { - zkvm.execute(&input) - .expect("Power of two alignment should execute successfully"); - } else { - zkvm.execute(&input) - .expect_err("Non-power of two aligment is expected to fail"); - } - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/risc0/src/program.rs b/crates/zkvm/risc0/src/program.rs new file mode 100644 index 0000000..3feb839 --- /dev/null +++ b/crates/zkvm/risc0/src/program.rs @@ -0,0 +1,19 @@ +use risc0_zkp::core::digest::Digest; +use serde::{Deserialize, Serialize}; + +/// Risc0 program that contains ELF of compiled guest and image ID. +#[derive(Clone, Serialize, Deserialize)] +pub struct Risc0Program { + pub(crate) elf: Vec, + pub(crate) image_id: Digest, +} + +impl Risc0Program { + pub fn elf(&self) -> &[u8] { + &self.elf + } + + pub fn image_id(&self) -> &Digest { + &self.image_id + } +} diff --git a/crates/zkvm/risc0/src/zkvm.rs b/crates/zkvm/risc0/src/zkvm.rs new file mode 100644 index 0000000..8b7874f --- /dev/null +++ b/crates/zkvm/risc0/src/zkvm.rs @@ -0,0 +1,298 @@ +use crate::program::Risc0Program; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use risc0_zkvm::{ + DEFAULT_MAX_PO2, DefaultProver, Digest, ExecutorEnv, ExternalProver, InnerReceipt, ProverOpts, + Receipt, default_executor, default_prover, +}; +use std::{env, ops::RangeInclusive, rc::Rc, time::Instant}; + +mod error; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +/// Default logarithmic segment size from [`DEFAULT_SEGMENT_LIMIT_PO2`]. +/// +/// [`DEFAULT_SEGMENT_LIMIT_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/rv32im/src/execute/mod.rs#L39. +const DEFAULT_SEGMENT_PO2: usize = 20; + +/// Supported range of logarithmic segment size. +/// +/// The minimum is by [`MIN_LIFT_PO2`] to be lifted. +/// +/// The maximum is by [`DEFAULT_MAX_PO2`], although the real maximum is `24`, +/// but it requires us to set the `control_ids` manually in the `ProverOpts`. +/// +/// [`MIN_LIFT_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/recursion/src/control_id.rs#L19 +/// [`DEFAULT_MAX_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/zkvm/src/receipt.rs#L884 +const SEGMENT_PO2_RANGE: RangeInclusive = 14..=DEFAULT_MAX_PO2; + +/// Default logarithmic keccak size from [`KECCAK_DEFAULT_PO2`]. +/// +/// [`KECCAK_DEFAULT_PO2`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/keccak/src/lib.rs#L27. +const DEFAULT_KECCAK_PO2: usize = 17; + +/// Supported range of logarithmic keccak size from [`KECCAK_PO2_RANGE`]. +/// +/// [`KECCAK_PO2_RANGE`]: https://github.com/risc0/risc0/blob/v3.0.3/risc0/circuit/keccak/src/lib.rs#L29. +const KECCAK_PO2_RANGE: RangeInclusive = 14..=18; + +pub struct EreRisc0 { + program: Risc0Program, + resource: ProverResourceType, + segment_po2: usize, + keccak_po2: usize, +} + +impl EreRisc0 { + pub fn new(program: Risc0Program, resource: ProverResourceType) -> Result { + if matches!(resource, ProverResourceType::Network(_)) { + panic!( + "Network proving not yet implemented for RISC Zero. Use CPU or GPU resource type." + ); + } + + let [segment_po2, keccak_po2] = [ + ("RISC0_SEGMENT_PO2", DEFAULT_SEGMENT_PO2, SEGMENT_PO2_RANGE), + ("RISC0_KECCAK_PO2", DEFAULT_KECCAK_PO2, KECCAK_PO2_RANGE), + ] + .map(|(key, default, range)| { + let val = env::var(key) + .ok() + .and_then(|po2| po2.parse::().ok()) + .unwrap_or(default); + if !range.contains(&val) { + panic!("Unsupported po2 value {val} of {key}, expected in range {range:?}") + } + val + }); + + Ok(Self { + program, + resource, + segment_po2, + keccak_po2, + }) + } +} + +impl zkVM for EreRisc0 { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let executor = default_executor(); + let env = ExecutorEnv::builder() + .write_slice(input) + .build() + .map_err(Error::BuildExecutorEnv)?; + + let start = Instant::now(); + let session_info = executor + .execute(env, &self.program.elf) + .map_err(Error::Execute)?; + + let public_values = session_info.journal.bytes.clone(); + + Ok(( + public_values, + ProgramExecutionReport { + total_num_cycles: session_info.cycles() as u64, + execution_duration: start.elapsed(), + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + let prover = match self.resource { + ProverResourceType::Cpu => Rc::new(ExternalProver::new("ipc", "r0vm")), + ProverResourceType::Gpu => { + if cfg!(feature = "metal") { + // When `metal` is enabled, we use the `LocalProver` to do + // proving. but it's not public so we use `default_prover` + // to instantiate it. + default_prover() + } else { + // The `DefaultProver` uses `r0vm-cuda` to spawn multiple + // workers to do multi-gpu proving. + // It uses env `RISC0_DEFAULT_PROVER_NUM_GPUS` to determine + // how many available GPUs there are. + Rc::new(DefaultProver::new("r0vm-cuda").map_err(Error::InitializeCudaProver)?) + } + } + ProverResourceType::Network(_) => { + panic!( + "Network proving not yet implemented for RISC Zero. Use CPU or GPU resource type." + ); + } + }; + + let env = ExecutorEnv::builder() + .write_slice(input) + .segment_limit_po2(self.segment_po2 as _) + .keccak_max_po2(self.keccak_po2 as _) + .and_then(|builder| builder.build()) + .map_err(Error::BuildExecutorEnv)?; + + let opts = match proof_kind { + ProofKind::Compressed => ProverOpts::succinct(), + ProofKind::Groth16 => ProverOpts::groth16(), + }; + + let now = Instant::now(); + let prove_info = prover + .prove_with_opts(env, &self.program.elf, &opts) + .map_err(Error::Prove)?; + let proving_time = now.elapsed(); + + let public_values = prove_info.receipt.journal.bytes.clone(); + let proof = Proof::new( + proof_kind, + borsh::to_vec(&prove_info.receipt) + .map_err(|err| CommonError::serialize("proof", "borsh", err))?, + ); + + Ok(( + public_values, + proof, + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let proof_kind = proof.kind(); + + let receipt: Receipt = borsh::from_slice(proof.as_bytes()) + .map_err(|err| CommonError::deserialize("proof", "borsh", err))?; + + if !matches!( + (proof_kind, &receipt.inner), + (ProofKind::Compressed, InnerReceipt::Succinct(_)) + | (ProofKind::Groth16, InnerReceipt::Groth16(_)) + ) { + let got = match &receipt.inner { + InnerReceipt::Composite(_) => "Composite", + InnerReceipt::Succinct(_) => "Succinct", + InnerReceipt::Groth16(_) => "Groth16", + InnerReceipt::Fake(_) => "Fake", + _ => "Unknown", + }; + bail!(Error::InvalidProofKind(proof_kind, got.to_string())); + } + + receipt + .verify(self.program.image_id) + .map_err(Error::Verify)?; + + let public_values = receipt.journal.bytes.clone(); + + Ok(public_values) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreRisc0 { + type ProgramDigest = Digest; + + fn program_digest(&self) -> anyhow::Result { + Ok(self.program.image_id) + } +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv32imaCustomized, program::Risc0Program, zkvm::EreRisc0}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> Risc0Program { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv32imaCustomized + .compile(&testing_guest_directory("risc0", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreRisc0::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } + + #[test] + fn test_aligned_allocs() { + let program = RustRv32imaCustomized + .compile(&testing_guest_directory("risc0", "allocs_alignment")) + .unwrap(); + + for i in 1..=16_u32 { + let zkvm = EreRisc0::new(program.clone(), ProverResourceType::Cpu).unwrap(); + + let input = i.to_le_bytes(); + + if i.is_power_of_two() { + zkvm.execute(&input) + .expect("Power of two alignment should execute successfully"); + } else { + zkvm.execute(&input) + .expect_err("Non-power of two aligment is expected to fail"); + } + } + } +} diff --git a/crates/zkvm/risc0/src/error.rs b/crates/zkvm/risc0/src/zkvm/error.rs similarity index 50% rename from crates/zkvm/risc0/src/error.rs rename to crates/zkvm/risc0/src/zkvm/error.rs index 1ec9aac..e6b21bb 100644 --- a/crates/zkvm/risc0/src/error.rs +++ b/crates/zkvm/risc0/src/zkvm/error.rs @@ -1,29 +1,9 @@ -use ere_zkvm_interface::ProofKind; +use ere_zkvm_interface::zkvm::ProofKind; use risc0_zkp::verify::VerificationError; -use std::path::PathBuf; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { - #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), - - #[error("`risc0_build::build_package` for {guest_path} failed: {err}")] - BuildFailure { - #[source] - err: anyhow::Error, - guest_path: PathBuf, - }, - - #[error("`risc0_build::build_package` succeeded but failed to find guest")] - Risc0BuildMissingGuest, - - #[error("ELF binary image calculation failure: {0}")] - ImageIDCalculationFailure(anyhow::Error), -} - -#[derive(Debug, Error)] -pub enum Risc0Error { +pub enum Error { // Execute #[error("Failed to build `ExecutorEnv`: {0}")] BuildExecutorEnv(anyhow::Error), diff --git a/crates/zkvm/sp1/Cargo.toml b/crates/zkvm/sp1/Cargo.toml index be8abb9..a54760e 100644 --- a/crates/zkvm/sp1/Cargo.toml +++ b/crates/zkvm/sp1/Cargo.toml @@ -8,15 +8,16 @@ license.workspace = true [dependencies] anyhow.workspace = true bincode = { workspace = true, features = ["alloc", "serde"] } +serde.workspace = true tempfile.workspace = true thiserror.workspace = true tracing.workspace = true # SP1 dependencies -sp1-sdk.workspace = true +sp1-sdk = { workspace = true, optional = true } # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -24,3 +25,11 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true + +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:sp1-sdk"] + +[lints] +workspace = true diff --git a/crates/zkvm/sp1/src/compiler.rs b/crates/zkvm/sp1/src/compiler.rs index 6c0c1cf..888b55b 100644 --- a/crates/zkvm/sp1/src/compiler.rs +++ b/crates/zkvm/sp1/src/compiler.rs @@ -1,7 +1,7 @@ +mod error; mod rust_rv32ima; mod rust_rv32ima_customized; +pub use error::Error; pub use rust_rv32ima::RustRv32ima; pub use rust_rv32ima_customized::RustRv32imaCustomized; - -pub type SP1Program = Vec; diff --git a/crates/zkvm/sp1/src/compiler/error.rs b/crates/zkvm/sp1/src/compiler/error.rs new file mode 100644 index 0000000..477e200 --- /dev/null +++ b/crates/zkvm/sp1/src/compiler/error.rs @@ -0,0 +1,8 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), +} diff --git a/crates/zkvm/sp1/src/compiler/rust_rv32ima.rs b/crates/zkvm/sp1/src/compiler/rust_rv32ima.rs index fd9902a..9858529 100644 --- a/crates/zkvm/sp1/src/compiler/rust_rv32ima.rs +++ b/crates/zkvm/sp1/src/compiler/rust_rv32ima.rs @@ -1,6 +1,6 @@ -use crate::{compiler::SP1Program, error::CompileError}; +use crate::{compiler::Error, program::SP1Program}; use ere_compile_utils::CargoBuildCmd; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{env, path::Path}; const TARGET_TRIPLE: &str = "riscv32ima-unknown-none-elf"; @@ -32,7 +32,7 @@ const CARGO_BUILD_OPTIONS: &[&str] = &[ pub struct RustRv32ima; impl Compiler for RustRv32ima { - type Error = CompileError; + type Error = Error; type Program = SP1Program; @@ -43,21 +43,24 @@ impl Compiler for RustRv32ima { .build_options(CARGO_BUILD_OPTIONS) .rustflags(RUSTFLAGS) .exec(guest_directory, TARGET_TRIPLE)?; - Ok(elf) + Ok(SP1Program { elf }) } } #[cfg(test)] mod tests { - use crate::{EreSP1, compiler::RustRv32ima}; + use crate::{compiler::RustRv32ima, zkvm::EreSP1}; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::{Compiler, ProverResourceType, zkVM}; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProverResourceType, zkVM}, + }; #[test] fn test_compile() { let guest_directory = testing_guest_directory("sp1", "stock_nightly_no_std"); - let elf = RustRv32ima.compile(&guest_directory).unwrap(); - assert!(!elf.is_empty(), "ELF bytes should not be empty."); + let program = RustRv32ima.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } #[test] diff --git a/crates/zkvm/sp1/src/compiler/rust_rv32ima_customized.rs b/crates/zkvm/sp1/src/compiler/rust_rv32ima_customized.rs index 4c3df10..dec4889 100644 --- a/crates/zkvm/sp1/src/compiler/rust_rv32ima_customized.rs +++ b/crates/zkvm/sp1/src/compiler/rust_rv32ima_customized.rs @@ -1,6 +1,6 @@ -use crate::{compiler::SP1Program, error::CompileError}; +use crate::{compiler::Error, program::SP1Program}; use ere_compile_utils::{CommonError, cargo_metadata}; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{fs, path::Path, process::Command}; use tempfile::tempdir; use tracing::info; @@ -10,7 +10,7 @@ use tracing::info; pub struct RustRv32imaCustomized; impl Compiler for RustRv32imaCustomized { - type Error = CompileError; + type Error = Error; type Program = SP1Program; @@ -46,11 +46,11 @@ impl Compiler for RustRv32imaCustomized { } let elf_path = output_dir.path().join("guest.elf"); - let elf_bytes = + let elf = fs::read(&elf_path).map_err(|err| CommonError::read_file("elf", &elf_path, err))?; - info!("SP1 program compiled OK - {} bytes", elf_bytes.len()); + info!("SP1 program compiled OK - {} bytes", elf.len()); - Ok(elf_bytes) + Ok(SP1Program { elf }) } } @@ -58,12 +58,12 @@ impl Compiler for RustRv32imaCustomized { mod tests { use crate::compiler::RustRv32imaCustomized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("sp1", "basic"); - let elf_bytes = RustRv32imaCustomized.compile(&guest_directory).unwrap(); - assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); + let program = RustRv32imaCustomized.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/sp1/src/lib.rs b/crates/zkvm/sp1/src/lib.rs index 41eedbf..a53c4ec 100644 --- a/crates/zkvm/sp1/src/lib.rs +++ b/crates/zkvm/sp1/src/lib.rs @@ -1,326 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{compiler::SP1Program, error::SP1Error}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, NetworkProverConfig, ProgramExecutionReport, ProgramProvingReport, Proof, - ProofKind, ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use sp1_sdk::{ - CpuProver, CudaProver, NetworkProver, Prover, ProverClient, SP1ProofMode, - SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey, -}; -use std::{panic, time::Instant}; -use tracing::info; - -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +pub mod program; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -#[allow(clippy::large_enum_variant)] -enum ProverType { - Cpu(CpuProver), - Gpu(CudaProver), - Network(NetworkProver), -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -impl ProverType { - fn setup(&self, program: &SP1Program) -> (SP1ProvingKey, SP1VerifyingKey) { - match self { - ProverType::Cpu(cpu_prover) => cpu_prover.setup(program), - ProverType::Gpu(cuda_prover) => cuda_prover.setup(program), - ProverType::Network(network_prover) => network_prover.setup(program), - } - } - - fn execute( - &self, - program: &SP1Program, - input: &SP1Stdin, - ) -> Result<(sp1_sdk::SP1PublicValues, sp1_sdk::ExecutionReport), SP1Error> { - let cpu_executor_builder = match self { - ProverType::Cpu(cpu_prover) => cpu_prover.execute(program, input), - ProverType::Gpu(cuda_prover) => cuda_prover.execute(program, input), - ProverType::Network(network_prover) => network_prover.execute(program, input), - }; - - cpu_executor_builder.run().map_err(SP1Error::Execute) - } - - fn prove( - &self, - pk: &SP1ProvingKey, - input: &SP1Stdin, - mode: SP1ProofMode, - ) -> Result { - match self { - ProverType::Cpu(cpu_prover) => cpu_prover.prove(pk, input).mode(mode).run(), - ProverType::Gpu(cuda_prover) => cuda_prover.prove(pk, input).mode(mode).run(), - ProverType::Network(network_prover) => network_prover.prove(pk, input).mode(mode).run(), - } - .map_err(SP1Error::Prove) - } - - fn verify( - &self, - proof: &SP1ProofWithPublicValues, - vk: &SP1VerifyingKey, - ) -> Result<(), SP1Error> { - match self { - ProverType::Cpu(cpu_prover) => cpu_prover.verify(proof, vk), - ProverType::Gpu(cuda_prover) => cuda_prover.verify(proof, vk), - ProverType::Network(network_prover) => network_prover.verify(proof, vk), - } - .map_err(SP1Error::Verify) - } -} - -pub struct EreSP1 { - program: SP1Program, - /// Proving key - pk: SP1ProvingKey, - /// Verification key - vk: SP1VerifyingKey, - /// Prover resource configuration for creating clients - resource: ProverResourceType, - // FIXME: The current version of SP1 (v5.0.5) has a problem where if proving the program crashes in the - // Moongate container, it leaves an internal mutex poisoned, which prevents further proving attempts. - // This is a workaround to avoid the poisoned mutex issue by creating a new client for each prove call. - // We still use the `setup(...)` method to create the proving and verification keys only once, such that when - // later calling `prove(...)` in a fresh client, we can reuse the keys and avoiding extra work. - // - // Eventually, this should be fixed in the SP1 SDK and we can create the `client` in the `new(...)` method. - // For more context see: https://github.com/eth-act/zkevm-benchmark-workload/issues/54 -} - -impl EreSP1 { - fn create_network_prover(config: &NetworkProverConfig) -> NetworkProver { - let mut builder = ProverClient::builder().network(); - // Check if we have a private key in the config or environment - if let Some(api_key) = &config.api_key { - builder = builder.private_key(api_key); - } else if let Ok(private_key) = std::env::var("NETWORK_PRIVATE_KEY") { - builder = builder.private_key(&private_key); - } else { - panic!( - "Network proving requires a private key. Set NETWORK_PRIVATE_KEY environment variable or provide api_key in NetworkProverConfig" - ); - } - // Set the RPC URL if provided - if !config.endpoint.is_empty() { - builder = builder.rpc_url(&config.endpoint); - } else if let Ok(rpc_url) = std::env::var("NETWORK_RPC_URL") { - builder = builder.rpc_url(&rpc_url); - } - // Otherwise SP1 SDK will use its default RPC URL - builder.build() - } - - fn create_client(resource: &ProverResourceType) -> ProverType { - match resource { - ProverResourceType::Cpu => ProverType::Cpu(ProverClient::builder().cpu().build()), - ProverResourceType::Gpu => ProverType::Gpu(ProverClient::builder().cuda().build()), - ProverResourceType::Network(config) => { - ProverType::Network(Self::create_network_prover(config)) - } - } - } - - pub fn new(program: SP1Program, resource: ProverResourceType) -> Result { - let (pk, vk) = Self::create_client(&resource).setup(&program); - - Ok(Self { - program, - pk, - vk, - resource, - }) - } -} - -impl zkVM for EreSP1 { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let mut stdin = SP1Stdin::new(); - stdin.write_slice(input); - - let client = Self::create_client(&self.resource); - let start = Instant::now(); - let (public_values, exec_report) = client.execute(&self.program, &stdin)?; - - Ok(( - public_values.to_vec(), - ProgramExecutionReport { - total_num_cycles: exec_report.total_instruction_count(), - region_cycles: exec_report.cycle_tracker.into_iter().collect(), - execution_duration: start.elapsed(), - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - info!("Generating proof…"); - - let mut stdin = SP1Stdin::new(); - stdin.write_slice(input); - - let mode = match proof_kind { - ProofKind::Compressed => SP1ProofMode::Compressed, - ProofKind::Groth16 => SP1ProofMode::Groth16, - }; - - let (proof, proving_time) = panic::catch_unwind(|| { - let client = Self::create_client(&self.resource); - let start = Instant::now(); - let proof = client.prove(&self.pk, &stdin, mode)?; - Ok::<_, SP1Error>((proof, start.elapsed())) - }) - .map_err(|err| SP1Error::Panic(panic_msg(err)))??; - - let public_values = proof.public_values.to_vec(); - let proof = Proof::new( - proof_kind, - bincode::serde::encode_to_vec(&proof, bincode::config::legacy()) - .map_err(|err| CommonError::serialize("proof", "bincode", err))?, - ); - - Ok(( - public_values, - proof, - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - info!("Verifying proof…"); - - let proof_kind = proof.kind(); - - let (proof, _): (SP1ProofWithPublicValues, _) = - bincode::serde::decode_from_slice(proof.as_bytes(), bincode::config::legacy()) - .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; - let inner_proof_kind = SP1ProofMode::from(&proof.proof); - - if !matches!( - (proof_kind, inner_proof_kind), - (ProofKind::Compressed, SP1ProofMode::Compressed) - | (ProofKind::Groth16, SP1ProofMode::Groth16) - ) { - bail!(SP1Error::InvalidProofKind(proof_kind, inner_proof_kind)); - } - - let client = Self::create_client(&self.resource); - client.verify(&proof, &self.vk)?; - - let public_values_bytes = proof.public_values.as_slice().to_vec(); - - Ok(public_values_bytes) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreSP1 { - type ProgramDigest = SP1VerifyingKey; - - fn program_digest(&self) -> anyhow::Result { - Ok(self.vk.clone()) - } -} - -fn panic_msg(err: Box) -> String { - None.or_else(|| err.downcast_ref::().cloned()) - .or_else(|| err.downcast_ref::<&'static str>().map(ToString::to_string)) - .unwrap_or_else(|| "unknown panic msg".to_string()) -} - -#[cfg(test)] -mod tests { - use crate::{EreSP1, compiler::RustRv32imaCustomized}; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, NetworkProverConfig, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - fn basic_program() -> Vec { - static PROGRAM: OnceLock> = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustRv32imaCustomized - .compile(&testing_guest_directory("sp1", "basic")) - .unwrap() - }) - .to_vec() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } - - #[test] - #[ignore = "Requires NETWORK_PRIVATE_KEY environment variable to be set"] - fn test_prove_sp1_network() { - // Check if we have the required environment variable - if std::env::var("NETWORK_PRIVATE_KEY").is_err() { - eprintln!("Skipping network test: NETWORK_PRIVATE_KEY not set"); - return; - } - - // Create a network prover configuration - let network_config = NetworkProverConfig { - endpoint: std::env::var("NETWORK_RPC_URL").unwrap_or_default(), - api_key: std::env::var("NETWORK_PRIVATE_KEY").ok(), - }; - let program = basic_program(); - let zkvm = EreSP1::new(program, ProverResourceType::Network(network_config)).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/sp1/src/program.rs b/crates/zkvm/sp1/src/program.rs new file mode 100644 index 0000000..d90d453 --- /dev/null +++ b/crates/zkvm/sp1/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// SP1 program that contains ELF of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct SP1Program { + pub(crate) elf: Vec, +} + +impl SP1Program { + pub fn elf(&self) -> &[u8] { + &self.elf + } +} diff --git a/crates/zkvm/sp1/src/zkvm.rs b/crates/zkvm/sp1/src/zkvm.rs new file mode 100644 index 0000000..27111c5 --- /dev/null +++ b/crates/zkvm/sp1/src/zkvm.rs @@ -0,0 +1,260 @@ +use crate::{program::SP1Program, zkvm::sdk::Prover}; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use sp1_sdk::{SP1ProofMode, SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey}; +use std::{ + mem::take, + panic, + sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}, + time::Instant, +}; +use tracing::info; + +mod error; +mod sdk; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +pub struct EreSP1 { + program: SP1Program, + /// Prover resource configuration for creating clients + resource: ProverResourceType, + /// Proving key + pk: SP1ProvingKey, + /// Verification key + vk: SP1VerifyingKey, + // The current version of SP1 (v5.2.1) has a problem where if GPU proving + // the program crashes in the Moongate container, it leaves an internal + // mutex poisoned, which prevents further proving attempts. + // This is a workaround to avoid the poisoned mutex issue by creating a new + // prover if the proving panics. + // Eventually, this should be fixed in the SP1 SDK. + // For more context see: https://github.com/eth-act/zkevm-benchmark-workload/issues/54 + prover: RwLock, +} + +impl EreSP1 { + pub fn new(program: SP1Program, resource: ProverResourceType) -> Result { + let prover = Prover::new(&resource); + let (pk, vk) = prover.setup(&program.elf); + Ok(Self { + program, + resource, + pk, + vk, + prover: RwLock::new(prover), + }) + } + + fn prover(&'_ self) -> Result, Error> { + self.prover.read().map_err(|_| Error::RwLockPosioned) + } + + fn prover_mut(&'_ self) -> Result, Error> { + self.prover.write().map_err(|_| Error::RwLockPosioned) + } +} + +impl zkVM for EreSP1 { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let mut stdin = SP1Stdin::new(); + stdin.write_slice(input); + + let prover = self.prover()?; + + let start = Instant::now(); + let (public_values, exec_report) = prover.execute(self.program.elf(), &stdin)?; + let execution_duration = start.elapsed(); + + Ok(( + public_values.to_vec(), + ProgramExecutionReport { + total_num_cycles: exec_report.total_instruction_count(), + region_cycles: exec_report.cycle_tracker.into_iter().collect(), + execution_duration, + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + info!("Generating proof…"); + + let mut stdin = SP1Stdin::new(); + stdin.write_slice(input); + + let mode = match proof_kind { + ProofKind::Compressed => SP1ProofMode::Compressed, + ProofKind::Groth16 => SP1ProofMode::Groth16, + }; + + let mut prover = self.prover_mut()?; + + let start = Instant::now(); + let proof = + panic::catch_unwind(|| prover.prove(&self.pk, &stdin, mode)).map_err(|err| { + if matches!(self.resource, ProverResourceType::Gpu) { + // Drop the panicked prover and create a new one. + // Note that `take` has to be done explicitly first so the + // Moongate container could be removed properly. + take(&mut *prover); + *prover = Prover::new(&self.resource); + } + + Error::Panic(panic_msg(err)) + })??; + let proving_time = start.elapsed(); + + let public_values = proof.public_values.to_vec(); + let proof = Proof::new( + proof_kind, + bincode::serde::encode_to_vec(&proof, bincode::config::legacy()) + .map_err(|err| CommonError::serialize("proof", "bincode", err))?, + ); + + Ok(( + public_values, + proof, + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + info!("Verifying proof…"); + + let proof_kind = proof.kind(); + + let (proof, _): (SP1ProofWithPublicValues, _) = + bincode::serde::decode_from_slice(proof.as_bytes(), bincode::config::legacy()) + .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; + let inner_proof_kind = SP1ProofMode::from(&proof.proof); + + if !matches!( + (proof_kind, inner_proof_kind), + (ProofKind::Compressed, SP1ProofMode::Compressed) + | (ProofKind::Groth16, SP1ProofMode::Groth16) + ) { + bail!(Error::InvalidProofKind(proof_kind, inner_proof_kind)); + } + + self.prover()?.verify(&proof, &self.vk)?; + + let public_values_bytes = proof.public_values.as_slice().to_vec(); + + Ok(public_values_bytes) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreSP1 { + type ProgramDigest = SP1VerifyingKey; + + fn program_digest(&self) -> anyhow::Result { + Ok(self.vk.clone()) + } +} + +fn panic_msg(err: Box) -> String { + None.or_else(|| err.downcast_ref::().cloned()) + .or_else(|| err.downcast_ref::<&'static str>().map(ToString::to_string)) + .unwrap_or_else(|| "unknown panic msg".to_string()) +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv32imaCustomized, program::SP1Program, zkvm::EreSP1}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{NetworkProverConfig, ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> SP1Program { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv32imaCustomized + .compile(&testing_guest_directory("sp1", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } + + #[test] + #[ignore = "Requires NETWORK_PRIVATE_KEY environment variable to be set"] + fn test_prove_sp1_network() { + // Check if we have the required environment variable + if std::env::var("NETWORK_PRIVATE_KEY").is_err() { + eprintln!("Skipping network test: NETWORK_PRIVATE_KEY not set"); + return; + } + + // Create a network prover configuration + let network_config = NetworkProverConfig { + endpoint: std::env::var("NETWORK_RPC_URL").unwrap_or_default(), + api_key: std::env::var("NETWORK_PRIVATE_KEY").ok(), + }; + let program = basic_program(); + let zkvm = EreSP1::new(program, ProverResourceType::Network(network_config)).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } +} diff --git a/crates/zkvm/sp1/src/error.rs b/crates/zkvm/sp1/src/zkvm/error.rs similarity index 70% rename from crates/zkvm/sp1/src/error.rs rename to crates/zkvm/sp1/src/zkvm/error.rs index 6920ffe..16f4ef2 100644 --- a/crates/zkvm/sp1/src/error.rs +++ b/crates/zkvm/sp1/src/zkvm/error.rs @@ -1,17 +1,14 @@ -use ere_zkvm_interface::ProofKind; +use ere_zkvm_interface::zkvm::{CommonError, ProofKind}; use sp1_sdk::{SP1ProofMode, SP1VerificationError}; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), -} + CommonError(#[from] CommonError), -#[derive(Debug, Error)] -pub enum SP1Error { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), + #[error("Prover RwLock posioned, panic not catched properly")] + RwLockPosioned, // Execute #[error("SP1 execution failed: {0}")] diff --git a/crates/zkvm/sp1/src/zkvm/sdk.rs b/crates/zkvm/sp1/src/zkvm/sdk.rs new file mode 100644 index 0000000..3b92666 --- /dev/null +++ b/crates/zkvm/sp1/src/zkvm/sdk.rs @@ -0,0 +1,99 @@ +use crate::zkvm::Error; +use ere_zkvm_interface::zkvm::{NetworkProverConfig, ProverResourceType}; +use sp1_sdk::{ + CpuProver, CudaProver, NetworkProver, Prover as _, ProverClient, SP1ProofMode, + SP1ProofWithPublicValues, SP1ProvingKey, SP1Stdin, SP1VerifyingKey, +}; + +#[allow(clippy::large_enum_variant)] +pub enum Prover { + Cpu(CpuProver), + Gpu(CudaProver), + Network(NetworkProver), +} + +impl Default for Prover { + fn default() -> Self { + Self::new(&ProverResourceType::Cpu) + } +} + +impl Prover { + pub fn new(resource: &ProverResourceType) -> Self { + match resource { + ProverResourceType::Cpu => Self::Cpu(ProverClient::builder().cpu().build()), + ProverResourceType::Gpu => Self::Gpu(ProverClient::builder().cuda().build()), + ProverResourceType::Network(config) => Self::Network(build_network_prover(config)), + } + } + + pub fn setup(&self, elf: &[u8]) -> (SP1ProvingKey, SP1VerifyingKey) { + match self { + Self::Cpu(cpu_prover) => cpu_prover.setup(elf), + Self::Gpu(cuda_prover) => cuda_prover.setup(elf), + Self::Network(network_prover) => network_prover.setup(elf), + } + } + + pub fn execute( + &self, + elf: &[u8], + input: &SP1Stdin, + ) -> Result<(sp1_sdk::SP1PublicValues, sp1_sdk::ExecutionReport), Error> { + match self { + Self::Cpu(cpu_prover) => cpu_prover.execute(elf, input).run(), + Self::Gpu(cuda_prover) => cuda_prover.execute(elf, input).run(), + Self::Network(network_prover) => network_prover.execute(elf, input).run(), + } + .map_err(Error::Execute) + } + + pub fn prove( + &self, + pk: &SP1ProvingKey, + input: &SP1Stdin, + mode: SP1ProofMode, + ) -> Result { + match self { + Self::Cpu(cpu_prover) => cpu_prover.prove(pk, input).mode(mode).run(), + Self::Gpu(cuda_prover) => cuda_prover.prove(pk, input).mode(mode).run(), + Self::Network(network_prover) => network_prover.prove(pk, input).mode(mode).run(), + } + .map_err(Error::Prove) + } + + pub fn verify( + &self, + proof: &SP1ProofWithPublicValues, + vk: &SP1VerifyingKey, + ) -> Result<(), Error> { + match self { + Self::Cpu(cpu_prover) => cpu_prover.verify(proof, vk), + Self::Gpu(cuda_prover) => cuda_prover.verify(proof, vk), + Self::Network(network_prover) => network_prover.verify(proof, vk), + } + .map_err(Error::Verify) + } +} + +fn build_network_prover(config: &NetworkProverConfig) -> NetworkProver { + let mut builder = ProverClient::builder().network(); + // Check if we have a private key in the config or environment + if let Some(api_key) = &config.api_key { + builder = builder.private_key(api_key); + } else if let Ok(private_key) = std::env::var("NETWORK_PRIVATE_KEY") { + builder = builder.private_key(&private_key); + } else { + panic!( + "Network proving requires a private key. Set NETWORK_PRIVATE_KEY environment variable or provide api_key in NetworkProverConfig" + ); + } + // Set the RPC URL if provided + if !config.endpoint.is_empty() { + builder = builder.rpc_url(&config.endpoint); + } else if let Ok(rpc_url) = std::env::var("NETWORK_RPC_URL") { + builder = builder.rpc_url(&rpc_url); + } + // Otherwise SP1 SDK will use its default RPC URL + builder.build() +} diff --git a/crates/zkvm/ziren/Cargo.toml b/crates/zkvm/ziren/Cargo.toml index aa917e4..e44448f 100644 --- a/crates/zkvm/ziren/Cargo.toml +++ b/crates/zkvm/ziren/Cargo.toml @@ -8,14 +8,15 @@ license.workspace = true [dependencies] anyhow.workspace = true bincode = { workspace = true, features = ["std", "serde"] } +serde.workspace = true thiserror.workspace = true tracing.workspace = true # Ziren dependencies -zkm-sdk.workspace = true +zkm-sdk = { workspace = true, optional = true } # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] @@ -24,5 +25,10 @@ ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = ["dep:zkm-sdk"] + [lints] workspace = true diff --git a/crates/zkvm/ziren/src/compiler.rs b/crates/zkvm/ziren/src/compiler.rs index 8664ba7..f835e0c 100644 --- a/crates/zkvm/ziren/src/compiler.rs +++ b/crates/zkvm/ziren/src/compiler.rs @@ -1,5 +1,5 @@ +mod error; mod rust_mips32r2_customized; +pub use error::Error; pub use rust_mips32r2_customized::RustMips32r2Customized; - -pub type ZirenProgram = Vec; diff --git a/crates/zkvm/ziren/src/compiler/error.rs b/crates/zkvm/ziren/src/compiler/error.rs new file mode 100644 index 0000000..477e200 --- /dev/null +++ b/crates/zkvm/ziren/src/compiler/error.rs @@ -0,0 +1,8 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), +} diff --git a/crates/zkvm/ziren/src/compiler/rust_mips32r2_customized.rs b/crates/zkvm/ziren/src/compiler/rust_mips32r2_customized.rs index 42776db..8db07b0 100644 --- a/crates/zkvm/ziren/src/compiler/rust_mips32r2_customized.rs +++ b/crates/zkvm/ziren/src/compiler/rust_mips32r2_customized.rs @@ -1,6 +1,6 @@ -use crate::{compiler::ZirenProgram, error::CompileError}; +use crate::{compiler::Error, program::ZirenProgram}; use ere_compile_utils::{CommonError, cargo_metadata, rustc_path}; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{fs, path::Path, process::Command}; const ZKM_TOOLCHAIN: &str = "zkm"; @@ -10,7 +10,7 @@ const ZKM_TOOLCHAIN: &str = "zkm"; pub struct RustMips32r2Customized; impl Compiler for RustMips32r2Customized { - type Error = CompileError; + type Error = Error; type Program = ZirenProgram; @@ -47,7 +47,7 @@ impl Compiler for RustMips32r2Customized { let elf = fs::read(&elf_path).map_err(|err| CommonError::read_file("elf", &elf_path, err))?; - Ok(elf) + Ok(ZirenProgram { elf }) } } @@ -55,12 +55,12 @@ impl Compiler for RustMips32r2Customized { mod tests { use crate::compiler::RustMips32r2Customized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("ziren", "basic"); - let elf_bytes = RustMips32r2Customized.compile(&guest_directory).unwrap(); - assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); + let program = RustMips32r2Customized.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/ziren/src/lib.rs b/crates/zkvm/ziren/src/lib.rs index fb07d92..a53c4ec 100644 --- a/crates/zkvm/ziren/src/lib.rs +++ b/crates/zkvm/ziren/src/lib.rs @@ -1,203 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{compiler::ZirenProgram, error::ZirenError}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use std::{panic, time::Instant}; -use tracing::info; -use zkm_sdk::{ - CpuProver, Prover, ZKMProofKind, ZKMProofWithPublicValues, ZKMProvingKey, ZKMStdin, - ZKMVerifyingKey, -}; - -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); +pub mod program; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -pub struct EreZiren { - program: ZirenProgram, - pk: ZKMProvingKey, - vk: ZKMVerifyingKey, -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -impl EreZiren { - pub fn new(program: ZirenProgram, resource: ProverResourceType) -> Result { - if matches!( - resource, - ProverResourceType::Gpu | ProverResourceType::Network(_) - ) { - panic!("Network or Gpu proving not yet implemented for ZKM. Use CPU resource type."); - } - let (pk, vk) = CpuProver::new().setup(&program); - Ok(Self { program, pk, vk }) - } -} - -impl zkVM for EreZiren { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let mut stdin = ZKMStdin::new(); - stdin.write_slice(input); - - let start = Instant::now(); - let (public_inputs, exec_report) = CpuProver::new() - .execute(&self.program, &stdin) - .map_err(ZirenError::Execute)?; - let execution_duration = start.elapsed(); - - Ok(( - public_inputs.to_vec(), - ProgramExecutionReport { - total_num_cycles: exec_report.total_instruction_count(), - region_cycles: exec_report.cycle_tracker.into_iter().collect(), - execution_duration, - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - info!("Generating proof…"); - - let mut stdin = ZKMStdin::new(); - stdin.write_slice(input); - - let inner_proof_kind = match proof_kind { - ProofKind::Compressed => ZKMProofKind::Compressed, - ProofKind::Groth16 => ZKMProofKind::Groth16, - }; - - let start = std::time::Instant::now(); - let proof = - panic::catch_unwind(|| CpuProver::new().prove(&self.pk, stdin, inner_proof_kind)) - .map_err(|err| ZirenError::ProvePanic(panic_msg(err)))? - .map_err(ZirenError::Prove)?; - let proving_time = start.elapsed(); - - let public_values = proof.public_values.to_vec(); - let proof = Proof::new( - proof_kind, - bincode::serde::encode_to_vec(&proof, bincode::config::legacy()) - .map_err(|err| CommonError::serialize("proof", "bincode", err))?, - ); - - Ok(( - public_values, - proof, - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - info!("Verifying proof…"); - - let proof_kind = proof.kind(); - - let (proof, _): (ZKMProofWithPublicValues, _) = - bincode::serde::decode_from_slice(proof.as_bytes(), bincode::config::legacy()) - .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; - let inner_proof_kind = ZKMProofKind::from(&proof.proof); - - if !matches!( - (proof_kind, inner_proof_kind), - (ProofKind::Compressed, ZKMProofKind::Compressed) - | (ProofKind::Groth16, ZKMProofKind::Groth16) - ) { - bail!(ZirenError::InvalidProofKind(proof_kind, inner_proof_kind)); - } - - CpuProver::new() - .verify(&proof, &self.vk) - .map_err(ZirenError::Verify)?; - - Ok(proof.public_values.to_vec()) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreZiren { - type ProgramDigest = ZKMVerifyingKey; - - fn program_digest(&self) -> anyhow::Result { - Ok(self.vk.clone()) - } -} - -fn panic_msg(err: Box) -> String { - None.or_else(|| err.downcast_ref::().cloned()) - .or_else(|| err.downcast_ref::<&'static str>().map(ToString::to_string)) - .unwrap_or_else(|| "unknown panic msg".to_string()) -} - -#[cfg(test)] -mod tests { - use crate::{EreZiren, compiler::RustMips32r2Customized}; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::OnceLock; - - fn basic_program() -> Vec { - static PROGRAM: OnceLock> = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustMips32r2Customized - .compile(&testing_guest_directory("ziren", "basic")) - .unwrap() - }) - .clone() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/ziren/src/program.rs b/crates/zkvm/ziren/src/program.rs new file mode 100644 index 0000000..ec439f4 --- /dev/null +++ b/crates/zkvm/ziren/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Ziren program that contains ELF of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct ZirenProgram { + pub(crate) elf: Vec, +} + +impl ZirenProgram { + pub fn elf(&self) -> &[u8] { + &self.elf + } +} diff --git a/crates/zkvm/ziren/src/zkvm.rs b/crates/zkvm/ziren/src/zkvm.rs new file mode 100644 index 0000000..acba3eb --- /dev/null +++ b/crates/zkvm/ziren/src/zkvm.rs @@ -0,0 +1,205 @@ +use crate::program::ZirenProgram; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use std::{panic, time::Instant}; +use tracing::info; +use zkm_sdk::{ + CpuProver, Prover, ZKMProofKind, ZKMProofWithPublicValues, ZKMProvingKey, ZKMStdin, + ZKMVerifyingKey, +}; + +mod error; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +pub struct EreZiren { + program: ZirenProgram, + pk: ZKMProvingKey, + vk: ZKMVerifyingKey, +} + +impl EreZiren { + pub fn new(program: ZirenProgram, resource: ProverResourceType) -> Result { + if matches!( + resource, + ProverResourceType::Gpu | ProverResourceType::Network(_) + ) { + panic!("Network or Gpu proving not yet implemented for ZKM. Use CPU resource type."); + } + let (pk, vk) = CpuProver::new().setup(program.elf()); + Ok(Self { program, pk, vk }) + } +} + +impl zkVM for EreZiren { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let mut stdin = ZKMStdin::new(); + stdin.write_slice(input); + + let start = Instant::now(); + let (public_inputs, exec_report) = CpuProver::new() + .execute(self.program.elf(), &stdin) + .map_err(Error::Execute)?; + let execution_duration = start.elapsed(); + + Ok(( + public_inputs.to_vec(), + ProgramExecutionReport { + total_num_cycles: exec_report.total_instruction_count(), + region_cycles: exec_report.cycle_tracker.into_iter().collect(), + execution_duration, + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + info!("Generating proof…"); + + let mut stdin = ZKMStdin::new(); + stdin.write_slice(input); + + let inner_proof_kind = match proof_kind { + ProofKind::Compressed => ZKMProofKind::Compressed, + ProofKind::Groth16 => ZKMProofKind::Groth16, + }; + + let start = std::time::Instant::now(); + let proof = + panic::catch_unwind(|| CpuProver::new().prove(&self.pk, stdin, inner_proof_kind)) + .map_err(|err| Error::ProvePanic(panic_msg(err)))? + .map_err(Error::Prove)?; + let proving_time = start.elapsed(); + + let public_values = proof.public_values.to_vec(); + let proof = Proof::new( + proof_kind, + bincode::serde::encode_to_vec(&proof, bincode::config::legacy()) + .map_err(|err| CommonError::serialize("proof", "bincode", err))?, + ); + + Ok(( + public_values, + proof, + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + info!("Verifying proof…"); + + let proof_kind = proof.kind(); + + let (proof, _): (ZKMProofWithPublicValues, _) = + bincode::serde::decode_from_slice(proof.as_bytes(), bincode::config::legacy()) + .map_err(|err| CommonError::deserialize("proof", "bincode", err))?; + let inner_proof_kind = ZKMProofKind::from(&proof.proof); + + if !matches!( + (proof_kind, inner_proof_kind), + (ProofKind::Compressed, ZKMProofKind::Compressed) + | (ProofKind::Groth16, ZKMProofKind::Groth16) + ) { + bail!(Error::InvalidProofKind(proof_kind, inner_proof_kind)); + } + + CpuProver::new() + .verify(&proof, &self.vk) + .map_err(Error::Verify)?; + + Ok(proof.public_values.to_vec()) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreZiren { + type ProgramDigest = ZKMVerifyingKey; + + fn program_digest(&self) -> anyhow::Result { + Ok(self.vk.clone()) + } +} + +fn panic_msg(err: Box) -> String { + None.or_else(|| err.downcast_ref::().cloned()) + .or_else(|| err.downcast_ref::<&'static str>().map(ToString::to_string)) + .unwrap_or_else(|| "unknown panic msg".to_string()) +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustMips32r2Customized, program::ZirenProgram, zkvm::EreZiren}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::OnceLock; + + fn basic_program() -> ZirenProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustMips32r2Customized + .compile(&testing_guest_directory("ziren", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreZiren::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/ziren/src/error.rs b/crates/zkvm/ziren/src/zkvm/error.rs similarity index 75% rename from crates/zkvm/ziren/src/error.rs rename to crates/zkvm/ziren/src/zkvm/error.rs index 1b6519c..8c450ff 100644 --- a/crates/zkvm/ziren/src/error.rs +++ b/crates/zkvm/ziren/src/zkvm/error.rs @@ -1,15 +1,9 @@ -use ere_zkvm_interface::ProofKind; +use ere_zkvm_interface::zkvm::ProofKind; use thiserror::Error; use zkm_sdk::{ZKMProofKind, ZKMVerificationError}; #[derive(Debug, Error)] -pub enum CompileError { - #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), -} - -#[derive(Debug, Error)] -pub enum ZirenError { +pub enum Error { // Execute #[error("Ziren execution failed: {0}")] Execute(#[source] anyhow::Error), diff --git a/crates/zkvm/zisk/Cargo.toml b/crates/zkvm/zisk/Cargo.toml index 01db299..099f621 100644 --- a/crates/zkvm/zisk/Cargo.toml +++ b/crates/zkvm/zisk/Cargo.toml @@ -9,18 +9,26 @@ license.workspace = true anyhow.workspace = true blake3.workspace = true bytemuck.workspace = true +serde.workspace = true strum = { workspace = true, features = ["derive"] } tempfile.workspace = true thiserror.workspace = true tracing.workspace = true # Local dependencies -ere-compile-utils.workspace = true +ere-compile-utils = { workspace = true, optional = true } ere-zkvm-interface.workspace = true [dev-dependencies] -# Local dependencies ere-test-utils = { workspace = true, features = ["host"] } [build-dependencies] ere-build-utils.workspace = true + +[features] +default = ["compiler", "zkvm"] +compiler = ["dep:ere-compile-utils"] +zkvm = [] + +[lints] +workspace = true diff --git a/crates/zkvm/zisk/src/compiler.rs b/crates/zkvm/zisk/src/compiler.rs index d985201..8a64704 100644 --- a/crates/zkvm/zisk/src/compiler.rs +++ b/crates/zkvm/zisk/src/compiler.rs @@ -1,5 +1,5 @@ +mod error; mod rust_rv64ima_customized; +pub use error::Error; pub use rust_rv64ima_customized::RustRv64imaCustomized; - -pub type ZiskProgram = Vec; diff --git a/crates/zkvm/zisk/src/compiler/error.rs b/crates/zkvm/zisk/src/compiler/error.rs new file mode 100644 index 0000000..477e200 --- /dev/null +++ b/crates/zkvm/zisk/src/compiler/error.rs @@ -0,0 +1,8 @@ +use ere_compile_utils::CommonError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + CommonError(#[from] CommonError), +} diff --git a/crates/zkvm/zisk/src/compiler/rust_rv64ima_customized.rs b/crates/zkvm/zisk/src/compiler/rust_rv64ima_customized.rs index 3c41727..dd3a832 100644 --- a/crates/zkvm/zisk/src/compiler/rust_rv64ima_customized.rs +++ b/crates/zkvm/zisk/src/compiler/rust_rv64ima_customized.rs @@ -1,6 +1,6 @@ -use crate::error::CompileError; +use crate::{compiler::Error, program::ZiskProgram}; use ere_compile_utils::{CommonError, cargo_metadata, rustc_path}; -use ere_zkvm_interface::Compiler; +use ere_zkvm_interface::compiler::Compiler; use std::{fs, path::Path, process::Command}; use tracing::info; @@ -12,9 +12,9 @@ const ZISK_TARGET: &str = "riscv64ima-zisk-zkvm-elf"; pub struct RustRv64imaCustomized; impl Compiler for RustRv64imaCustomized { - type Error = CompileError; + type Error = Error; - type Program = Vec; + type Program = ZiskProgram; fn compile(&self, guest_directory: &Path) -> Result { info!("Compiling ZisK program at {}", guest_directory.display()); @@ -43,10 +43,10 @@ impl Compiler for RustRv64imaCustomized { .join("riscv64ima-zisk-zkvm-elf") .join("release") .join(&package.name); - let elf_bytes = + let elf = fs::read(&elf_path).map_err(|err| CommonError::read_file("elf", elf_path, err))?; - Ok(elf_bytes) + Ok(ZiskProgram { elf }) } } @@ -54,12 +54,12 @@ impl Compiler for RustRv64imaCustomized { mod tests { use crate::compiler::RustRv64imaCustomized; use ere_test_utils::host::testing_guest_directory; - use ere_zkvm_interface::Compiler; + use ere_zkvm_interface::compiler::Compiler; #[test] fn test_compile() { let guest_directory = testing_guest_directory("zisk", "basic"); - let elf_bytes = RustRv64imaCustomized.compile(&guest_directory).unwrap(); - assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty."); + let program = RustRv64imaCustomized.compile(&guest_directory).unwrap(); + assert!(!program.elf().is_empty(), "ELF bytes should not be empty."); } } diff --git a/crates/zkvm/zisk/src/lib.rs b/crates/zkvm/zisk/src/lib.rs index c6d4cc5..a53c4ec 100644 --- a/crates/zkvm/zisk/src/lib.rs +++ b/crates/zkvm/zisk/src/lib.rs @@ -1,202 +1,15 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr( + all(not(test), feature = "compiler", feature = "zkvm"), + warn(unused_crate_dependencies) +)] -use crate::{ - client::{RomDigest, ZiskOptions, ZiskSdk, ZiskServer}, - compiler::ZiskProgram, - error::ZiskError, -}; -use anyhow::bail; -use ere_zkvm_interface::{ - CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, - ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, -}; -use std::{ - sync::{Mutex, MutexGuard}, - time::Instant, -}; +pub mod program; -include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); - -mod client; +#[cfg(feature = "compiler")] pub mod compiler; -pub mod error; -pub struct EreZisk { - sdk: ZiskSdk, - /// Use `Mutex` because the server can only handle signle proving task at a - /// time. - /// - /// Use `Option` inside to lazily initialize only when `prove` is called. - server: Mutex>, -} +#[cfg(feature = "zkvm")] +pub mod zkvm; -impl EreZisk { - pub fn new(elf: ZiskProgram, resource: ProverResourceType) -> Result { - if matches!(resource, ProverResourceType::Network(_)) { - panic!("Network proving not yet implemented for ZisK. Use CPU or GPU resource type."); - } - let sdk = ZiskSdk::new(elf, resource, ZiskOptions::from_env())?; - Ok(Self { - sdk, - server: Mutex::new(None), - }) - } - - fn server(&'_ self) -> Result>, ZiskError> { - let mut server = self.server.lock().map_err(|_| ZiskError::MutexPoisoned)?; - - match &mut *server { - // Recreate the server if it has been created but failed to get status. - Some(s) => { - if s.status().is_err() { - *server = Some(self.sdk.server()?); - } - } - // Create the server if it has not been created. - None => { - *server = Some(self.sdk.server()?); - } - } - - // FIXME: Use `MutexGuard::map` to unwrap the inner `Option` when it's stabilized. - Ok(server) - } -} - -impl zkVM for EreZisk { - fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { - let start = Instant::now(); - let (public_values, total_num_cycles) = self.sdk.execute(input)?; - let execution_duration = start.elapsed(); - - Ok(( - public_values, - ProgramExecutionReport { - total_num_cycles, - execution_duration, - ..Default::default() - }, - )) - } - - fn prove( - &self, - input: &[u8], - proof_kind: ProofKind, - ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { - if proof_kind != ProofKind::Compressed { - bail!(CommonError::unsupported_proof_kind( - proof_kind, - [ProofKind::Compressed] - )) - } - - let mut server = self.server()?; - let server = server.as_mut().expect("server initialized"); - - let start = Instant::now(); - let (public_values, proof) = server.prove(input)?; - let proving_time = start.elapsed(); - - Ok(( - public_values, - Proof::Compressed(proof), - ProgramProvingReport::new(proving_time), - )) - } - - fn verify(&self, proof: &Proof) -> anyhow::Result { - let Proof::Compressed(proof) = proof else { - bail!(CommonError::unsupported_proof_kind( - proof.kind(), - [ProofKind::Compressed] - )) - }; - - Ok(self.sdk.verify(proof)?) - } - - fn name(&self) -> &'static str { - NAME - } - - fn sdk_version(&self) -> &'static str { - SDK_VERSION - } -} - -impl zkVMProgramDigest for EreZisk { - type ProgramDigest = RomDigest; - - fn program_digest(&self) -> anyhow::Result { - Ok(self.sdk.rom_digest()?) - } -} - -#[cfg(test)] -mod tests { - use crate::{EreZisk, compiler::RustRv64imaCustomized}; - use ere_test_utils::{ - host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, - program::basic::BasicProgramInput, - }; - use ere_zkvm_interface::{Compiler, ProofKind, ProverResourceType, zkVM}; - use std::sync::{Mutex, OnceLock}; - - /// It fails if multiple servers created concurrently using the same port, - /// so we have a lock to avoid that. - static PROVE_LOCK: Mutex<()> = Mutex::new(()); - - fn basic_program() -> Vec { - static PROGRAM: OnceLock> = OnceLock::new(); - PROGRAM - .get_or_init(|| { - RustRv64imaCustomized - .compile(&testing_guest_directory("zisk", "basic")) - .unwrap() - }) - .to_vec() - } - - #[test] - fn test_execute() { - let program = basic_program(); - let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); - - let test_case = BasicProgramInput::valid().into_output_sha256(); - run_zkvm_execute(&zkvm, &test_case); - } - - #[test] - fn test_execute_invalid_input() { - let program = basic_program(); - let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.execute(&input).unwrap_err(); - } - } - - #[test] - fn test_prove() { - let program = basic_program(); - let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); - - let _guard = PROVE_LOCK.lock().unwrap(); - - let test_case = BasicProgramInput::valid().into_output_sha256(); - run_zkvm_prove(&zkvm, &test_case); - } - - #[test] - fn test_prove_invalid_input() { - let program = basic_program(); - let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); - - let _guard = PROVE_LOCK.lock().unwrap(); - - for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { - zkvm.prove(&input, ProofKind::default()).unwrap_err(); - } - } -} +#[cfg(feature = "zkvm")] +pub use zkvm::*; diff --git a/crates/zkvm/zisk/src/program.rs b/crates/zkvm/zisk/src/program.rs new file mode 100644 index 0000000..0829755 --- /dev/null +++ b/crates/zkvm/zisk/src/program.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +/// Zisk program that contains ELF of compiled guest. +#[derive(Clone, Serialize, Deserialize)] +pub struct ZiskProgram { + pub(crate) elf: Vec, +} + +impl ZiskProgram { + pub fn elf(&self) -> &[u8] { + &self.elf + } +} diff --git a/crates/zkvm/zisk/src/zkvm.rs b/crates/zkvm/zisk/src/zkvm.rs new file mode 100644 index 0000000..d0704e8 --- /dev/null +++ b/crates/zkvm/zisk/src/zkvm.rs @@ -0,0 +1,203 @@ +use crate::{ + program::ZiskProgram, + zkvm::sdk::{RomDigest, ZiskOptions, ZiskSdk, ZiskServer}, +}; +use anyhow::bail; +use ere_zkvm_interface::zkvm::{ + CommonError, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, + ProverResourceType, PublicValues, zkVM, zkVMProgramDigest, +}; +use std::{ + sync::{Mutex, MutexGuard}, + time::Instant, +}; + +mod error; +mod sdk; + +pub use error::Error; + +include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs")); + +pub struct EreZisk { + sdk: ZiskSdk, + /// Use `Mutex` because the server can only handle signle proving task at a + /// time. + /// + /// Use `Option` inside to lazily initialize only when `prove` is called. + server: Mutex>, +} + +impl EreZisk { + pub fn new(program: ZiskProgram, resource: ProverResourceType) -> Result { + if matches!(resource, ProverResourceType::Network(_)) { + panic!("Network proving not yet implemented for ZisK. Use CPU or GPU resource type."); + } + let sdk = ZiskSdk::new(program.elf, resource, ZiskOptions::from_env())?; + Ok(Self { + sdk, + server: Mutex::new(None), + }) + } + + fn server(&'_ self) -> Result>, Error> { + let mut server = self.server.lock().map_err(|_| Error::MutexPoisoned)?; + + match &mut *server { + // Recreate the server if it has been created but failed to get status. + Some(s) => { + if s.status().is_err() { + *server = Some(self.sdk.server()?); + } + } + // Create the server if it has not been created. + None => { + *server = Some(self.sdk.server()?); + } + } + + // FIXME: Use `MutexGuard::map` to unwrap the inner `Option` when it's stabilized. + Ok(server) + } +} + +impl zkVM for EreZisk { + fn execute(&self, input: &[u8]) -> anyhow::Result<(PublicValues, ProgramExecutionReport)> { + let start = Instant::now(); + let (public_values, total_num_cycles) = self.sdk.execute(input)?; + let execution_duration = start.elapsed(); + + Ok(( + public_values, + ProgramExecutionReport { + total_num_cycles, + execution_duration, + ..Default::default() + }, + )) + } + + fn prove( + &self, + input: &[u8], + proof_kind: ProofKind, + ) -> anyhow::Result<(PublicValues, Proof, ProgramProvingReport)> { + if proof_kind != ProofKind::Compressed { + bail!(CommonError::unsupported_proof_kind( + proof_kind, + [ProofKind::Compressed] + )) + } + + let mut server = self.server()?; + let server = server.as_mut().expect("server initialized"); + + let start = Instant::now(); + let (public_values, proof) = server.prove(input)?; + let proving_time = start.elapsed(); + + Ok(( + public_values, + Proof::Compressed(proof), + ProgramProvingReport::new(proving_time), + )) + } + + fn verify(&self, proof: &Proof) -> anyhow::Result { + let Proof::Compressed(proof) = proof else { + bail!(CommonError::unsupported_proof_kind( + proof.kind(), + [ProofKind::Compressed] + )) + }; + + Ok(self.sdk.verify(proof)?) + } + + fn name(&self) -> &'static str { + NAME + } + + fn sdk_version(&self) -> &'static str { + SDK_VERSION + } +} + +impl zkVMProgramDigest for EreZisk { + type ProgramDigest = RomDigest; + + fn program_digest(&self) -> anyhow::Result { + Ok(self.sdk.rom_digest()?) + } +} + +#[cfg(test)] +mod tests { + use crate::{compiler::RustRv64imaCustomized, program::ZiskProgram, zkvm::EreZisk}; + use ere_test_utils::{ + host::{TestCase, run_zkvm_execute, run_zkvm_prove, testing_guest_directory}, + program::basic::BasicProgramInput, + }; + use ere_zkvm_interface::{ + compiler::Compiler, + zkvm::{ProofKind, ProverResourceType, zkVM}, + }; + use std::sync::{Mutex, OnceLock}; + + /// It fails if multiple servers created concurrently using the same port, + /// so we have a lock to avoid that. + static PROVE_LOCK: Mutex<()> = Mutex::new(()); + + fn basic_program() -> ZiskProgram { + static PROGRAM: OnceLock = OnceLock::new(); + PROGRAM + .get_or_init(|| { + RustRv64imaCustomized + .compile(&testing_guest_directory("zisk", "basic")) + .unwrap() + }) + .clone() + } + + #[test] + fn test_execute() { + let program = basic_program(); + let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); + + let test_case = BasicProgramInput::valid().into_output_sha256(); + run_zkvm_execute(&zkvm, &test_case); + } + + #[test] + fn test_execute_invalid_input() { + let program = basic_program(); + let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.execute(&input).unwrap_err(); + } + } + + #[test] + fn test_prove() { + let program = basic_program(); + let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); + + let _guard = PROVE_LOCK.lock().unwrap(); + + let test_case = BasicProgramInput::valid().into_output_sha256(); + run_zkvm_prove(&zkvm, &test_case); + } + + #[test] + fn test_prove_invalid_input() { + let program = basic_program(); + let zkvm = EreZisk::new(program, ProverResourceType::Cpu).unwrap(); + + let _guard = PROVE_LOCK.lock().unwrap(); + + for input in [Vec::new(), BasicProgramInput::invalid().serialized_input()] { + zkvm.prove(&input, ProofKind::default()).unwrap_err(); + } + } +} diff --git a/crates/zkvm/zisk/src/error.rs b/crates/zkvm/zisk/src/zkvm/error.rs similarity index 82% rename from crates/zkvm/zisk/src/error.rs rename to crates/zkvm/zisk/src/zkvm/error.rs index 2ac9f03..a6a724a 100644 --- a/crates/zkvm/zisk/src/error.rs +++ b/crates/zkvm/zisk/src/zkvm/error.rs @@ -1,17 +1,12 @@ -use crate::client::RomDigest; +use crate::zkvm::sdk::RomDigest; use bytemuck::PodCastError; +use ere_zkvm_interface::zkvm::CommonError; use thiserror::Error; #[derive(Debug, Error)] -pub enum CompileError { +pub enum Error { #[error(transparent)] - CommonError(#[from] ere_compile_utils::CommonError), -} - -#[derive(Debug, Error)] -pub enum ZiskError { - #[error(transparent)] - CommonError(#[from] ere_zkvm_interface::CommonError), + CommonError(#[from] CommonError), // Execution #[error("Total steps not found in execution report")] diff --git a/crates/zkvm/zisk/src/client.rs b/crates/zkvm/zisk/src/zkvm/sdk.rs similarity index 93% rename from crates/zkvm/zisk/src/client.rs rename to crates/zkvm/zisk/src/zkvm/sdk.rs index 15213b5..fc174d8 100644 --- a/crates/zkvm/zisk/src/client.rs +++ b/crates/zkvm/zisk/src/zkvm/sdk.rs @@ -1,5 +1,5 @@ -use crate::error::ZiskError; -use ere_zkvm_interface::{CommonError, ProverResourceType, PublicValues}; +use crate::zkvm::Error; +use ere_zkvm_interface::zkvm::{CommonError, ProverResourceType, PublicValues}; use std::{ collections::BTreeMap, env, fs, @@ -147,7 +147,7 @@ impl ZiskSdk { elf: Vec, resource: ProverResourceType, options: ZiskOptions, - ) -> Result { + ) -> Result { // Save ELF to `~/.zisk/cache` along with the ROM binaries, to avoid it // been cleaned up during a long run process. let cache_dir_path = dot_zisk_dir_path().join("cache"); @@ -170,7 +170,7 @@ impl ZiskSdk { } /// Execute the ELF with the given `input`. - pub fn execute(&self, input: &[u8]) -> Result<(PublicValues, u64), ZiskError> { + pub fn execute(&self, input: &[u8]) -> Result<(PublicValues, u64), Error> { let tempdir = tempdir().map_err(CommonError::tempdir)?; let input_path = tempdir.path().join("input"); let output_path = tempdir.path().join("output"); @@ -208,7 +208,7 @@ impl ZiskSdk { .next() .and_then(|steps| steps.parse::().ok()) }) - .ok_or(ZiskError::TotalStepsNotFound)?; + .ok_or(Error::TotalStepsNotFound)?; let public_values = fs::read(&output_path) .map_err(|err| CommonError::read_file("output", &output_path, err))?; @@ -220,7 +220,7 @@ impl ZiskSdk { /// /// If it is not setup yet, it makes sure the global setup is done, then /// does ROM setup of the ELF and stores the ROM digest for later usage. - pub fn rom_digest(&self) -> Result { + pub fn rom_digest(&self) -> Result { // FIXME: Use `get_or_try_init` when it is stabilized let mut result = Ok(()); let rom_digest = *self.rom_digest.get_or_init(|| { @@ -230,11 +230,11 @@ impl ZiskSdk { .ok() }); result?; - rom_digest.ok_or(ZiskError::RomSetupFailedBefore) + rom_digest.ok_or(Error::RomSetupFailedBefore) } /// Start a server of the ELF. - pub fn server(&self) -> Result { + pub fn server(&self) -> Result { // Setup ROM and get ROM digest if it's not done yet. let rom_digest = self.rom_digest()?; @@ -278,7 +278,7 @@ impl ZiskSdk { } /// Verify the proof of the ELF, and returns public values. - pub fn verify(&self, proof: &[u8]) -> Result { + pub fn verify(&self, proof: &[u8]) -> Result { let rom_digest = self.rom_digest()?; let tempdir = tempdir().map_err(CommonError::tempdir)?; @@ -296,7 +296,7 @@ impl ZiskSdk { .map_err(|err| CommonError::command(&cmd, err))?; if !output.status.success() { - Err(ZiskError::InvalidProof( + Err(Error::InvalidProof( String::from_utf8_lossy(&output.stderr).to_string(), ))? } @@ -309,7 +309,7 @@ impl ZiskSdk { // The proved ROM digest should be equal to preprocessed one. if proved_rom_digest != rom_digest { - return Err(ZiskError::UnexpectedRomDigest { + return Err(Error::UnexpectedRomDigest { preprocessed: rom_digest, proved: proved_rom_digest, }); @@ -348,7 +348,7 @@ impl Drop for ZiskServer { impl ZiskServer { /// Get status of server. - pub fn status(&self) -> Result { + pub fn status(&self) -> Result { let mut cmd = Command::new("cargo-zisk"); let output = cmd .args(["prove-client", "status"]) @@ -370,14 +370,14 @@ impl ZiskServer { } else if stdout.contains("working") { Ok(ZiskServerStatus::Working) } else { - Err(ZiskError::UnknownServerStatus { + Err(Error::UnknownServerStatus { stdout: stdout.to_string(), }) } } /// Send prove request to server and wait for proof to be created. - pub fn prove(&mut self, input: &[u8]) -> Result<(PublicValues, Vec), ZiskError> { + pub fn prove(&mut self, input: &[u8]) -> Result<(PublicValues, Vec), Error> { // Prefix that ZisK server will add to the file name of the proof. // We use constant because the file will be save to a temporary dir, // so there will be no conflict. @@ -427,7 +427,7 @@ impl ZiskServer { // The proved ROM digest should be equal to preprocessed one. if proved_rom_digest != self.rom_digest { - return Err(ZiskError::UnexpectedRomDigest { + return Err(Error::UnexpectedRomDigest { preprocessed: self.rom_digest, proved: proved_rom_digest, }); @@ -437,14 +437,14 @@ impl ZiskServer { } /// Wait until the server status to be idle. - fn wait_until_ready(&self) -> Result<(), ZiskError> { + fn wait_until_ready(&self) -> Result<(), Error> { const TIMEOUT: Duration = Duration::from_secs(300); // 5mins const INTERVAL: Duration = Duration::from_secs(1); let start = Instant::now(); while !matches!(self.status(), Ok(ZiskServerStatus::Idle)) { if start.elapsed() > TIMEOUT { - return Err(ZiskError::TimeoutWaitingServerReady); + return Err(Error::TimeoutWaitingServerReady); } thread::sleep(INTERVAL); } @@ -454,7 +454,7 @@ impl ZiskServer { } /// Does global setup if it is not done yet. -fn check_setup() -> Result<(), ZiskError> { +fn check_setup() -> Result<(), Error> { info!("Running command `cargo-zisk check-setup --aggregation`..."); let mut cmd = Command::new("cargo-zisk"); @@ -477,7 +477,7 @@ fn check_setup() -> Result<(), ZiskError> { } /// Does ROM setup of the ELF and returns the ROM digest. -fn rom_setup(elf_path: &Path) -> Result { +fn rom_setup(elf_path: &Path) -> Result { info!("Running command `cargo-zisk rom-setup` ..."); let mut cmd = Command::new("cargo-zisk"); @@ -510,7 +510,7 @@ fn rom_setup(elf_path: &Path) -> Result { .try_into() .ok() }) - .ok_or(ZiskError::RomDigestNotFound)?; + .ok_or(Error::RomDigestNotFound)?; info!("Command `cargo-zisk rom-setup` succeeded"); @@ -519,14 +519,13 @@ fn rom_setup(elf_path: &Path) -> Result { /// Deserialize public values as json string sequence, and parse the `RomDigest` /// and user set public values as `Vec`. -fn deserialize_public_values(proof: &[u8]) -> Result<(RomDigest, Vec), ZiskError> { - let proof = - bytemuck::try_cast_slice::<_, u64>(proof).map_err(ZiskError::CastProofBytesToU64s)?; +fn deserialize_public_values(proof: &[u8]) -> Result<(RomDigest, Vec), Error> { + let proof = bytemuck::try_cast_slice::<_, u64>(proof).map_err(Error::CastProofBytesToU64s)?; // The public values contain at least the the total number of public values, // `RomDigest`, and the number of user set public values. if proof.len() < 6 { - return Err(ZiskError::InvalidPublicValuesLength(proof.len())); + return Err(Error::InvalidPublicValuesLength(proof.len())); } // The first element is total number of public values. @@ -543,7 +542,7 @@ fn deserialize_public_values(proof: &[u8]) -> Result<(RomDigest, Vec), ZiskE .map(|v| Some(u32::try_from(*v).ok()?.to_le_bytes())) .take(num_user_public_values) .collect::>>() - .ok_or(ZiskError::InvalidPublicValue)? + .ok_or(Error::InvalidPublicValue)? .into_iter() .flatten() .collect();