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

33
.github/workflows/test-nexus-docker.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Test Nexus (Docker)
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test-nexus-via-docker-build:
name: Build Nexus Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build ere-base image
run: |
docker build \
--tag ere-base:latest \
--file docker/base/Dockerfile.base .
- name: Build ere-builder-nexus image
run: |
docker build \
--tag ere-builder-nexus:latest \
--file docker/nexus/Dockerfile .

622
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,16 @@
[workspace]
members = [
"crates/build-utils",
# zkVM interface
"crates/zkvm-interface",
# zkVMs
"crates/ere-sp1",
"crates/ere-risczero",
"crates/ere-jolt",
"crates/ere-nexus",
"crates/ere-openvm",
"crates/ere-pico",
"crates/ere-jolt",
"crates/ere-risczero",
"crates/ere-sp1",
"crates/ere-zisk",
# zkVM interface
"crates/zkvm-interface",
# Guest compilers
"docker/sp1",

29
Makefile Normal file
View File

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

View File

@@ -46,6 +46,7 @@
- Jolt
- Pico
- Zisk
- Nexus
## Quick Start

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}");
}
};

40
docker/nexus/Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
ARG BASE_IMAGE_TAG=latest
FROM ere-base:${BASE_IMAGE_TAG}
# The ere-base image provides Rust, Cargo, and common tools.
# We operate as root for SDK installation.
# Copy the Nexus SDK installer script from the workspace context
COPY scripts/sdk_installers/install_nexus_sdk.sh /tmp/install_nexus_sdk.sh
RUN chmod +x /tmp/install_nexus_sdk.sh
RUN rustup default nightly-2025-06-05 && \
rustup target add riscv32i-unknown-none-elf
# Run the Nexus SDK installation script.
# This script installs the specific Rust toolchain (nightly-2025-06-05)
# and installs cargo-nexus
# The CARGO_HOME from ere-base (e.g., /root/.cargo) will be used, and cargo-nexus will be in its bin.
RUN /tmp/install_nexus_sdk.sh && rm /tmp/install_nexus_sdk.sh # Clean up the script
# Define the Nexus toolchain for convenience in subsequent commands if needed, though cargo-nexus should use it.
ENV NEXUS_TOOLCHAIN_VERSION="nightly-2025-06-05"
# Verify Nexus installation
RUN echo "Verifying Nexus installation in Dockerfile (post-script)..." && cargo-nexus --version
# Copy the entire ere project context
# The WORKDIR is /app from the base image
WORKDIR /app
COPY . .
# Build
RUN echo "Build tests for ere-nexus library..." && \
cargo build --tests --release -p ere-nexus
# Run tests
RUN echo "Running tests for ere-nexus library..." && \
cargo test --release -p ere-nexus --lib -- --color always && \
echo "Running Nexus tests Success..."
CMD ["/bin/bash"]

View File

@@ -20,7 +20,7 @@ RUN /tmp/install_pico_sdk.sh && rm /tmp/install_pico_sdk.sh # Clean up the scrip
ENV PICO_TOOLCHAIN_VERSION="nightly-2024-11-27"
# Verify Pico installation
RUN echo "Verifying Risc0 installation in Dockerfile (post-script)..." && cargo "+${PICO_TOOLCHAIN_VERSION}" pico --version
RUN echo "Verifying Pico installation in Dockerfile (post-script)..." && cargo "+${PICO_TOOLCHAIN_VERSION}" pico --version
# Copy the entire ere project context
# The WORKDIR is /app from the base image

View File

@@ -0,0 +1,54 @@
#!/bin/bash
set -e
# --- Utility functions (duplicated) ---
# Checks if a tool is installed and available in PATH.
is_tool_installed() {
command -v "$1" &> /dev/null
}
# Ensures a tool is installed. Exits with an error if not.
ensure_tool_installed() {
local tool_name="$1"
local purpose_message="$2"
if ! is_tool_installed "${tool_name}"; then
echo "Error: Required tool '${tool_name}' could not be found." >&2
if [ -n "${purpose_message}" ]; then
echo " It is needed ${purpose_message}." >&2
fi
echo " Please install it first and ensure it is in your PATH." >&2
exit 1
fi
}
# --- End of Utility functions ---
echo "Installing Nexus Toolchain and SDK using Nexus (prebuilt binaries)..."
# Prerequisites for Nexus (some of these are for the SDK itself beyond Nexus)
#ensure_tool_installed "curl" "to download the Nexus installer"
#ensure_tool_installed "bash" "to run the Nexus installer"
ensure_tool_installed "rustup" "for managing Rust toolchains"
ensure_tool_installed "cargo" "as cargo-nexus is a cargo subcommand"
# Step 1: Download and run the toolchain.
# Verify Nexus installation
echo "Verifying Nexus installation..."
echo "Checking for RISC-V target..."
if rustup target list | grep -q "riscv32i-unknown-none-elf"; then
echo "RISC-V target 'riscv32i-unknown-none-elf' not found."
else
echo "RISC-V 'riscv32i-unknown-none-elf' not found after installation!" >&2
echo "Install the RISC-V target:"
rustup target add riscv32i-unknown-none-elf
fi
echo "Checking for cargo-nexus..."
if cargo --list | grep "nexus"; then
echo "cargo-nexus found."
else
echo "cargo-nexus not found after installation!" >&2
echo "Install the cargo-nexus:"
cargo install --git https://github.com/nexus-xyz/nexus-zkvm cargo-nexus --tag 'v0.3.4'
fi

View File

@@ -0,0 +1,5 @@
[target.riscv32i-unknown-none-elf]
rustflags = [
"-C", "link-arg=-Tlink.x",
]
runner="nexus-run"

View File

@@ -0,0 +1,14 @@
[package]
name = "ere-nexus-guest"
version = "0.1.0"
edition = "2024"
[dependencies]
nexus-rt = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "0.3.4", version = "0.3.4" }
# Generated by cargo-nexus, do not remove!
#
[features]
cycles = [] # Enable cycle counting for run command
[workspace]

View File

@@ -0,0 +1,18 @@
#![cfg_attr(target_arch = "riscv32", no_std, no_main)]
use nexus_rt::println;
#[nexus_rt::main]
#[nexus_rt::private_input(x)]
fn main(x: u32) {
println!("Read public input: {}", x);
let res = fibonacci(x);
println!("fib result: {}", res);
}
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 1,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}