nexus: implement execute and io serialization (#171)

Co-authored-by: han0110 <tinghan0110@gmail.com>
This commit is contained in:
brechy
2025-10-17 21:55:36 -03:00
committed by GitHub
parent 7e0c8d7fe8
commit 7bd1789a31
12 changed files with 447 additions and 88 deletions

3
Cargo.lock generated
View File

@@ -3720,7 +3720,10 @@ dependencies = [
"ere-compile-utils",
"ere-test-utils",
"ere-zkvm-interface",
"nexus-core",
"nexus-sdk",
"nexus-vm",
"postcard",
"serde",
"thiserror 2.0.12",
"tracing",

View File

@@ -43,6 +43,7 @@ clap = "4.5.42"
dashmap = "6.1.0"
erased-serde = "0.4.6"
indexmap = "2.10.0"
postcard = "1.0.8"
prost = "0.13"
prost-build = "0.13"
rand = "0.9.2"
@@ -76,6 +77,8 @@ miden-verifier = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag =
# Nexus dependencies
nexus-sdk = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "v0.3.4" }
nexus-core = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "v0.3.4" }
nexus-vm = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "v0.3.4" }
# OpenVM dependencies
openvm-build = { git = "https://github.com/openvm-org/openvm.git", tag = "v1.4.0" }

View File

@@ -7,12 +7,15 @@ license.workspace = true
[dependencies]
bincode.workspace = true
postcard.workspace = true
serde.workspace = true
thiserror.workspace = true
tracing.workspace = true
# Nexus dependencies
nexus-sdk.workspace = true
nexus-core.workspace = true
nexus-vm.workspace = true
# Local dependencies
ere-compile-utils.workspace = true

View File

@@ -2,10 +2,19 @@ use crate::{
compiler::NexusProgram,
error::{CompileError, NexusError},
};
use ere_compile_utils::cargo_metadata;
use ere_compile_utils::CargoBuildCmd;
use ere_zkvm_interface::Compiler;
use nexus_sdk::compile::{Compile, Compiler as NexusCompiler, cargo::CargoPackager};
use std::{fs, path::Path};
use std::path::Path;
const TARGET_TRIPLE: &str = "riscv32i-unknown-none-elf";
// Linker script from nexus-sdk
// https://github.com/nexus-xyz/nexus-zkvm/blob/v0.3.4/sdk/src/compile/linker-scripts/default.x
const LINKER_SCRIPT: &str = include_str!("rust_rv32i/linker.x");
const RUSTFLAGS: &[&str] = &["-C", "relocation-model=pic", "-C", "panic=abort"];
const CARGO_BUILD_OPTIONS: &[&str] = &[
// For bare metal we have to build core and alloc
"-Zbuild-std=core,alloc",
];
/// Compiler for Rust guest program to RV32I architecture.
pub struct RustRv32i;
@@ -15,23 +24,16 @@ impl Compiler for RustRv32i {
type Program = NexusProgram;
fn compile(&self, guest_path: &Path) -> Result<Self::Program, Self::Error> {
// 1. Check guest path
if !guest_path.exists() {
return Err(CompileError::PathNotFound(guest_path.to_path_buf()))?;
}
std::env::set_current_dir(guest_path).map_err(|e| CompileError::Client(e.into()))?;
let metadata = cargo_metadata(guest_path).map_err(CompileError::CompileUtilError)?;
let package_name = &metadata.root_package().unwrap().name;
let mut prover_compiler = NexusCompiler::<CargoPackager>::new(package_name);
let elf_path = prover_compiler
.build()
.map_err(|e| CompileError::Client(e.into()))?;
let elf = fs::read(&elf_path).map_err(|_| CompileError::ElfNotFound(elf_path))?;
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
let elf = CargoBuildCmd::new()
.linker_script(Some(LINKER_SCRIPT))
// The compiled ELF will be incompatible with Nexus VM if we don't pin this version
// https://github.com/nexus-xyz/nexus-zkvm/blob/main/rust-toolchain.toml
.toolchain("nightly-2025-04-06")
.build_options(CARGO_BUILD_OPTIONS)
.rustflags(RUSTFLAGS)
.exec(guest_directory, TARGET_TRIPLE)
.map_err(CompileError::CompileUtilError)?;
Ok(elf)
}
}

