feat: Impl zkVM for Nexus zkvm (#47)

Co-authored-by: Han <tinghan0110@gmail.com>
This commit is contained in:
Paul
2025-07-23 17:37:10 +08:00
committed by GitHub
parent 498c484d1c
commit bc3d99fa1b
22 changed files with 1117 additions and 70 deletions

View File

@@ -153,7 +153,7 @@ mod tests {
let test_guest_path = get_compile_test_guest_program_path();
let program = JOLT_TARGET::compile(&test_guest_path, Path::new("")).unwrap();
let mut inputs = Input::new();
inputs.write(1 as u32);
inputs.write(1_u32);
let zkvm = EreJolt::new(program, ProverResourceType::Cpu);
let _execution = zkvm.execute(&inputs).unwrap();

View File

@@ -0,0 +1,26 @@
[package]
name = "ere-nexus"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
zkvm-interface = { workspace = true }
nexus-sdk = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "0.3.4", version = "0.3.4" }
thiserror = "2"
bincode = "1.3"
tracing = "0.1"
toml = { version = "0.9.2", features = ["parse", "display", "serde"] }
[dev-dependencies]
anyhow = "1.0"
[build-dependencies]
build-utils.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,5 @@
use build_utils::detect_and_generate_name_and_sdk_version;
fn main() {
detect_and_generate_name_and_sdk_version("nexus", "nexus-sdk");
}

View File

@@ -0,0 +1,51 @@
use std::path::PathBuf;
use thiserror::Error;
use zkvm_interface::zkVMError;
impl From<NexusError> for zkVMError {
fn from(value: NexusError) -> Self {
zkVMError::Other(Box::new(value))
}
}
#[derive(Debug, Error)]
pub enum NexusError {
#[error(transparent)]
Compile(#[from] CompileError),
#[error(transparent)]
Prove(#[from] ProveError),
#[error(transparent)]
Verify(#[from] VerifyError),
/// Guest program directory does not exist.
#[error("guest program directory not found: {0}")]
PathNotFound(PathBuf),
/// Expected ELF file was not produced.
#[error("ELF file not found at {0}")]
ElfNotFound(PathBuf),
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error("nexus execution failed: {0}")]
Client(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
}
#[derive(Debug, Error)]
pub enum ProveError {
#[error("nexus execution failed: {0}")]
Client(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Serialising proof with `bincode` failed: {0}")]
Bincode(#[from] bincode::Error),
}
#[derive(Debug, Error)]
pub enum VerifyError {
#[error("nexus verification failed: {0}")]
Client(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Deserialising proof failed: {0}")]
Bincode(#[from] bincode::Error),
}

219
crates/ere-nexus/src/lib.rs Normal file
View File

@@ -0,0 +1,219 @@
#![allow(clippy::uninlined_format_args)]
use std::path::{Path, PathBuf};
use std::time::Instant;
use nexus_sdk::compile::cargo::CargoPackager;
use nexus_sdk::compile::{Compile, Compiler as NexusCompiler};
use nexus_sdk::stwo::seq::{Proof, Stwo};
use nexus_sdk::{Local, Prover, Verifiable};
use tracing::info;
use zkvm_interface::{
Compiler, Input, ProgramExecutionReport, ProgramProvingReport, ProverResourceType, zkVM,
zkVMError,
};
include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
mod error;
pub(crate) mod utils;
use crate::error::ProveError;
use crate::utils::get_cargo_package_name;
use error::{CompileError, NexusError, VerifyError};
#[allow(non_camel_case_types)]
pub struct NEXUS_TARGET;
impl Compiler for NEXUS_TARGET {
type Error = NexusError;
type Program = PathBuf;
fn compile(
workspace_directory: &Path,
guest_relative: &Path,
) -> Result<Self::Program, Self::Error> {
let guest_path = workspace_directory.join(guest_relative);
// 1. Check guest path
if !guest_path.exists() {
return Err(NexusError::PathNotFound(guest_path.to_path_buf()));
}
std::env::set_current_dir(&guest_path).map_err(|e| CompileError::Client(e.into()))?;
let package_name = get_cargo_package_name(&guest_path)
.ok_or(CompileError::Client(Box::from(format!(
"Failed to get guest package name, where guest path: {:?}",
guest_path
))))
.map_err(|e| CompileError::Client(e.into()))?;
let mut prover_compiler = NexusCompiler::<CargoPackager>::new(&package_name);
let elf_path = prover_compiler
.build()
.map_err(|e| CompileError::Client(e.into()))?;
Ok(elf_path)
}
}
pub struct EreNexus {
program: <NEXUS_TARGET as Compiler>::Program,
}
impl EreNexus {
pub fn new(
program: <NEXUS_TARGET as Compiler>::Program,
_resource_type: ProverResourceType,
) -> Self {
Self { program }
}
}
impl zkVM for EreNexus {
fn execute(&self, inputs: &Input) -> Result<zkvm_interface::ProgramExecutionReport, zkVMError> {
let start = Instant::now();
// let mut public_input = vec![];
let mut private_input = vec![];
for input in inputs.iter() {
private_input.extend(
input
.as_bytes()
.map_err(|err| NexusError::Prove(ProveError::Client(err)))
.map_err(zkVMError::from)?,
);
}
// TODO: Doesn't catch execute for guest in nexus. so only left some dummy code(parse input) here.
// Besides, public input is not supported yet, so we just pass an empty tuple
Ok(ProgramExecutionReport {
execution_duration: start.elapsed(),
..Default::default()
})
}
fn prove(
&self,
inputs: &Input,
) -> Result<(Vec<u8>, zkvm_interface::ProgramProvingReport), zkVMError> {
let prover: Stwo<Local> = Stwo::new_from_file(&self.program.to_string_lossy().to_string())
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))
.map_err(zkVMError::from)?;
// One convention that may be useful for simplifying the design is that all inputs to the vm are private and all outputs are public.
// If an input should be public, then it could just be returned from the function.
// let mut public_input = vec![];
let mut private_input = vec![];
for input in inputs.iter() {
private_input.extend(
input
.as_bytes()
.map_err(|err| NexusError::Prove(ProveError::Client(err)))
.map_err(zkVMError::from)?,
);
}
let now = Instant::now();
let (_view, proof) = prover
.prove_with_input(&private_input, &())
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))
.map_err(zkVMError::from)?;
let elapsed = now.elapsed();
let bytes = bincode::serialize(&proof)
.map_err(|err| NexusError::Prove(ProveError::Bincode(err)))?;
Ok((bytes, ProgramProvingReport::new(elapsed)))
}
fn verify(&self, proof: &[u8]) -> Result<(), zkVMError> {
info!("Verifying proof...");
let proof: Proof = bincode::deserialize(proof)
.map_err(|err| NexusError::Verify(VerifyError::Bincode(err)))?;
let prover: Stwo<Local> = Stwo::new_from_file(&self.program.to_string_lossy().to_string())
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))
.map_err(zkVMError::from)?;
let elf = prover.elf.clone(); // save elf for use with verification
#[rustfmt::skip]
proof
.verify_expected::<(), ()>(
&(), // no public input
nexus_sdk::KnownExitCodes::ExitSuccess as u32,
&(), // no public output
&elf, // expected elf (program binary)
&[], // no associated data,
)
.map_err(|e| NexusError::Verify(VerifyError::Client(e.into())))
.map_err(zkVMError::from)?;
info!("Verify Succeeded!");
Ok(())
}
fn name(&self) -> &'static str {
NAME
}
fn sdk_version(&self) -> &'static str {
SDK_VERSION
}
}
#[cfg(test)]
mod tests {
use zkvm_interface::Compiler;
use crate::NEXUS_TARGET;
use super::*;
use std::path::PathBuf;
fn get_test_guest_program_path() -> PathBuf {
let workspace_dir = env!("CARGO_WORKSPACE_DIR");
PathBuf::from(workspace_dir)
.join("tests")
.join("nexus")
.join("guest")
.canonicalize()
.expect("Failed to find or canonicalize test guest program at <CARGO_WORKSPACE_DIR>/tests/compile/nexus")
}
#[test]
fn test_compile() -> anyhow::Result<()> {
let test_guest_path = get_test_guest_program_path();
let elf_path = NEXUS_TARGET::compile(&test_guest_path, Path::new(""))?;
let prover: Stwo<Local> = Stwo::new_from_file(&elf_path.to_string_lossy().to_string())?;
let elf = prover.elf.clone();
assert!(
!elf.instructions.is_empty(),
"ELF bytes should not be empty."
);
Ok(())
}
#[test]
fn test_execute() {
let test_guest_path = get_test_guest_program_path();
let elf =
NEXUS_TARGET::compile(&test_guest_path, Path::new("")).expect("compilation failed");
let mut input = Input::new();
input.write(10u64);
let zkvm = EreNexus::new(elf, ProverResourceType::Cpu);
zkvm.execute(&input).unwrap();
}
#[test]
fn test_prove_verify() -> anyhow::Result<()> {
let test_guest_path = get_test_guest_program_path();
let elf = NEXUS_TARGET::compile(&test_guest_path, Path::new(""))?;
let mut input = Input::new();
input.write(10u64);
let zkvm = EreNexus::new(elf, ProverResourceType::Cpu);
let (proof, _) = zkvm.prove(&input).unwrap();
zkvm.verify(&proof).expect("proof should verify");
Ok(())
}
}

