diff --git a/Cargo.toml b/Cargo.toml index 22a01fdfa..cd8148cc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,9 @@ ruint = { version = "1.12.3", features = ["num-traits", "rand"] } seq-macro = "0.3.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sha2 = "0.10.9" +sha2 = { version = "0.10.9", features = ["asm"] } +sha3 = "0.11.0-rc.3" +blake3 = "1.5.6" test-case = "3.3.1" tikv-jemallocator = "0.6" toml = "0.8.8" @@ -157,7 +159,7 @@ noirc_driver = { git = "https://github.com/noir-lang/noir", rev = "v1.0.0-beta.1 ark-bn254 = { version = "0.5.0", default-features = false, features = [ "scalar_field", ] } -ark-crypto-primitives = { version = "0.5", features = ["merkle_tree"] } +ark-crypto-primitives = { version = "0.5", features = ["merkle_tree", "parallel"] } ark-ff = { version = "0.5", features = ["asm", "std"] } ark-poly = "0.5" ark-serialize = "0.5" diff --git a/provekit/common/Cargo.toml b/provekit/common/Cargo.toml index 8826dd32c..9ad3212b6 100644 --- a/provekit/common/Cargo.toml +++ b/provekit/common/Cargo.toml @@ -8,6 +8,9 @@ license.workspace = true homepage.workspace = true repository.workspace = true +[features] +default = [] + [dependencies] # Workspace crates skyscraper.workspace = true @@ -32,6 +35,7 @@ bytes.workspace = true hex.workspace = true itertools.workspace = true postcard.workspace = true +rand.workspace = true rand08.workspace = true rayon.workspace = true ruint.workspace = true diff --git a/provekit/common/src/file/bin.rs b/provekit/common/src/file/bin.rs index 82e061d79..09cb66350 100644 --- a/provekit/common/src/file/bin.rs +++ b/provekit/common/src/file/bin.rs @@ -1,6 +1,6 @@ use { super::BufExt as _, - crate::utils::human, + crate::{utils::human, HashConfig}, anyhow::{ensure, Context as _, Result}, bytes::{Buf, BufMut as _, Bytes, BytesMut}, serde::{Deserialize, Serialize}, @@ -12,8 +12,13 @@ use { tracing::{info, instrument}, }; -const HEADER_SIZE: usize = 20; +/// Header layout: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1) = +/// 21 bytes +const HEADER_SIZE: usize = 21; const MAGIC_BYTES: &[u8] = b"\xDC\xDFOZkp\x01\x00"; +/// Byte offset where hash config is stored: MAGIC(8) + FORMAT(8) + MAJOR(2) + +/// MINOR(2) = 20 +const HASH_CONFIG_OFFSET: usize = 20; /// Zstd magic number: `28 B5 2F FD`. const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd]; @@ -36,6 +41,7 @@ pub fn write_bin( format: [u8; 8], (major, minor): (u16, u16), compression: Compression, + hash_config: Option, ) -> Result<()> { let postcard_data = postcard::to_allocvec(value).context("while encoding to postcard")?; let uncompressed = postcard_data.len(); @@ -57,11 +63,14 @@ pub fn write_bin( let mut file = File::create(path).context("while creating output file")?; + // Write header: MAGIC(8) + FORMAT(8) + MAJOR(2) + MINOR(2) + HASH_CONFIG(1) let mut header = BytesMut::with_capacity(HEADER_SIZE); header.put(MAGIC_BYTES); header.put(&format[..]); header.put_u16_le(major); header.put_u16_le(minor); + header.put_u8(hash_config.map(|c| c.to_byte()).unwrap_or(0xff)); + file.write_all(&header).context("while writing header")?; file.write_all(&compressed_data) @@ -84,6 +93,40 @@ pub fn write_bin( Ok(()) } +/// Read just the hash_config from the file header (byte 20). +#[instrument(fields(size = path.metadata().map(|m| m.len()).ok()))] +pub fn read_hash_config( + path: &Path, + format: [u8; 8], + (major, minor): (u16, u16), +) -> Result { + let mut file = File::open(path).context("while opening input file")?; + + // Read header + let mut buffer = [0; HEADER_SIZE]; + file.read_exact(&mut buffer) + .context("while reading header")?; + let mut header = Bytes::from_owner(buffer); + + ensure!( + header.get_bytes::<8>() == MAGIC_BYTES, + "Invalid magic bytes" + ); + ensure!(header.get_bytes::<8>() == format, "Invalid format"); + + let file_major = header.get_u16_le(); + let file_minor = header.get_u16_le(); + + ensure!(file_major == major, "Incompatible format major version"); + ensure!(file_minor >= minor, "Incompatible format minor version"); + + // Read hash_config at HASH_CONFIG_OFFSET (byte 20) + debug_assert_eq!(header.remaining(), HEADER_SIZE - HASH_CONFIG_OFFSET); + let hash_config_byte = header.get_u8(); + HashConfig::from_byte(hash_config_byte) + .with_context(|| format!("Invalid hash config byte: 0x{:02X}", hash_config_byte)) +} + /// Read a compressed binary file, auto-detecting zstd or XZ compression. #[instrument(fields(size = path.metadata().map(|m| m.len()).ok()))] pub fn read_bin Deserialize<'a>>( @@ -111,6 +154,9 @@ pub fn read_bin Deserialize<'a>>( "Incompatible format minor version" ); + // Skip hash_config byte (can be read separately via read_hash_config if needed) + let _hash_config_byte = header.get_u8(); + let uncompressed = decompress_stream(&mut file)?; postcard::from_bytes(&uncompressed).context("while decoding from postcard") diff --git a/provekit/common/src/file/mod.rs b/provekit/common/src/file/mod.rs index 2107e3487..adcadf15d 100644 --- a/provekit/common/src/file/mod.rs +++ b/provekit/common/src/file/mod.rs @@ -5,12 +5,12 @@ mod json; use { self::{ - bin::{read_bin, write_bin, Compression}, + bin::{read_bin, read_hash_config as read_hash_config_bin, write_bin, Compression}, buf_ext::BufExt, counting_writer::CountingWriter, json::{read_json, write_json}, }, - crate::{NoirProof, NoirProofScheme, Prover, Verifier}, + crate::{HashConfig, NoirProof, NoirProofScheme, Prover, Verifier}, anyhow::Result, serde::{Deserialize, Serialize}, std::{ffi::OsStr, path::Path}, @@ -25,6 +25,39 @@ pub trait FileFormat: Serialize + for<'a> Deserialize<'a> { const COMPRESSION: Compression; } +/// Helper trait to optionally extract hash config. +pub(crate) trait MaybeHashAware { + fn maybe_hash_config(&self) -> Option; +} + +/// Impl for Prover (has hash config). +impl MaybeHashAware for Prover { + fn maybe_hash_config(&self) -> Option { + Some(self.hash_config) + } +} + +/// Impl for Verifier (has hash config). +impl MaybeHashAware for Verifier { + fn maybe_hash_config(&self) -> Option { + Some(self.hash_config) + } +} + +/// Impl for NoirProof (no hash config). +impl MaybeHashAware for NoirProof { + fn maybe_hash_config(&self) -> Option { + None + } +} + +/// Impl for NoirProofScheme (has hash config). +impl MaybeHashAware for NoirProofScheme { + fn maybe_hash_config(&self) -> Option { + Some(self.hash_config) + } +} + impl FileFormat for NoirProofScheme { const FORMAT: [u8; 8] = *b"NrProScm"; const EXTENSION: &'static str = "nps"; @@ -54,12 +87,13 @@ impl FileFormat for NoirProof { } /// Write a file with format determined from extension. +#[allow(private_bounds)] #[instrument(skip(value))] -pub fn write(value: &T, path: &Path) -> Result<()> { +pub fn write(value: &T, path: &Path) -> Result<()> { match path.extension().and_then(OsStr::to_str) { Some("json") => write_json(value, path), Some(ext) if ext == T::EXTENSION => { - write_bin(value, path, T::FORMAT, T::VERSION, T::COMPRESSION) + write_bin_with_hash_config(value, path, T::FORMAT, T::VERSION, T::COMPRESSION) } _ => Err(anyhow::anyhow!( "Unsupported file extension, please specify .{} or .json", @@ -68,6 +102,19 @@ pub fn write(value: &T, path: &Path) -> Result<()> { } } +/// Helper to write binary files with hash_config if T implements +/// MaybeHashAware. +fn write_bin_with_hash_config( + value: &T, + path: &Path, + format: [u8; 8], + version: (u16, u16), + compression: Compression, +) -> Result<()> { + let hash_config = value.maybe_hash_config(); + write_bin(value, path, format, version, compression, hash_config) +} + /// Read a file with format determined from extension. #[instrument()] pub fn read(path: &Path) -> Result { @@ -80,3 +127,24 @@ pub fn read(path: &Path) -> Result { )), } } + +/// Read just the hash configuration from a file. +#[instrument()] +pub fn read_hash_config(path: &Path) -> Result { + match path.extension().and_then(OsStr::to_str) { + Some("json") => { + // For JSON, parse and extract hash_config field (required) + let json_str = std::fs::read_to_string(path)?; + let value: serde_json::Value = serde_json::from_str(&json_str)?; + value + .get("hash_config") + .and_then(|v| serde_json::from_value(v.clone()).ok()) + .ok_or_else(|| anyhow::anyhow!("Missing hash_config field in JSON file")) + } + Some(ext) if ext == T::EXTENSION => read_hash_config_bin(path, T::FORMAT, T::VERSION), + _ => Err(anyhow::anyhow!( + "Unsupported file extension, please specify .{} or .json", + T::EXTENSION + )), + } +} diff --git a/provekit/common/src/hash_config.rs b/provekit/common/src/hash_config.rs new file mode 100644 index 000000000..eb73dea88 --- /dev/null +++ b/provekit/common/src/hash_config.rs @@ -0,0 +1,112 @@ +/// Runtime hash configuration selection for ProveKit. +/// +/// This module provides runtime selection of hash algorithms. The selected +/// hash is used for Merkle tree commitments (via WHIR's `EngineId`) and +/// the Fiat-Shamir transcript sponge (via [`crate::TranscriptSponge`]). +use { + serde::{Deserialize, Serialize}, + std::fmt, +}; + +/// Hash algorithm configuration that can be selected at runtime. +/// +/// Each variant uses the same hash algorithm for: +/// - **Merkle tree commitments**: Binds polynomial data +/// - **Fiat-Shamir transcript**: Interactive proof made non-interactive +/// - **Proof of Work**: Optional computational puzzle +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum HashConfig { + #[serde(alias = "sky")] + Skyscraper, + + #[serde(alias = "sha", alias = "sha-256")] + Sha256, + + #[serde(alias = "keccak-256", alias = "shake")] + Keccak, + + #[serde(alias = "blake-3", alias = "b3")] + Blake3, +} + +impl HashConfig { + /// Returns the canonical name of this hash configuration. + pub fn name(&self) -> &'static str { + match self { + Self::Skyscraper => "skyscraper", + Self::Sha256 => "sha256", + Self::Keccak => "keccak", + Self::Blake3 => "blake3", + } + } + + /// Returns the WHIR 2.0 engine ID for this hash configuration. + pub fn engine_id(&self) -> whir::engines::EngineId { + match self { + Self::Skyscraper => crate::skyscraper::SKYSCRAPER, + Self::Sha256 => whir::hash::SHA2, + Self::Keccak => whir::hash::KECCAK, + Self::Blake3 => whir::hash::BLAKE3, + } + } + + /// Converts hash configuration to a single byte for binary file headers. + pub fn to_byte(&self) -> u8 { + match self { + Self::Skyscraper => 0, + Self::Sha256 => 1, + Self::Keccak => 2, + Self::Blake3 => 3, + } + } + + /// Converts a byte from binary file header to hash configuration. + pub fn from_byte(byte: u8) -> Option { + match byte { + 0 => Some(Self::Skyscraper), + 1 => Some(Self::Sha256), + 2 => Some(Self::Keccak), + 3 => Some(Self::Blake3), + _ => None, + } + } + + /// Parses a hash configuration from a string. + pub fn parse(s: &str) -> Option { + let lower = s.to_lowercase(); + match lower.as_str() { + "skyscraper" | "sky" => Some(Self::Skyscraper), + "sha256" | "sha" | "sha-256" => Some(Self::Sha256), + "keccak" | "keccak-256" | "shake" => Some(Self::Keccak), + "blake3" | "blake-3" | "b3" => Some(Self::Blake3), + _ => None, + } + } +} + +impl Default for HashConfig { + fn default() -> Self { + Self::Skyscraper + } +} + +impl fmt::Display for HashConfig { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl std::str::FromStr for HashConfig { + type Err = String; + + fn from_str(s: &str) -> Result { + Self::parse(s).ok_or_else(|| { + format!( + "Invalid hash configuration: '{}'. Valid options: skyscraper, sha256, keccak, \ + blake3", + s + ) + }) + } +} diff --git a/provekit/common/src/lib.rs b/provekit/common/src/lib.rs index f54249a79..5eef79de5 100644 --- a/provekit/common/src/lib.rs +++ b/provekit/common/src/lib.rs @@ -1,4 +1,5 @@ pub mod file; +pub mod hash_config; mod interner; mod noir_proof_scheme; pub mod prefix_covector; @@ -6,6 +7,7 @@ mod prover; mod r1cs; pub mod skyscraper; pub mod sparse_matrix; +mod transcript_sponge; pub mod utils; mod verifier; mod whir_r1cs; @@ -18,35 +20,37 @@ use crate::{ pub use { acir::FieldElement as NoirElement, ark_bn254::Fr as FieldElement, + hash_config::HashConfig, noir_proof_scheme::{NoirProof, NoirProofScheme}, prefix_covector::{OffsetCovector, PrefixCovector}, prover::Prover, r1cs::R1CS, + transcript_sponge::TranscriptSponge, verifier::Verifier, - whir_r1cs::{ - WhirConfig, WhirDomainSeparator, WhirProof, WhirProverState, WhirR1CSProof, WhirR1CSScheme, - WhirZkConfig, - }, + whir_r1cs::{WhirConfig, WhirR1CSProof, WhirR1CSScheme, WhirZkConfig}, witness::PublicInputs, }; -/// SHA-256 based transcript sponge for Fiat-Shamir. -pub type TranscriptSponge = spongefish::instantiations::SHA256; - /// Register provekit's custom implementations in whir's global registries. /// +/// This registers: +/// - NTT implementation for polynomial operations +/// - Skyscraper hash engine (ProveKit-specific; standard hashes are built into +/// WHIR) +/// /// Must be called once before any prove/verify operations. /// Idempotent — safe to call multiple times. pub fn register_ntt() { use std::sync::{Arc, Once}; static INIT: Once = Once::new(); INIT.call_once(|| { + // Register NTT for polynomial operations let ntt: Arc> = Arc::new(whir::algebra::ntt::ArkNtt::::default()); whir::algebra::ntt::NTT.insert(ntt); - let skyscraper: Arc = - Arc::new(skyscraper::SkyscraperHashEngine); - whir::hash::ENGINES.register(skyscraper); + // Register Skyscraper (ProveKit-specific); WHIR's built-in engines + // (SHA2, Keccak, Blake3, etc.) are pre-registered via whir::hash::ENGINES. + whir::hash::ENGINES.register(Arc::new(skyscraper::SkyscraperHashEngine)); }); } diff --git a/provekit/common/src/noir_proof_scheme.rs b/provekit/common/src/noir_proof_scheme.rs index 7552ab268..19788788b 100644 --- a/provekit/common/src/noir_proof_scheme.rs +++ b/provekit/common/src/noir_proof_scheme.rs @@ -2,13 +2,14 @@ use { crate::{ whir_r1cs::{WhirR1CSProof, WhirR1CSScheme}, witness::{NoirWitnessGenerator, SplitWitnessBuilders}, - NoirElement, PublicInputs, R1CS, + HashConfig, NoirElement, PublicInputs, R1CS, }, acir::circuit::Program, serde::{Deserialize, Serialize}, }; /// A scheme for proving a Noir program. +/// Hash algorithm is selected at construction time and baked into all configs. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NoirProofScheme { pub program: Program, @@ -16,6 +17,7 @@ pub struct NoirProofScheme { pub split_witness_builders: SplitWitnessBuilders, pub witness_generator: NoirWitnessGenerator, pub whir_for_witness: WhirR1CSScheme, + pub hash_config: HashConfig, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/provekit/common/src/prover.rs b/provekit/common/src/prover.rs index 32b1fcc23..105f1c21f 100644 --- a/provekit/common/src/prover.rs +++ b/provekit/common/src/prover.rs @@ -3,14 +3,17 @@ use { noir_proof_scheme::NoirProofScheme, whir_r1cs::WhirR1CSScheme, witness::{NoirWitnessGenerator, SplitWitnessBuilders}, - NoirElement, R1CS, + HashConfig, NoirElement, R1CS, }, acir::circuit::Program, serde::{Deserialize, Serialize}, }; +/// A prover for a Noir Proof Scheme. +/// Hash algorithm is baked in at prepare time. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Prover { + pub hash_config: HashConfig, pub program: Program, pub r1cs: R1CS, pub split_witness_builders: SplitWitnessBuilders, @@ -27,9 +30,11 @@ impl Prover { split_witness_builders, witness_generator, whir_for_witness, + hash_config, } = scheme; Self { + hash_config, program, r1cs, split_witness_builders, diff --git a/provekit/common/src/skyscraper/pow.rs b/provekit/common/src/skyscraper/pow.rs index 7c0678c5a..48e21d6e1 100644 --- a/provekit/common/src/skyscraper/pow.rs +++ b/provekit/common/src/skyscraper/pow.rs @@ -4,7 +4,8 @@ use { zerocopy::transmute, }; -#[derive(Clone, Copy)] +/// Skyscraper proof of work +#[derive(Clone, Copy, Debug, PartialEq)] pub struct SkyscraperPoW { challenge: [u8; 32], bits: f64, diff --git a/provekit/common/src/skyscraper/whir.rs b/provekit/common/src/skyscraper/whir.rs index 6ee863cf8..0095cc877 100644 --- a/provekit/common/src/skyscraper/whir.rs +++ b/provekit/common/src/skyscraper/whir.rs @@ -19,6 +19,10 @@ pub const SKYSCRAPER: EngineId = EngineId::new([ 0x77, 0xb5, 0x82, 0xb0, 0xb2, 0xdd, 0x42, 0x1c, 0x66, 0x19, 0x13, 0xe6, 0xa5, 0x63, 0xf8, 0xa1, ]); +// ============================================================================ +// WHIR 2.0 HashEngine Implementation +// ============================================================================ + #[derive(Clone, Copy, Debug)] pub struct SkyscraperHashEngine; @@ -66,8 +70,7 @@ impl HashEngine for SkyscraperHashEngine { } // Leaf hashing: left-fold 32-byte chunks, batched across messages - // for SIMD throughput. Equivalent to main's SkyscraperCRH::evaluate: - // elements.reduce(compress) + // for SIMD throughput (equivalent to elements.reduce(compress)). // Processes in fixed-size groups to avoid heap allocation. const GROUP: usize = 4 * skyscraper::WIDTH_LCM; // fits in 3 KiB on stack let chunks_per_msg = size / 32; @@ -97,6 +100,10 @@ impl HashEngine for SkyscraperHashEngine { } } +// ============================================================================ +// Tests +// ============================================================================ + #[cfg(test)] mod tests { use {super::*, zerocopy::IntoBytes}; diff --git a/provekit/common/src/transcript_sponge.rs b/provekit/common/src/transcript_sponge.rs new file mode 100644 index 000000000..5af0ce322 --- /dev/null +++ b/provekit/common/src/transcript_sponge.rs @@ -0,0 +1,99 @@ +//! Runtime-selectable Fiat-Shamir transcript sponge. +//! +//! Instead of making every function generic over a sponge type parameter, +//! we use a single enum that delegates to the concrete sponge at runtime. +//! The branch cost is negligible — the sponge is called O(log n) times +//! per proof for Fiat-Shamir challenges, not in a tight inner loop. + +use { + crate::{hash_config::HashConfig, skyscraper::SkyscraperSponge}, + spongefish::{instantiations, DuplexSpongeInterface}, +}; + +/// Fiat-Shamir transcript sponge, selected at runtime by [`HashConfig`]. +/// +/// Wraps one of the four supported sponge implementations and delegates +/// all [`DuplexSpongeInterface`] calls to the active variant. +#[derive(Clone)] +pub enum TranscriptSponge { + Sha256(instantiations::SHA256), + Blake3(instantiations::Blake3), + Keccak(instantiations::Keccak), + Skyscraper(SkyscraperSponge), +} + +impl TranscriptSponge { + /// Create a sponge matching the given hash configuration. + pub fn from_config(config: HashConfig) -> Self { + match config { + HashConfig::Sha256 => Self::Sha256(Default::default()), + HashConfig::Blake3 => Self::Blake3(Default::default()), + HashConfig::Keccak => Self::Keccak(Default::default()), + HashConfig::Skyscraper => Self::Skyscraper(Default::default()), + } + } +} + +impl Default for TranscriptSponge { + fn default() -> Self { + Self::from_config(HashConfig::default()) + } +} + +impl DuplexSpongeInterface for TranscriptSponge { + type U = u8; + + fn absorb(&mut self, input: &[u8]) -> &mut Self { + match self { + Self::Sha256(s) => { + s.absorb(input); + } + Self::Blake3(s) => { + s.absorb(input); + } + Self::Keccak(s) => { + s.absorb(input); + } + Self::Skyscraper(s) => { + s.absorb(input); + } + } + self + } + + fn squeeze(&mut self, output: &mut [u8]) -> &mut Self { + match self { + Self::Sha256(s) => { + s.squeeze(output); + } + Self::Blake3(s) => { + s.squeeze(output); + } + Self::Keccak(s) => { + s.squeeze(output); + } + Self::Skyscraper(s) => { + s.squeeze(output); + } + } + self + } + + fn ratchet(&mut self) -> &mut Self { + match self { + Self::Sha256(s) => { + s.ratchet(); + } + Self::Blake3(s) => { + s.ratchet(); + } + Self::Keccak(s) => { + s.ratchet(); + } + Self::Skyscraper(s) => { + s.ratchet(); + } + } + self + } +} diff --git a/provekit/common/src/verifier.rs b/provekit/common/src/verifier.rs index 842598d44..bd216d5c6 100644 --- a/provekit/common/src/verifier.rs +++ b/provekit/common/src/verifier.rs @@ -1,13 +1,17 @@ use { crate::{ - noir_proof_scheme::NoirProofScheme, utils::serde_jsonify, whir_r1cs::WhirR1CSScheme, R1CS, + noir_proof_scheme::NoirProofScheme, utils::serde_jsonify, whir_r1cs::WhirR1CSScheme, + HashConfig, R1CS, }, noirc_abi::Abi, serde::{Deserialize, Serialize}, }; +/// A verifier for a Noir Proof Scheme. +/// Hash algorithm is baked in at prepare time. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Verifier { + pub hash_config: HashConfig, pub r1cs: R1CS, pub whir_for_witness: Option, #[serde(with = "serde_jsonify")] @@ -18,6 +22,7 @@ impl Verifier { #[must_use] pub fn from_noir_proof_scheme(scheme: NoirProofScheme) -> Self { Self { + hash_config: scheme.hash_config, r1cs: scheme.r1cs, whir_for_witness: Some(scheme.whir_for_witness), abi: scheme.witness_generator.abi.clone(), diff --git a/provekit/common/src/whir_r1cs.rs b/provekit/common/src/whir_r1cs.rs index f876f5bd4..11e7901da 100644 --- a/provekit/common/src/whir_r1cs.rs +++ b/provekit/common/src/whir_r1cs.rs @@ -1,3 +1,11 @@ +/// WHIR R1CS Scheme. +/// +/// Hash algorithm is selected at construction time via `EngineId` and +/// propagated through all nested WHIR configs by the WHIR library's +/// `Config::new`. + +#[cfg(debug_assertions)] +use std::fmt::Debug; #[cfg(debug_assertions)] use whir::transcript::Interaction; use { @@ -13,13 +21,7 @@ pub type WhirConfig = GenericWhirConfig; pub type WhirZkConfig = GenericWhirZkConfig; /// Type alias for the whir domain separator used in provekit's outer protocol. -pub type WhirDomainSeparator = transcript::DomainSeparator<'static, ()>; - -/// Type alias for the whir prover transcript state. -pub type WhirProverState = transcript::ProverState; - -/// Type alias for the whir proof. -pub type WhirProof = transcript::Proof; +type WhirDomainSeparator = transcript::DomainSeparator<'static, ()>; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WhirR1CSScheme { diff --git a/provekit/prover/Cargo.toml b/provekit/prover/Cargo.toml index c666d9d85..cf772ef43 100644 --- a/provekit/prover/Cargo.toml +++ b/provekit/prover/Cargo.toml @@ -21,9 +21,11 @@ noir_artifact_cli.workspace = true noirc_abi.workspace = true # Cryptography and proof systems +ark-crypto-primitives.workspace = true ark-ff.workspace = true ark-std.workspace = true spongefish.workspace = true +spongefish-pow.workspace = true whir.workspace = true # 3rd party diff --git a/provekit/prover/src/lib.rs b/provekit/prover/src/lib.rs index a17fc1265..2c8d86f87 100644 --- a/provekit/prover/src/lib.rs +++ b/provekit/prover/src/lib.rs @@ -77,12 +77,12 @@ impl Prove for Prover { let num_witnesses = compressed_r1cs.num_witnesses(); let num_constraints = compressed_r1cs.num_constraints(); - // Set up transcript + // Set up transcript with sponge selected by hash_config. let ds = self .whir_for_witness .create_domain_separator() .instance(&Empty); - let mut merlin = ProverState::new(&ds, TranscriptSponge::default()); + let mut merlin = ProverState::new(&ds, TranscriptSponge::from_config(self.hash_config)); let mut witness: Vec> = vec![None; num_witnesses]; diff --git a/provekit/r1cs-compiler/Cargo.toml b/provekit/r1cs-compiler/Cargo.toml index 253970d39..cfbc80645 100644 --- a/provekit/r1cs-compiler/Cargo.toml +++ b/provekit/r1cs-compiler/Cargo.toml @@ -20,6 +20,7 @@ noirc_abi.workspace = true noirc_artifacts.workspace = true # Cryptography and proof systems +ark-crypto-primitives.workspace = true ark-ff.workspace = true ark-std.workspace = true whir.workspace = true diff --git a/provekit/r1cs-compiler/src/noir_proof_scheme.rs b/provekit/r1cs-compiler/src/noir_proof_scheme.rs index b89a87847..7ce174495 100644 --- a/provekit/r1cs-compiler/src/noir_proof_scheme.rs +++ b/provekit/r1cs-compiler/src/noir_proof_scheme.rs @@ -15,26 +15,38 @@ use { }; pub trait NoirProofSchemeBuilder { - fn from_file(path: impl AsRef + std::fmt::Debug) -> Result + fn from_file( + path: impl AsRef + std::fmt::Debug, + hash_config: provekit_common::HashConfig, + ) -> Result where Self: Sized; - fn from_program(program: ProgramArtifact) -> Result + fn from_program( + program: ProgramArtifact, + hash_config: provekit_common::HashConfig, + ) -> Result where Self: Sized; } impl NoirProofSchemeBuilder for NoirProofScheme { #[instrument(fields(size = path.as_ref().metadata().map(|m| m.len()).ok()))] - fn from_file(path: impl AsRef + std::fmt::Debug) -> Result { + fn from_file( + path: impl AsRef + std::fmt::Debug, + hash_config: provekit_common::HashConfig, + ) -> Result { let file = File::open(path).context("while opening Noir program")?; let program = serde_json::from_reader(file).context("while reading Noir program")?; - Self::from_program(program) + Self::from_program(program, hash_config) } #[instrument(skip_all)] - fn from_program(program: ProgramArtifact) -> Result { + fn from_program( + program: ProgramArtifact, + hash_config: provekit_common::HashConfig, + ) -> Result { info!("Program noir version: {}", program.noir_version); info!("Program entry point: fn main{};", PrintAbi(&program.abi)); ensure!( @@ -93,6 +105,7 @@ impl NoirProofSchemeBuilder for NoirProofScheme { split_witness_builders.w1_size, num_challenges, has_public_inputs, + hash_config.engine_id(), ); Ok(Self { @@ -101,6 +114,7 @@ impl NoirProofSchemeBuilder for NoirProofScheme { split_witness_builders, witness_generator, whir_for_witness, + hash_config, }) } } @@ -137,7 +151,8 @@ mod tests { #[test] fn test_noir_proof_scheme_serde() { let path = PathBuf::from("../../tooling/provekit-bench/benches/poseidon_rounds.json"); - let proof_schema = NoirProofScheme::from_file(path).unwrap(); + let proof_schema = + NoirProofScheme::from_file(path, provekit_common::HashConfig::default()).unwrap(); test_serde(&proof_schema.r1cs); test_serde(&proof_schema.split_witness_builders); diff --git a/provekit/r1cs-compiler/src/whir_r1cs.rs b/provekit/r1cs-compiler/src/whir_r1cs.rs index 7dfd83497..bf1dc2303 100644 --- a/provekit/r1cs-compiler/src/whir_r1cs.rs +++ b/provekit/r1cs-compiler/src/whir_r1cs.rs @@ -1,7 +1,11 @@ use { provekit_common::{utils::next_power_of_two, WhirR1CSScheme, WhirZkConfig, R1CS}, - whir::parameters::{ - default_max_pow, FoldingFactor, MultivariateParameters, ProtocolParameters, SoundnessType, + whir::{ + engines::EngineId, + parameters::{ + default_max_pow, FoldingFactor, MultivariateParameters, ProtocolParameters, + SoundnessType, + }, }, }; @@ -14,9 +18,14 @@ pub trait WhirR1CSSchemeBuilder { w1_size: usize, num_challenges: usize, has_public_inputs: bool, + hash_id: EngineId, ) -> Self; - fn new_whir_zk_config_for_size(num_variables: usize, num_polynomials: usize) -> WhirZkConfig; + fn new_whir_zk_config_for_size( + num_variables: usize, + num_polynomials: usize, + hash_id: EngineId, + ) -> WhirZkConfig; } impl WhirR1CSSchemeBuilder for WhirR1CSScheme { @@ -25,6 +34,7 @@ impl WhirR1CSSchemeBuilder for WhirR1CSScheme { w1_size: usize, num_challenges: usize, has_public_inputs: bool, + hash_id: EngineId, ) -> Self { let total_witnesses = r1cs.num_witnesses(); assert!( @@ -51,24 +61,28 @@ impl WhirR1CSSchemeBuilder for WhirR1CSScheme { m_0, a_num_terms: next_power_of_two(r1cs.a().iter().count()), num_challenges, - whir_witness: Self::new_whir_zk_config_for_size(m_raw, 1), + whir_witness: Self::new_whir_zk_config_for_size(m_raw, 1, hash_id), has_public_inputs, } } - fn new_whir_zk_config_for_size(num_variables: usize, num_polynomials: usize) -> WhirZkConfig { + fn new_whir_zk_config_for_size( + num_variables: usize, + num_polynomials: usize, + hash_id: EngineId, + ) -> WhirZkConfig { let nv = num_variables.max(MIN_WHIR_NUM_VARIABLES); let mv_params = MultivariateParameters::new(nv); let whir_params = ProtocolParameters { - initial_statement: true, - security_level: 128, - pow_bits: default_max_pow(nv, 1), - folding_factor: FoldingFactor::Constant(4), - soundness_type: SoundnessType::ConjectureList, + initial_statement: true, + security_level: 128, + pow_bits: default_max_pow(nv, 1), + folding_factor: FoldingFactor::Constant(4), + soundness_type: SoundnessType::ConjectureList, starting_log_inv_rate: 1, - batch_size: 1, - hash_id: whir::hash::SHA2, + batch_size: 1, + hash_id, }; WhirZkConfig::new( mv_params, diff --git a/provekit/verifier/src/lib.rs b/provekit/verifier/src/lib.rs index 1c3461fa8..328ef141d 100644 --- a/provekit/verifier/src/lib.rs +++ b/provekit/verifier/src/lib.rs @@ -19,7 +19,12 @@ impl Verify for Verifier { self.whir_for_witness .take() .context("Verifier has already been consumed; cannot verify twice")? - .verify(&proof.whir_r1cs_proof, &proof.public_inputs, &self.r1cs)?; + .verify( + &proof.whir_r1cs_proof, + &proof.public_inputs, + &self.r1cs, + self.hash_config, + )?; Ok(()) } diff --git a/provekit/verifier/src/whir_r1cs.rs b/provekit/verifier/src/whir_r1cs.rs index d3a148b9a..59288172b 100644 --- a/provekit/verifier/src/whir_r1cs.rs +++ b/provekit/verifier/src/whir_r1cs.rs @@ -8,7 +8,8 @@ use { utils::sumcheck::{ calculate_eq, eval_cubic_poly, multiply_transposed_by_eq_alpha, transpose_r1cs_matrices, }, - FieldElement, PublicInputs, TranscriptSponge, WhirR1CSProof, WhirR1CSScheme, R1CS, + FieldElement, HashConfig, PublicInputs, TranscriptSponge, WhirR1CSProof, WhirR1CSScheme, + R1CS, }, tracing::instrument, whir::{ @@ -30,6 +31,7 @@ pub trait WhirR1CSVerifier { proof: &WhirR1CSProof, public_inputs: &PublicInputs, r1cs: &R1CS, + hash_config: HashConfig, ) -> Result<()>; } @@ -40,6 +42,7 @@ impl WhirR1CSVerifier for WhirR1CSScheme { proof: &WhirR1CSProof, public_inputs: &PublicInputs, r1cs: &R1CS, + hash_config: HashConfig, ) -> Result<()> { let ds = self.create_domain_separator().instance(&Empty); let whir_proof = Proof { @@ -48,7 +51,8 @@ impl WhirR1CSVerifier for WhirR1CSScheme { #[cfg(debug_assertions)] pattern: proof.pattern.clone(), }; - let mut arthur = VerifierState::new(&ds, &whir_proof, TranscriptSponge::default()); + let mut arthur = + VerifierState::new(&ds, &whir_proof, TranscriptSponge::from_config(hash_config)); let commitment_1 = self .whir_witness diff --git a/tooling/cli/src/cmd/prepare.rs b/tooling/cli/src/cmd/prepare.rs index 4a0ffa14e..4a647c133 100644 --- a/tooling/cli/src/cmd/prepare.rs +++ b/tooling/cli/src/cmd/prepare.rs @@ -2,9 +2,9 @@ use { super::Command, anyhow::{Context, Result}, argh::FromArgs, - provekit_common::{file::write, NoirProofScheme, Prover, Verifier}, + provekit_common::{file::write, HashConfig, NoirProofScheme, Prover, Verifier}, provekit_r1cs_compiler::NoirProofSchemeBuilder, - std::path::PathBuf, + std::{path::PathBuf, str::FromStr}, tracing::instrument, }; @@ -33,20 +33,31 @@ pub struct Args { default = "PathBuf::from(\"noir_proof_scheme.pkv\")" )] pkv_path: PathBuf, + + /// hash algorithm for Merkle commitments (skyscraper, sha256, keccak, + /// blake3) + #[argh(option, long = "hash", default = "String::from(\"skyscraper\")")] + hash: String, } impl Command for Args { #[instrument(skip_all)] fn run(&self) -> Result<()> { - let scheme = NoirProofScheme::from_file(&self.program_path) + // Parse hash configuration + let hash_config = HashConfig::from_str(&self.hash).map_err(|e| anyhow::anyhow!("{}", e))?; + + // Build the scheme with the selected hash configuration + let scheme = NoirProofScheme::from_file(&self.program_path, hash_config) .context("while compiling Noir program")?; - write( - &Prover::from_noir_proof_scheme(scheme.clone()), - &self.pkp_path, - ) - .context("while writing Noir proof scheme")?; - write(&Verifier::from_noir_proof_scheme(scheme), &self.pkv_path) - .context("while writing Noir proof scheme")?; + + // Convert to prover and verifier + let prover = Prover::from_noir_proof_scheme(scheme.clone()); + let verifier = Verifier::from_noir_proof_scheme(scheme); + + // Write to files (hash_config is stored in serialized data) + write(&prover, &self.pkp_path).context("while writing Provekit Prover")?; + write(&verifier, &self.pkv_path).context("while writing Provekit Verifier")?; + Ok(()) } } diff --git a/tooling/provekit-bench/Cargo.toml b/tooling/provekit-bench/Cargo.toml index b90f5c9ae..a7957c1ae 100644 --- a/tooling/provekit-bench/Cargo.toml +++ b/tooling/provekit-bench/Cargo.toml @@ -19,15 +19,21 @@ provekit-verifier.workspace = true nargo.workspace = true nargo_cli.workspace = true nargo_toml.workspace = true -noirc_driver.workspace = true +noir_artifact_cli.workspace = true noirc_artifacts.workspace = true +noirc_abi.workspace = true +noirc_driver.workspace = true # 3rd party anyhow.workspace = true +ark-ff.workspace = true +ark-std.workspace = true divan.workspace = true +rand.workspace = true serde.workspace = true test-case.workspace = true toml.workspace = true +whir.workspace = true [lints] workspace = true diff --git a/tooling/provekit-bench/benches/bench.rs b/tooling/provekit-bench/benches/bench.rs index ce7034533..c9bd1fa95 100644 --- a/tooling/provekit-bench/benches/bench.rs +++ b/tooling/provekit-bench/benches/bench.rs @@ -60,10 +60,14 @@ fn prove_poseidon_1000_with_io(bencher: Bencher) { fn verify_poseidon_1000(bencher: Bencher) { let crate_dir: &Path = "../../noir-examples/poseidon-rounds".as_ref(); let proof_verifier_path = crate_dir.join("noir-provekit-verifier.pkv"); - let mut verifier: Verifier = read(&proof_verifier_path).unwrap(); let proof_path = crate_dir.join("noir-proof.np"); let proof: NoirProof = read(&proof_path).unwrap(); - bencher.bench_local(|| black_box(&mut verifier).verify(black_box(&proof))); + + bencher.bench_local(|| { + // Read a fresh verifier for each iteration (verify consumes internal state) + let mut verifier: Verifier = read(&proof_verifier_path).unwrap(); + black_box(&mut verifier).verify(black_box(&proof)) + }); } fn main() { diff --git a/tooling/provekit-bench/tests/compiler.rs b/tooling/provekit-bench/tests/compiler.rs index 8643bcfba..96c5a696a 100644 --- a/tooling/provekit-bench/tests/compiler.rs +++ b/tooling/provekit-bench/tests/compiler.rs @@ -38,7 +38,8 @@ fn test_compiler(test_case_path: impl AsRef) { let circuit_path = test_case_path.join(format!("target/{package_name}.json")); let witness_file_path = test_case_path.join("Prover.toml"); - let schema = NoirProofScheme::from_file(&circuit_path).expect("Reading proof scheme"); + let schema = NoirProofScheme::from_file(&circuit_path, provekit_common::HashConfig::default()) + .expect("Reading proof scheme"); let prover = Prover::from_noir_proof_scheme(schema.clone()); let mut verifier = Verifier::from_noir_proof_scheme(schema.clone());