diff --git a/crates/examples/Cargo.toml b/crates/examples/Cargo.toml index 9bdd4a511..8b095bb5a 100644 --- a/crates/examples/Cargo.toml +++ b/crates/examples/Cargo.toml @@ -13,11 +13,11 @@ base64.workspace = true binius-circuits = { path = "../circuits" } binius-core = { path = "../core" } binius-frontend = { path = "../frontend" } -binius-hash = { path = "../hash", default-features = false } +binius-hash = { path = "../hash" } binius-prover = { path = "../prover", default-features = false } binius-transcript = { path = "../transcript" } binius-utils = { path = "../utils", features = ["platform-diagnostics"] } -binius-verifier = { path = "../verifier", default-features = false } +binius-verifier = { path = "../verifier" } bitcoin_hashes.workspace = true bitcoin.workspace = true blake2.workspace = true @@ -70,7 +70,7 @@ harness = false [features] default = ["rayon"] perfetto = ["tracing-profile/perfetto"] -rayon = ["binius-hash/rayon", "binius-prover/rayon", "binius-verifier/rayon"] +rayon = ["binius-hash/rayon", "binius-prover/rayon"] # Tutorial examples [[example]] diff --git a/crates/hash/Cargo.toml b/crates/hash/Cargo.toml index 4e5ec2059..5db74c04f 100644 --- a/crates/hash/Cargo.toml +++ b/crates/hash/Cargo.toml @@ -26,5 +26,5 @@ tracing.workspace = true rand.workspace = true [features] -default = ["rayon"] +default = [] rayon = ["binius-utils/rayon"] diff --git a/crates/iop-prover/Cargo.toml b/crates/iop-prover/Cargo.toml index b2f3cf6ea..db617c4c9 100644 --- a/crates/iop-prover/Cargo.toml +++ b/crates/iop-prover/Cargo.toml @@ -9,8 +9,8 @@ workspace = true [dependencies] binius-field = { path = "../field" } -binius-hash = { path = "../hash", default-features = false } -binius-iop = { path = "../iop", default-features = false } +binius-hash = { path = "../hash" } +binius-iop = { path = "../iop" } binius-ip = { path = "../ip" } binius-ip-prover = { path = "../ip-prover", default-features = false } binius-math = { path = "../math" } @@ -34,4 +34,4 @@ sha2.workspace = true [features] default = ["rayon"] -rayon = ["binius-utils/rayon", "binius-hash/rayon", "binius-iop/rayon", "binius-ip-prover/rayon"] +rayon = ["binius-utils/rayon"] diff --git a/crates/iop/Cargo.toml b/crates/iop/Cargo.toml index 3320082be..779494710 100644 --- a/crates/iop/Cargo.toml +++ b/crates/iop/Cargo.toml @@ -9,7 +9,7 @@ workspace = true [dependencies] binius-field = { path = "../field" } -binius-hash = { path = "../hash", default-features = false } +binius-hash = { path = "../hash" } binius-ip = { path = "../ip" } binius-math = { path = "../math" } binius-transcript = { path = "../transcript" } @@ -25,7 +25,3 @@ tracing.workspace = true binius-math = { path = "../math", features = ["test-utils"] } rand.workspace = true sha2.workspace = true - -[features] -default = ["rayon"] -rayon = ["binius-utils/rayon", "binius-hash/rayon"] diff --git a/crates/prover/Cargo.toml b/crates/prover/Cargo.toml index 19910bb4c..4f239cf2f 100644 --- a/crates/prover/Cargo.toml +++ b/crates/prover/Cargo.toml @@ -10,12 +10,16 @@ workspace = true [dependencies] binius-core = { path = "../core" } binius-field = { path = "../field" } -binius-hash = { path = "../hash", default-features = false } +binius-hash = { path = "../hash" } +binius-iop = { path = "../iop" } binius-iop-prover = { path = "../iop-prover", default-features = false } binius-ip-prover = { path = "../ip-prover", default-features = false } binius-math = { path = "../math" } +binius-spartan-frontend = { path = "../spartan-frontend" } +binius-spartan-prover = { path = "../spartan-prover", default-features = false } +binius-spartan-verifier = { path = "../spartan-verifier" } binius-transcript = { path = "../transcript" } -binius-verifier = { path = "../verifier", default-features = false } +binius-verifier = { path = "../verifier" } binius-utils = { path = "../utils" } bytemuck.workspace = true bytes.workspace = true @@ -87,4 +91,4 @@ harness = false [features] default = ["rayon"] -rayon = ["binius-utils/rayon", "binius-hash/rayon", "binius-iop-prover/rayon", "binius-ip-prover/rayon"] +rayon = ["binius-utils/rayon"] diff --git a/crates/prover/src/lib.rs b/crates/prover/src/lib.rs index 691759a49..c3600cee4 100644 --- a/crates/prover/src/lib.rs +++ b/crates/prover/src/lib.rs @@ -17,6 +17,7 @@ //! //! - [`Prover`] - Main proving interface; call [`Prover::setup`] with a verifier, then //! [`Prover::prove`] with witness data +//! - [`IOPProver`] - Core IOP proving logic, independent of the compilation strategy //! - [`KeyCollection`] - Precomputed keys for shift reduction (can be serialized for reuse) //! //! # Related crates @@ -33,6 +34,7 @@ pub mod fold_word; pub mod protocols; mod prove; pub mod ring_switch; +pub mod zk_config; pub use binius_field::arch::OptimalPackedB128; pub use binius_hash as hash; diff --git a/crates/prover/src/prove.rs b/crates/prover/src/prove.rs index 129e3507d..4aa6d0907 100644 --- a/crates/prover/src/prove.rs +++ b/crates/prover/src/prove.rs @@ -1,7 +1,7 @@ // Copyright 2025 Irreducible Inc. use binius_core::{ - constraint_system::{AndConstraint, MulConstraint, ValueVec}, + constraint_system::{AndConstraint, ConstraintSystem, MulConstraint, ValueVec}, verify::eval_operand, word::Word, }; @@ -23,7 +23,7 @@ use binius_math::{ use binius_transcript::{ProverTranscript, fiat_shamir::Challenger}; use binius_utils::{SerializeBytes, checked_arithmetics::checked_log_2, rayon::prelude::*}; use binius_verifier::{ - Verifier, + IOPVerifier, Verifier, and_reduction::verifier::AndCheckOutput, config::{ B1, B128, LOG_WORD_SIZE_BITS, LOG_WORDS_PER_ELEM, PROVER_SMALL_FIELD_ZEROCHECK_CHALLENGES, @@ -53,84 +53,36 @@ type ProverNTT = NeighborsLastMultiThread>; type ProverMerkleProver = BinaryMerkleTreeProver; -/// Struct for proving instances of a particular constraint system. +/// IOP prover for a particular constraint system. /// -/// The [`Self::setup`] constructor pre-processes reusable structures for proving instances of the -/// given constraint system. Then [`Self::prove`] is called one or more times with individual -/// instances. +/// This struct encapsulates the constraint system and pre-computed keys, +/// providing the core proving logic independent of the specific IOP compilation strategy. +/// Most users should use [`Prover`] instead, which wraps this with a BaseFold compiler. #[derive(Debug)] -pub struct Prover -where - P: PackedField, - ParallelMerkleHasher: ParallelDigest, - ParallelMerkleHasher::Digest: Digest + BlockSizeUser + FixedOutputReset, - ParallelMerkleCompress: ParallelPseudoCompression, 2>, -{ +pub struct IOPProver { + constraint_system: ConstraintSystem, + log_public_words: usize, + log_witness_elems: usize, key_collection: KeyCollection, - verifier: Verifier, - #[allow(clippy::type_complexity)] - basefold_compiler: BaseFoldProverCompiler< - P, - ProverNTT, - ProverMerkleProver, - >, } -impl - Prover -where - P: PackedField - + PackedExtension - + PackedExtension - + WithUnderlier, - MerkleHash: Digest + BlockSizeUser + FixedOutputReset, - ParallelMerkleHasher: ParallelDigest, - ParallelMerkleCompress: ParallelPseudoCompression, 2>, - Output: SerializeBytes, -{ - /// Constructs a prover corresponding to a constraint system verifier. - /// - /// See [`Prover`] struct documentation for details. - pub fn setup( - verifier: Verifier, - compression: ParallelMerkleCompress, - ) -> Result { - let key_collection = build_key_collection(verifier.constraint_system()); - Self::setup_with_key_collection(verifier, compression, key_collection) +impl IOPProver { + /// Constructs an IOP prover from an IOP verifier and pre-computed keys. + pub fn new(iop_verifier: IOPVerifier, key_collection: KeyCollection) -> Self { + let log_public_words = iop_verifier.log_public_words(); + let log_witness_elems = iop_verifier.log_witness_elems(); + let constraint_system = iop_verifier.into_constraint_system(); + Self { + constraint_system, + log_public_words, + log_witness_elems, + key_collection, + } } - /// Constructs a prover with a pre-built KeyCollection. - /// - /// This allows loading a previously serialized KeyCollection to avoid - /// the expensive key building phase during setup. - pub fn setup_with_key_collection( - verifier: Verifier, - compression: ParallelMerkleCompress, - key_collection: KeyCollection, - ) -> Result { - // Get max subspace from verifier's IOP compiler (reuses FRI params) - let subspace = verifier.iop_compiler().max_subspace(); - let domain_context = GenericPreExpanded::generate_from_subspace(subspace); - // FIXME TODO For mobile phones, the number of shares should potentially be more than the - // number of threads, because the threads/cores have different performance (but in the NTT - // each share has the same amount of work) - let log_num_shares = binius_utils::rayon::current_num_threads().ilog2() as usize; - let ntt = NeighborsLastMultiThread::new(domain_context, log_num_shares); - - let merkle_prover = BinaryMerkleTreeProver::<_, ParallelMerkleHasher, _>::new(compression); - - // Create prover compiler from verifier compiler (reuses FRI params and oracle specs) - let basefold_compiler = BaseFoldProverCompiler::from_verifier_compiler( - verifier.iop_compiler(), - ntt, - merkle_prover, - ); - - Ok(Prover { - key_collection, - verifier, - basefold_compiler, - }) + /// Returns the constraint system. + pub fn constraint_system(&self) -> &ConstraintSystem { + &self.constraint_system } /// Returns a reference to the KeyCollection. @@ -140,22 +92,19 @@ where &self.key_collection } - pub fn prove( - &self, - witness: ValueVec, - transcript: &mut ProverTranscript, - ) -> Result<(), Error> { - // Create channel and delegate to prove_iop - let channel = BaseFoldProverChannel::from_compiler(&self.basefold_compiler, transcript); - self.prove_iop(witness, channel) - } - - fn prove_iop(&self, witness: ValueVec, mut channel: Channel) -> Result<(), Error> + /// Proves using an IOP channel interface. + /// + /// This is the core proving logic, independent of the specific IOP compilation strategy. + /// For most users, [`Prover::prove`] is the simpler interface. + pub fn prove(&self, witness: ValueVec, mut channel: Channel) -> Result<(), Error> where + P: PackedField + + PackedExtension + + PackedExtension + + WithUnderlier, Channel: IOPProverChannel

