feat: ere-miden (#136)

Co-authored-by: Han <tinghan0110@gmail.com>
This commit is contained in:
Brechy
2025-09-17 00:14:01 -03:00
committed by GitHub
parent 2f979dbd01
commit 1cb4e9238e
15 changed files with 1290 additions and 28 deletions

20
.github/workflows/test-zkvm-miden.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Test and clippy Miden
on:
push:
branches:
- master
pull_request:
jobs:
test:
uses: ./.github/workflows/test-zkvm.yml
permissions:
contents: read
packages: write
with:
zkvm: miden
toolchain: 1.88.0
test_ere_dockerized: false
default_features: true
test_options: ""

746
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ members = [
"crates/test-utils",
# zkVMs
"crates/ere-jolt",
"crates/ere-miden",
"crates/ere-nexus",
"crates/ere-openvm",
"crates/ere-pico",
@@ -57,6 +58,14 @@ jolt = { git = "https://github.com/a16z/jolt.git", rev = "55b9830a3944dde55d33a5
jolt-core = { git = "https://github.com/a16z/jolt.git", rev = "55b9830a3944dde55d33a55c42522b81dd49f87a" }
jolt-sdk = { git = "https://github.com/a16z/jolt.git", rev = "55b9830a3944dde55d33a55c42522b81dd49f87a" }
# Miden dependencies
miden-assembly = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag = "v0.17.1" }
miden-core = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag = "v0.17.1" }
miden-processor = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag = "v0.17.1" }
miden-prover = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag = "v0.17.1" }
miden-stdlib = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag = "v0.17.1" }
miden-verifier = { git = "https://github.com/0xPolygonMiden/miden-vm.git", tag = "v0.17.1" }
# Nexus dependencies
nexus-sdk = { git = "https://github.com/nexus-xyz/nexus-zkvm.git", tag = "v0.3.4" }
@@ -90,6 +99,7 @@ test-utils = { path = "crates/test-utils" }
ere-cli = { path = "crates/ere-cli", default-features = false }
ere-dockerized = { path = "crates/ere-dockerized" }
ere-jolt = { path = "crates/ere-jolt", default-features = false }
ere-miden = { path = "crates/ere-miden", default-features = false }
ere-nexus = { path = "crates/ere-nexus", default-features = false }
ere-openvm = { path = "crates/ere-openvm", default-features = false }
ere-pico = { path = "crates/ere-pico", default-features = false }

View File

@@ -45,6 +45,7 @@
- Pico
- Zisk
- Nexus
- Miden
## Quick Start

View File

@@ -14,6 +14,7 @@ tracing-subscriber = { workspace = true, features = ["env-filter"], optional = t
# Local dependencies
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 }
@@ -34,6 +35,7 @@ cli = ["dep:clap", "dep:tracing-subscriber"]
# zkVM
jolt = ["dep:ere-jolt"]
miden = ["dep:ere-miden"]
nexus = ["dep:ere-nexus"]
openvm = ["dep:ere-openvm"]
pico = ["dep:ere-pico"]

View File