View File

@@ -0,0 +1,55 @@
ENTRY(_start);
/* nb: when proving we will rebuild the memory model based on the first
pass' usages, so there is no cost for a "suboptimal" layout here */
SECTIONS
{
__memory_top = 0x80400000;
. = 0x88;
.text : ALIGN(4)
{
KEEP(*(.init));
. = ALIGN(4);
KEEP(*(.init.rust));
*(.text .text.*);
}
. = ALIGN(8);
.data : ALIGN(4)
{
/* Must be called __global_pointer$ for linker relaxations to work. */
__global_pointer$ = . + 0x800;
*(.srodata .srodata.*);
*(.rodata .rodata.*);
*(.sdata .sdata.* .sdata2 .sdata2.*);
*(.data .data.*);
/* this is used by the global allocator (see:src/lib.rs) */
. = ALIGN(4);
_heap = .;
LONG(_ebss);
}
.bss (NOLOAD) : ALIGN(4)
{
*(.sbss .sbss.* .bss .bss.*);
. = ALIGN(4);
_ebss = .;
_end = .;
}
/DISCARD/ :
{
*(.comment*)
*(.debug*)
}
/* Stack unwinding is not supported, but we will keep these for now */
.eh_frame (INFO) : { KEEP(*(.eh_frame)) }
.eh_frame_hdr (INFO) : { *(.eh_frame_hdr) }
}
ASSERT(. < __memory_top, "Program is too large for the VM memory.");

View File

