From b8852573953a901567fb83991129efee2a693975 Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 30 Jul 2026 09:29:01 +0700 Subject: [PATCH 1/7] add blake3 in zkm-zkvm feature. --- Cargo.lock | 15 ++++ Cargo.toml | 1 + crates/build/src/build.rs | 21 +++-- crates/zkvm/entrypoint/Cargo.toml | 4 + crates/zkvm/entrypoint/src/lib.rs | 20 ++++- crates/zkvm/entrypoint/src/syscalls/halt.rs | 18 +++-- crates/zkvm/entrypoint/src/syscalls/io.rs | 5 +- examples/Cargo.lock | 60 ++++++++++++-- examples/Cargo.toml | 6 ++ examples/imm-wrap-vk-add/guest/Cargo.toml | 13 +++ examples/imm-wrap-vk-add/guest/src/main.rs | 17 ++++ examples/imm-wrap-vk-add/host/Cargo.toml | 20 +++++ examples/imm-wrap-vk-add/host/build.rs | 3 + examples/imm-wrap-vk-add/host/src/main.rs | 87 +++++++++++++++++++++ 14 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 examples/imm-wrap-vk-add/guest/Cargo.toml create mode 100644 examples/imm-wrap-vk-add/guest/src/main.rs create mode 100644 examples/imm-wrap-vk-add/host/Cargo.toml create mode 100644 examples/imm-wrap-vk-add/host/build.rs create mode 100644 examples/imm-wrap-vk-add/host/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 547d2b9d3..a7807f37a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,6 +878,20 @@ dependencies = [ "constant_time_eq 0.4.2", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -8274,6 +8288,7 @@ name = "zkm-zkvm" version = "1.2.7" dependencies = [ "bincode", + "blake3", "cfg-if", "critical-section", "embedded-alloc", diff --git a/Cargo.toml b/Cargo.toml index 4cdb9955d..9287466c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ serde_json = "1.0.132" rand = "0.8.5" sha2 = { version = "0.10.8", default-features = false } +blake3 = "" anyhow = "1.0.75" zkm-recursion-derive = { path = "crates/recursion/derive", default-features = false } diff --git a/crates/build/src/build.rs b/crates/build/src/build.rs index 17111120c..bb618cd60 100644 --- a/crates/build/src/build.rs +++ b/crates/build/src/build.rs @@ -99,11 +99,11 @@ pub(crate) fn build_program_internal(path: &str, args: Option) { } // Build the program with the given arguments. - let path_output = if let Some(args) = args { - execute_build_program(&args, Some(program_dir.to_path_buf())) - } else { - execute_build_program(&BuildArgs::default(), Some(program_dir.to_path_buf())) - }; + let mut args = args.unwrap_or_default(); + if imm_wrap_vk_mode() { + args.features.push("imm-wrap-vk".to_string()); + } + let path_output = execute_build_program(&args, Some(program_dir.to_path_buf())); if let Err(err) = path_output { panic!("Failed to build Ziren program: {err}."); } @@ -111,6 +111,17 @@ pub(crate) fn build_program_internal(path: &str, args: Option) { println!("cargo:warning={} built at {}", root_package_name, current_datetime()); } +/// Returns true if the `ZKM_IMM_WRAP_VK` environment variable is enabled, mirroring +/// `zkm_recursion_core::stark::zkm_imm_wrap_vk_mode`'s environment check. When enabled, the guest +/// program is built with the `imm-wrap-vk` feature, which the guest program's `Cargo.toml` is +/// expected to forward to `zkm-zkvm/imm-wrap-vk` so it hashes public values with BLAKE3 instead of +/// SHA256, matching the Groth16 wrap circuit's immutable-vk mode. +fn imm_wrap_vk_mode() -> bool { + std::env::var("ZKM_IMM_WRAP_VK") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false) +} + /// Collects the list of targets that would be built and their output ELF file paths. pub fn generate_elf_paths( metadata: &cargo_metadata::Metadata, diff --git a/crates/zkvm/entrypoint/Cargo.toml b/crates/zkvm/entrypoint/Cargo.toml index 360d4b978..350ee824f 100644 --- a/crates/zkvm/entrypoint/Cargo.toml +++ b/crates/zkvm/entrypoint/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0.201", features = ["derive"] } libm = { version = "0.2.8", optional = true } lazy_static = "1.4.0" sha2 = { version = "0.10.8", default-features = false } +blake3 = { version = "1.8.5", default-features = false, optional = true } zkm-primitives = { workspace = true } p3-koala-bear = { workspace = true, optional = true } p3-field = { workspace = true, optional = true } @@ -29,3 +30,6 @@ verify = [ "dep:p3-field", "zkm-lib/verify", ] +# Use BLAKE3 instead of SHA256 to hash the public values, matching the Groth16 wrap circuit's +# `imm-wrap-vk` mode. See `zkm-recursion-core`'s `zkm_imm_wrap_vk_mode` for the full picture. +imm-wrap-vk = ["dep:blake3"] diff --git a/crates/zkvm/entrypoint/src/lib.rs b/crates/zkvm/entrypoint/src/lib.rs index d55195538..68e5160a7 100644 --- a/crates/zkvm/entrypoint/src/lib.rs +++ b/crates/zkvm/entrypoint/src/lib.rs @@ -149,7 +149,6 @@ mod zkvm { use cfg_if::cfg_if; use getrandom::{register_custom_getrandom, Error}; - use sha2::{Digest, Sha256}; cfg_if! { if #[cfg(feature = "verify")] { @@ -160,7 +159,16 @@ mod zkvm { } } - pub static mut PUBLIC_VALUES_HASHER: Option = None; + // In `imm-wrap-vk` mode, the public values are hashed with BLAKE3 instead of SHA256 + cfg_if! { + if #[cfg(feature = "imm-wrap-vk")] { + pub static mut PUBLIC_VALUES_HASHER: Option = None; + } else { + use sha2::{Digest, Sha256}; + + pub static mut PUBLIC_VALUES_HASHER: Option = None; + } + } #[no_mangle] fn _main() { @@ -168,7 +176,13 @@ mod zkvm { crate::allocators::init(); unsafe { - PUBLIC_VALUES_HASHER = Some(Sha256::new()); + cfg_if! { + if #[cfg(feature = "imm-wrap-vk")] { + PUBLIC_VALUES_HASHER = Some(blake3::Hasher::new()); + } else { + PUBLIC_VALUES_HASHER = Some(Sha256::new()); + } + } #[cfg(feature = "verify")] { DEFERRED_PROOFS_DIGEST = Some([KoalaBear::ZERO; 8]); diff --git a/crates/zkvm/entrypoint/src/syscalls/halt.rs b/crates/zkvm/entrypoint/src/syscalls/halt.rs index ae50c724b..ead039d09 100644 --- a/crates/zkvm/entrypoint/src/syscalls/halt.rs +++ b/crates/zkvm/entrypoint/src/syscalls/halt.rs @@ -1,12 +1,15 @@ cfg_if::cfg_if! { if #[cfg(target_os = "zkvm")] { use core::arch::asm; - use sha2::Digest; use crate::zkvm; use crate::{PV_DIGEST_NUM_WORDS, POSEIDON_NUM_WORDS}; } } +// `blake3::Hasher::finalize` is an inherent method; `Sha256`'s comes from this trait. +#[cfg(all(target_os = "zkvm", not(feature = "imm-wrap-vk")))] +use sha2::Digest; + cfg_if::cfg_if! { if #[cfg(all(target_os = "zkvm", feature = "verify"))] { use p3_field::PrimeField32; @@ -23,10 +26,15 @@ pub extern "C" fn syscall_halt(exit_code: u8) -> ! { unsafe { // When we halt, we retrieve the public values finalized digest. This is the hash of all // the bytes written to the public values fd. - let pv_digest_bytes = - core::mem::take(&mut *core::ptr::addr_of_mut!(zkvm::PUBLIC_VALUES_HASHER)) - .unwrap() - .finalize(); + let hasher = core::mem::take(&mut *core::ptr::addr_of_mut!(zkvm::PUBLIC_VALUES_HASHER)) + .unwrap(); + cfg_if::cfg_if! { + if #[cfg(feature = "imm-wrap-vk")] { + let pv_digest_bytes: [u8; 32] = *hasher.finalize().as_bytes(); + } else { + let pv_digest_bytes = hasher.finalize(); + } + } // For each digest word, call COMMIT ecall. In the runtime, this will store the digest // words into the runtime's execution record's public values digest. In the AIR, it diff --git a/crates/zkvm/entrypoint/src/syscalls/io.rs b/crates/zkvm/entrypoint/src/syscalls/io.rs index e7040c103..7c95ddc0a 100644 --- a/crates/zkvm/entrypoint/src/syscalls/io.rs +++ b/crates/zkvm/entrypoint/src/syscalls/io.rs @@ -2,11 +2,14 @@ cfg_if::cfg_if! { if #[cfg(target_os = "zkvm")] { use core::arch::asm; use crate::zkvm; - use sha2::digest::Update; use zkm_primitives::consts::fd::FD_PUBLIC_VALUES; } } +// `blake3::Hasher::update` is an inherent method; `Sha256`'s comes from this trait. +#[cfg(all(target_os = "zkvm", not(feature = "imm-wrap-vk")))] +use sha2::digest::Update; + /// Write `nbytes` of data to the prover to a given file descriptor `fd` from `write_buf`. #[allow(unused_variables)] #[no_mangle] diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 886832859..41a2030c7 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -844,6 +844,20 @@ dependencies = [ "constant_time_eq 0.4.2", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.4.2", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1463,7 +1477,7 @@ dependencies = [ "rustc_version 0.4.1", "subtle", "zeroize", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -3443,6 +3457,25 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imm-wrap-vk-add" +version = "1.1.0" +dependencies = [ + "zkm-zkvm", +] + +[[package]] +name = "imm-wrap-vk-add-host" +version = "1.1.0" +dependencies = [ + "blake3", + "p3-field", + "sha2 0.10.8", + "zkm-build", + "zkm-sdk", + "zkm-stark", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -3745,7 +3778,7 @@ dependencies = [ "once_cell", "sha2 0.10.8", "signature", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -4453,7 +4486,7 @@ dependencies = [ "hex", "primeorder", "sha2 0.10.8", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -5861,7 +5894,7 @@ dependencies = [ "spki 0.7.3", "subtle", "zeroize", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -6760,7 +6793,7 @@ dependencies = [ "num-bigint 0.4.6", "rand 0.8.5", "rustc-hex", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -8753,10 +8786,22 @@ dependencies = [ "zkm-primitives 1.2.7", ] +[[package]] +name = "zkm-lib" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren#e6945a76b084e87570b7be55e4479ce98603f43a" +dependencies = [ + "bincode", + "cfg-if", + "elliptic-curve", + "serde", + "sha2 0.10.8", + "zkm-primitives 1.2.7", +] + [[package]] name = "zkm-primitives" -version = "1.2.4" -source = "git+https://github.com/ProjectZKM/Ziren#1d43121312d4b93c0989984bf0c7ab77d9a0ce04" +version = "1.2.5" dependencies = [ "bincode", "hex", @@ -9064,6 +9109,7 @@ name = "zkm-zkvm" version = "1.2.7" dependencies = [ "bincode", + "blake3", "cfg-if", "critical-section", "embedded-alloc", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f5e00ec45..989738863 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -41,6 +41,8 @@ members = [ "large-sum/host", "simple-go/host", "keeper/host", + "imm-wrap-vk-add/guest", + "imm-wrap-vk-add/host", ] resolver = "2" @@ -64,6 +66,7 @@ zkm-recursion-derive = { path = "../crates/recursion/derive", default-features = zkm-recursion-gnark-ffi = { path = "../crates/recursion/gnark-ffi", default-features = false } zkm-recursion-circuit = { path = "../crates/recursion/circuit", default-features = false } zkm-sdk = { path = "../crates/sdk" } +zkm-stark = { path = "../crates/stark" } zkm-lib = { path = "../crates/zkvm/lib", default-features = false } zkm-zkvm = { path = "../crates/zkvm/entrypoint", default-features = false } @@ -71,6 +74,9 @@ zkm-zkvm = { path = "../crates/zkvm/entrypoint", default-features = false } serde = "1.0.204" serde_json = "1.0.132" tracing = "0.1.40" +p3-field = { git = "https://github.com/ProjectZKM/Plonky3" } +blake3 = "1.8.5" +sha2 = "0.10.8" [patch.crates-io] curve25519-dalek = { git = "https://github.com/ziren-patches/curve25519-dalek", branch = "patch-4.1.3" } diff --git a/examples/imm-wrap-vk-add/guest/Cargo.toml b/examples/imm-wrap-vk-add/guest/Cargo.toml new file mode 100644 index 000000000..6a6d783cb --- /dev/null +++ b/examples/imm-wrap-vk-add/guest/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "imm-wrap-vk-add" +version = "1.1.0" +edition = "2021" +publish = false + +[dependencies] +zkm-zkvm = { path = "../../../crates/zkvm/entrypoint", features = ["embedded"] } + +[features] +# Forwarded so `ZKM_IMM_WRAP_VK=1` (handled by `zkm-build`) can switch this guest to hash its +# public values with BLAKE3 instead of SHA256. +imm-wrap-vk = ["zkm-zkvm/imm-wrap-vk"] \ No newline at end of file diff --git a/examples/imm-wrap-vk-add/guest/src/main.rs b/examples/imm-wrap-vk-add/guest/src/main.rs new file mode 100644 index 000000000..722d5fd54 --- /dev/null +++ b/examples/imm-wrap-vk-add/guest/src/main.rs @@ -0,0 +1,17 @@ +//! A minimal program that adds two numbers, used to exercise the SHA256 / BLAKE3 public-values +//! hashing switch controlled by the `imm-wrap-vk` feature (see `ZKM_IMM_WRAP_VK`). + +#![no_std] +#![no_main] +zkm_zkvm::entrypoint!(main); + +pub fn main() { + let a = zkm_zkvm::io::read::(); + let b = zkm_zkvm::io::read::(); + + let sum = a + b; + + zkm_zkvm::io::commit(&a); + zkm_zkvm::io::commit(&b); + zkm_zkvm::io::commit(&sum); +} \ No newline at end of file diff --git a/examples/imm-wrap-vk-add/host/Cargo.toml b/examples/imm-wrap-vk-add/host/Cargo.toml new file mode 100644 index 000000000..a7845a5e9 --- /dev/null +++ b/examples/imm-wrap-vk-add/host/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "imm-wrap-vk-add-host" +version = { workspace = true } +edition = { workspace = true } +default-run = "imm-wrap-vk-add-host" +publish = false + +[dependencies] +zkm-sdk = { workspace = true } +zkm-stark = { workspace = true } +p3-field = { workspace = true } +blake3 = { workspace = true } +sha2 = { workspace = true } + +[build-dependencies] +zkm-build = { workspace = true } + +[[bin]] +name = "imm-wrap-vk-add-host" +path = "src/main.rs" \ No newline at end of file diff --git a/examples/imm-wrap-vk-add/host/build.rs b/examples/imm-wrap-vk-add/host/build.rs new file mode 100644 index 000000000..032c1d8f9 --- /dev/null +++ b/examples/imm-wrap-vk-add/host/build.rs @@ -0,0 +1,3 @@ +fn main() { + zkm_build::build_program("../guest"); +} \ No newline at end of file diff --git a/examples/imm-wrap-vk-add/host/src/main.rs b/examples/imm-wrap-vk-add/host/src/main.rs new file mode 100644 index 000000000..f86197fd1 --- /dev/null +++ b/examples/imm-wrap-vk-add/host/src/main.rs @@ -0,0 +1,87 @@ +use core::borrow::Borrow; + +use p3_field::PrimeField32; +use sha2::{Digest, Sha256}; +use zkm_sdk::{include_elf, utils, ProverClient, ZKMProof, ZKMStdin}; +use zkm_stark::{air::PublicValues, Word}; + +/// The ELF we want to execute inside the zkVM. +/// +/// Build it in BLAKE3 mode with `ZKM_IMM_WRAP_VK=1 cargo run --release`, or in the default +/// SHA256 mode by leaving `ZKM_IMM_WRAP_VK` unset. +const ELF: &[u8] = include_elf!("imm-wrap-vk-add"); + +fn main() { + utils::setup_logger(); + + let imm_wrap_vk_mode = std::env::var("ZKM_IMM_WRAP_VK") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + println!( + "guest built in {} mode", + if imm_wrap_vk_mode { "imm-wrap-vk (BLAKE3)" } else { "default (SHA256)" } + ); + + let a = 5u32; + let b = 7u32; + + let mut stdin = ZKMStdin::new(); + stdin.write(&a); + stdin.write(&b); + + let client = ProverClient::new(); + + let (_, report) = client.execute(ELF, &stdin).run().unwrap(); + println!("executed program with {} cycles", report.total_instruction_count()); + + // Core mode is the default proof kind, and already carries the committed public-values + // digest in the last shard's public values, so there is no need for a compressed proof here. + let (pk, vk) = client.setup(ELF); + let proof = client.prove(&pk, stdin).run().unwrap(); + println!("generated proof"); + + let mut public_values = proof.public_values.clone(); + let a_out = public_values.read::(); + let b_out = public_values.read::(); + let sum = public_values.read::(); + println!("{a_out} + {b_out} = {sum}"); + assert_eq!(sum, a + b); + + // client.verify(&proof, &vk).expect("verification failed"); + + // Pull the digest the guest actually committed to out of the last shard, and compare it + // against an independently computed hash of the raw public values, using whichever + // algorithm this guest build should have used. This checks the guest hasher itself, rather + // than relying on the host-side verification path (which does not yet branch on + // `imm-wrap-vk` mode). + let ZKMProof::Core(shard_proofs) = &proof.proof else { + panic!("expected a core proof"); + }; + let last_shard = shard_proofs.last().expect("proof has no shards"); + let proof_public_values: &PublicValues, _> = + last_shard.public_values.as_slice().borrow(); + let committed_value_digest: Vec = proof_public_values + .committed_value_digest + .iter() + .flat_map(|w| w.0.iter().map(|x| x.as_canonical_u32() as u8)) + .collect(); + + let raw_public_values = proof.public_values.as_slice(); + let expected_digest: Vec = if imm_wrap_vk_mode { + blake3::hash(raw_public_values).as_bytes().to_vec() + } else { + Sha256::digest(raw_public_values).to_vec() + }; + + assert_eq!( + committed_value_digest, expected_digest, + "committed public-values digest does not match {} of the raw public values", + if imm_wrap_vk_mode { "BLAKE3" } else { "SHA256" } + ); + println!( + "committed public-values digest matches {} of the raw public values", + if imm_wrap_vk_mode { "BLAKE3" } else { "SHA256" } + ); + + println!("successfully generated and verified proof for the program!") +} \ No newline at end of file From cac92b6a45fe698933b7c357abac9456d81e4bf7 Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 30 Jul 2026 11:12:52 +0700 Subject: [PATCH 2/7] verify blake3 pubval in imm_wrap_vk mode. --- Cargo.lock | 18 ++----------- Cargo.toml | 2 +- crates/build/src/build.rs | 5 ++++ crates/primitives/Cargo.toml | 7 +++++ crates/primitives/src/io.rs | 33 +++++++++++++++++------ crates/sdk/Cargo.toml | 1 - crates/sdk/src/utils.rs | 6 ++--- crates/verifier/src/utils.rs | 6 ++--- examples/Cargo.lock | 18 ++----------- examples/imm-wrap-vk-add/host/src/main.rs | 20 ++++++-------- 10 files changed, 56 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7807f37a..ac6df92f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,21 +113,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "alloy-signer" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f447aefab0f1c0649f71edc33f590992d4e122bc35fb9cdbbf67d4421ace85" -dependencies = [ - "alloy-primitives", - "async-trait", - "auto_impl", - "either", - "elliptic-curve", - "k256", - "thiserror 2.0.18", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -7972,6 +7957,7 @@ name = "zkm-primitives" version = "1.2.7" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -7982,6 +7968,7 @@ dependencies = [ "p3-symmetric", "serde", "sha2", + "tracing", ] [[package]] @@ -8161,7 +8148,6 @@ name = "zkm-sdk" version = "1.2.7" dependencies = [ "alloy-primitives", - "alloy-signer", "anyhow", "async-trait", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 9287466c5..f1c2b1b62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ serde_json = "1.0.132" rand = "0.8.5" sha2 = { version = "0.10.8", default-features = false } -blake3 = "" +blake3 = { version = "1.8.5", default-features = false } anyhow = "1.0.75" zkm-recursion-derive = { path = "crates/recursion/derive", default-features = false } diff --git a/crates/build/src/build.rs b/crates/build/src/build.rs index bb618cd60..ddde02481 100644 --- a/crates/build/src/build.rs +++ b/crates/build/src/build.rs @@ -81,6 +81,11 @@ pub(crate) fn build_program_internal(path: &str, args: Option) { // Activate the build command if the dependencies change. cargo_rerun_if_changed(&metadata, program_dir); + // Also rebuild if `ZKM_IMM_WRAP_VK` changes, since it decides whether the guest is built with + // the `imm-wrap-vk` feature. Cargo only tracks what's declared here, so without this the guest + // would stay stale (built in the old mode) whenever the env var changes but no source changes. + println!("cargo:rerun-if-env-changed=ZKM_IMM_WRAP_VK"); + // Check if RUSTC_WORKSPACE_WRAPPER is set to clippy-driver (i.e. if `cargo clippy` is the // current compiler). If so, don't execute `cargo ziren build` because it breaks // rust-analyzer's `cargo clippy` feature. diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 566360920..8d2db828c 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -21,3 +21,10 @@ p3-symmetric = { workspace = true } p3-monty-31 = { workspace = true } serde = { workspace = true, features = ["derive"] } sha2 = "0.10.8" +blake3 = { version = "1.8.5", default-features = false } +tracing = { workspace = true } + +[features] +# Hash `committed_values_digest` with BLAKE3 instead of SHA256, matching the guest's own switch +# (see `zkm-zkvm`'s `imm-wrap-vk` feature) and the Groth16 wrap circuit's immutable-vk mode. +imm-wrap-vk = [] diff --git a/crates/primitives/src/io.rs b/crates/primitives/src/io.rs index d30224631..9c05afcda 100644 --- a/crates/primitives/src/io.rs +++ b/crates/primitives/src/io.rs @@ -3,6 +3,20 @@ use num_bigint::BigUint; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use sha2::{Digest, Sha256}; +/// Returns true if either the `ZKM_IMM_WRAP_VK` environment variable is set or the `imm-wrap-vk` +/// feature is enabled. +/// By default, the variable is disabled. +pub fn zkm_imm_wrap_vk_mode() -> bool { + let value = std::env::var("ZKM_IMM_WRAP_VK").unwrap_or_else(|_| "false".to_string()); + let enabled = value == "1" || value.to_lowercase() == "true" || cfg!(feature = "imm-wrap-vk"); + if enabled { + tracing::warn!( + "`ZKM_IMM_WRAP_VK` environment variable or `imm-wrap-vk` feature is enabled." + ); + } + enabled +} + /// Public values for the prover. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ZKMPublicValues { @@ -53,10 +67,16 @@ impl ZKMPublicValues { } /// Hash the public values. - pub fn hash(&self) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(self.buffer.data.as_slice()); - hasher.finalize().to_vec() + /// + /// Uses BLAKE3 in `imm-wrap-vk` mode, SHA256 otherwise (see [`zkm_imm_wrap_vk_mode`]). + pub fn hash(&self) -> [u8; 32] { + if zkm_imm_wrap_vk_mode() { + *blake3::hash(self.buffer.data.as_slice()).as_bytes() + } else { + let mut hasher = Sha256::new(); + hasher.update(self.buffer.data.as_slice()); + hasher.finalize().into() + } } /// Hash the public values, mask the top 3 bits and return a BigUint. Matches the implementation @@ -67,10 +87,7 @@ impl ZKMPublicValues { /// ``` pub fn hash_bn254(&self) -> BigUint { // Hash the public values. - let mut hasher = Sha256::new(); - hasher.update(self.buffer.data.as_slice()); - let hash_result = hasher.finalize(); - let mut hash = hash_result.to_vec(); + let mut hash = self.hash(); // Mask the top 3 bits. hash[0] &= 0b00011111; diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 79980c29e..6c402346e 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -48,7 +48,6 @@ zkm-primitives = { workspace = true } zkm-cuda = { workspace = true } itertools = { workspace = true } tonic = { version = "0.8.1", features = ["tls", "tls-roots", "transport"]} -alloy-signer = { version = "1.0" } alloy-primitives = { version = "1.0", optional = true } num-bigint = "0.4.6" serde_json = "1.0.140" diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs index b02211a9d..7ead0d9f8 100644 --- a/crates/sdk/src/utils.rs +++ b/crates/sdk/src/utils.rs @@ -2,11 +2,11 @@ //! //! A collection of utilities for the Ziren SDK. -use alloy_signer::k256::sha2::{Digest, Sha256}; use p3_field::{FieldAlgebra, PrimeField}; use p3_koala_bear::KoalaBear; use zkm_core_machine::io::ZKMStdin; pub use zkm_core_machine::utils::setup_logger; +use zkm_primitives::io::ZKMPublicValues; use zkm_prover::utils::koalabear_bytes_to_bn254; use zkm_prover::{HashableKey, ZKMVerifyingKey}; @@ -52,8 +52,8 @@ pub fn compute_groth16_public_values( } pub fn committed_public_values(guest_committed_values: &[u8]) -> String { - // Calculate the SHA-256 hash of the input bytes. - let hash_result: [u8; 32] = Sha256::digest(guest_committed_values).into(); + // Hash the input bytes (BLAKE3 in `imm-wrap-vk` mode, SHA256 otherwise). + let hash_result = ZKMPublicValues::from(guest_committed_values).hash(); // Convert the [u8; 32] hash result into a [KoalaBear; 32] array. let committed_values_digest_bytes = hash_result.map(KoalaBear::from_canonical_u8); diff --git a/crates/verifier/src/utils.rs b/crates/verifier/src/utils.rs index 6fb084d24..46ea1e73d 100644 --- a/crates/verifier/src/utils.rs +++ b/crates/verifier/src/utils.rs @@ -1,17 +1,17 @@ -use sha2::{Digest, Sha256}; use substrate_bn::Fr; +use zkm_primitives::io::ZKMPublicValues; use crate::error::Error; /// Hashes the public inputs in the same format as the Plonk and Groth16 verifiers. pub fn hash_public_inputs(public_inputs: &[u8]) -> [u8; 32] { - let mut result = Sha256::digest(public_inputs); + let mut result = ZKMPublicValues::from(public_inputs).hash(); // The Plonk and Groth16 verifiers operate over a 254 bit field, so we need to zero // out the first 3 bits. The same logic happens in the Ziren Ethereum verifier contract. result[0] &= 0x1F; - result.into() + result } /// Formats the Ziren vkey hash and public inputs for use in either the Plonk or Groth16 verifier. diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 41a2030c7..27264759a 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -130,21 +130,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "alloy-signer" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f447aefab0f1c0649f71edc33f590992d4e122bc35fb9cdbbf67d4421ace85" -dependencies = [ - "alloy-primitives", - "async-trait", - "auto_impl", - "either", - "elliptic-curve", - "k256", - "thiserror 2.0.18", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -8804,6 +8789,7 @@ name = "zkm-primitives" version = "1.2.5" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -8814,6 +8800,7 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.8", + "tracing", ] [[package]] @@ -8993,7 +8980,6 @@ name = "zkm-sdk" version = "1.2.7" dependencies = [ "alloy-primitives", - "alloy-signer", "anyhow", "async-trait", "bincode", diff --git a/examples/imm-wrap-vk-add/host/src/main.rs b/examples/imm-wrap-vk-add/host/src/main.rs index f86197fd1..7c3da6efc 100644 --- a/examples/imm-wrap-vk-add/host/src/main.rs +++ b/examples/imm-wrap-vk-add/host/src/main.rs @@ -34,10 +34,8 @@ fn main() { let (_, report) = client.execute(ELF, &stdin).run().unwrap(); println!("executed program with {} cycles", report.total_instruction_count()); - // Core mode is the default proof kind, and already carries the committed public-values - // digest in the last shard's public values, so there is no need for a compressed proof here. let (pk, vk) = client.setup(ELF); - let proof = client.prove(&pk, stdin).run().unwrap(); + let proof = client.prove(&pk, stdin).compressed().run().unwrap(); println!("generated proof"); let mut public_values = proof.public_values.clone(); @@ -47,19 +45,17 @@ fn main() { println!("{a_out} + {b_out} = {sum}"); assert_eq!(sum, a + b); - // client.verify(&proof, &vk).expect("verification failed"); + client.verify(&proof, &vk).expect("verification failed"); - // Pull the digest the guest actually committed to out of the last shard, and compare it + // Also pull the digest the guest actually committed to out of the proof, and compare it // against an independently computed hash of the raw public values, using whichever - // algorithm this guest build should have used. This checks the guest hasher itself, rather - // than relying on the host-side verification path (which does not yet branch on - // `imm-wrap-vk` mode). - let ZKMProof::Core(shard_proofs) = &proof.proof else { - panic!("expected a core proof"); + // algorithm this guest build should have used. This checks the guest hasher itself directly, + // in addition to the host-side verification path above. + let ZKMProof::Compressed(compressed_proof) = &proof.proof else { + panic!("expected a compressed proof"); }; - let last_shard = shard_proofs.last().expect("proof has no shards"); let proof_public_values: &PublicValues, _> = - last_shard.public_values.as_slice().borrow(); + compressed_proof.proof.public_values.as_slice().borrow(); let committed_value_digest: Vec = proof_public_values .committed_value_digest .iter() From 1f5bb2cdd73c61ee96aff5865747d61d2904358a Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 30 Jul 2026 15:27:54 +0700 Subject: [PATCH 3/7] add tests and check mode --- Cargo.lock | 1 + crates/primitives/Cargo.toml | 3 + crates/primitives/src/io.rs | 28 +++++ crates/sdk/src/lib.rs | 110 ++++++++++++++++++ crates/sdk/src/provers/cpu.rs | 33 +++++- crates/sdk/src/provers/cuda.rs | 33 +++++- crates/test-artifacts/guests/Cargo.toml | 1 + .../guests/hello-world-imm-wrap-vk/Cargo.toml | 8 ++ .../hello-world-imm-wrap-vk/src/main.rs | 11 ++ crates/test-artifacts/src/lib.rs | 1 + examples/Cargo.lock | 3 +- examples/imm-wrap-vk-add/host/Cargo.toml | 3 +- examples/imm-wrap-vk-add/host/src/main.rs | 38 +++--- 13 files changed, 246 insertions(+), 27 deletions(-) create mode 100644 crates/test-artifacts/guests/hello-world-imm-wrap-vk/Cargo.toml create mode 100644 crates/test-artifacts/guests/hello-world-imm-wrap-vk/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index ac6df92f0..266a97d1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7967,6 +7967,7 @@ dependencies = [ "p3-poseidon2", "p3-symmetric", "serde", + "serial_test", "sha2", "tracing", ] diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 8d2db828c..8bead5aef 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -24,6 +24,9 @@ sha2 = "0.10.8" blake3 = { version = "1.8.5", default-features = false } tracing = { workspace = true } +[dev-dependencies] +serial_test = "3.1.1" + [features] # Hash `committed_values_digest` with BLAKE3 instead of SHA256, matching the guest's own switch # (see `zkm-zkvm`'s `imm-wrap-vk` feature) and the Groth16 wrap circuit's immutable-vk mode. diff --git a/crates/primitives/src/io.rs b/crates/primitives/src/io.rs index 9c05afcda..069701781 100644 --- a/crates/primitives/src/io.rs +++ b/crates/primitives/src/io.rs @@ -106,8 +106,13 @@ impl AsRef<[u8]> for ZKMPublicValues { #[cfg(test)] mod tests { use super::*; + use serial_test::serial; + + // `zkm_imm_wrap_vk_mode()` reads a process-wide env var, so any test that touches it is + // `#[serial]` to avoid racing with the others in this module. #[test] + #[serial] fn test_hash_public_values() { let test_hex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let test_bytes = hex::decode(test_hex).unwrap(); @@ -121,4 +126,27 @@ mod tests { assert_eq!(hash, expected_hash_biguint); } + + #[test] + #[serial] + fn test_hash_public_values_imm_wrap_vk() { + std::env::set_var("ZKM_IMM_WRAP_VK", "1"); + + let test_hex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let test_bytes = hex::decode(test_hex).unwrap(); + + let mut public_values = ZKMPublicValues::new(); + public_values.write_slice(&test_bytes); + + let hash = public_values.hash(); + let expected_hash = *blake3::hash(&test_bytes).as_bytes(); + assert_eq!(hash, expected_hash); + + let hash_bn254 = public_values.hash_bn254(); + let mut expected_masked = expected_hash; + expected_masked[0] &= 0b00011111; + assert_eq!(hash_bn254, BigUint::from_bytes_be(&expected_masked)); + + std::env::remove_var("ZKM_IMM_WRAP_VK"); + } } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 65a81371e..42cce003e 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -552,6 +552,116 @@ mod tests { ); } + /// BLAKE3-mode version of [`test_groth16_public_values`]: the guest is always built with the + /// `imm-wrap-vk` feature (see `test-artifacts/guests/hello-world-imm-wrap-vk`), and + /// `ZKM_IMM_WRAP_VK` is set so the host hashes with BLAKE3 to match. + /// + /// Mutates the process-wide `ZKM_IMM_WRAP_VK` env var, so this is `#[ignore]`d like other + /// env-var-dependent e2e tests in this codebase (e.g. `zkm-verifier`'s + /// `test_e2e_verify_groth16`); run explicitly with `-- --ignored`. + #[test] + #[ignore] + fn test_groth16_public_values_imm_wrap_vk() { + std::env::set_var("ZKM_IMM_WRAP_VK", "1"); + + let client = ProverClient::cpu(); + let elf = test_artifacts::HELLO_WORLD_IMM_WRAP_VK_ELF; + let (pk, vk) = client.setup(elf); + let stdin = ZKMStdin::new(); + + // Generate proof & verify. + let proof = client.prove(&pk, stdin).groth16().run().unwrap(); + client.verify(&proof, &vk).unwrap(); + + let string_input = b"hello world".to_vec(); + let guest_committed_values = bincode::serialize(&string_input).unwrap(); + assert_eq!(proof.public_values.as_ref(), guest_committed_values); + + let inner_proof = match proof.proof.clone() { + Groth16(proof) => proof, + _ => panic!("expected a compressed proof"), + }; + + let vk_hash = vk.hash_bn254().as_canonical_biguint().to_string(); + assert_eq!(vk_hash, inner_proof.public_inputs[0], "vk hash does not match"); + + let committed_public_values = committed_public_values(proof.public_values.as_ref()); + assert_eq!( + committed_public_values, inner_proof.public_inputs[1], + "committed public values does not match" + ); + } + + /// Verifies the guest/host mode-mismatch check in `CpuProver`'s `prove_impl`/ + /// `compress_to_groth16`: proving a guest that was built with `imm-wrap-vk` (BLAKE3), while + /// the host does not believe it's in that mode (`ZKM_IMM_WRAP_VK` unset), should fail fast + /// with a clear error -- before ever reaching the Go/gnark proving step, so this test is cheap + /// to run despite exercising the Groth16 path. + #[test] + #[ignore] + fn test_groth16_guest_host_mode_mismatch() { + std::env::remove_var("ZKM_IMM_WRAP_VK"); + + let client = ProverClient::cpu(); + let elf = test_artifacts::HELLO_WORLD_IMM_WRAP_VK_ELF; + let (pk, _vk) = client.setup(elf); + let stdin = ZKMStdin::new(); + + let err = client.prove(&pk, stdin).groth16().run().unwrap_err(); + assert!( + err.to_string().contains("guest committed-values digest doesn't match"), + "unexpected error: {err}" + ); + } + + /// BLAKE3-mode version of [`test_e2e_core`]: the guest is always built with the + /// `imm-wrap-vk` feature, and `ZKM_IMM_WRAP_VK` is set so `client.verify()` checks the + /// committed public-values digest with BLAKE3 to match. + #[test] + #[ignore] + fn test_e2e_core_imm_wrap_vk() { + std::env::set_var("ZKM_IMM_WRAP_VK", "1"); + + utils::setup_logger(); + let client = ProverClient::cpu(); + let elf = test_artifacts::HELLO_WORLD_IMM_WRAP_VK_ELF; + let (pk, vk) = client.setup(elf); + let stdin = ZKMStdin::new(); + + // Generate proof & verify. + let mut proof = client.prove(&pk, stdin).run().unwrap(); + client.verify(&proof, &vk).unwrap(); + + // Test invalid public values. + proof.public_values = ZKMPublicValues::from(&[255, 4, 84]); + if client.verify(&proof, &vk).is_ok() { + panic!("verified proof with invalid public values") + } + } + + /// BLAKE3-mode version of [`test_e2e_compressed`]. + #[test] + #[ignore] + fn test_e2e_compressed_imm_wrap_vk() { + std::env::set_var("ZKM_IMM_WRAP_VK", "1"); + + utils::setup_logger(); + let client = ProverClient::cpu(); + let elf = test_artifacts::HELLO_WORLD_IMM_WRAP_VK_ELF; + let (pk, vk) = client.setup(elf); + let stdin = ZKMStdin::new(); + + // Generate proof & verify. + let mut proof = client.prove(&pk, stdin).compressed().run().unwrap(); + client.verify(&proof, &vk).unwrap(); + + // Test invalid public values. + proof.public_values = ZKMPublicValues::from(&[255, 4, 84]); + if client.verify(&proof, &vk).is_ok() { + panic!("verified proof with invalid public values") + } + } + #[test] fn test_compress_to_groth16() { utils::setup_logger(); diff --git a/crates/sdk/src/provers/cpu.rs b/crates/sdk/src/provers/cpu.rs index aa664f41c..99142d0fa 100644 --- a/crates/sdk/src/provers/cpu.rs +++ b/crates/sdk/src/provers/cpu.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use p3_field::PrimeField; use std::fs; use std::path::PathBuf; use zkm_core_executor::ZKMContext; @@ -37,7 +38,8 @@ impl CpuProver { opts: ProofOpts, ) -> Result { assert_eq!(stdin.buffer.len(), 1); - let public_values = bincode::deserialize(stdin.buffer.last().unwrap())?; + let public_values: crate::ZKMPublicValues = + bincode::deserialize(stdin.buffer.last().unwrap())?; assert_eq!(stdin.proofs.len(), 1); let (proof, _) = stdin.proofs.pop().unwrap(); @@ -48,6 +50,19 @@ impl CpuProver { // Generate the wrap proof. let outer_proof = self.prover.wrap_bn254(shrink_proof, opts.zkm_prover_opts)?; + // See the equivalent check in `prove_impl` for why this is here. + let actual_digest = + zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let expected_digest = public_values.hash_bn254(); + if actual_digest != expected_digest { + anyhow::bail!( + "guest committed-values digest doesn't match the hash algorithm this prover \ + currently expects (ZKM_IMM_WRAP_VK={}); the guest ELF may have been built in a \ + different mode", + zkm_prover::build::zkm_imm_wrap_vk_mode() + ); + } + let groth16_bn254_artifacts = if zkm_prover::build::zkm_dev_mode() { zkm_prover::build::try_build_groth16_bn254_artifacts_dev( &outer_proof.vk, @@ -134,6 +149,22 @@ impl Prover for CpuProver { // Generate the wrap proof. let outer_proof = self.prover.wrap_bn254(compress_proof, opts.zkm_prover_opts)?; + // Check that the guest's committed-values digest was hashed with whichever algorithm this + // process currently expects (see `zkm_imm_wrap_vk_mode`), before spending time on the + // (potentially expensive) Plonk/Groth16/DvSnark proving below. A mismatch here means the + // guest ELF was built in a different mode than this prover currently believes. + let actual_digest = + zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let expected_digest = public_values.hash_bn254(); + if actual_digest != expected_digest { + anyhow::bail!( + "guest committed-values digest doesn't match the hash algorithm this prover \ + currently expects (ZKM_IMM_WRAP_VK={}); the guest ELF may have been built in a \ + different mode", + zkm_prover::build::zkm_imm_wrap_vk_mode() + ); + } + if kind == ZKMProofKind::Plonk { let plonk_bn254_artifacts = if zkm_prover::build::zkm_dev_mode() { zkm_prover::build::try_build_plonk_bn254_artifacts_dev( diff --git a/crates/sdk/src/provers/cuda.rs b/crates/sdk/src/provers/cuda.rs index c6dc6f3af..075b16d40 100644 --- a/crates/sdk/src/provers/cuda.rs +++ b/crates/sdk/src/provers/cuda.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use p3_field::PrimeField; use std::fs; use std::path::PathBuf; use tonic::async_trait; @@ -77,6 +78,22 @@ impl CudaProver { // Generate the wrap proof. let outer_proof = self.cuda_prover.wrap_bn254(compress_proof)?; + // Check that the guest's committed-values digest was hashed with whichever algorithm this + // process currently expects (see `zkm_imm_wrap_vk_mode`), before spending time on the + // (potentially expensive) Plonk/Groth16/DvSnark proving below. A mismatch here means the + // guest ELF was built in a different mode than this prover currently believes. + let actual_digest = + zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let expected_digest = public_values.hash_bn254(); + if actual_digest != expected_digest { + anyhow::bail!( + "guest committed-values digest doesn't match the hash algorithm this prover \ + currently expects (ZKM_IMM_WRAP_VK={}); the guest ELF may have been built in a \ + different mode", + zkm_prover::build::zkm_imm_wrap_vk_mode() + ); + } + if kind == ZKMProofKind::Plonk { let plonk_bn254_artifacts = if zkm_prover::build::zkm_dev_mode() { zkm_prover::build::try_build_plonk_bn254_artifacts_dev( @@ -140,7 +157,8 @@ impl CudaProver { fn compress_to_groth16(&self, mut stdin: ZKMStdin) -> Result { assert_eq!(stdin.buffer.len(), 1); - let public_values = bincode::deserialize(stdin.buffer.last().unwrap())?; + let public_values: crate::ZKMPublicValues = + bincode::deserialize(stdin.buffer.last().unwrap())?; assert_eq!(stdin.proofs.len(), 1); let (proof, _) = stdin.proofs.pop().unwrap(); @@ -151,6 +169,19 @@ impl CudaProver { // Generate the wrap proof. let outer_proof = self.cuda_prover.wrap_bn254(shrink_proof)?; + // See the equivalent check in `prove_with_cycles` for why this is here. + let actual_digest = + zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let expected_digest = public_values.hash_bn254(); + if actual_digest != expected_digest { + anyhow::bail!( + "guest committed-values digest doesn't match the hash algorithm this prover \ + currently expects (ZKM_IMM_WRAP_VK={}); the guest ELF may have been built in a \ + different mode", + zkm_prover::build::zkm_imm_wrap_vk_mode() + ); + } + let groth16_bn254_artifacts = if zkm_prover::build::zkm_dev_mode() { zkm_prover::build::try_build_groth16_bn254_artifacts_dev( &outer_proof.vk, diff --git a/crates/test-artifacts/guests/Cargo.toml b/crates/test-artifacts/guests/Cargo.toml index 7ea8222bb..e5e3b766e 100644 --- a/crates/test-artifacts/guests/Cargo.toml +++ b/crates/test-artifacts/guests/Cargo.toml @@ -27,6 +27,7 @@ members = [ "ed25519", "fibonacci", "hello-world", + "hello-world-imm-wrap-vk", "hint-io", "poseidon2-permute", "secp256k1-add", diff --git a/crates/test-artifacts/guests/hello-world-imm-wrap-vk/Cargo.toml b/crates/test-artifacts/guests/hello-world-imm-wrap-vk/Cargo.toml new file mode 100644 index 000000000..aa9b9ab53 --- /dev/null +++ b/crates/test-artifacts/guests/hello-world-imm-wrap-vk/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello-world-imm-wrap-vk" +version = "1.1.0" +edition = "2021" +publish = false + +[dependencies] +zkm-zkvm = { path = "../../../../crates/zkvm/entrypoint", features = ["imm-wrap-vk"] } diff --git a/crates/test-artifacts/guests/hello-world-imm-wrap-vk/src/main.rs b/crates/test-artifacts/guests/hello-world-imm-wrap-vk/src/main.rs new file mode 100644 index 000000000..8c2f1561e --- /dev/null +++ b/crates/test-artifacts/guests/hello-world-imm-wrap-vk/src/main.rs @@ -0,0 +1,11 @@ +//! Same as `hello-world`, but always built with the `imm-wrap-vk` feature, so it hashes its +//! public values with BLAKE3 instead of SHA256, regardless of `ZKM_IMM_WRAP_VK` at build time. + +#![no_std] +#![no_main] +zkm_zkvm::entrypoint!(main); + +pub fn main() { + let a = "hello world"; + zkm_zkvm::io::commit(&a); +} diff --git a/crates/test-artifacts/src/lib.rs b/crates/test-artifacts/src/lib.rs index 8898fd0cf..4875ff984 100644 --- a/crates/test-artifacts/src/lib.rs +++ b/crates/test-artifacts/src/lib.rs @@ -5,6 +5,7 @@ use zkm_build::include_elf; pub const SHA2_RUST_ELF: &[u8] = include_elf!("sha2-rust"); pub const FIBONACCI_ELF: &[u8] = include_elf!("fibonacci"); pub const HELLO_WORLD_ELF: &[u8] = include_elf!("hello-world"); +pub const HELLO_WORLD_IMM_WRAP_VK_ELF: &[u8] = include_elf!("hello-world-imm-wrap-vk"); pub const POSEIDON2_PERMUTE_ELF: &[u8] = include_elf!("poseidon2-permute-test"); diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 27264759a..3365f7033 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -3454,11 +3454,10 @@ name = "imm-wrap-vk-add-host" version = "1.1.0" dependencies = [ "blake3", - "p3-field", + "num-bigint 0.4.6", "sha2 0.10.8", "zkm-build", "zkm-sdk", - "zkm-stark", ] [[package]] diff --git a/examples/imm-wrap-vk-add/host/Cargo.toml b/examples/imm-wrap-vk-add/host/Cargo.toml index a7845a5e9..cf8b5a243 100644 --- a/examples/imm-wrap-vk-add/host/Cargo.toml +++ b/examples/imm-wrap-vk-add/host/Cargo.toml @@ -7,10 +7,9 @@ publish = false [dependencies] zkm-sdk = { workspace = true } -zkm-stark = { workspace = true } -p3-field = { workspace = true } blake3 = { workspace = true } sha2 = { workspace = true } +num-bigint = "0.4.6" [build-dependencies] zkm-build = { workspace = true } diff --git a/examples/imm-wrap-vk-add/host/src/main.rs b/examples/imm-wrap-vk-add/host/src/main.rs index 7c3da6efc..78ca91dcc 100644 --- a/examples/imm-wrap-vk-add/host/src/main.rs +++ b/examples/imm-wrap-vk-add/host/src/main.rs @@ -1,9 +1,6 @@ -use core::borrow::Borrow; - -use p3_field::PrimeField32; +use num_bigint::BigUint; use sha2::{Digest, Sha256}; use zkm_sdk::{include_elf, utils, ProverClient, ZKMProof, ZKMStdin}; -use zkm_stark::{air::PublicValues, Word}; /// The ELF we want to execute inside the zkVM. /// @@ -35,7 +32,7 @@ fn main() { println!("executed program with {} cycles", report.total_instruction_count()); let (pk, vk) = client.setup(ELF); - let proof = client.prove(&pk, stdin).compressed().run().unwrap(); + let proof = client.prove(&pk, stdin).groth16().run().unwrap(); println!("generated proof"); let mut public_values = proof.public_values.clone(); @@ -47,30 +44,29 @@ fn main() { client.verify(&proof, &vk).expect("verification failed"); - // Also pull the digest the guest actually committed to out of the proof, and compare it - // against an independently computed hash of the raw public values, using whichever + // Also pull the committed-values digest out of the Groth16 proof's own public inputs, and + // compare it against an independently computed hash of the raw public values, using whichever // algorithm this guest build should have used. This checks the guest hasher itself directly, - // in addition to the host-side verification path above. - let ZKMProof::Compressed(compressed_proof) = &proof.proof else { - panic!("expected a compressed proof"); + // in addition to the host-side verification path above (rather than reusing + // `ZKMPublicValues::hash_bn254()`, which is the same function under test). + let ZKMProof::Groth16(groth16_proof) = &proof.proof else { + panic!("expected a groth16 proof"); }; - let proof_public_values: &PublicValues, _> = - compressed_proof.proof.public_values.as_slice().borrow(); - let committed_value_digest: Vec = proof_public_values - .committed_value_digest - .iter() - .flat_map(|w| w.0.iter().map(|x| x.as_canonical_u32() as u8)) - .collect(); + let committed_value_digest = &groth16_proof.public_inputs[1]; let raw_public_values = proof.public_values.as_slice(); - let expected_digest: Vec = if imm_wrap_vk_mode { - blake3::hash(raw_public_values).as_bytes().to_vec() + let mut hash: [u8; 32] = if imm_wrap_vk_mode { + blake3::hash(raw_public_values).into() } else { - Sha256::digest(raw_public_values).to_vec() + Sha256::digest(raw_public_values).into() }; + // Mask the top 3 bits, matching the BN254 scalar field encoding used for Groth16 public + // inputs (same masking `ZKMPublicValues::hash_bn254()` applies internally). + hash[0] &= 0b00011111; + let expected_digest = BigUint::from_bytes_be(&hash).to_string(); assert_eq!( - committed_value_digest, expected_digest, + *committed_value_digest, expected_digest, "committed public-values digest does not match {} of the raw public values", if imm_wrap_vk_mode { "BLAKE3" } else { "SHA256" } ); From b533bb906be57a4d860d10155d5f421c5923511f Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 30 Jul 2026 16:58:06 +0700 Subject: [PATCH 4/7] chore: change vk_hash computation in test. --- crates/prover/src/verify.rs | 2 +- crates/sdk/src/lib.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/prover/src/verify.rs b/crates/prover/src/verify.rs index fb20914e0..b8d418aca 100644 --- a/crates/prover/src/verify.rs +++ b/crates/prover/src/verify.rs @@ -495,7 +495,7 @@ pub fn verify_groth16_bn254_public_inputs( } /// Compute the verification key hash committed into Groth16 public inputs. -fn groth16_vk_hash(vk: &ZKMVerifyingKey) -> Result { +pub fn groth16_vk_hash(vk: &ZKMVerifyingKey) -> Result { const PART_STARK_VK_BYTES: &[u8] = include_bytes!("../../verifier/bn254-vk/part_stark_vk.bin"); let vk_hash = vk.hash_bn254(); diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 42cce003e..f8664cb54 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -582,7 +582,10 @@ mod tests { _ => panic!("expected a compressed proof"), }; - let vk_hash = vk.hash_bn254().as_canonical_biguint().to_string(); + // In `imm-wrap-vk` mode, `vkey_hash` is combined with `vk_commitment`/`pc_start` + // (see `hash_vkey_with_part_vk`), so it isn't just `vk.hash_bn254()` like in normal mode + // -- reuse the same mode-aware computation `client.verify()` already uses internally. + let vk_hash = zkm_prover::verify::groth16_vk_hash(&vk).unwrap().to_string(); assert_eq!(vk_hash, inner_proof.public_inputs[0], "vk hash does not match"); let committed_public_values = committed_public_values(proof.public_values.as_ref()); From 6712de461e58c7c20e3dc1a35751de65ef808822 Mon Sep 17 00:00:00 2001 From: vanhger Date: Thu, 30 Jul 2026 17:11:00 +0700 Subject: [PATCH 5/7] style: fmt code --- crates/sdk/src/provers/cpu.rs | 8 ++++---- crates/sdk/src/provers/cuda.rs | 8 ++++---- crates/zkvm/entrypoint/src/syscalls/halt.rs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/sdk/src/provers/cpu.rs b/crates/sdk/src/provers/cpu.rs index 99142d0fa..39ee19af6 100644 --- a/crates/sdk/src/provers/cpu.rs +++ b/crates/sdk/src/provers/cpu.rs @@ -51,8 +51,8 @@ impl CpuProver { let outer_proof = self.prover.wrap_bn254(shrink_proof, opts.zkm_prover_opts)?; // See the equivalent check in `prove_impl` for why this is here. - let actual_digest = - zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let actual_digest = zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof) + .as_canonical_biguint(); let expected_digest = public_values.hash_bn254(); if actual_digest != expected_digest { anyhow::bail!( @@ -153,8 +153,8 @@ impl Prover for CpuProver { // process currently expects (see `zkm_imm_wrap_vk_mode`), before spending time on the // (potentially expensive) Plonk/Groth16/DvSnark proving below. A mismatch here means the // guest ELF was built in a different mode than this prover currently believes. - let actual_digest = - zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let actual_digest = zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof) + .as_canonical_biguint(); let expected_digest = public_values.hash_bn254(); if actual_digest != expected_digest { anyhow::bail!( diff --git a/crates/sdk/src/provers/cuda.rs b/crates/sdk/src/provers/cuda.rs index 075b16d40..2df52a91e 100644 --- a/crates/sdk/src/provers/cuda.rs +++ b/crates/sdk/src/provers/cuda.rs @@ -82,8 +82,8 @@ impl CudaProver { // process currently expects (see `zkm_imm_wrap_vk_mode`), before spending time on the // (potentially expensive) Plonk/Groth16/DvSnark proving below. A mismatch here means the // guest ELF was built in a different mode than this prover currently believes. - let actual_digest = - zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let actual_digest = zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof) + .as_canonical_biguint(); let expected_digest = public_values.hash_bn254(); if actual_digest != expected_digest { anyhow::bail!( @@ -170,8 +170,8 @@ impl CudaProver { let outer_proof = self.cuda_prover.wrap_bn254(shrink_proof)?; // See the equivalent check in `prove_with_cycles` for why this is here. - let actual_digest = - zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof).as_canonical_biguint(); + let actual_digest = zkm_prover::utils::zkm_committed_values_digest_bn254(&outer_proof) + .as_canonical_biguint(); let expected_digest = public_values.hash_bn254(); if actual_digest != expected_digest { anyhow::bail!( diff --git a/crates/zkvm/entrypoint/src/syscalls/halt.rs b/crates/zkvm/entrypoint/src/syscalls/halt.rs index ead039d09..67beb880c 100644 --- a/crates/zkvm/entrypoint/src/syscalls/halt.rs +++ b/crates/zkvm/entrypoint/src/syscalls/halt.rs @@ -26,8 +26,8 @@ pub extern "C" fn syscall_halt(exit_code: u8) -> ! { unsafe { // When we halt, we retrieve the public values finalized digest. This is the hash of all // the bytes written to the public values fd. - let hasher = core::mem::take(&mut *core::ptr::addr_of_mut!(zkvm::PUBLIC_VALUES_HASHER)) - .unwrap(); + let hasher = + core::mem::take(&mut *core::ptr::addr_of_mut!(zkvm::PUBLIC_VALUES_HASHER)).unwrap(); cfg_if::cfg_if! { if #[cfg(feature = "imm-wrap-vk")] { let pv_digest_bytes: [u8; 32] = *hasher.finalize().as_bytes(); From fd48f379185678e48250f05244ff2fc227e09f02 Mon Sep 17 00:00:00 2001 From: vanhger Date: Fri, 31 Jul 2026 10:00:45 +0700 Subject: [PATCH 6/7] delegate feature flag. --- crates/recursion/core/Cargo.toml | 4 +++- crates/recursion/core/src/stark/utils.rs | 13 +++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/recursion/core/Cargo.toml b/crates/recursion/core/Cargo.toml index 7ba484152..029c83e73 100644 --- a/crates/recursion/core/Cargo.toml +++ b/crates/recursion/core/Cargo.toml @@ -77,4 +77,6 @@ debug = ["zkm-core-machine/debug"] sys = ["zkm-core-machine/sys", "dep:glob", "dep:cc", "dep:cbindgen", "dep:pathdiff"] program_validation = ["dep:range-set-blaze", "dep:smallvec"] # The Groth16 verification key (vk) is not affected by the Ziren upgrade. -imm-wrap-vk = [] +# Forwards to `zkm-primitives/imm-wrap-vk` so the two crates share a single canonical +# `imm-wrap-vk` mode (see `zkm_imm_wrap_vk_mode` in `stark/utils.rs`). +imm-wrap-vk = ["zkm-primitives/imm-wrap-vk"] diff --git a/crates/recursion/core/src/stark/utils.rs b/crates/recursion/core/src/stark/utils.rs index e34dfd536..f9730793d 100644 --- a/crates/recursion/core/src/stark/utils.rs +++ b/crates/recursion/core/src/stark/utils.rs @@ -28,15 +28,12 @@ pub fn zkm_dev_mode() -> bool { /// inputs for verification. /// /// By default, the variable is disabled. +/// +/// Delegates to `zkm_primitives::io::zkm_imm_wrap_vk_mode` so there is a single canonical source of +/// truth: this crate's `imm-wrap-vk` feature forwards to `zkm-primitives/imm-wrap-vk` (see this +/// crate's `Cargo.toml`), so enabling either crate's feature enables both. pub fn zkm_imm_wrap_vk_mode() -> bool { - let value = std::env::var("ZKM_IMM_WRAP_VK").unwrap_or_else(|_| "false".to_string()); - let enabled = value == "1" || value.to_lowercase() == "true" || cfg!(feature = "imm-wrap-vk"); - if enabled { - tracing::warn!( - "`ZKM_IMM_WRAP_VK` environment variable or `imm-wrap-vk` feature is enabled." - ); - } - enabled + zkm_primitives::io::zkm_imm_wrap_vk_mode() } /// Combine the base vkey hash with `vk_commitment` and `pc_start` using a Poseidon2 permutation. From 03c38ae8dac89495d6ebf0c47f59159504bb0397 Mon Sep 17 00:00:00 2001 From: vanhger Date: Fri, 14 Aug 2026 09:46:57 +0700 Subject: [PATCH 7/7] update Cargo.lock --- examples/Cargo.lock | 51 +++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 3365f7033..48206a8b4 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -673,7 +673,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "log", "prettyplease 0.2.37", "proc-macro2", @@ -1462,7 +1462,7 @@ dependencies = [ "rustc_version 0.4.1", "subtle", "zeroize", - "zkm-lib 1.2.7", + "zkm-lib 1.2.7 (git+https://github.com/ProjectZKM/Ziren)", ] [[package]] @@ -3753,7 +3753,7 @@ dependencies = [ [[package]] name = "k256" version = "0.13.4" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#6ad84b9b604911c6f5dd353b88c04927d2739a32" dependencies = [ "cfg-if", "ecdsa", @@ -3762,7 +3762,7 @@ dependencies = [ "once_cell", "sha2 0.10.8", "signature", - "zkm-lib 1.2.7", + "zkm-lib 1.2.7 (git+https://github.com/ProjectZKM/Ziren.git?tag=v1.2.7)", ] [[package]] @@ -4463,14 +4463,14 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "p256" version = "0.13.2" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "ecdsa", "elliptic-curve", "hex", "primeorder", "sha2 0.10.8", - "zkm-lib 1.2.7", + "zkm-lib 1.2.7 (git+https://github.com/ProjectZKM/Ziren.git?tag=v1.2.7)", ] [[package]] @@ -5222,7 +5222,7 @@ dependencies = [ [[package]] name = "primeorder" version = "0.13.1" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "elliptic-curve", ] @@ -5878,7 +5878,7 @@ dependencies = [ "spki 0.7.3", "subtle", "zeroize", - "zkm-lib 1.2.7", + "zkm-lib 1.2.7 (git+https://github.com/ProjectZKM/Ziren)", ] [[package]] @@ -6767,7 +6767,7 @@ dependencies = [ [[package]] name = "substrate-bn" version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#0dfeeb1b7bfc21941b6e4964d678abd094a54a6f" dependencies = [ "bytemuck", "byteorder", @@ -6777,7 +6777,7 @@ dependencies = [ "num-bigint 0.4.6", "rand 0.8.5", "rustc-hex", - "zkm-lib 1.2.7", + "zkm-lib 1.2.7 (git+https://github.com/ProjectZKM/Ziren.git?tag=v1.2.7)", ] [[package]] @@ -8747,27 +8747,27 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.4" -source = "git+https://github.com/ProjectZKM/Ziren#1d43121312d4b93c0989984bf0c7ab77d9a0ce04" +version = "1.2.7" dependencies = [ "bincode", "cfg-if", "elliptic-curve", "serde", "sha2 0.10.8", - "zkm-primitives 1.2.4", + "zkm-primitives 1.2.7", ] [[package]] name = "zkm-lib" version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren.git?tag=v1.2.7#e6945a76b084e87570b7be55e4479ce98603f43a" dependencies = [ "bincode", "cfg-if", "elliptic-curve", "serde", "sha2 0.10.8", - "zkm-primitives 1.2.7", + "zkm-primitives 1.2.7 (git+https://github.com/ProjectZKM/Ziren.git?tag=v1.2.7)", ] [[package]] @@ -8780,12 +8780,12 @@ dependencies = [ "elliptic-curve", "serde", "sha2 0.10.8", - "zkm-primitives 1.2.7", + "zkm-primitives 1.2.7 (git+https://github.com/ProjectZKM/Ziren)", ] [[package]] name = "zkm-primitives" -version = "1.2.5" +version = "1.2.7" dependencies = [ "bincode", "blake3", @@ -8805,6 +8805,25 @@ dependencies = [ [[package]] name = "zkm-primitives" version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren.git?tag=v1.2.7#e6945a76b084e87570b7be55e4479ce98603f43a" +dependencies = [ + "bincode", + "hex", + "lazy_static", + "num-bigint 0.4.6", + "p3-field", + "p3-koala-bear", + "p3-monty-31", + "p3-poseidon2", + "p3-symmetric", + "serde", + "sha2 0.10.8", +] + +[[package]] +name = "zkm-primitives" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren#e6945a76b084e87570b7be55e4479ce98603f43a" dependencies = [ "bincode", "hex",