@@ -10,6 +10,7 @@ const _: () = {
if cfg!(feature = "cli") {
assert!(
(cfg!(feature = "jolt") as u8
+ cfg!(feature = "miden") as u8
+ cfg!(feature = "nexus") as u8
+ cfg!(feature = "openvm") as u8
+ cfg!(feature = "pico") as u8
@@ -136,6 +137,9 @@ fn compile(guest_path: PathBuf, program_path: PathBuf) -> Result<(), Error> {
#[cfg(feature = "jolt")]
let program = ere_jolt::JOLT_TARGET.compile(&guest_path);
#[cfg(feature = "miden")]
let program = ere_miden::MIDEN_TARGET.compile(&guest_path);
#[cfg(feature = "nexus")]
let program = ere_nexus::NEXUS_TARGET.compile(&guest_path);
@@ -232,6 +236,9 @@ fn construct_zkvm(program_path: PathBuf, resource: ProverResourceType) -> Result
#[cfg(feature = "jolt")]
let zkvm = ere_jolt::EreJolt::new(program, resource);
#[cfg(feature = "miden")]
let zkvm = ere_miden::EreMiden::new(program, resource);
#[cfg(feature = "nexus")]
let zkvm = Ok::<_, Error>(ere_nexus::EreNexus::new(program, resource));

View File

@@ -0,0 +1,29 @@
[package]
name = "ere-miden"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
bincode = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
zkvm-interface = { 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-stdlib = { workspace = true, features = ["std"] }
miden-verifier = { workspace = true }
[dev-dependencies]
test-utils = { workspace = true, features = ["host"] }
[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("miden", "miden-core");
}

View File

@@ -0,0 +1,64 @@
use crate::{
MIDEN_TARGET, MidenProgram,
error::{CompileError, MidenError},
};
use miden_assembly::Assembler;
use miden_core::utils::Serializable;
use miden_stdlib::StdLibrary;
use std::{fs, path::Path};
use zkvm_interface::Compiler;
impl Compiler for MIDEN_TARGET {
type Error = MidenError;
type Program = MidenProgram;
fn compile(&self, guest_directory: &Path) -> Result<Self::Program, Self::Error> {
let dir_name = guest_directory
.file_name()
.and_then(|name| name.to_str())
.ok_or(CompileError::InvalidProgramPath)?;
let entrypoint = format!("{dir_name}.masm");
let main_path = guest_directory.join(&entrypoint);
if !main_path.exists() {
return Err(CompileError::MissingEntrypoint {
program_dir: guest_directory.display().to_string(),
entrypoint,
}
.into());
}
// Compile using Miden assembler
let mut assembler = Assembler::default().with_debug_mode(true);
assembler
.link_dynamic_library(StdLibrary::default())
.map_err(|e| CompileError::LoadStdLibrary(e.to_string()))?;
let source = fs::read_to_string(&main_path).map_err(|e| CompileError::ReadSource {
path: main_path.clone(),
source: e,
})?;
let program = assembler
.assemble_program(&source)
.map_err(|e| CompileError::AssemblyCompilation(e.to_string()))?;
Ok(MidenProgram {
program_bytes: program.to_bytes(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_utils::host::testing_guest_directory;
use zkvm_interface::Compiler;
#[test]
fn test_compile() {
let guest_directory = testing_guest_directory("miden", "fib");
let program = MIDEN_TARGET.compile(&guest_directory).unwrap();
assert!(!program.program_bytes.is_empty());
}
}

View File

@@ -0,0 +1,77 @@
use miden_core::utils::DeserializationError;
use miden_processor::ExecutionError;
use miden_verifier::VerificationError;
use std::path::PathBuf;
use thiserror::Error;
use zkvm_interface::zkVMError;
impl From<MidenError> for zkVMError {
fn from(value: MidenError) -> Self {
zkVMError::Other(Box::new(value))
}
}
#[derive(Debug, Error)]
pub enum MidenError {
#[error(transparent)]
Compile(#[from] CompileError),
#[error(transparent)]
Execute(#[from] ExecuteError),
#[error(transparent)]
Prove(#[from] ProveError),
#[error(transparent)]
Verify(#[from] VerifyError),
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error("Invalid program directory name")]
InvalidProgramPath,
#[error("Entrypoint '{entrypoint}' not found in {program_dir}")]
MissingEntrypoint {
program_dir: String,
entrypoint: String,
},
#[error("Failed to read assembly source at {path}")]
ReadSource {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Miden assembly compilation failed: {0}")]
AssemblyCompilation(String),
#[error("Failed to load Miden standard library: {0}")]
LoadStdLibrary(String),
}
#[derive(Debug, Error)]
pub enum ExecuteError {
#[error("Miden execution failed")]
Execution(#[from] ExecutionError),
#[error("Invalid input format: {0}")]
InvalidInput(String),
#[error("Serialization failed")]
Serialization(#[from] bincode::Error),
#[error("Failed to deserialize Miden program")]
ProgramDeserialization(#[from] DeserializationError),
}
#[derive(Debug, Error)]
pub enum ProveError {
#[error("Miden proving failed")]
Proving(#[from] ExecutionError),
#[error("Invalid input format: {0}")]
InvalidInput(String),
#[error("Serialization failed")]
Serialization(#[from] bincode::Error),
}
#[derive(Debug, Error)]
pub enum VerifyError {
#[error("Miden verification failed")]
Verification(#[from] VerificationError),
#[error("Proof or associated data deserialization failed")]
MidenDeserialization(#[from] DeserializationError),
#[error("Proof bundle deserialization failed")]
BundleDeserialization(#[from] bincode::Error),
}

View File

@@ -0,0 +1,52 @@
use crate::error::{ExecuteError, MidenError};
use miden_processor::{AdviceInputs, StackInputs, StackOutputs};
use zkvm_interface::{Input, InputItem, PublicValues};
/// Returns Miden compatible inputs from `zkvm_interface::Input`.
///
/// All inputs are serialized and concatenated, then placed onto the advice tape.
/// The stack is left empty.
pub fn generate_miden_inputs(inputs: &Input) -> Result<(StackInputs, AdviceInputs), MidenError> {
let mut all_bytes = Vec::new();
for item in inputs.iter() {
match item {
InputItem::Object(obj) => {
bincode::serialize_into(&mut all_bytes, &**obj)
.map_err(ExecuteError::Serialization)?;
}
InputItem::SerializedObject(bytes) | InputItem::Bytes(bytes) => {
all_bytes.extend_from_slice(bytes);
}
}
}
// Convert the byte stream into u64 words for the Miden VM.
let advice_words: Vec<u64> = {
let mut words: Vec<u64> = all_bytes
.chunks_exact(8)
.map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
.collect();
let remainder = all_bytes.chunks_exact(8).remainder();
if !remainder.is_empty() {
let mut last_chunk = [0u8; 8];
last_chunk[..remainder.len()].copy_from_slice(remainder);
words.push(u64::from_le_bytes(last_chunk));
}
words
};
let advice_inputs = AdviceInputs::default()
.with_stack_values(advice_words)
.map_err(|e| ExecuteError::InvalidInput(e.to_string()))?;
Ok((StackInputs::default(), advice_inputs))
}
// Convert Miden stack outputs to public values
pub fn outputs_to_public_values(outputs: &StackOutputs) -> Result<PublicValues, bincode::Error> {
let output_ints: Vec<u64> = outputs.iter().map(|f| f.as_int()).collect();
bincode::serialize(&output_ints)
}

245
crates/ere-miden/src/lib.rs Normal file
View File

@@ -0,0 +1,245 @@
pub mod compile;
pub mod error;
pub mod io;
use self::error::{ExecuteError, MidenError, VerifyError};
use self::io::{generate_miden_inputs, outputs_to_public_values};
use miden_core::{
Program,
utils::{Deserializable, Serializable},
};
use miden_processor::{
DefaultHost, ExecutionOptions, ProgramInfo, StackInputs, StackOutputs, execute as miden_execute,
};
use miden_prover::{ExecutionProof, ProvingOptions, prove as miden_prove};
use miden_stdlib::StdLibrary;
use miden_verifier::verify as miden_verify;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::{env, io::Read, time::Instant};
use zkvm_interface::{
Input, ProgramExecutionReport, ProgramProvingReport, Proof, ProverResourceType, PublicValues,
zkVM, zkVMError,
};
include!(concat!(env!("OUT_DIR"), "/name_and_sdk_version.rs"));
#[allow(non_camel_case_types)]
pub struct MIDEN_TARGET;
#[derive(Clone, Serialize, Deserialize)]
pub struct MidenProgram {
pub program_bytes: Vec<u8>,
}
#[derive(Serialize, Deserialize)]
struct MidenProofBundle {
stack_inputs: Vec<u8>,
stack_outputs: Vec<u8>,
proof: Vec<u8>,
}
pub struct EreMiden {
program: Program,
}
impl EreMiden {
pub fn new(program: MidenProgram, _resource: ProverResourceType) -> Result<Self, MidenError> {
let program = Program::read_from_bytes(&program.program_bytes)
.map_err(ExecuteError::ProgramDeserialization)
.map_err(MidenError::Execute)?;
Ok(Self { program })
}
fn setup_host() -> Result<DefaultHost, MidenError> {
let mut host = DefaultHost::default();
host.load_library(&StdLibrary::default())
.map_err(ExecuteError::Execution)
.map_err(MidenError::Execute)?;
Ok(host)
}
}
impl zkVM for EreMiden {
fn execute(&self, inputs: &Input) -> Result<(PublicValues, ProgramExecutionReport), zkVMError> {
let (stack_inputs, advice_inputs) = generate_miden_inputs(inputs)?;
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(|e| MidenError::Execute(e.into()))?;
let public_values = outputs_to_public_values(trace.stack_outputs())
.map_err(|e| MidenError::Execute(e.into()))?;
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,
inputs: &Input,
) -> Result<(PublicValues, Proof, ProgramProvingReport), zkVMError> {
let (stack_inputs, advice_inputs) = generate_miden_inputs(inputs)?;
let mut host = Self::setup_host()?;
let start = Instant::now();
let proving_options = ProvingOptions::with_96_bit_security(env::var("MIDEN_DEBUG").is_ok());
let (stack_outputs, proof) = miden_prove(
&self.program,
stack_inputs.clone(),
advice_inputs,
&mut host,
proving_options,
)
.map_err(|e| MidenError::Prove(e.into()))?;
let public_values =
outputs_to_public_values(&stack_outputs).map_err(|e| MidenError::Prove(e.into()))?;
let bundle = MidenProofBundle {
stack_inputs: stack_inputs.to_bytes(),
stack_outputs: stack_outputs.to_bytes(),
proof: proof.to_bytes(),
};
let proof_bytes = bincode::serialize(&bundle).map_err(|e| MidenError::Prove(e.into()))?;
Ok((
public_values,
proof_bytes,
ProgramProvingReport::new(start.elapsed()),
))
}
fn verify(&self, proof: &[u8]) -> Result<PublicValues, zkVMError> {
let bundle: MidenProofBundle = bincode::deserialize(proof)
.map_err(|e| MidenError::Verify(VerifyError::BundleDeserialization(e)))?;
let program_info: ProgramInfo = self.program.clone().into();
let stack_inputs = StackInputs::read_from_bytes(&bundle.stack_inputs)
.map_err(|e| MidenError::Verify(VerifyError::MidenDeserialization(e)))?;
let stack_outputs = StackOutputs::read_from_bytes(&bundle.stack_outputs)
.map_err(|e| MidenError::Verify(VerifyError::MidenDeserialization(e)))?;
let execution_proof = ExecutionProof::from_bytes(&bundle.proof)
.map_err(|e| MidenError::Verify(VerifyError::MidenDeserialization(e)))?;
miden_verify(
program_info,
stack_inputs,
stack_outputs.clone(),
execution_proof,
)
.map_err(|e| MidenError::Verify(e.into()))?;
Ok(outputs_to_public_values(&stack_outputs)
.map_err(|e| MidenError::Verify(VerifyError::BundleDeserialization(e)))?)
}
fn deserialize_from<R: Read, T: DeserializeOwned>(&self, reader: R) -> Result<T, zkVMError> {
bincode::deserialize_from(reader).map_err(|e| MidenError::Execute(e.into()).into())
}
fn name(&self) -> &'static str {
NAME
}
fn sdk_version(&self) -> &'static str {
SDK_VERSION
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_utils::host::testing_guest_directory;
use zkvm_interface::Compiler;
fn load_miden_program(guest_name: &str) -> MidenProgram {
MIDEN_TARGET
.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 = 2518446814u64;
let const_b = 1949327098u64;
let expected_sum = const_a + const_b;
let mut inputs = Input::new();
inputs.write(const_a);
inputs.write(const_b);
// Prove
let (prover_public_values, proof, _) = zkvm.prove(&inputs).unwrap();
// Verify
let verifier_public_values = zkvm.verify(&proof).unwrap();
assert_eq!(prover_public_values, verifier_public_values,);
// Assert output
let output: Vec<u64> = zkvm
.deserialize_from(verifier_public_values.as_slice())
.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 = 50u64;
let expected_fib = 12_586_269_025u64;
let mut inputs = Input::new();
inputs.write(0u64);
inputs.write(1u64);
inputs.write(n_iterations);
// Prove
let (prover_public_values, proof, _) = zkvm.prove(&inputs).unwrap();
// Verify
let verifier_public_values = zkvm.verify(&proof).unwrap();
assert_eq!(prover_public_values, verifier_public_values,);
// Assert output
let output: Vec<u64> = zkvm
.deserialize_from(verifier_public_values.as_slice())
.unwrap();
assert_eq!(output[0], expected_fib);
}
#[test]
fn test_invalid_inputs() {
let program = load_miden_program("add");
let zkvm = EreMiden::new(program, ProverResourceType::Cpu).unwrap();
let empty_inputs = Input::new();
assert!(zkvm.execute(&empty_inputs).is_err());
let mut insufficient_inputs = Input::new();
insufficient_inputs.write(5u64);
assert!(zkvm.execute(&insufficient_inputs).is_err());
}
}

19
docker/miden/Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
ARG BASE_IMAGE_TAG=ere-base:latest
FROM ${BASE_IMAGE_TAG}
# Set default toolchain to MSRV of Miden
RUN rustup default 1.88.0
# Miden Configuration
ENV MIDEN_VERSION="v0.17.1" \
MIDEN_TOOLCHAIN_VERSION="1.88.0"
# Miden CLI Installation
# COPY --chmod=755 scripts/sdk_installers/install_miden_sdk.sh /tmp/
# RUN /tmp/install_miden_sdk.sh && rm /tmp/install_miden_sdk.sh
# Verify
# RUN miden-vm --version
CMD ["/bin/bash"]

12
tests/miden/add/add.masm Normal file
View File

@@ -0,0 +1,12 @@
# Adds two numbers from advice stack
# Input: advice_stack contains second_number, first_number
use.std::sys
begin
adv_push.1
adv_push.1
add
exec.sys::truncate_stack
end

29
tests/miden/fib/fib.masm Normal file
View File

@@ -0,0 +1,29 @@
# Fibonacci
# Reads three u64 values from advice stack: fib_a, fib_b, n
# Returns nth fibonacci number
use.std::sys
begin
# Read inputs from advice stack
adv_push.1 # fib_a
adv_push.1 # fib_b
adv_push.1 # n
# Compute fibonacci
dup neq.0
while.true
movdn.2
dup.1
add
swap
movup.2
sub.1
dup neq.0
end
# Drop counter and one of the fibonacci values
drop
drop
exec.sys::truncate_stack
end