@@ -40,6 +40,8 @@ pub enum ProveError {
Client(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
#[error("Serialising proof with `bincode` failed: {0}")]
Bincode(#[from] bincode::Error),
#[error("Serialising input with `postcard` failed: {0}")]
Postcard(String),
}
#[derive(Debug, Error)]

View File

@@ -5,11 +5,16 @@ use crate::{
error::{NexusError, ProveError, VerifyError},
};
use ere_zkvm_interface::{
Input, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind, ProverResourceType,
PublicValues, zkVM, zkVMError,
Input, InputItem, ProgramExecutionReport, ProgramProvingReport, Proof, ProofKind,
ProverResourceType, PublicValues, zkVM, zkVMError,
};
use nexus_sdk::{Local, Prover, Verifiable, stwo::seq::Stwo};
use serde::de::DeserializeOwned;
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, de::DeserializeOwned};
use std::{io::Read, time::Instant};
use tracing::info;
@@ -18,65 +23,100 @@ include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
pub mod compiler;
pub mod error;
#[derive(Serialize, Deserialize)]
pub struct NexusProofBundle {
proof: NexusProof,
public_values: Vec<u8>,
}
pub struct EreNexus {
program: NexusProgram,
elf: NexusProgram,
}
impl EreNexus {
pub fn new(program: NexusProgram, _resource_type: ProverResourceType) -> Self {
Self { program }
pub fn new(elf: NexusProgram, _resource_type: ProverResourceType) -> Self {
Self { elf }
}
}
impl zkVM for EreNexus {
fn execute(
&self,
_inputs: &Input,
) -> Result<(PublicValues, ProgramExecutionReport), zkVMError> {
// TODO: Serialize inputs by `postcard` and make sure there is no double serailization.
// Issue for tracking: https://github.com/eth-act/ere/issues/63.
fn execute(&self, inputs: &Input) -> Result<(PublicValues, ProgramExecutionReport), zkVMError> {
let elf = ElfFile::from_bytes(&self.elf)
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))?;
// TODO: Execute and get cycle count
let input_bytes = serialize_inputs(inputs)?;
// TODO: Public values
let public_values = Vec::new();
// 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_bytes.is_empty() {
Vec::new()
} else {
postcard::to_stdvec_cobs(&input_bytes)
.map_err(|e| NexusError::Prove(ProveError::Postcard(e.to_string())))?
};
Ok((public_values, ProgramExecutionReport::default()))
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(|e| NexusError::Prove(ProveError::Client(e.into())))?;
let public_values = view
.public_output::<Vec<u8>>()
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))?;
Ok((
public_values,
ProgramExecutionReport {
total_num_cycles: trace.get_num_steps() as u64,
region_cycles: Default::default(), // not available
execution_duration: start.elapsed(),
},
))
}
fn prove(
&self,
_inputs: &Input,
inputs: &Input,
proof_kind: ProofKind,
) -> Result<(PublicValues, Proof, ProgramProvingReport), zkVMError> {
if proof_kind != ProofKind::Compressed {
panic!("Only Compressed proof kind is supported.");
}
let prover: Stwo<Local> = Stwo::new_from_bytes(&self.program)
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))
.map_err(zkVMError::from)?;
let elf = ElfFile::from_bytes(&self.elf)
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))?;
// TODO: Serialize inputs by `postcard` and make sure there is no double serailization.
// Issue for tracking: https://github.com/eth-act/ere/issues/63.
let prover =
Stwo::new(&elf).map_err(|e| NexusError::Prove(ProveError::Client(e.into())))?;
let now = Instant::now();
let (_view, proof) = prover
.prove_with_input(&(), &())
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))
.map_err(zkVMError::from)?;
let elapsed = now.elapsed();
let input_bytes = serialize_inputs(inputs)?;
let bytes = bincode::serialize(&proof)
let start = Instant::now();
let (view, proof) = prover
.prove_with_input::<Vec<u8>, ()>(&input_bytes, &())
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))?;
let public_values = view
.public_output::<Vec<u8>>()
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))?;
let proof_bundle = NexusProofBundle {
proof,
public_values: public_values.clone(),
};
let proof_bytes = bincode::serialize(&proof_bundle)
.map_err(|err| NexusError::Prove(ProveError::Bincode(err)))?;
// TODO: Public values
let public_values = Vec::new();
Ok((
public_values,
Proof::Compressed(bytes),
ProgramProvingReport::new(elapsed),
Proof::Compressed(proof_bytes),
ProgramProvingReport::new(start.elapsed()),
))
}
@@ -87,30 +127,23 @@ impl zkVM for EreNexus {
info!("Verifying proof...");
let proof: nexus_sdk::stwo::seq::Proof = bincode::deserialize(proof)
let proof_bundle = bincode::deserialize::<NexusProofBundle>(proof)
.map_err(|err| NexusError::Verify(VerifyError::Bincode(err)))?;
let prover: Stwo<Local> = Stwo::new_from_bytes(&self.program)
.map_err(|e| NexusError::Prove(ProveError::Client(e.into())))
.map_err(zkVMError::from)?;
let elf = prover.elf.clone(); // save elf for use with verification
proof
.verify_expected::<(), ()>(
&(), // no public input
nexus_sdk::KnownExitCodes::ExitSuccess as u32,
&(), // no public output
&elf, // expected elf (program binary)
&[], // no associated data,
proof_bundle
.proof
.verify_expected_from_program_bytes::<(), Vec<u8>>(
&(),
KnownExitCodes::ExitSuccess as u32,
&proof_bundle.public_values,
&self.elf,
&[],
)
.map_err(|e| NexusError::Verify(VerifyError::Client(e.into())))
.map_err(zkVMError::from)?;
.map_err(|e| NexusError::Verify(VerifyError::Client(e.into())))?;
info!("Verify Succeeded!");
// TODO: Public values
let public_values = Vec::new();
Ok(public_values)
Ok(proof_bundle.public_values)
}
fn name(&self) -> &'static str {
@@ -121,8 +154,154 @@ impl zkVM for EreNexus {
SDK_VERSION
}
fn deserialize_from<R: Read, T: DeserializeOwned>(&self, _reader: R) -> Result<T, zkVMError> {
// Issue for tracking: https://github.com/eth-act/ere/issues/63.
todo!()
fn deserialize_from<R: Read, T: DeserializeOwned>(&self, reader: R) -> Result<T, zkVMError> {
let mut buf = vec![0; 1 << 20]; // allocate 1MiB as buffer.
let (value, _) = postcard::from_io((reader, &mut buf)).map_err(zkVMError::other)?;
Ok(value)
}
}
/// Serializes nexus program inputs
pub fn serialize_inputs(inputs: &Input) -> Result<Vec<u8>, NexusError> {
inputs
.iter()
.try_fold(Vec::new(), |mut acc, item| -> Result<Vec<u8>, NexusError> {
match item {
InputItem::Object(obj) => {
let buffer = postcard::to_allocvec(obj.as_ref())
.map_err(|e| NexusError::Prove(ProveError::Postcard(e.to_string())))?;
acc.extend_from_slice(&buffer);
Ok(acc)
}
InputItem::SerializedObject(bytes) => {
acc.extend_from_slice(bytes);
Ok(acc)
}
InputItem::Bytes(bytes) => {
let buffer = postcard::to_allocvec(bytes)
.map_err(|e| NexusError::Prove(ProveError::Postcard(e.to_string())))?;
acc.extend_from_slice(&buffer);
Ok(acc)
}
}
})
}
#[cfg(test)]
mod tests {
use crate::{EreNexus, compiler::RustRv32i};
use ere_test_utils::host::{
BasicProgramIo, run_zkvm_execute, run_zkvm_prove, testing_guest_directory,
};
use ere_zkvm_interface::{Compiler, Input, ProofKind, ProverResourceType, zkVM};
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
static BASIC_PROGRAM: OnceLock<Vec<u8>> = OnceLock::new();
static FIB_PROGRAM: OnceLock<Vec<u8>> = OnceLock::new();
fn basic_program() -> Vec<u8> {
BASIC_PROGRAM
.get_or_init(|| {
RustRv32i
.compile(&testing_guest_directory("nexus", "basic"))
.unwrap()
})
.clone()
}
fn fib_program() -> Vec<u8> {
FIB_PROGRAM
.get_or_init(|| {
RustRv32i
.compile(&testing_guest_directory("nexus", "fib"))
.unwrap()
})
.clone()
}
#[test]
fn test_execute() {
let program = basic_program();
let zkvm = EreNexus::new(program, ProverResourceType::Cpu);
let io = BasicProgramIo::valid();
run_zkvm_execute(&zkvm, &io);
}
#[test]
fn test_execute_invalid_inputs() {
let program = basic_program();
let zkvm = EreNexus::new(program, ProverResourceType::Cpu);
for inputs in [
BasicProgramIo::empty(),
BasicProgramIo::invalid_type(),
BasicProgramIo::invalid_data(),
] {
zkvm.execute(&inputs).unwrap_err();
}
}
#[test]
fn test_prove() {
let program = basic_program();
let zkvm = EreNexus::new(program, ProverResourceType::Cpu);
let io = BasicProgramIo::valid();
run_zkvm_prove(&zkvm, &io);
}
#[test]
fn test_prove_invalid_inputs() {
let program = basic_program();
let zkvm = EreNexus::new(program, ProverResourceType::Cpu);
for inputs in [
BasicProgramIo::empty(),
BasicProgramIo::invalid_type(),
BasicProgramIo::invalid_data(),
] {
zkvm.prove(&inputs, ProofKind::default()).unwrap_err();
}
}
#[test]
fn test_fibonacci() {
#[derive(Serialize, Deserialize)]
struct FibInput {
n: u32,
}
let program = fib_program();
let zkvm = EreNexus::new(program, ProverResourceType::Cpu);
let mut input = Input::new();
input.write(FibInput { n: 10 });
let (public_values, _report) = zkvm.execute(&input).expect("Execution failed");
let result: u32 = zkvm
.deserialize_from(&public_values[..])
.expect("Failed to deserialize output");
assert_eq!(result, 55, "fib(10) should be 55");
let mut input = Input::new();
input.write(FibInput { n: 0 });
let (public_values, _report) = zkvm.execute(&input).expect("Execution failed");
let result: u32 = zkvm
.deserialize_from(&public_values[..])
.expect("Failed to deserialize output");
assert_eq!(result, 0, "fib(0) should be 0");
let mut input = Input::new();
input.write(FibInput { n: 1 });
let (public_values, _report) = zkvm.execute(&input).expect("Execution failed");
let result: u32 = zkvm
.deserialize_from(&public_values[..])
.expect("Failed to deserialize output");
assert_eq!(result, 1, "fib(1) should be 1");
}
}

View File

@@ -23,4 +23,7 @@ RUN /tmp/install_nexus_sdk.sh && rm /tmp/install_nexus_sdk.sh # Clean up the scr
# Verify Nexus installation
RUN echo "Verifying Nexus installation in Dockerfile (post-script)..." && cargo-nexus --version
# Add `rust-src` component to enable std build for nightly rust.
RUN rustup component add rust-src --toolchain "$NEXUS_TOOLCHAIN_VERSION"
CMD ["/bin/bash"]

View File

@@ -1,10 +1,12 @@
[package]
name = "ere-nexus-guest"
version = "0.1.0"
edition = "2024"
edition = "2021"
[dependencies]
nexus-rt = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "v0.3.4" }
postcard = { version = "1.0", default-features = false, features = ["alloc"] }
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
# Generated by cargo-nexus, do not remove!
#

