diff --git a/Cargo.lock b/Cargo.lock index 63ce05462..9322f3f3a 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" @@ -878,6 +863,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" @@ -7958,6 +7957,7 @@ name = "zkm-primitives" version = "1.2.5" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -7967,7 +7967,9 @@ dependencies = [ "p3-poseidon2", "p3-symmetric", "serde", + "serial_test", "sha2", + "tracing", ] [[package]] @@ -8147,7 +8149,6 @@ name = "zkm-sdk" version = "1.2.5" dependencies = [ "alloy-primitives", - "alloy-signer", "anyhow", "async-trait", "bincode", @@ -8274,6 +8275,7 @@ name = "zkm-zkvm" version = "1.2.5" dependencies = [ "bincode", + "blake3", "cfg-if", "critical-section", "embedded-alloc", diff --git a/Cargo.toml b/Cargo.toml index a92151346..88c77fc21 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 = { 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 17111120c..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. @@ -99,11 +104,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 +116,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/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 566360920..8bead5aef 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -21,3 +21,13 @@ 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 } + +[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. +imm-wrap-vk = [] diff --git a/crates/primitives/src/io.rs b/crates/primitives/src/io.rs index d30224631..069701781 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; @@ -89,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(); @@ -104,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/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/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. 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/lib.rs b/crates/sdk/src/lib.rs index 65a81371e..f8664cb54 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -552,6 +552,119 @@ 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"), + }; + + // 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()); + 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..39ee19af6 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..2df52a91e 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/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/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/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/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..67beb880c 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 07c23cab2..081927661 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" @@ -844,6 +829,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 +1462,7 @@ dependencies = [ "rustc_version 0.4.1", "subtle", "zeroize", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -3443,6 +3442,24 @@ 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", + "num-bigint 0.4.6", + "sha2 0.10.8", + "zkm-build", + "zkm-sdk", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -3745,7 +3762,7 @@ dependencies = [ "once_cell", "sha2 0.10.8", "signature", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -4453,7 +4470,7 @@ dependencies = [ "hex", "primeorder", "sha2 0.10.8", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -5861,7 +5878,7 @@ dependencies = [ "spki 0.7.3", "subtle", "zeroize", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -6760,7 +6777,7 @@ dependencies = [ "num-bigint 0.4.6", "rand 0.8.5", "rustc-hex", - "zkm-lib 1.2.4", + "zkm-lib 1.2.7", ] [[package]] @@ -8730,35 +8747,35 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.4" -source = "git+https://github.com/ProjectZKM/Ziren#1d43121312d4b93c0989984bf0c7ab77d9a0ce04" +version = "1.2.5" dependencies = [ "bincode", "cfg-if", "elliptic-curve", "serde", "sha2 0.10.8", - "zkm-primitives 1.2.4", + "zkm-primitives 1.2.5", ] [[package]] name = "zkm-lib" -version = "1.2.5" +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.5", + "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", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -8769,11 +8786,13 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.8", + "tracing", ] [[package]] name = "zkm-primitives" -version = "1.2.5" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren#e6945a76b084e87570b7be55e4479ce98603f43a" dependencies = [ "bincode", "hex", @@ -8948,7 +8967,6 @@ name = "zkm-sdk" version = "1.2.5" dependencies = [ "alloy-primitives", - "alloy-signer", "anyhow", "async-trait", "bincode", @@ -9064,6 +9082,7 @@ name = "zkm-zkvm" version = "1.2.5" dependencies = [ "bincode", + "blake3", "cfg-if", "critical-section", "embedded-alloc", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 21430d6e4..223c8be82 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..cf8b5a243 --- /dev/null +++ b/examples/imm-wrap-vk-add/host/Cargo.toml @@ -0,0 +1,19 @@ +[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 } +blake3 = { workspace = true } +sha2 = { workspace = true } +num-bigint = "0.4.6" + +[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..78ca91dcc --- /dev/null +++ b/examples/imm-wrap-vk-add/host/src/main.rs @@ -0,0 +1,79 @@ +use num_bigint::BigUint; +use sha2::{Digest, Sha256}; +use zkm_sdk::{include_elf, utils, ProverClient, ZKMProof, ZKMStdin}; + +/// 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()); + + let (pk, vk) = client.setup(ELF); + let proof = client.prove(&pk, stdin).groth16().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"); + + // 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 (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 committed_value_digest = &groth16_proof.public_inputs[1]; + + let raw_public_values = proof.public_values.as_slice(); + let mut hash: [u8; 32] = if imm_wrap_vk_mode { + blake3::hash(raw_public_values).into() + } else { + 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 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