, { - let verifier = &self.verifier; - let cs = self.verifier.constraint_system(); + let cs = &self.constraint_system; let _prove_guard = tracing::info_span!( "Prove", @@ -171,11 +120,11 @@ where let setup_guard = tracing::info_span!("[phase] Setup", phase = "setup", perfetto_category = "phase") .entered(); - let witness_packed = pack_witness::

(verifier.log_witness_elems(), &witness)?; + let witness_packed = pack_witness::

(self.log_witness_elems, &witness)?; drop(setup_guard); // Observe the public input as B128 elements (includes it in Fiat-Shamir). - let n_public_elems = 1 << (verifier.log_public_words() - LOG_WORDS_PER_ELEM); + let n_public_elems = 1 << (self.log_public_words - LOG_WORDS_PER_ELEM); let public_elems = witness_packed .iter_scalars() .take(n_public_elems) @@ -296,7 +245,7 @@ where // Public input check batched with ring-switch let log_packing = >::LOG_DEGREE; - let log_public_elems = verifier.log_public_words() - LOG_WORDS_PER_ELEM; + let log_public_elems = self.log_public_words - LOG_WORDS_PER_ELEM; let pubcheck_point = &eval_point[log_packing..][..log_public_elems]; let pubcheck_claim = { let public_elems_buf = FieldSlice::from_slice(log_public_elems, &public_elems); @@ -319,6 +268,108 @@ where } } +/// Struct for proving instances of a particular constraint system. +/// +/// The [`Self::setup`] constructor pre-processes reusable structures for proving instances of the +/// given constraint system. Then [`Self::prove`] is called one or more times with individual +/// instances. +pub struct Prover +where + P: PackedField, + ParallelMerkleHasher: ParallelDigest, + ParallelMerkleHasher::Digest: Digest + BlockSizeUser + FixedOutputReset, + ParallelMerkleCompress: ParallelPseudoCompression, 2>, +{ + iop_prover: IOPProver, + #[allow(clippy::type_complexity)] + basefold_compiler: BaseFoldProverCompiler< + P, + ProverNTT, + ProverMerkleProver, + >, +} + +impl + Prover +where + P: PackedField + + PackedExtension + + PackedExtension + + WithUnderlier, + MerkleHash: Digest + BlockSizeUser + FixedOutputReset, + ParallelMerkleHasher: ParallelDigest, + ParallelMerkleCompress: ParallelPseudoCompression, 2>, + Output: SerializeBytes, +{ + /// Constructs a prover corresponding to a constraint system verifier. + /// + /// See [`Prover`] struct documentation for details. + pub fn setup( + verifier: Verifier, + compression: ParallelMerkleCompress, + ) -> Result { + let key_collection = build_key_collection(verifier.constraint_system()); + Self::setup_with_key_collection(verifier, compression, key_collection) + } + + /// Constructs a prover with a pre-built KeyCollection. + /// + /// This allows loading a previously serialized KeyCollection to avoid + /// the expensive key building phase during setup. + pub fn setup_with_key_collection( + verifier: Verifier, + compression: ParallelMerkleCompress, + key_collection: KeyCollection, + ) -> Result { + // Get max subspace from verifier's IOP compiler (reuses FRI params) + let subspace = verifier.iop_compiler().max_subspace(); + let domain_context = GenericPreExpanded::generate_from_subspace(subspace); + // FIXME TODO For mobile phones, the number of shares should potentially be more than the + // number of threads, because the threads/cores have different performance (but in the NTT + // each share has the same amount of work) + let log_num_shares = binius_utils::rayon::current_num_threads().ilog2() as usize; + let ntt = NeighborsLastMultiThread::new(domain_context, log_num_shares); + + let merkle_prover = BinaryMerkleTreeProver::<_, ParallelMerkleHasher, _>::new(compression); + + // Create prover compiler from verifier compiler (reuses FRI params and oracle specs) + let basefold_compiler = BaseFoldProverCompiler::from_verifier_compiler( + verifier.iop_compiler(), + ntt, + merkle_prover, + ); + + let iop_prover = IOPProver::new(verifier.into_iop_verifier(), key_collection); + + Ok(Prover { + iop_prover, + basefold_compiler, + }) + } + + /// Returns a reference to the IOP prover. + pub fn iop_prover(&self) -> &IOPProver { + &self.iop_prover + } + + /// Returns a reference to the KeyCollection. + /// + /// This can be used to serialize the KeyCollection for later use. + pub fn key_collection(&self) -> &KeyCollection { + self.iop_prover.key_collection() + } + + pub fn prove( + &self, + witness: ValueVec, + transcript: &mut ProverTranscript, + ) -> Result<(), Error> { + // Create channel and delegate to IOPProver::prove + let channel = BaseFoldProverChannel::from_compiler(&self.basefold_compiler, transcript); + self.iop_prover.prove::(witness, channel) + } +} + /// Batches the pubcheck transparent polynomial with the ring-switch equality indicator. /// /// Computes `rs_eq_ind + batch_coeff * eq(pubcheck_point || 0, ยท)`, adding the scaled diff --git a/crates/prover/src/zk_config.rs b/crates/prover/src/zk_config.rs new file mode 100644 index 000000000..1e32df700 --- /dev/null +++ b/crates/prover/src/zk_config.rs @@ -0,0 +1,182 @@ +// Copyright 2026 The Binius Developers + +//! Zero-knowledge proving configuration for Binius64 constraint systems. +//! +//! This module provides [`ZKProver`], which wraps the Binius64 IOP prover with a +//! Spartan-based zero-knowledge wrapper. The prover counterpart to +//! [`binius_verifier::zk_config::ZKVerifier`]. + +use binius_core::{constraint_system::ValueVec, word::Word}; +use binius_field::{ + BinaryField128bGhash as B128, PackedExtension, PackedField, UnderlierWithBitOps, WithUnderlier, +}; +use binius_iop_prover::basefold_compiler::BaseFoldZKProverCompiler; +use binius_math::ntt::{NeighborsLastMultiThread, domain_context::GenericPreExpanded}; +use binius_spartan_frontend::{ + circuit_builder::ConstraintBuilder, compiler::compile, constraint_system::WitnessLayout, +}; +use binius_spartan_prover::wrapper::ZKWrappedProverChannel; +use binius_spartan_verifier::{constraint_system::ConstraintSystemPadded, wrapper::ReplayChannel}; +use binius_transcript::{ProverTranscript, fiat_shamir::Challenger}; +use binius_utils::SerializeBytes; +use binius_verifier::{IOPVerifier, zk_config::ZKVerifier}; +use digest::{Digest, FixedOutputReset, Output, core_api::BlockSizeUser}; +use rand::CryptoRng; + +use crate::{ + IOPProver, + hash::{ParallelDigest, parallel_compression::ParallelPseudoCompression}, + merkle_tree::prover::BinaryMerkleTreeProver, + protocols::shift::build_key_collection, +}; + +type ProverNTT = NeighborsLastMultiThread>; +type ProverMerkleProver = + BinaryMerkleTreeProver; + +/// Zero-knowledge prover for Binius64 constraint systems. +/// +/// Wraps the Binius64 IOP prover with a Spartan-based ZK wrapper. Call [`Self::setup`] with +/// a [`ZKVerifier`], then [`Self::prove`] with witness data and a proof transcript. +pub struct ZKProver +where + P: PackedField, + ParallelMerkleHasher: ParallelDigest, + ParallelMerkleHasher::Digest: Digest + BlockSizeUser + FixedOutputReset, + ParallelMerkleCompress: ParallelPseudoCompression, 2>, +{ + inner_iop_prover: IOPProver, + inner_iop_verifier: IOPVerifier, + outer_iop_prover: binius_spartan_prover::IOPProver, + outer_layout: WitnessLayout, + #[allow(clippy::type_complexity)] + basefold_compiler: BaseFoldZKProverCompiler< + P, + ProverNTT, + ProverMerkleProver, + >, +} + +impl + ZKProver +where + P: PackedField + + PackedExtension + + PackedExtension + + WithUnderlier, + MerkleHash: Digest + BlockSizeUser + FixedOutputReset, + ParallelMerkleHasher: ParallelDigest, + ParallelMerkleCompress: ParallelPseudoCompression, 2>, + Output: SerializeBytes, +{ + /// Constructs a ZK prover from a [`ZKVerifier`]. + pub fn setup( + zk_verifier: ZKVerifier, + compression: ParallelMerkleCompress, + ) -> Result { + // Build the inner IOPProver. + let inner_iop_verifier = zk_verifier.inner_iop_verifier().clone(); + let key_collection = build_key_collection(inner_iop_verifier.constraint_system()); + let inner_iop_prover = IOPProver::new(inner_iop_verifier.clone(), key_collection); + + // Re-derive the outer constraint system and layout via symbolic execution. + let dummy_public_words = + vec![Word::from_u64(0); 1 << inner_iop_verifier.log_public_words()]; + let mut builder_channel = binius_spartan_verifier::wrapper::IronSpartanBuilderChannel::new( + ConstraintBuilder::new(), + ); + inner_iop_verifier + .verify(&dummy_public_words, &mut builder_channel) + .expect("symbolic verify should not fail"); + let outer_builder = builder_channel.finish(); + let (outer_cs, outer_layout) = compile(outer_builder); + + // Pad the outer constraint system with the same blinding as the verifier. + let outer_cs = ConstraintSystemPadded::new( + outer_cs, + zk_verifier + .outer_iop_verifier() + .constraint_system() + .blinding_info() + .clone(), + ); + let outer_layout = outer_layout.with_blinding(outer_cs.blinding_info().clone()); + + let outer_iop_prover = binius_spartan_prover::IOPProver::new(outer_cs); + + // Build the BaseFoldZK prover compiler from the verifier compiler. + let subspace = zk_verifier.basefold_compiler().max_subspace(); + let domain_context = GenericPreExpanded::generate_from_subspace(subspace); + let log_num_shares = binius_utils::rayon::current_num_threads().ilog2() as usize; + let ntt = NeighborsLastMultiThread::new(domain_context, log_num_shares); + let merkle_prover = BinaryMerkleTreeProver::<_, ParallelMerkleHasher, _>::new(compression); + let basefold_compiler = BaseFoldZKProverCompiler::from_verifier_compiler( + zk_verifier.basefold_compiler(), + ntt, + merkle_prover, + ); + + Ok(Self { + inner_iop_prover, + inner_iop_verifier, + outer_iop_prover, + outer_layout, + basefold_compiler, + }) + } + + /// Returns a reference to the inner IOP prover. + pub fn inner_iop_prover(&self) -> &IOPProver { + &self.inner_iop_prover + } + + /// Returns a reference to the KeyCollection. + pub fn key_collection(&self) -> &crate::protocols::shift::KeyCollection { + self.inner_iop_prover.key_collection() + } + + /// Generates a ZK proof for a witness. + pub fn prove( + &self, + witness: ValueVec, + mut rng: impl CryptoRng, + transcript: &mut ProverTranscript, + ) -> Result<(), Error> { + // Clone public words before moving witness into prove(). + let public_words = witness.public().to_vec(); + + // Create BaseFoldZK prover channel and wrap with outer prover. + let basefold_channel = self.basefold_compiler.create_channel(transcript, &mut rng); + let mut wrapped_channel = ZKWrappedProverChannel::new( + basefold_channel, + &self.outer_iop_prover, + &self.outer_layout, + { + let inner_iop_verifier = &self.inner_iop_verifier; + move |replay_channel: &mut ReplayChannel<'_, B128>| { + inner_iop_verifier + .verify(&public_words, replay_channel) + .expect("replay verification should not fail"); + } + }, + ); + + // Run the inner IOP proof through the wrapped channel. + self.inner_iop_prover + .prove::(witness, &mut wrapped_channel)?; + + // Finish runs the outer spartan proof. + wrapped_channel.finish(rng)?; + + Ok(()) + } +} + +/// Error type for ZK proving. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("inner proving error: {0}")] + InnerProving(#[from] crate::error::Error), + #[error("outer proving error: {0}")] + OuterProving(#[from] binius_spartan_prover::Error), +} diff --git a/crates/prover/tests/prove_verify.rs b/crates/prover/tests/prove_verify.rs index a4f468329..cfd0e4b1d 100644 --- a/crates/prover/tests/prove_verify.rs +++ b/crates/prover/tests/prove_verify.rs @@ -7,13 +7,17 @@ use binius_core::{ }; use binius_field::arch::OptimalPackedB128; use binius_frontend::{CircuitBuilder, Wire}; -use binius_prover::{Prover, hash::parallel_compression::ParallelCompressionAdaptor}; +use binius_prover::{ + Prover, hash::parallel_compression::ParallelCompressionAdaptor, zk_config::ZKProver, +}; use binius_transcript::ProverTranscript; use binius_verifier::{ Verifier, config::StdChallenger, hash::{StdCompression, StdDigest}, + zk_config::ZKVerifier, }; +use rand::{SeedableRng, rngs::StdRng}; fn prove_verify(cs: ConstraintSystem, witness: ValueVec) { const LOG_INV_RATE: usize = 1; @@ -39,8 +43,32 @@ fn prove_verify(cs: ConstraintSystem, witness: ValueVec) { verifier_transcript.finalize().unwrap(); } -#[test] -fn test_prove_verify_sha256_preimage() { +fn prove_verify_zk(cs: ConstraintSystem, witness: ValueVec) { + const LOG_INV_RATE: usize = 1; + + let zk_verifier = + ZKVerifier::::setup(cs, LOG_INV_RATE, StdCompression::default()).unwrap(); + + let zk_prover = ZKProver::::setup( + zk_verifier.clone(), + ParallelCompressionAdaptor::new(StdCompression::default()), + ) + .unwrap(); + + let mut rng = StdRng::seed_from_u64(0); + let mut prover_transcript = ProverTranscript::new(StdChallenger::default()); + zk_prover + .prove(witness.clone(), &mut rng, &mut prover_transcript) + .unwrap(); + + let mut verifier_transcript = prover_transcript.into_verifier(); + zk_verifier + .verify(witness.public(), &mut verifier_transcript) + .unwrap(); + verifier_transcript.finalize().unwrap(); +} + +fn sha256_preimage_circuit() -> (ConstraintSystem, ValueVec) { // Use the test-vector for SHA256 single block message: "abc". let mut preimage: [u8; 64] = [0; 64]; preimage[0..3].copy_from_slice(b"abc"); @@ -76,5 +104,17 @@ fn test_prove_verify_sha256_preimage() { } circuit.populate_wire_witness(&mut w).unwrap(); - prove_verify(circuit.constraint_system().clone(), w.into_value_vec()) + (circuit.constraint_system().clone(), w.into_value_vec()) +} + +#[test] +fn test_prove_verify_sha256_preimage() { + let (cs, witness) = sha256_preimage_circuit(); + prove_verify(cs, witness); +} + +#[test] +fn test_zk_prove_verify_sha256_preimage() { + let (cs, witness) = sha256_preimage_circuit(); + prove_verify_zk(cs, witness); } diff --git a/crates/spartan-prover/Cargo.toml b/crates/spartan-prover/Cargo.toml index e6478768a..8de3e5e59 100644 --- a/crates/spartan-prover/Cargo.toml +++ b/crates/spartan-prover/Cargo.toml @@ -9,18 +9,16 @@ workspace = true [dependencies] binius-field = { path = "../field" } +binius-hash = { path = "../hash" } binius-iop = { path = "../iop" } -binius-iop-prover = { path = "../iop-prover" } +binius-iop-prover = { path = "../iop-prover", default-features = false } binius-ip = { path = "../ip" } -binius-ip-prover = { path = "../ip-prover" } +binius-ip-prover = { path = "../ip-prover", default-features = false } binius-math = { path = "../math" } binius-transcript = { path = "../transcript" } binius-spartan-frontend = { path = "../spartan-frontend" } binius-spartan-verifier = { path = "../spartan-verifier" } binius-utils = { path = "../utils" } -# Reuse infrastructure from binius-prover -binius-prover = { path = "../prover" } -binius-verifier = { path = "../verifier" } digest.workspace = true itertools.workspace = true rand.workspace = true diff --git a/crates/spartan-prover/src/error.rs b/crates/spartan-prover/src/error.rs index 0bdba517e..fd08f2735 100644 --- a/crates/spartan-prover/src/error.rs +++ b/crates/spartan-prover/src/error.rs @@ -5,13 +5,11 @@ pub enum Error { #[error("invalid argument {arg}: {msg}")] ArgumentError { arg: String, msg: String }, #[error("FRI error: {0}")] - Fri(#[from] binius_prover::fri::Error), + Fri(#[from] binius_iop_prover::fri::Error), #[error("basefold error: {0}")] - Basefold(#[from] binius_prover::protocols::basefold::Error), + Basefold(#[from] binius_iop_prover::basefold::Error), #[error("transcript error: {0}")] Transcript(#[from] binius_transcript::Error), - #[error("prover error: {0}")] - Prover(#[from] binius_prover::Error), #[error("sumcheck error: {0}")] - Sumcheck(#[from] binius_prover::protocols::sumcheck::Error), + Sumcheck(#[from] binius_ip_prover::sumcheck::Error), } diff --git a/crates/spartan-prover/src/lib.rs b/crates/spartan-prover/src/lib.rs index 1649dee0f..47c7287c8 100644 --- a/crates/spartan-prover/src/lib.rs +++ b/crates/spartan-prover/src/lib.rs @@ -37,17 +37,19 @@ use std::{ }; use binius_field::{BinaryField, Field, PackedExtension, PackedField}; -use binius_iop_prover::{basefold_compiler::BaseFoldZKProverCompiler, channel::IOPProverChannel}; -use binius_ip_prover::channel::IPProverChannel; +use binius_hash::{ParallelDigest, parallel_compression::ParallelPseudoCompression}; +use binius_iop_prover::{ + basefold_compiler::BaseFoldZKProverCompiler, channel::IOPProverChannel, + merkle_tree::prover::BinaryMerkleTreeProver, +}; +use binius_ip_prover::{ + channel::IPProverChannel, + sumcheck::{quadratic_mle::QuadraticMleCheckProver, zk_mlecheck}, +}; use binius_math::{ FieldBuffer, FieldSlice, ntt::{NeighborsLastMultiThread, domain_context::GenericPreExpanded}, }; -use binius_prover::{ - hash::{ParallelDigest, parallel_compression::ParallelPseudoCompression}, - merkle_tree::prover::BinaryMerkleTreeProver, - protocols::sumcheck::{quadratic_mle::QuadraticMleCheckProver, zk_mlecheck}, -}; use binius_spartan_frontend::constraint_system::{MulConstraint, WitnessIndex}; use binius_spartan_verifier::{ Verifier, diff --git a/crates/spartan-prover/src/wrapper/zk_wrapped_prover_channel.rs b/crates/spartan-prover/src/wrapper/zk_wrapped_prover_channel.rs index 3bfc7a201..41a0f04de 100644 --- a/crates/spartan-prover/src/wrapper/zk_wrapped_prover_channel.rs +++ b/crates/spartan-prover/src/wrapper/zk_wrapped_prover_channel.rs @@ -1,15 +1,14 @@ // Copyright 2026 The Binius Developers -//! ZK-wrapped prover channel that runs an inner Spartan proof and then proves the outer +//! ZK-wrapped prover channel that runs an inner proof and then proves the outer //! wrapper constraint system. //! //! [`ZKWrappedProverChannel`] wraps a [`BaseFoldZKProverChannel`] and records all channel values. //! On `send_*`/`sample`/`observe_*`, it delegates to the inner BaseFoldZK channel and records //! each value. After the inner proof is run, [`finish`] replays the recorded interaction through -//! a [`ReplayChannel`] to fill the outer witness, then runs the outer IOP prover. +//! a caller-provided closure to fill the outer witness, then runs the outer IOP prover. //! //! [`BaseFoldZKProverChannel`]: binius_iop_prover::basefold_zk_channel::BaseFoldZKProverChannel -//! [`ReplayChannel`]: binius_spartan_verifier::wrapper::ReplayChannel //! [`finish`]: ZKWrappedProverChannel::finish use binius_field::{BinaryField, PackedExtension, PackedField}; @@ -19,7 +18,6 @@ use binius_iop_prover::{ channel::IOPProverChannel, merkle_tree::MerkleTreeProver, }; -use binius_ip::channel::IPVerifierChannel; use binius_ip_prover::channel::IPProverChannel; use binius_math::{FieldBuffer, FieldSlice, ntt::AdditiveNTT}; use binius_spartan_frontend::constraint_system::WitnessLayout; @@ -35,9 +33,13 @@ use crate::IOPProver; /// This channel records all channel values. On /// `send_*`/`sample`/`observe_*`, it delegates to the inner BaseFoldZK channel and records each /// value. After the inner proof is run through this channel, call -/// [`finish`](Self::finish) to replay the interaction through a [`ReplayChannel`], fill the outer -/// witness, and generate the outer proof. -pub struct ZKWrappedProverChannel<'a, P, NTT, MTProver, Challenger_> +/// [`finish`](Self::finish) to replay the interaction, fill the outer witness, and generate the +/// outer proof. +/// +/// The `ReplayFn` closure is called during [`finish`](Self::finish) with a [`ReplayChannel`] to +/// replay the inner verification and fill the outer witness. This allows the channel to be generic +/// over different inner verification protocols. +pub struct ZKWrappedProverChannel<'a, P, NTT, MTProver, Challenger_, ReplayFn> where P: PackedField, NTT: AdditiveNTT + Sync, @@ -46,14 +48,14 @@ where { inner_channel: BaseFoldZKProverChannel<'a, P::Scalar, P, NTT, MTProver, Challenger_>, outer_prover: &'a IOPProver, - inner_verifier: &'a IOPVerifier, outer_layout: &'a WitnessLayout, + replay_fn: ReplayFn, interaction: Vec, n_outer_oracles: usize, } -impl<'a, F, P, NTT, MTScheme, MTProver, Challenger_> - ZKWrappedProverChannel<'a, P, NTT, MTProver, Challenger_> +impl<'a, F, P, NTT, MTScheme, MTProver, Challenger_, ReplayFn> + ZKWrappedProverChannel<'a, P, NTT, MTProver, Challenger_, ReplayFn> where F: BinaryField, P: PackedField + PackedExtension, @@ -69,13 +71,14 @@ where /// * `inner_channel` - The BaseFold ZK channel with oracle specs for both inner and outer /// proofs /// * `outer_prover` - The IOP prover for the outer (wrapper) constraint system - /// * `inner_verifier` - The IOP verifier for the inner constraint system (used for replay) /// * `outer_layout` - The witness layout for the outer constraint system + /// * `replay_fn` - Closure called during [`finish`](Self::finish) with a [`ReplayChannel`] to + /// replay the inner verification and fill the outer witness pub fn new( inner_channel: BaseFoldZKProverChannel<'a, F, P, NTT, MTProver, Challenger_>, outer_prover: &'a IOPProver, - inner_verifier: &'a IOPVerifier, outer_layout: &'a WitnessLayout, + replay_fn: ReplayFn, ) -> Self { let outer_oracle_specs = IOPVerifier::new(outer_prover.constraint_system().clone()).oracle_specs(); @@ -95,8 +98,8 @@ where Self { inner_channel, outer_prover, - inner_verifier, outer_layout, + replay_fn, interaction: Vec::new(), n_outer_oracles: n_outer, } @@ -104,36 +107,27 @@ where /// Consumes the channel and runs the outer proof. /// - /// This should be called after the inner proof has been run through this channel - /// (via [`IOPProver::prove`]). It: - /// 1. Replays the recorded interaction through a [`ReplayChannel`] to fill the outer witness - /// 2. Validates and generates the outer IOP proof - /// - /// [`ReplayChannel`]: binius_spartan_verifier::wrapper::ReplayChannel - pub fn finish(self, rng: impl CryptoRng) -> Result<(), crate::Error> { + /// This should be called after the inner proof has been run through this channel. + /// It: + /// 1. Creates a [`ReplayChannel`] from the recorded interaction + /// 2. Calls the `replay_fn` closure to replay the inner verification and fill the outer witness + /// 3. Validates and generates the outer IOP proof + pub fn finish(self, rng: impl CryptoRng) -> Result<(), crate::Error> + where + ReplayFn: FnOnce(&mut ReplayChannel<'_, F>), + { let Self { inner_channel, outer_prover, - inner_verifier, outer_layout, + replay_fn, interaction, .. } = self; - // Extract inner public values from the initial events. - let inner_cs = inner_verifier.constraint_system(); - let inner_public_size = 1 << inner_cs.log_public(); - let public: Vec = interaction[..inner_public_size].to_vec(); - // Replay the inner verification through the outer witness generator. - // First observe the public input (mirrors the prover-side observe_many). let mut replay_channel = ReplayChannel::new(outer_layout, interaction); - let inner_public_elems = replay_channel.observe_many(&public); - - // Run the inner verification to fill private wires. - inner_verifier - .verify(inner_public_elems, &mut replay_channel) - .expect("replay verification should not fail"); + replay_fn(&mut replay_channel); let witness = replay_channel .finish() .expect("outer witness generation should not fail"); @@ -146,8 +140,8 @@ where } } -impl IPProverChannel - for &mut ZKWrappedProverChannel<'_, P, NTT, MTProver, Challenger_> +impl IPProverChannel + for &mut ZKWrappedProverChannel<'_, P, NTT, MTProver, Challenger_, ReplayFn> where F: BinaryField, P: PackedField + PackedExtension, @@ -183,8 +177,8 @@ where } } -impl IOPProverChannel

- for &mut ZKWrappedProverChannel<'_, P, NTT, MTProver, Challenger_> +impl IOPProverChannel

+ for &mut ZKWrappedProverChannel<'_, P, NTT, MTProver, Challenger_, ReplayFn> where F: BinaryField, P: PackedField + PackedExtension, diff --git a/crates/spartan-prover/tests/wrapper_integration_test.rs b/crates/spartan-prover/tests/wrapper_integration_test.rs index 97fc80001..64e4ce0df 100644 --- a/crates/spartan-prover/tests/wrapper_integration_test.rs +++ b/crates/spartan-prover/tests/wrapper_integration_test.rs @@ -3,7 +3,9 @@ use binius_field::{BinaryField128bGhash as B128, Field, Random, arch::OptimalPackedB128}; use binius_hash::{ParallelCompressionAdaptor, StdCompression, StdDigest}; use binius_iop::{ - basefold_compiler::BaseFoldZKVerifierCompiler, merkle_tree::BinaryMerkleTreeScheme, + basefold_compiler::BaseFoldZKVerifierCompiler, + fri::{self, MinProofSizeStrategy}, + merkle_tree::BinaryMerkleTreeScheme, }; use binius_iop_prover::{ basefold_compiler::BaseFoldZKProverCompiler, merkle_tree::prover::BinaryMerkleTreeProver, @@ -22,10 +24,9 @@ use binius_spartan_verifier::{ IOPVerifier, SECURITY_BITS, config::StdChallenger, constraint_system::ConstraintSystemPadded, - wrapper::{IronSpartanBuilderChannel, ZKWrappedVerifierChannel}, + wrapper::{IronSpartanBuilderChannel, ReplayChannel, ZKWrappedVerifierChannel}, }; use binius_transcript::ProverTranscript; -use binius_verifier::fri::{self, MinProofSizeStrategy}; use rand::{SeedableRng, rngs::StdRng}; /// Build a power7 circuit: assert that x^7 = y @@ -132,12 +133,16 @@ fn test_zk_wrapped_prove_verify() { prover_transcript.observe().write_slice(public); let basefold_channel = zk_basefold_prover.create_channel(&mut prover_transcript, &mut rng); - let mut wrapped_prover_channel = ZKWrappedProverChannel::new( - basefold_channel, - &outer_iop_prover, - &inner_iop_verifier, - &outer_layout, - ); + let mut wrapped_prover_channel = + ZKWrappedProverChannel::new(basefold_channel, &outer_iop_prover, &outer_layout, { + let inner_iop_verifier = &inner_iop_verifier; + |replay_channel: &mut ReplayChannel<'_, B128>| { + let inner_public_elems = replay_channel.observe_many(public); + inner_iop_verifier + .verify(inner_public_elems, replay_channel) + .expect("replay verification should not fail"); + } + }); // Observe public input through the wrapped channel. (&mut wrapped_prover_channel).observe_many(public); diff --git a/crates/spartan-verifier/Cargo.toml b/crates/spartan-verifier/Cargo.toml index d84ad8010..e7a205495 100644 --- a/crates/spartan-verifier/Cargo.toml +++ b/crates/spartan-verifier/Cargo.toml @@ -9,14 +9,13 @@ workspace = true [dependencies] binius-field = { path = "../field" } +binius-hash = { path = "../hash", default-features = false } binius-iop = { path = "../iop" } binius-ip = { path = "../ip" } binius-math = { path = "../math" } binius-transcript = { path = "../transcript" } binius-spartan-frontend = { path = "../spartan-frontend" } binius-utils = { path = "../utils" } -# TODO: Refactor common protocols out into shared crate -binius-verifier = { path = "../verifier" } digest.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/spartan-verifier/src/config.rs b/crates/spartan-verifier/src/config.rs index f7c706297..372506167 100644 --- a/crates/spartan-verifier/src/config.rs +++ b/crates/spartan-verifier/src/config.rs @@ -1,10 +1,10 @@ // Copyright 2025 Irreducible Inc. use binius_field::{BinaryField1b, BinaryField128bGhash}; -pub use binius_verifier::{ - config::StdChallenger, - hash::{StdCompression, StdDigest}, -}; +pub use binius_hash::{StdCompression, StdDigest}; + +/// The default [`binius_transcript::fiat_shamir::Challenger`] implementation. +pub type StdChallenger = binius_transcript::fiat_shamir::HasherChallenger; pub type B1 = BinaryField1b; pub type B128 = BinaryField128bGhash; diff --git a/crates/spartan-verifier/src/constraint_system.rs b/crates/spartan-verifier/src/constraint_system.rs index c16918925..1a2176bb7 100644 --- a/crates/spartan-verifier/src/constraint_system.rs +++ b/crates/spartan-verifier/src/constraint_system.rs @@ -2,12 +2,12 @@ // Copyright 2026 The Binius Developers use binius_field::Field; +use binius_ip::mlecheck::mask_buffer_dimensions; pub use binius_spartan_frontend::constraint_system::BlindingInfo; use binius_spartan_frontend::constraint_system::{ ConstraintSystem, MulConstraint, Operand, WitnessIndex, }; use binius_utils::checked_arithmetics::{checked_log_2, log2_ceil_usize}; -use binius_verifier::protocols::mlecheck::mask_buffer_dimensions; /// A constraint system with blinding and power-of-two padding. /// diff --git a/crates/spartan-verifier/src/lib.rs b/crates/spartan-verifier/src/lib.rs index e9adf7ba3..93a1e2358 100644 --- a/crates/spartan-verifier/src/lib.rs +++ b/crates/spartan-verifier/src/lib.rs @@ -34,21 +34,19 @@ pub mod wiring; pub mod wrapper; use binius_field::{BinaryField, Field, field::FieldOps}; +use binius_hash::PseudoCompressionFunction; use binius_iop::{ + basefold, basefold_compiler::BaseFoldZKVerifierCompiler, channel::{IOPVerifierChannel, OracleLinearRelation, OracleSpec}, + fri::{self, MinProofSizeStrategy}, + merkle_tree::BinaryMerkleTreeScheme, }; -use binius_ip::channel::IPVerifierChannel; +use binius_ip::{channel::IPVerifierChannel, mlecheck, sumcheck}; use binius_math::multilinear::evaluate::evaluate_inplace_scalars; use binius_spartan_frontend::constraint_system::ConstraintSystem; use binius_transcript::{VerifierTranscript, fiat_shamir::Challenger}; use binius_utils::{DeserializeBytes, checked_arithmetics::checked_log_2}; -use binius_verifier::{ - fri::{self, MinProofSizeStrategy}, - hash::PseudoCompressionFunction, - merkle_tree::BinaryMerkleTreeScheme, - protocols::{basefold, mlecheck, sumcheck}, -}; use digest::{Digest, Output, core_api::BlockSizeUser}; use crate::constraint_system::{BlindingInfo, ConstraintSystemPadded}; diff --git a/crates/spartan-verifier/src/wiring.rs b/crates/spartan-verifier/src/wiring.rs index 55c2c2b3e..58c35c0ac 100644 --- a/crates/spartan-verifier/src/wiring.rs +++ b/crates/spartan-verifier/src/wiring.rs @@ -3,13 +3,13 @@ use std::iter; use binius_field::{Field, field::FieldOps}; -use binius_ip::channel::IPVerifierChannel; +use binius_iop::basefold; +use binius_ip::{channel::IPVerifierChannel, sumcheck}; use binius_math::{ multilinear::eq::{eq_ind, eq_ind_partial_eval_scalars, eq_one_var}, univariate::evaluate_univariate, }; use binius_spartan_frontend::constraint_system::{MulConstraint, WitnessIndex}; -use binius_verifier::protocols::{basefold, sumcheck}; use crate::constraint_system::ConstraintSystemPadded; diff --git a/crates/spartan-verifier/src/wrapper/circuit_elem.rs b/crates/spartan-verifier/src/wrapper/circuit_elem.rs index a18d5c1cb..93caa63e7 100644 --- a/crates/spartan-verifier/src/wrapper/circuit_elem.rs +++ b/crates/spartan-verifier/src/wrapper/circuit_elem.rs @@ -400,3 +400,9 @@ impl FieldOps for CircuitElem { } } } + +impl> From for CircuitElem { + fn from(val: F) -> Self { + CircuitElem::Constant(val) + } +} diff --git a/crates/utils/src/rayon/iter/mod.rs b/crates/utils/src/rayon/iter/mod.rs index ebf77f86c..eb6417f16 100644 --- a/crates/utils/src/rayon/iter/mod.rs +++ b/crates/utils/src/rayon/iter/mod.rs @@ -10,7 +10,11 @@ mod par_bridge; mod parallel_iterator; mod parallel_wrapper; -pub use core::iter::{empty, once, repeat, repeat_n as repeatn}; +pub use core::iter::{empty, once, repeat}; + +pub fn repeat_n(elt: T, n: usize) -> ParallelWrapper> { + ParallelWrapper::new(core::iter::repeat_n(elt, n)) +} pub use from_parallel_iterator::FromParallelIterator; pub use indexed_parallel_iterator::IndexedParallelIterator; diff --git a/crates/verifier/Cargo.toml b/crates/verifier/Cargo.toml index 432be93a2..2052b5d9d 100644 --- a/crates/verifier/Cargo.toml +++ b/crates/verifier/Cargo.toml @@ -15,6 +15,8 @@ binius-hash = { path = "../hash", default-features = false } binius-iop = { path = "../iop", default-features = false } binius-math = { path = "../math" } binius-transcript = { path = "../transcript" } +binius-spartan-frontend = { path = "../spartan-frontend", default-features = false } +binius-spartan-verifier = { path = "../spartan-verifier", default-features = false } binius-utils = { path = "../utils" } bytemuck.workspace = true bytes.workspace = true @@ -29,7 +31,3 @@ binius-math = { path = "../math", features = ["test-utils"] } rand.workspace = true itertools.workspace = true sha2.workspace = true - -[features] -default = ["rayon"] -rayon = ["binius-utils/rayon", "binius-hash/rayon", "binius-iop/rayon"] diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs index 8849fe573..b17a18e63 100644 --- a/crates/verifier/src/lib.rs +++ b/crates/verifier/src/lib.rs @@ -16,6 +16,7 @@ //! //! - [`Verifier`] - Main verification interface; call [`Verifier::setup`] with a constraint system, //! then [`Verifier::verify`] with a proof and public inputs +//! - [`IOPVerifier`] - Core IOP verification logic, independent of the compilation strategy //! - [`VerificationError`] - Error type returned when proof verification fails //! //! # Design philosophy @@ -38,6 +39,7 @@ mod error; pub mod protocols; pub mod ring_switch; mod verify; +pub mod zk_config; pub use binius_hash as hash; pub use binius_iop::{fri, merkle_tree}; diff --git a/crates/verifier/src/verify.rs b/crates/verifier/src/verify.rs index 81532c98e..3ff1d4cc9 100644 --- a/crates/verifier/src/verify.rs +++ b/crates/verifier/src/verify.rs @@ -39,88 +39,27 @@ use crate::{ pub const SECURITY_BITS: usize = 96; -/// Struct for verifying instances of a particular constraint system. +/// IOP verifier for a particular constraint system. /// -/// The [`Self::setup`] constructor determines public parameters for proving instances of the given -/// constraint system. Then [`Self::verify`] is called one or more times with individual instances. +/// This struct encapsulates the constraint system, providing the core verification logic +/// independent of the specific IOP compilation strategy. Most users should use [`Verifier`] +/// instead, which wraps this with a BaseFold compiler. #[derive(Debug, Clone)] -pub struct Verifier -where - MerkleHash: Digest + BlockSizeUser, - MerkleCompress: PseudoCompressionFunction, 2>, -{ +pub struct IOPVerifier { constraint_system: ConstraintSystem, - iop_compiler: - BaseFoldVerifierCompiler>, log_public_words: usize, } -impl Verifier -where - MerkleHash: Digest + BlockSizeUser, - MerkleCompress: PseudoCompressionFunction, 2>, - Output: DeserializeBytes, -{ - /// Constructs a verifier for a constraint system. +impl IOPVerifier { + /// Constructs an IOP verifier for a constraint system. /// - /// See [`Verifier`] struct documentation for details. - pub fn setup( - mut constraint_system: ConstraintSystem, - log_inv_rate: usize, - compression: MerkleCompress, - ) -> Result { - constraint_system.validate_and_prepare()?; - - // Use offset_witness which is guaranteed to be power of two and be at least one full - // element. - let n_public = constraint_system.value_vec_layout.offset_witness; - let log_public_words = log2_ceil_usize(n_public); - assert!(n_public.is_power_of_two()); - assert!(log_public_words >= LOG_WORDS_PER_ELEM); - - // The number of field elements that constitute the packed witness. - let log_witness_words = - log2_ceil_usize(constraint_system.value_vec_len()).max(LOG_WORDS_PER_ELEM); - let log_witness_elems = log_witness_words - LOG_WORDS_PER_ELEM; - - let log_code_len = log_witness_elems + log_inv_rate; - let merkle_scheme = BinaryMerkleTreeScheme::new(compression); - let fri_arity = - ConstantArityStrategy::with_optimal_arity::(&merkle_scheme, log_code_len) - .arity; - - let n_test_queries = calculate_n_test_queries(SECURITY_BITS, log_inv_rate); - - // Create oracle spec for the single witness oracle (not ZK) - let oracle_specs = vec![OracleSpec { - log_msg_len: log_witness_elems, - }]; - - let iop_compiler = BaseFoldVerifierCompiler::new( - merkle_scheme, - oracle_specs, - log_inv_rate, - n_test_queries, - &ConstantArityStrategy::new(fri_arity), - ); - - Ok(Self { + /// The constraint system must already be validated via + /// [`ConstraintSystem::validate_and_prepare`]. + pub fn new(constraint_system: ConstraintSystem, log_public_words: usize) -> Self { + Self { constraint_system, - iop_compiler, log_public_words, - }) - } - - /// Returns log2 of the number of words in the witness. - pub fn log_witness_words(&self) -> usize { - self.log_witness_elems() + LOG_WORDS_PER_ELEM - } - - /// Returns log2 of the number of field elements in the packed trace. - pub fn log_witness_elems(&self) -> usize { - let fri_params = self.fri_params(); - let rs_code = fri_params.rs_code(); - rs_code.log_dim() + fri_params.log_batch_size() + } } /// Returns the constraint system. @@ -128,15 +67,9 @@ where &self.constraint_system } - /// Returns the chosen FRI parameters. - pub fn fri_params(&self) -> &FRIParams { - // There is exactly one oracle spec (the witness) - &self.iop_compiler.fri_params()[0] - } - - /// Returns the [`crate::merkle_tree::MerkleTreeScheme`] instance used. - pub fn merkle_scheme(&self) -> &BinaryMerkleTreeScheme { - self.iop_compiler.merkle_scheme() + /// Consumes the IOP verifier and returns the inner constraint system. + pub fn into_constraint_system(self) -> ConstraintSystem { + self.constraint_system } /// Returns log2 of the number of public constants and input/output words. @@ -144,27 +77,35 @@ where self.log_public_words } - /// Returns the IOP compiler for creating verifier channels. - pub fn iop_compiler( - &self, - ) -> &BaseFoldVerifierCompiler> - { - &self.iop_compiler + /// Returns log2 of the number of field elements in the packed trace. + pub fn log_witness_elems(&self) -> usize { + let log_witness_words = + log2_ceil_usize(self.constraint_system.value_vec_len()).max(LOG_WORDS_PER_ELEM); + log_witness_words - LOG_WORDS_PER_ELEM } - pub fn verify( - &self, - public: &[Word], - transcript: &mut VerifierTranscript, - ) -> Result<(), Error> { - // Create channel and delegate to verify_iop - let mut channel = self.iop_compiler.create_channel(transcript); - self.verify_iop(public, &mut channel) + /// Returns log2 of the number of words in the witness. + pub fn log_witness_words(&self) -> usize { + self.log_witness_elems() + LOG_WORDS_PER_ELEM + } + + /// Returns the oracle specs for the IOP channel. + /// + /// These describe the oracles (the witness) that the prover commits to. + pub fn oracle_specs(&self) -> Vec { + vec![OracleSpec { + log_msg_len: self.log_witness_elems(), + }] } - fn verify_iop(&self, public: &[Word], channel: &mut Channel) -> Result<(), Error> + /// Verifies a proof using an IOP channel. + /// + /// This is the core verification logic, independent of the specific IOP compilation strategy. + /// For most users, [`Verifier::verify`] is the simpler interface. + pub fn verify(&self, public: &[Word], channel: &mut Channel) -> Result<(), Error> where - Channel: IOPVerifierChannel, + Channel: IOPVerifierChannel, + Channel::Elem: FieldOps + From, { // Check that the public input length is correct if public.len() != 1 << self.log_public_words() { @@ -234,8 +175,8 @@ where eval_point, } = intmul_output; - let r_zhat_prime = bitand_claim.r_zhat_prime; - let l_tilde = lagrange_evals_scalars(&domain_subspace, r_zhat_prime); + let r_zhat_prime = bitand_claim.r_zhat_prime.clone(); + let l_tilde = lagrange_evals_scalars(&domain_subspace, r_zhat_prime.clone()); let make_final_claim = |evals| inner_product_scalars(evals, l_tilde.iter().cloned()); OperatorData::new( r_zhat_prime, @@ -290,7 +231,7 @@ where let ring_switch::RingSwitchVerifyOutput { eq_r_double_prime, sumcheck_claim, - } = ring_switch::verify(*shift_output.witness_eval(), &eval_point, channel)?; + } = ring_switch::verify(shift_output.witness_eval().clone(), &eval_point, channel)?; // Public input check batched with ring-switch let log_packing = >::LOG_DEGREE; @@ -301,14 +242,14 @@ where let pubcheck_claim = evaluate_inplace_scalars(public_elems, &pubcheck_point); let batch_coeff = channel.sample(); - let batched_claim = sumcheck_claim + batch_coeff * pubcheck_claim; + let batched_claim = sumcheck_claim + batch_coeff.clone() * pubcheck_claim; // Build the transparent closure combining ring-switch and public input check let transparent = Box::new(move |point: &[Channel::Elem]| { let rs_eq_eval = ring_switch::eval_rs_eq(&eval_point_high, point, eq_r_double_prime.as_ref()); let pubcheck_eq_eval = eval_pubcheck_eq(&pubcheck_point, point); - rs_eq_eval + batch_coeff * pubcheck_eq_eval + rs_eq_eval + batch_coeff.clone() * pubcheck_eq_eval }); // Verify oracle relations (runs BaseFold internally and verifies the product check) @@ -324,6 +265,131 @@ where } } +/// Struct for verifying instances of a particular constraint system. +/// +/// The [`Self::setup`] constructor determines public parameters for proving instances of the given +/// constraint system. Then [`Self::verify`] is called one or more times with individual instances. +#[derive(Debug, Clone)] +pub struct Verifier +where + MerkleHash: Digest + BlockSizeUser, + MerkleCompress: PseudoCompressionFunction, 2>, +{ + iop_verifier: IOPVerifier, + iop_compiler: + BaseFoldVerifierCompiler>, +} + +impl Verifier +where + MerkleHash: Digest + BlockSizeUser, + MerkleCompress: PseudoCompressionFunction, 2>, + Output: DeserializeBytes, +{ + /// Constructs a verifier for a constraint system. + /// + /// See [`Verifier`] struct documentation for details. + pub fn setup( + mut constraint_system: ConstraintSystem, + log_inv_rate: usize, + compression: MerkleCompress, + ) -> Result { + constraint_system.validate_and_prepare()?; + + // Use offset_witness which is guaranteed to be power of two and be at least one full + // element. + let n_public = constraint_system.value_vec_layout.offset_witness; + let log_public_words = log2_ceil_usize(n_public); + assert!(n_public.is_power_of_two()); + assert!(log_public_words >= LOG_WORDS_PER_ELEM); + + let iop_verifier = IOPVerifier::new(constraint_system, log_public_words); + + let log_witness_elems = iop_verifier.log_witness_elems(); + let oracle_specs = iop_verifier.oracle_specs(); + + let log_code_len = log_witness_elems + log_inv_rate; + let merkle_scheme = BinaryMerkleTreeScheme::new(compression); + let fri_arity = + ConstantArityStrategy::with_optimal_arity::(&merkle_scheme, log_code_len) + .arity; + + let n_test_queries = calculate_n_test_queries(SECURITY_BITS, log_inv_rate); + + let iop_compiler = BaseFoldVerifierCompiler::new( + merkle_scheme, + oracle_specs, + log_inv_rate, + n_test_queries, + &ConstantArityStrategy::new(fri_arity), + ); + + Ok(Self { + iop_verifier, + iop_compiler, + }) + } + + /// Returns a reference to the IOP verifier. + pub fn iop_verifier(&self) -> &IOPVerifier { + &self.iop_verifier + } + + /// Consumes the verifier and returns the inner IOP verifier. + pub fn into_iop_verifier(self) -> IOPVerifier { + self.iop_verifier + } + + /// Returns log2 of the number of words in the witness. + pub fn log_witness_words(&self) -> usize { + self.iop_verifier.log_witness_words() + } + + /// Returns log2 of the number of field elements in the packed trace. + pub fn log_witness_elems(&self) -> usize { + self.iop_verifier.log_witness_elems() + } + + /// Returns the constraint system. + pub fn constraint_system(&self) -> &ConstraintSystem { + self.iop_verifier.constraint_system() + } + + /// Returns the chosen FRI parameters. + pub fn fri_params(&self) -> &FRIParams { + // There is exactly one oracle spec (the witness) + &self.iop_compiler.fri_params()[0] + } + + /// Returns the [`crate::merkle_tree::MerkleTreeScheme`] instance used. + pub fn merkle_scheme(&self) -> &BinaryMerkleTreeScheme { + self.iop_compiler.merkle_scheme() + } + + /// Returns log2 of the number of public constants and input/output words. + pub fn log_public_words(&self) -> usize { + self.iop_verifier.log_public_words() + } + + /// Returns the IOP compiler for creating verifier channels. + pub fn iop_compiler( + &self, + ) -> &BaseFoldVerifierCompiler> + { + &self.iop_compiler + } + + pub fn verify( + &self, + public: &[Word], + transcript: &mut VerifierTranscript, + ) -> Result<(), Error> { + // Create channel and delegate to IOPVerifier::verify + let mut channel = self.iop_compiler.create_channel(transcript); + self.iop_verifier.verify(public, &mut channel) + } +} + fn verify_bitand_reduction( log_constraint_count: usize, eval_domain: &BinarySubspace, diff --git a/crates/verifier/src/zk_config.rs b/crates/verifier/src/zk_config.rs new file mode 100644 index 000000000..aac077bb6 --- /dev/null +++ b/crates/verifier/src/zk_config.rs @@ -0,0 +1,179 @@ +// Copyright 2026 The Binius Developers + +//! Zero-knowledge verification configuration for Binius64 constraint systems. +//! +//! This module provides [`ZKVerifier`], which wraps the Binius64 IOP verifier with a +//! Spartan-based zero-knowledge wrapper. The wrapper transforms the non-ZK Binius64 +//! verification into a ZK proof by: +//! +//! 1. Symbolically executing the inner verifier to build an outer Spartan constraint system +//! 2. Combining inner and outer oracle specs into a single BaseFold ZK compiler +//! 3. At verification time, running the inner verifier through a [`ZKWrappedVerifierChannel`] that +//! records all values as outer public inputs, then finishing with outer Spartan verification +//! +//! [`ZKWrappedVerifierChannel`]: binius_spartan_verifier::wrapper::ZKWrappedVerifierChannel + +use binius_core::{constraint_system::ConstraintSystem, word::Word}; +use binius_field::BinaryField128bGhash as B128; +use binius_iop::{ + basefold_compiler::BaseFoldZKVerifierCompiler, + channel::OracleSpec, + fri::{self, MinProofSizeStrategy}, + merkle_tree::BinaryMerkleTreeScheme, +}; +use binius_spartan_frontend::{ + circuit_builder::ConstraintBuilder, compiler::compile, constraint_system::BlindingInfo, +}; +use binius_spartan_verifier::{ + IOPVerifier as IronSpartanIOPVerifier, + constraint_system::ConstraintSystemPadded, + wrapper::{IronSpartanBuilderChannel, ZKWrappedVerifierChannel}, +}; +use binius_transcript::{VerifierTranscript, fiat_shamir::Challenger}; +use binius_utils::{DeserializeBytes, checked_arithmetics::log2_ceil_usize}; +use digest::{Digest, Output, core_api::BlockSizeUser}; + +use crate::{ + config::LOG_WORDS_PER_ELEM, + hash::PseudoCompressionFunction, + verify::{IOPVerifier, SECURITY_BITS}, +}; + +/// Zero-knowledge verifier for Binius64 constraint systems. +/// +/// Wraps the Binius64 IOP verifier with a Spartan-based ZK wrapper. Call [`Self::setup`] with +/// a constraint system, then [`Self::verify`] with public inputs and a proof transcript. +#[derive(Debug, Clone)] +pub struct ZKVerifier +where + MerkleHash: Digest + BlockSizeUser, + MerkleCompress: PseudoCompressionFunction, 2>, +{ + inner_iop_verifier: IOPVerifier, + outer_iop_verifier: IronSpartanIOPVerifier, + basefold_compiler: + BaseFoldZKVerifierCompiler>, +} + +impl ZKVerifier +where + MerkleHash: Digest + BlockSizeUser, + MerkleCompress: PseudoCompressionFunction, 2>, + Output: DeserializeBytes, +{ + /// Constructs a ZK verifier for a constraint system. + pub fn setup( + mut constraint_system: ConstraintSystem, + log_inv_rate: usize, + compression: MerkleCompress, + ) -> Result { + constraint_system.validate_and_prepare()?; + + let n_public = constraint_system.value_vec_layout.offset_witness; + let log_public_words = log2_ceil_usize(n_public); + assert!(n_public.is_power_of_two()); + assert!(log_public_words >= LOG_WORDS_PER_ELEM); + + let inner_iop_verifier = IOPVerifier::new(constraint_system, log_public_words); + + // Symbolically execute the inner verifier to build the outer constraint system. + let dummy_public_words = + vec![Word::from_u64(0); 1 << inner_iop_verifier.log_public_words()]; + let mut builder_channel = IronSpartanBuilderChannel::new(ConstraintBuilder::new()); + inner_iop_verifier + .verify(&dummy_public_words, &mut builder_channel) + .expect("symbolic verify should not fail"); + let outer_builder = builder_channel.finish(); + let (outer_cs, _outer_layout) = compile(outer_builder); + + // Pad the outer constraint system for zero-knowledge. + let n_test_queries = fri::calculate_n_test_queries(SECURITY_BITS, log_inv_rate); + let blinding_info = BlindingInfo { + n_dummy_wires: n_test_queries, + n_dummy_constraints: 2, + }; + let outer_cs = ConstraintSystemPadded::new(outer_cs, blinding_info); + let outer_iop_verifier = IronSpartanIOPVerifier::new(outer_cs); + + // Combine inner and outer oracle specs. + let oracle_specs: Vec = [ + inner_iop_verifier.oracle_specs(), + outer_iop_verifier.oracle_specs(), + ] + .concat(); + + let merkle_scheme = BinaryMerkleTreeScheme::new(compression); + let basefold_compiler = BaseFoldZKVerifierCompiler::new( + merkle_scheme, + oracle_specs, + log_inv_rate, + n_test_queries, + &MinProofSizeStrategy, + ); + + Ok(Self { + inner_iop_verifier, + outer_iop_verifier, + basefold_compiler, + }) + } + + /// Returns a reference to the inner IOP verifier. + pub fn inner_iop_verifier(&self) -> &IOPVerifier { + &self.inner_iop_verifier + } + + /// Returns a reference to the outer spartan IOP verifier. + pub fn outer_iop_verifier(&self) -> &IronSpartanIOPVerifier { + &self.outer_iop_verifier + } + + /// Returns the BaseFold ZK verifier compiler. + pub fn basefold_compiler( + &self, + ) -> &BaseFoldZKVerifierCompiler> + { + &self.basefold_compiler + } + + /// Returns the constraint system. + pub fn constraint_system(&self) -> &ConstraintSystem { + self.inner_iop_verifier.constraint_system() + } + + /// Returns log2 of the number of public words. + pub fn log_public_words(&self) -> usize { + self.inner_iop_verifier.log_public_words() + } + + /// Verifies a ZK proof against the constraint system. + pub fn verify( + &self, + public: &[Word], + transcript: &mut VerifierTranscript, + ) -> Result<(), Error> { + // Create BaseFoldZK channel and wrap with outer verifier. + let channel = self.basefold_compiler.create_channel(transcript); + let mut wrapped_channel = ZKWrappedVerifierChannel::new(channel, &self.outer_iop_verifier); + + // Run the inner IOP verification through the wrapped channel. + self.inner_iop_verifier + .verify(public, &mut wrapped_channel)?; + + // Finish runs the outer spartan verification. + wrapped_channel.finish()?; + + Ok(()) + } +} + +/// Error type for ZK verification. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("inner verification error: {0}")] + InnerVerification(#[from] crate::error::Error), + #[error("outer verification error: {0}")] + OuterVerification(#[from] binius_spartan_verifier::Error), + #[error("constraint system error: {0}")] + ConstraintSystem(#[from] binius_core::ConstraintSystemError), +}