View File

@@ -1,18 +1,60 @@
#![cfg_attr(target_arch = "riscv32", no_std, no_main)]
use nexus_rt::println;
extern crate alloc;
use alloc::vec::Vec;
use nexus_rt::{read_private_input, write_public_output};
use serde::{Deserialize, Serialize};
#[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);
fn main() {
let input_bytes: Vec<u8> = read_private_input().expect("failed to read input");
// Deserialize the first input (Vec<u8>)
let (bytes, remaining): (Vec<u8>, &[u8]) =
postcard::take_from_bytes(&input_bytes).expect("failed to deserialize bytes");
// Deserialize the second input (BasicStruct)
let basic_struct: BasicStruct =
postcard::from_bytes(remaining).expect("failed to deserialize struct");
// Check `bytes` length is as expected.
assert_eq!(bytes.len(), BYTES_LENGTH);
// Do some computation on `bytes` and `basic_struct`.
let rev_bytes: Vec<u8> = bytes.iter().rev().copied().collect();
let basic_struct_output = basic_struct.output();
// Write `rev_bytes` and `basic_struct_output`
let mut output_bytes = Vec::new();
output_bytes.extend_from_slice(&rev_bytes);
output_bytes.extend_from_slice(&postcard::to_allocvec(&basic_struct_output).unwrap());
write_public_output(&output_bytes).expect("failed to write output");
}
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 1,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
// Copied from test_utils
// test_utils is not used due to no_std conflicts with sha2 dependency.
const BYTES_LENGTH: usize = 32;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BasicStruct {
pub a: u8,
pub b: u16,
pub c: u32,
pub d: u64,
pub e: Vec<u8>,
}
impl BasicStruct {
/// Performs some computation (Wrapping add all fields by 1).
pub fn output(&self) -> Self {
Self {
a: self.a.wrapping_add(1),
b: self.b.wrapping_add(1),
c: self.c.wrapping_add(1),
d: self.d.wrapping_add(1),
e: self.e.iter().map(|byte| byte.wrapping_add(1)).collect(),
}
}
}