View File

@@ -0,0 +1,13 @@
use std::fs;
use toml::Table;
pub fn get_cargo_package_name(crate_path: &std::path::Path) -> Option<String> {
let cargo_contents = fs::read_to_string(crate_path.join("Cargo.toml")).ok()?;
let cargo_toml: Table = toml::from_str(&cargo_contents).ok()?;
cargo_toml
.get("package")?
.get("name")?
.as_str()
.map(|s| s.to_string())
}

View File

@@ -161,7 +161,7 @@ mod tests {
"Attempting to find test guest program at: {}",
path.display()
);
println!("Workspace dir is: {}", workspace_dir);
println!("Workspace dir is: {workspace_dir}");
path.canonicalize()
.expect("Failed to find or canonicalize test guest program at <CARGO_WORKSPACE_DIR>/tests/pico/compile/basic/app")
@@ -176,11 +176,8 @@ mod tests {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}
Err(e) => {
panic!(
"compile_sp1_program direct call failed for dedicated guest: {:?}",
e
);
Err(err) => {
panic!("compile_sp1_program direct call failed for dedicated guest: {err}");
}
}
}

View File

@@ -8,8 +8,8 @@ license.workspace = true
[dependencies]
zkvm-interface = { workspace = true }
anyhow = "1.0" #TODO: remove only needed in tests
toml = "0.8"
risc0-zkvm = { version = "^2.3.0", features = ["unstable"] }
#toml = "0.8"
risc0-zkvm = { version = "2.3.0", features = ["unstable"] }
borsh = "1.5.7"
hex = "*"