View File

@@ -0,0 +1,15 @@
[package]
name = "ere-nexus-guest-fib"
version = "0.1.0"
edition = "2021"
[dependencies]
nexus-rt = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "v0.3.4" }
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
postcard = { version = "1.0", default-features = false, features = ["alloc"] }
# Generated by cargo-nexus, do not remove!
#
[features]
cycles = [] # Enable cycle counting for run command
[workspace]

View File

@@ -0,0 +1,50 @@
#![cfg_attr(target_arch = "riscv32", no_std, no_main)]
extern crate alloc;
use alloc::vec::Vec;
use nexus_rt::{read_private_input, write_public_output};
use postcard;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct FibInput {
n: u32,
}
#[nexus_rt::main]
fn main() {
let input_bytes: Vec<u8> = read_private_input().expect("failed to read input");
// Deserialize FibInput from the postcard bytes
let fib_input: FibInput =
postcard::from_bytes(&input_bytes).expect("failed to deserialize input");
let n = fib_input.n;
let result = fibonacci(n);
// Serialize result to bytes before writing
let output_bytes = postcard::to_allocvec(&result).expect("failed to serialize output");
write_public_output(&output_bytes).expect("failed to write output");
}
fn fibonacci(n: u32) -> u32 {
if n == 0 {
return 0;
}
if n == 1 {
return 1;
}
let mut a = 0u32;
let mut b = 1u32;
for _ in 2..=n {
let temp = a.wrapping_add(b);
a = b;
b = temp;
}
b
}