View File

@@ -102,7 +102,7 @@ impl zkVM for EreRisc0 {
env.write(serialize).unwrap();
}
InputItem::Bytes(items) => {
env.write_frame(&items);
env.write_frame(items);
}
}
}
@@ -121,7 +121,7 @@ impl zkVM for EreRisc0 {
fn verify(&self, proof: &[u8]) -> Result<(), zkVMError> {
let decoded: Receipt =
borsh::from_slice(&proof).map_err(|err| zkVMError::Other(Box::new(err)))?;
borsh::from_slice(proof).map_err(|err| zkVMError::Other(Box::new(err)))?;
decoded
.verify(self.program.image_id)
@@ -175,7 +175,7 @@ mod prove_tests {
let proof_bytes = match zkvm.prove(&input_builder) {
Ok((prove_result, _)) => prove_result,
Err(err) => {
panic!("Proving error in test: {:?}", err);
panic!("Proving error in test: {err}",);
}
};
@@ -238,8 +238,8 @@ mod execute_tests {
let result = zkvm.execute(&input_builder);
if let Err(e) = &result {
panic!("Execution error: {:?}", e);
if let Err(err) = &result {
panic!("Execution error: {err}");
}
}

View File

@@ -93,8 +93,8 @@ mod tests {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}
Err(e) => {
panic!("compile failed for dedicated guest: {:?}", e);
Err(err) => {
panic!("compile failed for dedicated guest: {err}");
}
}
}
@@ -106,11 +106,8 @@ mod tests {
Ok(elf_bytes) => {
assert!(!elf_bytes.is_empty(), "ELF bytes should not be empty.");
}
Err(e) => {
panic!(
"compile_sp1_program direct call failed for dedicated guest: {:?}",
e
);
Err(err) => {
panic!("compile_sp1_program direct call failed for dedicated guest: {err}");
}
}
}

View File

@@ -264,8 +264,8 @@ mod execute_tests {
let result = zkvm.execute(&input_builder);
if let Err(e) = &result {
panic!("Execution error: {:?}", e);
if let Err(err) = &result {
panic!("Execution error: {err}");
}
}
@@ -325,7 +325,7 @@ mod prove_tests {
let proof_bytes = match zkvm.prove(&input_builder) {
Ok((prove_result, _)) => prove_result,
Err(err) => {
panic!("Proving error in test: {:?}", err);
panic!("Proving error in test: {err}");
}
};
@@ -387,7 +387,7 @@ mod prove_tests {
prove_result
}
Err(err) => {
panic!("Network proving error: {:?}", err);
panic!("Network proving error: {err}");
}
};