From c022ef9b8e22ab188c50e34b162d4c9f0b7505d6 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 23 Sep 2026 11:07:35 +0530 Subject: [PATCH 1/7] refactor: share helpers for OOD commitments --- crates/pcs/src/challenger.rs | 39 +----------------------------- crates/pcs/src/lib.rs | 1 + crates/pcs/src/pow.rs | 47 ++++++++++++++++++++++++++++++++++++ crates/poly/src/mle.rs | 8 +++--- 4 files changed, 54 insertions(+), 41 deletions(-) create mode 100644 crates/pcs/src/pow.rs diff --git a/crates/pcs/src/challenger.rs b/crates/pcs/src/challenger.rs index 24ff2eac..0a341c49 100644 --- a/crates/pcs/src/challenger.rs +++ b/crates/pcs/src/challenger.rs @@ -1,6 +1,7 @@ //! Flock challenger adapters over the project transcript. use crate::bridge::{as_flock_f128, from_flock_f128}; +use crate::pow::{find as find_pow, valid as pow_valid}; use field::F128 as LocalF128; use flock_core::challenger::Challenger; use flock_core::field::F128 as FlockF128; @@ -232,44 +233,6 @@ impl Challenger for VerifierChallenger<'_, '_> { } } -/// todo: parallel pow? use potentially spongefish? -fn find_pow(seed: &[u8; 16], bits: u32) -> u64 { - if bits == 0 { - return 0; - } - let mut nonce = 0u64; - loop { - if pow_valid(seed, nonce, bits) { - return nonce; - } - nonce = nonce.checked_add(1).expect("proof-of-work nonce exhausted"); - } -} - -fn pow_valid(seed: &[u8; 16], nonce: u64, bits: u32) -> bool { - if bits == 0 { - return nonce == 0; - } - let mut hasher = blake3::Hasher::new(); - hasher.update(b"bitz-pcs-pow-v1"); - hasher.update(seed); - hasher.update(&nonce.to_le_bytes()); - let digest = hasher.finalize(); - leading_zero_bits(digest.as_bytes()) >= bits -} - -fn leading_zero_bits(bytes: &[u8]) -> u32 { - let mut total = 0; - for byte in bytes { - let zeros = byte.leading_zeros(); - total += zeros; - if zeros != 8 { - break; - } - } - total -} - #[cfg(test)] mod tests { use proptest::prelude::*; diff --git a/crates/pcs/src/lib.rs b/crates/pcs/src/lib.rs index 68ab2c40..de18655a 100644 --- a/crates/pcs/src/lib.rs +++ b/crates/pcs/src/lib.rs @@ -94,6 +94,7 @@ mod commitment; mod ligerito; mod mle; mod opening; +mod pow; mod profiles; mod transpose; diff --git a/crates/pcs/src/pow.rs b/crates/pcs/src/pow.rs new file mode 100644 index 00000000..c0f2f890 --- /dev/null +++ b/crates/pcs/src/pow.rs @@ -0,0 +1,47 @@ +//! Nonce search and validation shared by PCS grinding rounds. +//! +//! A positive difficulty requires that many leading zero bits in +//! `BLAKE3(POW_HASH_TAG || seed || nonce.to_le_bytes())`. At zero difficulty, +//! only nonce zero is accepted. Transcript framing belongs to each calling round. + +const POW_HASH_TAG: &[u8] = b"bitz-pcs-pow-v1"; + +/// Returns the first valid nonce in ascending order. +// todo: parallel pow? use potentially spongefish? +pub(crate) fn find(seed: &[u8; 16], bits: u32) -> u64 { + if bits == 0 { + return 0; + } + let mut nonce = 0u64; + loop { + if valid(seed, nonce, bits) { + return nonce; + } + nonce = nonce.checked_add(1).expect("proof-of-work nonce exhausted"); + } +} + +/// Checks the hash difficulty, or the canonical zero nonce at zero difficulty. +pub(crate) fn valid(seed: &[u8; 16], nonce: u64, bits: u32) -> bool { + if bits == 0 { + return nonce == 0; + } + let mut hasher = blake3::Hasher::new(); + hasher.update(POW_HASH_TAG); + hasher.update(seed); + hasher.update(&nonce.to_le_bytes()); + let digest = hasher.finalize(); + leading_zero_bits(digest.as_bytes()) >= bits +} + +fn leading_zero_bits(bytes: &[u8]) -> u32 { + let mut total = 0; + for byte in bytes { + let zeros = byte.leading_zeros(); + total += zeros; + if zeros != 8 { + break; + } + } + total +} diff --git a/crates/poly/src/mle.rs b/crates/poly/src/mle.rs index 43d1e817..ba1d331e 100644 --- a/crates/poly/src/mle.rs +++ b/crates/poly/src/mle.rs @@ -166,11 +166,13 @@ impl DenseMultilinearExtension { Ok(Self::evaluate_exact(&self.evaluations, r)) } - #[inline] + /// Evaluates an MLE table whose length is exactly `2^r.len()`. + /// /// Unrolled base cases adapted from WHIR's `eval_exact` (Apache-2.0): /// - fn evaluate_exact(evaluations: &[F], r: &[F]) -> F { - debug_assert_eq!(evaluations.len(), 1 << r.len()); + #[inline] + pub fn evaluate_exact(evaluations: &[F], r: &[F]) -> F { + assert_eq!(evaluations.len(), 1 << r.len(), "MLE table length"); let interpolate = |zero: F, one: F, challenge: F| zero + challenge * (one - zero); From f47b83dc04ace18a113d0f6933ec75054968070d Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 23 Sep 2026 12:11:49 +0530 Subject: [PATCH 2/7] feat: authenticate initial OOD claims in PCS openings --- Cargo.lock | 1 + crates/pcs/Cargo.toml | 1 + crates/pcs/src/commitment.rs | 68 +++++++++- crates/pcs/src/lib.rs | 76 ++++++++++- crates/pcs/src/ood.rs | 217 ++++++++++++++++++++++++++++++++ crates/pcs/src/opening.rs | 80 +++++++++--- crates/pcs/src/opening/tests.rs | 4 +- crates/pcs/src/profiles.rs | 37 ++++++ crates/pcs/tests/round_trip.rs | 64 ++++++++++ 9 files changed, 524 insertions(+), 24 deletions(-) create mode 100644 crates/pcs/src/ood.rs diff --git a/Cargo.lock b/Cargo.lock index 7681eee9..65fe2035 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1123,6 +1123,7 @@ dependencies = [ "field", "flock-core", "num-traits", + "poly", "post_gkr", "proptest", "tracing", diff --git a/crates/pcs/Cargo.toml b/crates/pcs/Cargo.toml index b627754a..2c3c8aa7 100644 --- a/crates/pcs/Cargo.toml +++ b/crates/pcs/Cargo.toml @@ -15,6 +15,7 @@ post_gkr = { workspace = true } transcript = { workspace = true } tracing = { workspace = true } num-traits = { workspace = true } +poly = { workspace = true } [dev-dependencies] divan = { workspace = true } diff --git a/crates/pcs/src/commitment.rs b/crates/pcs/src/commitment.rs index 0b17b5ce..2a2ca593 100644 --- a/crates/pcs/src/commitment.rs +++ b/crates/pcs/src/commitment.rs @@ -8,15 +8,18 @@ use core::mem::size_of; +use crate::VerifyError; use crate::bridge::as_flock_f128s; use crate::ligerito::CheckedLigerito; +use crate::ood::{OodClaim, prove, verify}; +use crate::profiles::{ood_grinding_bits, security_config}; use common::{Root, Shape}; use field::F128; pub use flock_core::hash::HashKind; use flock_core::pcs::Commitment as FlockCommitment; use flock_core::pcs::ligerito::LigeritoProfile; use flock_core::pcs::{PcsParams, ProverData as FlockProverData}; -use transcript::Encoding; +use transcript::{Encoding, ProverState, VerifierState}; /// Errors from PCS configuration. #[derive(Clone, Debug, PartialEq, Eq)] @@ -38,14 +41,32 @@ pub enum CommitError { pub struct Pcs { params: PcsParams, checked_ligerito: CheckedLigerito, + ood_grinding_bits: Option, bit_len: usize, packed_len: usize, } /// Flock state retained between commitment and openings. +/// OOD-aware commitment creation also retains the initial evaluation for each opening. pub struct ProverData { commitment: FlockCommitment, flock_prover_data: FlockProverData, + pub(crate) ood: Option, +} + +/// Commitment and out-of-domain claim read from the verifier transcript. +/// The claim is authenticated only after [`CommitScheme::verify_lin_with_ood`](crate::CommitScheme::verify_lin_with_ood) succeeds. +#[derive(Debug)] +pub struct VerifierData { + pub(crate) root: Root, + pub(crate) ood: Option, +} + +impl VerifierData { + /// Returns the public commitment root. + pub fn root(&self) -> Root { + self.root + } } impl Pcs { @@ -61,7 +82,7 @@ impl Pcs { // The ladder fixes the L0 interleaving: the commit must use the same // `log_batch_size` as the opening's `initial_k`, or the L0 tree is not // reusable as Ligerito's first oracle. - let security = crate::profiles::security_config(m, security_profile, merkle_hash)?; + let security = security_config(m, security_profile, merkle_hash)?; let params = PcsParams { m, log_inv_rate: security_profile.log_inv_rate(), @@ -70,6 +91,7 @@ impl Pcs { merkle_hash, }; let checked_ligerito = CheckedLigerito::new(¶ms, &security)?; + let ood_grinding_bits = ood_grinding_bits(&security, checked_ligerito.log_n_u32() as usize); let packed_len = 1usize .checked_shl(checked_ligerito.log_n_u32()) .ok_or(ConfigError::Invalid("packed length overflow"))?; @@ -77,12 +99,14 @@ impl Pcs { Ok(Self { params, checked_ligerito, + ood_grinding_bits, bit_len, packed_len, }) } - /// Commits to the exact configured number of packed field elements. + /// Commits to the packed codeword without sampling an OOD claim. + /// Use [`Self::commit_with_ood`] for protocols requiring initial OOD sampling. #[tracing::instrument(name = "Commit witness", skip_all)] pub fn commit(&self, packed_witness: &[F128]) -> Result<(Root, ProverData), CommitError> { // 1. Input Validation @@ -103,10 +127,44 @@ impl Pcs { ProverData { commitment: flock_commitment, flock_prover_data, + ood: None, }, )) } + /// Commits and retains the initial OOD claim for subsequent batched openings. + /// + /// Call before witness-dependent challenges and continue with the same transcript. + /// [`CommitScheme::prove_lin`](crate::CommitScheme::prove_lin) batches the retained claim into each opening. + /// Profiles using unique decoding omit the OOD round. + /// + /// Returns [`CommitError::PackedWitnessLengthMismatch`] before transcript mutation + /// if `packed_witness` does not have the configured length. + #[tracing::instrument(name = "Commit witness with OOD", skip_all)] + pub fn commit_with_ood( + &self, + packed_witness: &[F128], + transcript: &mut ProverState, + ) -> Result<(Root, ProverData), CommitError> { + let (root, mut data) = self.commit(packed_witness)?; + data.ood = prove(self, &root.0, packed_witness, transcript); + Ok((root, data)) + } + + /// Receives the OOD claim for the public root before subsequent protocol challenges. + /// + /// Mirrors [`Self::commit_with_ood`]. Invalid grinding or a truncated evaluation + /// returns [`VerifyError::MalformedProof`]; authentication of the evaluation is + /// deferred to [`CommitScheme::verify_lin_with_ood`](crate::CommitScheme::verify_lin_with_ood). + pub fn receive_commitment( + &self, + root: Root, + transcript: &mut VerifierState<'_>, + ) -> Result { + let ood = verify(self, &root.0, transcript)?; + Ok(VerifierData { root, ood }) + } + pub fn bit_len(&self) -> usize { self.bit_len } @@ -120,6 +178,10 @@ impl Pcs { &self.params } + pub(crate) fn ood_grinding_bits(&self) -> Option { + self.ood_grinding_bits + } + pub(crate) fn prover_config(&self) -> &flock_core::pcs::ligerito::ProverConfig { self.checked_ligerito.prover_config() } diff --git a/crates/pcs/src/lib.rs b/crates/pcs/src/lib.rs index de18655a..3ea0528e 100644 --- a/crates/pcs/src/lib.rs +++ b/crates/pcs/src/lib.rs @@ -29,6 +29,7 @@ //! - [`Pcs`] stores trusted Flock parameters and the expected bit length. //! - [`Root`] is the public Merkle root. //! - [`ProverData`] retains the codeword and Merkle tree after commitment. +//! - [`VerifierData`] retains the root and OOD claim received before opening. //! - [`OpeningQuery`] contains an MLE point and target, or a `common::LinearClaim`. //! - [`CommitScheme`] connects commitment, proving, and verification to project transcripts. //! - [`ConfigError`] reports configuration failures. @@ -39,6 +40,9 @@ //! It consumes the packed witness and borrows [`ProverData`]. //! The caller must use matching transcript session and instance labels. //! The caller must also call `VerifierState::check_eof` after successful verification. +//! Use [`Pcs::commit_with_ood`] and [`Pcs::receive_commitment`] before any +//! witness-dependent challenges to include the initial OOD claim. Proving batches +//! that retained claim automatically; verification uses [`Pcs::verify_lin_with_ood`]. //! //! # Example //! @@ -65,8 +69,8 @@ //! target: F128::from(0u64), //! }; //! -//! let (commitment, prover_data) = pcs.commit(&packed_witness).unwrap(); //! let mut prover = build_prover(b"pcs-example", b"zero-polynomial"); +//! let (commitment, prover_data) = pcs.commit_with_ood(&packed_witness, &mut prover).unwrap(); //! pcs.prove_lin( //! &prover_data, //! packed_witness, @@ -78,7 +82,8 @@ //! let proof = prover.finish(); //! //! let mut verifier = build_verifier(b"pcs-example", b"zero-polynomial", &proof); -//! pcs.verify_lin( +//! let commitment = pcs.receive_commitment(commitment, &mut verifier).unwrap(); +//! pcs.verify_lin_with_ood( //! &commitment, //! &query, //! StatementBinding::Bind, @@ -93,6 +98,7 @@ mod challenger; mod commitment; mod ligerito; mod mle; +mod ood; mod opening; mod pow; mod profiles; @@ -105,7 +111,7 @@ mod transpose_tests; use field::F128; use transcript::{ProverState, VerifierState}; -pub use commitment::{CommitError, ConfigError, HashKind, Pcs, ProverData}; +pub use commitment::{CommitError, ConfigError, HashKind, Pcs, ProverData, VerifierData}; pub use common::{OpeningQuery, Root}; pub use flock_core::pcs::ligerito::LigeritoProfile; pub use opening::{ProveError, VerifyError}; @@ -136,6 +142,8 @@ pub trait CommitScheme { type Commitment; /// Private data retained by the prover after commitment. type ProverData; + /// Commitment and OOD claim retained by the verifier before opening. + type VerifierData; /// Commits the caller-owned packed witness to `Enc_C(q_pkd)`, where /// `q_pkd(y) = Σ_{v ∈ {0,1}^7} q(y, v) · basis[v]`. @@ -145,9 +153,30 @@ pub trait CommitScheme { packed_witness: &[F128], ) -> Result<(Self::Commitment, Self::ProverData), CommitError>; + /// Commits and retains the initial OOD claim for subsequent openings. + /// + /// Call before witness-dependent challenges and continue with the same transcript. + /// Profiles without initial OOD sampling omit that round. + fn commit_with_ood( + &self, + packed_witness: &[F128], + transcript: &mut ProverState, + ) -> Result<(Self::Commitment, Self::ProverData), CommitError>; + + /// Receives the OOD claim for the public commitment before protocol challenges. + /// + /// Mirrors [`Self::commit_with_ood`]. The returned claim must be authenticated + /// by [`Self::verify_lin_with_ood`] on the same transcript. + fn receive_commitment( + &self, + commitment: Self::Commitment, + transcript: &mut VerifierState<'_>, + ) -> Result; + /// Consumes the exact packed witness and proves either opening query. /// /// Inner-product claims first pass through the post-GKR sumcheck and then the MLE opening protocol. + /// An OOD claim retained by [`Self::commit_with_ood`] is batched into the opening. fn prove_lin( &self, data: &Self::ProverData, @@ -167,11 +196,24 @@ pub trait CommitScheme { statement_binding: StatementBinding, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError>; + + /// Verifies the linear query batched with the retained OOD claim. + /// + /// Continue the transcript used by [`Self::receive_commitment`]. Borrowing + /// the retained state permits multiple openings against the same commitment. + fn verify_lin_with_ood( + &self, + commitment: &Self::VerifierData, + query: &OpeningQuery, + statement_binding: StatementBinding, + transcript: &mut VerifierState<'_>, + ) -> Result<(), VerifyError>; } impl CommitScheme for Pcs { type Commitment = Root; type ProverData = ProverData; + type VerifierData = VerifierData; fn commit( &self, @@ -180,6 +222,22 @@ impl CommitScheme for Pcs { Pcs::commit(self, packed_witness) } + fn commit_with_ood( + &self, + packed_witness: &[F128], + transcript: &mut ProverState, + ) -> Result<(Self::Commitment, Self::ProverData), CommitError> { + Pcs::commit_with_ood(self, packed_witness, transcript) + } + + fn receive_commitment( + &self, + commitment: Self::Commitment, + transcript: &mut VerifierState<'_>, + ) -> Result { + Pcs::receive_commitment(self, commitment, transcript) + } + fn prove_lin( &self, data: &Self::ProverData, @@ -205,6 +263,16 @@ impl CommitScheme for Pcs { statement_binding: StatementBinding, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError> { - opening::verify(self, commitment, query, statement_binding, transcript) + opening::verify(self, commitment, query, statement_binding, None, transcript) + } + + fn verify_lin_with_ood( + &self, + commitment: &Self::VerifierData, + query: &OpeningQuery, + statement_binding: StatementBinding, + transcript: &mut VerifierState<'_>, + ) -> Result<(), VerifyError> { + opening::verify_lin_with_ood(self, commitment, query, statement_binding, transcript) } } diff --git a/crates/pcs/src/ood.rs b/crates/pcs/src/ood.rs new file mode 100644 index 00000000..bb5d718a --- /dev/null +++ b/crates/pcs/src/ood.rs @@ -0,0 +1,217 @@ +//! Initial out-of-domain claim on the packed commitment polynomial. +//! +//! After binding the root and PCS parameters, the prover performs any configured +//! grinding, samples `zeta`, and sends `value = p(point)`, where `p` is the packed +//! witness MLE and `point[i] = zeta^(2^i)` in low-bit-first order. +//! The verifier derives the same point and reads the claimed value. +//! +//! After ring switching, a fresh `coefficient` batches this claim into Ligerito: +//! `basis += coefficient * eq(point, ·)` and `target += coefficient * value`. +//! The claim remains borrowed from commitment state so it can be used by multiple +//! openings. Unique-decoding profiles omit this initial round. + +use field::F128; +use flock_core::field::F128 as FlockF128; +use num_traits::ConstOne; +use poly::{DenseMultilinearExtension, eq_table}; +use transcript::{ProverState, PublicTranscript, VerifierState}; + +use crate::bridge::{as_flock_f128, from_flock_f128}; +use crate::pow::{find, valid}; +use crate::{Pcs, VerifyError}; + +const OOD_ROUND_TAG: &[u8] = b"bitz/pcs/ood/v1"; +const OOD_BATCHING_TAG: &[u8] = b"bitz/pcs/ood-batching/v1"; +const OOD_POW_TAG: &[u8] = b"bitz/pcs/ood-pow/v1"; +const BLOCK_LOG: usize = 12; + +/// An evaluation of the packed witness MLE, authenticated by the batched opening. +#[derive(Debug)] +pub(crate) struct OodClaim { + /// Successive squares of the sampled challenge, in low-bit-first order. + pub(crate) point: Vec, + /// Claimed MLE evaluation at `point`. + pub(crate) value: F128, +} + +/// Sends the initial evaluation after binding the commitment and configuration. +/// Returns `None` without transcript events when the profile omits OOD sampling. +pub(crate) fn prove( + pcs: &Pcs, + root: &[u8; 32], + packed: &[F128], + transcript: &mut ProverState, +) -> Option { + let grinding_bits = pcs.ood_grinding_bits()?; + absorb_header(pcs, root, grinding_bits, transcript); + if grinding_bits != 0 { + prove_pow(transcript, grinding_bits); + } + let point = ood_point(transcript.verifier_message_f128(), pcs.packed_len()); + let value = DenseMultilinearExtension::evaluate_exact(packed, &point); + transcript.prover_message(&value); + Some(OodClaim { point, value }) +} + +/// Reads the initial evaluation, checking grinding before sampling its point. +/// Reading the value does not authenticate it; the caller must verify its opening. +pub(crate) fn verify( + pcs: &Pcs, + root: &[u8; 32], + transcript: &mut VerifierState<'_>, +) -> Result, VerifyError> { + let Some(grinding_bits) = pcs.ood_grinding_bits() else { + return Ok(None); + }; + absorb_header(pcs, root, grinding_bits, transcript); + if grinding_bits != 0 { + verify_pow(transcript, grinding_bits).map_err(|_| VerifyError::MalformedProof)?; + } + let point = ood_point(transcript.verifier_message_f128(), pcs.packed_len()); + let value = transcript + .prover_message::() + .map_err(|_| VerifyError::MalformedProof)?; + Ok(Some(OodClaim { point, value })) +} + +/// Samples the OOD batching coefficient after the ring-switch claims are bound. +pub(crate) fn batching_challenge(transcript: &mut impl PublicTranscript) -> F128 { + transcript.public_message(OOD_BATCHING_TAG); + transcript.verifier_message_f128() +} + +/// Adds `coefficient * eq(claim.point, ·)` to the prover's Boolean evaluation table. +pub(crate) fn add_dense_basis(basis: &mut [FlockF128], claim: &OodClaim, coefficient: F128) { + let low = claim.point.len().min(BLOCK_LOG); + let block = 1usize << low; + let tail = eq_table(&claim.point[..low]); + let head = eq_table(&claim.point[low..]); + for (chunk, &scale) in basis.chunks_exact_mut(block).zip(&head) { + for (basis, &weight) in chunk.iter_mut().zip(&tail) { + *basis += as_flock_f128(coefficient * scale * weight); + } + } +} + +/// Adds the same equality polynomial after its low coordinates are fixed to `ris`. +pub(crate) fn add_succinct_basis( + basis: &mut [FlockF128], + claim: &OodClaim, + coefficient: F128, + ris: &[FlockF128], +) { + let suffix_vars = basis.len().ilog2() as usize; + if claim.point.len() != ris.len() + suffix_vars { + basis.fill(FlockF128::ZERO); + return; + } + let prefix = claim.point[..ris.len()] + .iter() + .zip(ris) + .fold(F128::ONE, |weight, (&point, &query)| { + weight * (F128::ONE + point + from_flock_f128(query)) + }); + let scale = coefficient * prefix; + for (basis, weight) in basis.iter_mut().zip(eq_table(&claim.point[ris.len()..])) { + *basis += as_flock_f128(scale * weight); + } +} + +fn absorb_header( + pcs: &Pcs, + root: &[u8; 32], + grinding_bits: u32, + transcript: &mut impl PublicTranscript, +) { + transcript.public_message(OOD_ROUND_TAG); + transcript.public_message(root); + transcript.public_message(pcs); + transcript.public_message(&(pcs.packed_len() as u64)); + transcript.public_message(&grinding_bits); +} + +fn ood_point(zeta: F128, packed_len: usize) -> Vec { + let mut point = Vec::with_capacity(packed_len.ilog2() as usize); + let mut coordinate = zeta; + for _ in 0..packed_len.ilog2() { + point.push(coordinate); + coordinate *= coordinate; + } + point +} + +fn prove_pow(transcript: &mut ProverState, bits: u32) { + transcript.public_message(OOD_POW_TAG); + transcript.public_message(&bits); + let seed = transcript.verifier_message::().to_bytes(); + let nonce = find(&seed, bits); + transcript.prover_message(&nonce.to_le_bytes()); +} + +fn verify_pow(transcript: &mut VerifierState<'_>, bits: u32) -> Result<(), ()> { + transcript.public_message(OOD_POW_TAG); + transcript.public_message(&bits); + let seed = transcript.verifier_message::().to_bytes(); + let nonce = transcript + .prover_message::<[u8; 8]>() + .map(u64::from_le_bytes) + .map_err(|_| ())?; + valid(&seed, nonce, bits).then_some(()).ok_or(()) +} + +#[cfg(test)] +mod tests { + use num_traits::ConstZero; + use transcript::{build_prover, build_verifier}; + + use super::*; + + #[test] + fn dense_and_succinct_ood_bases_agree_after_folding() { + let point = ood_point(F128::new(7, 11), 1 << 14); + let coefficient = F128::new(13, 17); + let claim = OodClaim { + point, + value: F128::ZERO, + }; + let mut dense = vec![FlockF128::ZERO; 1 << 14]; + add_dense_basis(&mut dense, &claim, coefficient); + let queries: Vec<_> = (0..9) + .map(|i| as_flock_f128(F128::new(i + 2, i + 19))) + .collect(); + for &query in &queries { + for i in 0..dense.len() / 2 { + dense[i] = dense[2 * i] + query * (dense[2 * i] + dense[2 * i + 1]); + } + dense.truncate(dense.len() / 2); + } + let mut succinct = vec![FlockF128::ZERO; dense.len()]; + add_succinct_basis(&mut succinct, &claim, coefficient, &queries); + assert_eq!(dense, succinct); + } + + #[test] + fn grinding_binds_the_following_challenge_and_rejects_invalid_nonces() { + const BITS: u32 = 8; + let mut prover = build_prover(b"ood-test", b"grinding"); + prove_pow(&mut prover, BITS); + let challenge = prover.verifier_message::(); + let mut proof = prover.finish(); + let mut verifier = build_verifier(b"ood-test", b"grinding", &proof); + verify_pow(&mut verifier, BITS).unwrap(); + assert_eq!(verifier.verifier_message::(), challenge); + verifier.check_eof().unwrap(); + + let mut seed_transcript = build_prover(b"ood-test", b"grinding"); + seed_transcript.public_message(OOD_POW_TAG); + seed_transcript.public_message(&BITS); + let seed = seed_transcript.verifier_message::().to_bytes(); + let invalid = (0..).find(|&nonce| !valid(&seed, nonce, BITS)).unwrap(); + proof.narg_string.copy_from_slice(&invalid.to_le_bytes()); + let mut verifier = build_verifier(b"ood-test", b"grinding", &proof); + assert!(verify_pow(&mut verifier, BITS).is_err()); + proof.narg_string.truncate(7); + let mut verifier = build_verifier(b"ood-test", b"grinding", &proof); + assert!(verify_pow(&mut verifier, BITS).is_err()); + } +} diff --git a/crates/pcs/src/opening.rs b/crates/pcs/src/opening.rs index 1bdf5d54..95971052 100644 --- a/crates/pcs/src/opening.rs +++ b/crates/pcs/src/opening.rs @@ -4,11 +4,16 @@ use common::LinearClaim; use field::F128; use flock_core::field::F128 as FlockF128; use flock_core::pcs::pack::PACKING_WIDTH as CLAIM_COUNT; +use post_gkr::{ + ProveError as PostGkrProveError, VerifyError as PostGkrVerifyError, prove as prove_post_gkr, + verify as verify_post_gkr, +}; use transcript::{ProverState, PublicTranscript, VerifierState}; use crate::bridge::{as_flock_f128, as_flock_f128s, from_flock_f128}; use crate::ligerito::{self, ReducedProver, validate_prover_data}; -use crate::{OpeningQuery, Pcs, ProverData, Root, StatementBinding, mle}; +use crate::ood::{OodClaim, add_dense_basis, add_succinct_basis, batching_challenge}; +use crate::{OpeningQuery, Pcs, ProverData, Root, StatementBinding, VerifierData, mle}; const MLE_STATEMENT_LABEL: &[u8] = b"bitz/pcs/mle-opening/v1"; const INNER_PRODUCT_STATEMENT_LABEL: &[u8] = b"bitz/pcs/bit-inner-product/v3"; @@ -83,24 +88,46 @@ impl From for VerifyError { } } -impl From for ProveError { - fn from(error: post_gkr::ProveError) -> Self { +impl From for ProveError { + fn from(error: PostGkrProveError) -> Self { match error { - post_gkr::ProveError::WitnessLengthMismatch => Self::PackedWitnessLengthMismatch, - post_gkr::ProveError::ClaimDoesNotHold => Self::InvalidClaim, + PostGkrProveError::WitnessLengthMismatch => Self::PackedWitnessLengthMismatch, + PostGkrProveError::ClaimDoesNotHold => Self::InvalidClaim, } } } -impl From for VerifyError { - fn from(error: post_gkr::VerifyError) -> Self { +impl From for VerifyError { + fn from(error: PostGkrVerifyError) -> Self { match error { - post_gkr::VerifyError::MalformedProof => Self::MalformedProof, - post_gkr::VerifyError::EvaluationMismatch => Self::VerificationFailed, + PostGkrVerifyError::MalformedProof => Self::MalformedProof, + PostGkrVerifyError::EvaluationMismatch => Self::VerificationFailed, } } } +/// Verifies an opening batched with the OOD claim retained at commitment ingestion. +/// +/// Use the state returned by [`Pcs::receive_commitment`] and continue its transcript. +/// The state is borrowed so multiple openings can authenticate the same OOD claim. +/// Profiles without OOD sampling verify the ordinary linear claim. +pub(crate) fn verify_lin_with_ood( + pcs: &Pcs, + commitment: &VerifierData, + query: &OpeningQuery, + statement_binding: StatementBinding, + transcript: &mut VerifierState<'_>, +) -> Result<(), VerifyError> { + verify( + pcs, + &commitment.root, + query, + statement_binding, + commitment.ood.as_ref(), + transcript, + ) +} + #[tracing::instrument(name = "Prove PCS opening", skip_all)] pub(crate) fn prove( pcs: &Pcs, @@ -117,7 +144,7 @@ pub(crate) fn prove( if statement_binding == StatementBinding::Bind { bind_mle_statement(pcs, &data.commitment().root, point, *target, transcript); } - prove_mle(prover, ring_switch, *target, transcript) + prove_mle(prover, ring_switch, *target, data.ood.as_ref(), transcript) } OpeningQuery::InnerProduct { claim } => { validate_inner_product_claim(pcs, claim)?; @@ -126,7 +153,7 @@ pub(crate) fn prove( bind_inner_product_statement(pcs, &data.commitment().root, claim, transcript); } transcript.public_message(SUMCHECK_LABEL); - let reduced = post_gkr::prove(claim, &packed_witness, transcript)?; + let reduced = prove_post_gkr(claim, &packed_witness, transcript)?; // The evaluation claim the sumcheck leaves is opened like any // other, and bound whatever the caller's mode: `AlreadyBound` // covers the original claim only. @@ -148,6 +175,7 @@ pub(crate) fn verify( commitment: &Root, query: &OpeningQuery, statement_binding: StatementBinding, + ood_claim: Option<&OodClaim>, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError> { match query { @@ -156,7 +184,7 @@ pub(crate) fn verify( if statement_binding == StatementBinding::Bind { bind_mle_statement(pcs, &commitment.0, point, *target, transcript); } - verify_mle(pcs, commitment, ring_switch, *target, transcript) + verify_mle(pcs, commitment, ring_switch, *target, ood_claim, transcript) } OpeningQuery::InnerProduct { claim } => { validate_inner_product_claim(pcs, claim)?; @@ -164,12 +192,13 @@ pub(crate) fn verify( bind_inner_product_statement(pcs, &commitment.0, claim, transcript); } transcript.public_message(SUMCHECK_LABEL); - let reduced = post_gkr::verify(claim, transcript)?; + let reduced = verify_post_gkr(claim, transcript)?; verify( pcs, commitment, &reduced, StatementBinding::Bind, + ood_claim, transcript, ) } @@ -194,6 +223,7 @@ fn prove_mle( prover: ReducedProver<'_>, ring_switch: mle::RingSwitch<'_>, target: F128, + ood_claim: Option<&OodClaim>, transcript: &mut ProverState, ) -> Result<(), ProveError> { let dense_reduction = { @@ -202,7 +232,13 @@ fn prove_mle( ring_switch.prepare_claims(as_flock_f128s(prover.witness()), target)?; write_claims(transcript, &prepared_claims.claims); let batching_point = sample_challenges(transcript); - prepared_claims.reduce_dense(&batching_point) + let mut reduced = prepared_claims.reduce_dense(&batching_point); + if let Some(claim) = ood_claim { + let coefficient = batching_challenge(transcript); + add_dense_basis(&mut reduced.packed_basis, claim, coefficient); + reduced.packed_target += as_flock_f128(coefficient * claim.value); + } + reduced }; prover.prove(dense_reduction, transcript) } @@ -213,6 +249,7 @@ fn verify_mle( commitment: &Root, ring_switch: mle::RingSwitch<'_>, target: F128, + ood_claim: Option<&OodClaim>, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError> { let proof = ligerito::read_proof(pcs, commitment, transcript)?; @@ -225,13 +262,24 @@ fn verify_mle( let batching_point = sample_challenges(transcript); ring_switch.reduce_succinct(&claims, &batching_point) }; + let ood = ood_claim.map(|claim| (claim, batching_challenge(transcript))); + let mut packed_target = reduction.packed_target; + if let Some((claim, coefficient)) = ood { + packed_target += as_flock_f128(coefficient * claim.value); + } ligerito::verify_succinct( pcs, commitment, &proof, ring_switch.suffix_dimension(), - reduction.packed_target, - |ris, yr_log_n| reduction.evaluate_basis(ris, yr_log_n), + packed_target, + |ris, yr_log_n| { + let mut basis = reduction.evaluate_basis(ris, yr_log_n); + if let Some((claim, coefficient)) = ood { + add_succinct_basis(&mut basis, claim, coefficient, ris); + } + basis + }, transcript, ) } diff --git a/crates/pcs/src/opening/tests.rs b/crates/pcs/src/opening/tests.rs index e4c03182..a0e16ab9 100644 --- a/crates/pcs/src/opening/tests.rs +++ b/crates/pcs/src/opening/tests.rs @@ -69,12 +69,13 @@ fn inner_product_proof_composes_sumcheck_with_a_bound_mle_opening() { let mut verifier = build_verifier(SESSION, INSTANCE, &proof); bind_inner_product_statement(&fixture.pcs, &fixture.root.0, &fixture.claim, &mut verifier); verifier.public_message(SUMCHECK_LABEL); - let reduced = post_gkr::verify(&fixture.claim, &mut verifier).unwrap(); + let reduced = verify_post_gkr(&fixture.claim, &mut verifier).unwrap(); verify( &fixture.pcs, &fixture.root, &reduced, StatementBinding::Bind, + None, &mut verifier, ) .unwrap(); @@ -152,6 +153,7 @@ fn opening_leaves_matching_transcripts_for_following_protocols() { &fixture.root, &query, StatementBinding::Bind, + None, &mut verifier, ) .unwrap(); diff --git a/crates/pcs/src/profiles.rs b/crates/pcs/src/profiles.rs index 572d5b12..1050dbf3 100644 --- a/crates/pcs/src/profiles.rs +++ b/crates/pcs/src/profiles.rs @@ -42,6 +42,32 @@ pub(crate) fn security_config( Ok(security) } +/// Derives initial OOD grinding from the level-zero collision bound. +/// +/// With `rho = 2^-log_inv_rate`, let `list_size = 1 / (2 * eta * sqrt(rho))`, +/// `pairs = max(list_size * (list_size - 1) / 2, 1)`, and +/// `degree = max(2^packed_vars - 1, 1)`. The unground bound is +/// `128 - log2(pairs) - log2(degree)` bits; grinding covers its rounded-up +/// deficit against the target. Unique-decoding profiles return `None`. +pub(crate) fn ood_grinding_bits( + security: &LigeritoSecurityConfig, + packed_vars: usize, +) -> Option { + let level = security.levels.first()?; + let eta = match level.regime { + SoundnessRegime::JohnsonOod => level.eta?, + SoundnessRegime::Udr => return None, + }; + let rho = (-(level.log_inv_rate as f64)).exp2(); + let list_size = 1.0 / (2.0 * eta * rho.sqrt()); + let pairs = (list_size * (list_size - 1.0) / 2.0).max(1.0); + let degree = ((packed_vars as f64).exp2() - 1.0).max(1.0); + let collision_bits = 128.0 - pairs.log2() - degree.log2(); + let deficit = security.target_security_bits as f64 - collision_bits; + let grinding_bits = deficit.ceil().max(0.0) as u32; + Some(grinding_bits) +} + /// Reproduces the reference prover's k = 4 profile with 16-bit query grinding. /// Flock's `derive_profile` fixes k = 6 and zero query grinding for Fast. fn fast_security_config(m: usize) -> Result { @@ -181,4 +207,15 @@ mod tests { assert!(security_config(m, LigeritoProfile::Fast, HashKind::Blake3).is_err()); } } + + #[test] + fn ood_round_parameters_match_the_level_zero_bound() { + let security = security_config(22, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + assert_eq!(ood_grinding_bits(&security, 15), Some(0)); + + let mut unique = security; + unique.levels[0].regime = SoundnessRegime::Udr; + unique.levels[0].eta = None; + assert_eq!(ood_grinding_bits(&unique, 15), None); + } } diff --git a/crates/pcs/tests/round_trip.rs b/crates/pcs/tests/round_trip.rs index d738a41e..561964eb 100644 --- a/crates/pcs/tests/round_trip.rs +++ b/crates/pcs/tests/round_trip.rs @@ -263,6 +263,70 @@ fn real_pcs_opening_round_trip_succeeds() { verifier.check_eof().unwrap(); } +#[test] +fn real_pcs_ood_round_batches_into_opening() { + let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let mut packed_witness = vec![F128::ZERO; pcs.packed_len()]; + packed_witness[SINGLETON / 128] = F128::new(0, 1 << (SINGLETON % 128 - 64)); + let point = vec![F128::from(2u64); M]; + let query = OpeningQuery::Mle { + target: singleton_target(&point, SINGLETON), + point, + }; + ood_round_trip(&pcs, packed_witness, query); +} + +fn ood_round_trip(pcs: &impl CommitScheme, packed_witness: Vec, query: OpeningQuery) { + let mut prover = build_prover(SESSION, b"ood-round-trip"); + let (commitment, data) = pcs.commit_with_ood(&packed_witness, &mut prover).unwrap(); + pcs.prove_lin( + &data, + packed_witness, + &query, + StatementBinding::Bind, + &mut prover, + ) + .unwrap(); + let next_challenge = prover.verifier_message::(); + let proof = prover.finish(); + + let mut verifier = build_verifier(SESSION, b"ood-round-trip", &proof); + let received = pcs.receive_commitment(commitment, &mut verifier).unwrap(); + pcs.verify_lin_with_ood(&received, &query, StatementBinding::Bind, &mut verifier) + .unwrap(); + assert_eq!(verifier.verifier_message::(), next_challenge); + verifier.check_eof().unwrap(); +} + +#[test] +fn real_pcs_ood_round_rejects_a_changed_evaluation() { + let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); + let packed_witness = vec![F128::ZERO; pcs.packed_len()]; + let query = OpeningQuery::Mle { + point: vec![F128::from(2u64); M], + target: F128::ZERO, + }; + let mut prover = build_prover(SESSION, b"ood-tampering"); + let (commitment, data) = pcs.commit_with_ood(&packed_witness, &mut prover).unwrap(); + pcs.prove_lin( + &data, + packed_witness, + &query, + StatementBinding::Bind, + &mut prover, + ) + .unwrap(); + let mut proof = prover.finish(); + proof.narg_string[0] ^= 1; + + let mut verifier = build_verifier(SESSION, b"ood-tampering", &proof); + let received = pcs.receive_commitment(commitment, &mut verifier).unwrap(); + assert!( + pcs.verify_lin_with_ood(&received, &query, StatementBinding::Bind, &mut verifier,) + .is_err() + ); +} + #[test] fn factored_inner_product_round_trip_succeeds_for_all_profiles_and_bindings() { for profile in [ From 1605dccb14810edda963f6b93d3495213593c5bd Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 23 Sep 2026 12:18:01 +0530 Subject: [PATCH 3/7] feat: bind OOD claims before circuit proof challenges --- crates/prover/src/prove.rs | 8 +-- crates/tests/examples/dump_bitz.rs | 6 ++- crates/tests/tests/host.rs | 6 ++- crates/tests/tests/prove.rs | 23 +++++--- crates/tests/tests/virtual_prove.rs | 16 ++++-- crates/verifier/src/verify.rs | 60 ++++++++++++++++----- tooling/cli/benches/circuits.rs | 12 +++-- tooling/cli/src/benchmark.rs | 2 +- tooling/cli/src/end_to_end.rs | 81 +++++++++++++++++++++++------ tooling/cli/tests/circuits.rs | 4 +- tooling/cli/tests/end_to_end.rs | 31 +++++++++-- 11 files changed, 191 insertions(+), 58 deletions(-) diff --git a/crates/prover/src/prove.rs b/crates/prover/src/prove.rs index 2763b2cc..3fa89897 100644 --- a/crates/prover/src/prove.rs +++ b/crates/prover/src/prove.rs @@ -42,8 +42,8 @@ pub struct VirtualWitness<'a> { impl BitZProver { /// Proves the caller's linear claim about the committed bits. /// - /// The caller commits first and passes what that produced: the `data` the - /// opening reads and the packed witness itself. The root is read back off + /// Call `Pcs::commit_with_ood` on this transcript, then pass its retained + /// `data` and the packed witness. The root is read back off /// `data` rather than passed alongside it, so the two cannot disagree. /// `pcs` must be the scheme that committed, or the opening will not verify. /// @@ -82,8 +82,8 @@ impl BitZProver { /// Proves a claim on `h = M (1 || f)` against the commitment to `f`. /// - /// Build the setup from `statement.params().claim()`. Commit `witness.committed_bits` - /// with `pcs` under the committed shape and pass its returned `data`. GKR reduces + /// Build the setup from `statement.params().claim()`. Use `Pcs::commit_with_ood` + /// on `witness.committed_bits` and this transcript, then pass its `data`. GKR reduces /// the input claim to an inner product on padded virtual bits. This method /// transposes its coefficients before PCS opens the committed bits. /// diff --git a/crates/tests/examples/dump_bitz.rs b/crates/tests/examples/dump_bitz.rs index 92f98f77..85f6d316 100644 --- a/crates/tests/examples/dump_bitz.rs +++ b/crates/tests/examples/dump_bitz.rs @@ -29,12 +29,16 @@ fn main() -> Result<(), Box> { let started = std::time::Instant::now(); let mut transcript = prover_transcript(); + let (_, data) = instance + .pcs + .commit_with_ood(&instance.packed, &mut transcript) + .unwrap(); instance .prover .prove( &instance.claim, &instance.pcs, - &instance.data, + &data, instance.packed.clone(), &mut transcript, ) diff --git a/crates/tests/tests/host.rs b/crates/tests/tests/host.rs index 60d101be..6dfb080e 100644 --- a/crates/tests/tests/host.rs +++ b/crates/tests/tests/host.rs @@ -11,12 +11,16 @@ use tests::{Instance, narrow_shape, prover_transcript, verifier_transcript, wide /// Runs an honest prover and hands back what a caller would ship. fn shipped(instance: &Instance) -> Vec { let mut transcript = prover_transcript(); + let (_, data) = instance + .pcs + .commit_with_ood(&instance.packed, &mut transcript) + .unwrap(); instance .prover .prove( &instance.claim, &instance.pcs, - &instance.data, + &data, instance.packed.clone(), &mut transcript, ) diff --git a/crates/tests/tests/prove.rs b/crates/tests/tests/prove.rs index 3c87650f..761bea98 100644 --- a/crates/tests/tests/prove.rs +++ b/crates/tests/tests/prove.rs @@ -11,12 +11,16 @@ use verifier::{ReceiveError, VerifyError}; fn prove(instance: &Instance) -> Proof { let mut transcript = prover_transcript(); + let (_, data) = instance + .pcs + .commit_with_ood(&instance.packed, &mut transcript) + .unwrap(); instance .prover .prove( &instance.claim, &instance.pcs, - &instance.data, + &data, instance.packed.clone(), &mut transcript, ) @@ -106,12 +110,16 @@ fn an_opening_against_another_commitment_is_refused() { let committed = Instance::honest(narrow_shape(), 36); let mut transcript = prover_transcript(); + let (_, data) = committed + .pcs + .commit_with_ood(&committed.packed, &mut transcript) + .unwrap(); proved .prover .prove( &proved.claim, &proved.pcs, - &committed.data, + &data, proved.packed.clone(), &mut transcript, ) @@ -151,9 +159,8 @@ fn a_tampered_opening_proof_is_refused() { #[test] fn a_proof_verified_under_a_different_profile_is_refused() { - // The profile is not in the frame step 1 absorbs, so what rejects this is - // the opening binding its own parameters: a different profile encodes - // differently, the two sponges part, and the ring-switch check fails. + // OOD binds PCS parameters before the first fold challenge, so a different + // profile changes the fold transcript and GKR rejects. let instance = Instance::honest(narrow_shape(), 38); let slim = Pcs::new( instance.params.shape(), @@ -163,15 +170,15 @@ fn a_proof_verified_under_a_different_profile_is_refused() { .unwrap(); let proof = prove(&instance); - assert_eq!( + assert!(matches!( instance.verifier.verify( &instance.claim, &slim, instance.com, verifier_transcript(&proof) ), - Err(VerifyError::Opening(PcsVerifyError::VerificationFailed)) - ); + Err(VerifyError::Reduction(_)) + )); } #[test] diff --git a/crates/tests/tests/virtual_prove.rs b/crates/tests/tests/virtual_prove.rs index 424de53c..7d4a9048 100644 --- a/crates/tests/tests/virtual_prove.rs +++ b/crates/tests/tests/virtual_prove.rs @@ -94,11 +94,15 @@ impl Instance { fn prove(&self) -> Proof { let mut transcript = prover_transcript(); + let (_, data) = self + .pcs + .commit_with_ood(&self.committed_bits, &mut transcript) + .unwrap(); BitZProver::new(self.params, WINDOW) .prove_virtual( &self.statement(), &self.pcs, - &self.data, + &data, VirtualWitness { committed_bits: self.committed_bits.clone(), virtual_bits: &self.virtual_bits, @@ -297,11 +301,15 @@ fn virtual_bits_inconsistent_with_the_map_cannot_be_opened() { // but the virtual witness no longer equals M (1 || f). virtual_bits[0] = F128::from(6u64); let mut transcript = prover_transcript(); + let (_, data) = instance + .pcs + .commit_with_ood(&instance.committed_bits, &mut transcript) + .unwrap(); assert_eq!( BitZProver::new(instance.params, WINDOW).prove_virtual( &instance.statement(), &instance.pcs, - &instance.data, + &data, VirtualWitness { committed_bits: instance.committed_bits.clone(), virtual_bits: &virtual_bits, @@ -363,8 +371,10 @@ fn sha256_virtual_inner_product_opens_the_committed_bits() { let claim = LinearClaim::new(¶ms, rows, columns, target).unwrap(); let statement = VirtualStatement::new(params, committed_shape, &map, &claim).unwrap(); let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); - let (root, data) = pcs.commit(&committed_bits).unwrap(); let mut transcript = prover_transcript(); + let (root, data) = pcs + .commit_with_ood(&committed_bits, &mut transcript) + .unwrap(); BitZProver::new(params, WINDOW) .prove_virtual( &statement, diff --git a/crates/verifier/src/verify.rs b/crates/verifier/src/verify.rs index e90f2770..e90da561 100644 --- a/crates/verifier/src/verify.rs +++ b/crates/verifier/src/verify.rs @@ -2,7 +2,7 @@ use common::{LinearClaim, OpeningQuery, Root, VirtualMap, VirtualMapError, VirtualStatement}; use field::Fq; -use pcs::{CommitScheme, Pcs, StatementBinding, VerifyError as OpeningVerifyError}; +use pcs::{CommitScheme, Pcs, StatementBinding, VerifierData, VerifyError as OpeningVerifyError}; use transcript::VerifierState; use crate::{BitZVerifier, ReceiveError, ReduceError, reduce::gkr_reduce}; @@ -25,6 +25,39 @@ pub enum VerifyError { } impl BitZVerifier { + /// Receives the commitment's OOD claim and verifies the virtual BitZ proof. + pub fn verify_virtual( + &self, + statement: &VirtualStatement<'_, Q, impl VirtualMap>, + pcs: &Pcs, + root: Root, + mut transcript: VerifierState<'_>, + ) -> Result<(), VerifyError> { + if self.params() != statement.params().claim() + || pcs.bit_len() != 1 << statement.params().committed_shape().log_bits() + { + return Err(VerifyError::ParameterMismatch); + } + let commitment = pcs + .receive_commitment(root, &mut transcript) + .map_err(VerifyError::Opening)?; + self.verify_virtual_with_commitment(statement, pcs, &commitment, transcript) + } + + /// Receives the commitment's OOD claim and verifies the BitZ proof. + pub fn verify( + &self, + claim: &LinearClaim>, + pcs: &Pcs, + root: Root, + mut transcript: VerifierState<'_>, + ) -> Result<(), VerifyError> { + let commitment = pcs + .receive_commitment(root, &mut transcript) + .map_err(VerifyError::Opening)?; + self.verify_with_commitment(claim, pcs, &commitment, transcript) + } + /// Verifies a claim on `h = M (1 || f)` against the commitment to `f`. /// /// Build the setup from `statement.params().claim()` and match the commitment's @@ -32,15 +65,16 @@ impl BitZVerifier { /// padded virtual bits. Supply the public circuit's map; its digest must cover /// its shape and entries. /// - /// Start the transcript with the prover's session, instance, and public-input events. + /// Continue the transcript that produced `commitment` through + /// [`Pcs::receive_commitment`], using the prover's public-input events. /// This method binds the inputs in [`VirtualStatement`], transposes the reduced /// claim, verifies the PCS opening, and rejects trailing proof or hint bytes. #[tracing::instrument(name = "Verify virtual BitZ", skip_all)] - pub fn verify_virtual( + pub fn verify_virtual_with_commitment( &self, statement: &VirtualStatement<'_, Q, impl VirtualMap>, pcs: &Pcs, - com: Root, + commitment: &VerifierData, mut transcript: VerifierState<'_>, ) -> Result<(), VerifyError> { let params = statement.params(); @@ -51,7 +85,7 @@ impl BitZVerifier { return Err(VerifyError::ParameterMismatch); } transcript.public_message(b"bitz/virtual-statement/v1"); - transcript.public_message(&com.0); + transcript.public_message(&commitment.root().0); transcript.public_message(params); transcript.public_message(&statement.map().digest()); transcript.public_message(claim); @@ -59,7 +93,7 @@ impl BitZVerifier { let query = statement .transpose_query(query) .map_err(VerifyError::VirtualMap)?; - pcs.verify_lin(&com, &query, StatementBinding::Bind, &mut transcript) + pcs.verify_lin_with_ood(commitment, &query, StatementBinding::Bind, &mut transcript) .map_err(VerifyError::Opening)?; transcript .check_eof() @@ -68,29 +102,29 @@ impl BitZVerifier { /// Replays the proof of the caller's linear claim about the committed bits. /// - /// `pcs` must be the scheme the commitment was made under. The transcript - /// arrives carrying the caller's events; this appends and consumes it. + /// `pcs` must be the scheme the commitment was made under. Continue the + /// transcript used by [`Pcs::receive_commitment`]; this consumes it and checks EOF. #[tracing::instrument(name = "Verify BitZ", skip_all)] - pub fn verify( + pub fn verify_with_commitment( &self, claim: &LinearClaim>, pcs: &Pcs, - com: Root, + commitment: &VerifierData, mut transcript: VerifierState<'_>, ) -> Result<(), VerifyError> { // Step 1: the admissibility and precondition checks have already run -- // the shape gates in Shape::new, the modulus in Fq's own const assertions, // the generator's order in BitZParams::new and the weight counts in // LinearClaim::new. What is left is binding, before any challenge. - transcript.public_message(&com.0); + transcript.public_message(&commitment.root().0); transcript.public_message(self.params()); // Steps 3 and 4: check integer folds and replay GKR to obtain a bit claim. let query = self.fold_and_reduce(claim, &mut transcript)?; // Step 6: verify the inner-product sumcheck, ring switch, and opening. - // Acceptance requires authenticating GKR's terminal claim against com. - pcs.verify_lin(&com, &query, StatementBinding::Bind, &mut transcript) + // Acceptance requires authenticating GKR's terminal claim against the commitment. + pcs.verify_lin_with_ood(commitment, &query, StatementBinding::Bind, &mut transcript) .map_err(VerifyError::Opening)?; // Both streams must be spent. Taking the transcript by value is what diff --git a/tooling/cli/benches/circuits.rs b/tooling/cli/benches/circuits.rs index 987f2952..13fec1c1 100644 --- a/tooling/cli/benches/circuits.rs +++ b/tooling/cli/benches/circuits.rs @@ -52,11 +52,13 @@ fn commit(bencher: Bencher, circuit: BuiltinCircuit) { #[divan::bench(args = BuiltinCircuit::ALL)] fn prove(bencher: Bencher, circuit: BuiltinCircuit) { let (system, inputs) = setup(circuit); - let witness = system.witness(&inputs).unwrap(); - let data = system.commit(&witness).unwrap(); bencher - .with_inputs(|| system.witness(&inputs).unwrap()) - .bench_local_values(|witness| system.prove(witness, &data).unwrap()); + .with_inputs(|| { + let witness = system.witness(&inputs).unwrap(); + let data = system.commit(&witness).unwrap(); + (witness, data) + }) + .bench_local_values(|(witness, data)| system.prove(witness, data).unwrap()); } #[divan::bench(args = BuiltinCircuit::ALL)] @@ -64,6 +66,6 @@ fn verify(bencher: Bencher, circuit: BuiltinCircuit) { let (system, inputs) = setup(circuit); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); - let proof = system.prove(witness, &data).unwrap(); + let proof = system.prove(witness, data).unwrap(); bencher.bench_local(|| system.verify(&proof).unwrap()); } diff --git a/tooling/cli/src/benchmark.rs b/tooling/cli/src/benchmark.rs index e5bbc09f..cbd8f6a9 100644 --- a/tooling/cli/src/benchmark.rs +++ b/tooling/cli/src/benchmark.rs @@ -38,7 +38,7 @@ pub fn run(statement: S, inputs: &[bool]) -> Result, } +/// Commitment data and the transcript that sampled its OOD claim. +pub struct CommittedWitness { + data: ProverData, + transcript: ProverState, +} + #[derive(Clone, Debug)] pub struct Proof { pub root: Root, @@ -201,23 +209,31 @@ impl CircuitProofSystem { }) } + /// Commits and sends the initial OOD evaluation before any PIOP challenge. + /// The returned state retains both PCS data and the transcript for proving. #[tracing::instrument(name = "commit", skip_all)] - pub fn commit(&self, witness: &Witness) -> Result { - self.pcs - .commit(&witness.committed) - .map(|(_, data)| data) - .map_err(Error::Commit) + pub fn commit(&self, witness: &Witness) -> Result { + let mut transcript = build_prover(SESSION, self.statement.domain()); + let (_, data) = self + .pcs + .commit_with_ood(&witness.committed, &mut transcript) + .map_err(Error::Commit)?; + Ok(CommittedWitness { data, transcript }) } + /// Continues the commitment transcript through Spartan and the BitZ opening. #[tracing::instrument(name = "prove", skip_all, fields(opening_path = ?self.opening_path))] - pub fn prove(&self, witness: Witness, data: &ProverData) -> Result { + pub fn prove(&self, witness: Witness, commitment: CommittedWitness) -> Result { + let CommittedWitness { + data, + mut transcript, + } = commitment; let root = data.root(); - let mut transcript = build_prover(SESSION, self.statement.domain()); self.bind(&mut transcript, root); if self.opening_path == OpeningPath::Direct { self.pcs .prove_lin( - data, + &data, witness.committed.clone(), &self.constant_query(), StatementBinding::Bind, @@ -236,7 +252,7 @@ impl CircuitProofSystem { let prover = BitZProver::new(self.params, WINDOW); match self.opening_path { OpeningPath::Direct => { - prover.prove(&claim, &self.pcs, data, witness.committed, &mut transcript) + prover.prove(&claim, &self.pcs, &data, witness.committed, &mut transcript) } OpeningPath::Virtual => { let statement = @@ -245,7 +261,7 @@ impl CircuitProofSystem { prover.prove_virtual( &statement, &self.pcs, - data, + &data, VirtualWitness { committed_bits: witness.committed, virtual_bits: &witness.assignment_bits, @@ -265,11 +281,15 @@ impl CircuitProofSystem { #[tracing::instrument(name = "verify", skip_all)] pub fn verify(&self, proof: &Proof) -> Result<(), Error> { let mut transcript = build_verifier(SESSION, self.statement.domain(), &proof.opening); + let commitment = self + .pcs + .receive_commitment(proof.root, &mut transcript) + .map_err(Error::OodVerify)?; self.bind(&mut transcript, proof.root); if self.opening_path == OpeningPath::Direct { self.pcs - .verify_lin( - &proof.root, + .verify_lin_with_ood( + &commitment, &self.constant_query(), StatementBinding::Bind, &mut transcript, @@ -281,12 +301,19 @@ impl CircuitProofSystem { let claim = opening_claim(&self.params, &terminal)?; let verifier = BitZVerifier::new(self.params, WINDOW); match self.opening_path { - OpeningPath::Direct => verifier.verify(&claim, &self.pcs, proof.root, transcript), + OpeningPath::Direct => { + verifier.verify_with_commitment(&claim, &self.pcs, &commitment, transcript) + } OpeningPath::Virtual => { let statement = VirtualStatement::new(self.params, self.committed_shape, &self.map, &claim) .map_err(|_| Error::Configuration("invalid virtual statement"))?; - verifier.verify_virtual(&statement, &self.pcs, proof.root, transcript) + verifier.verify_virtual_with_commitment( + &statement, + &self.pcs, + &commitment, + transcript, + ) } } .map_err(Error::Verify) @@ -405,12 +432,28 @@ mod tests { } } + #[test] + fn commitment_sends_ood_before_proving() { + let system = CircuitProofSystem::new(IdentityBit).unwrap(); + let witness = system.witness(&[true]).unwrap(); + let committed = system.commit(&witness).unwrap(); + let proof = committed.transcript.finish(); + assert_eq!(proof.narg_string.len(), 16); + assert!(proof.hints.is_empty()); + let mut verifier = build_verifier(SESSION, system.statement.domain(), &proof); + system + .pcs + .receive_commitment(committed.data.root(), &mut verifier) + .unwrap(); + verifier.check_eof().unwrap(); + } + #[test] fn direct_opening_requires_constant_one_on_both_sides() { let mut system = CircuitProofSystem::new(IdentityBit).unwrap(); let witness = system.witness(&[true]).unwrap(); let data = system.commit(&witness).unwrap(); - let mut proof = system.prove(witness, &data).unwrap(); + let mut proof = system.prove(witness, data).unwrap(); system.verify(&proof).unwrap(); system.opening_path = OpeningPath::Virtual; assert!(system.verify(&proof).is_err()); @@ -420,13 +463,17 @@ mod tests { bad_witness.committed.fill(F128::ZERO); let bad_data = system.commit(&bad_witness).unwrap(); assert!(matches!( - system.prove(bad_witness, &bad_data), + system.prove(bad_witness, bad_data), Err(Error::ConstantProve(_)) )); // A valid opening to zero must not substitute for the required one. let packed = vec![F128::ZERO; 1 << system.committed_shape.log_packed_len()]; let mut transcript = build_prover(SESSION, system.statement.domain()); + let (_, bad_data) = system + .pcs + .commit_with_ood(&packed, &mut transcript) + .unwrap(); system.bind(&mut transcript, bad_data.root()); let query = OpeningQuery::Mle { point: vec![F128::ZERO; system.committed_shape.log_bits()], diff --git a/tooling/cli/tests/circuits.rs b/tooling/cli/tests/circuits.rs index 4f46dcd6..bb0f5153 100644 --- a/tooling/cli/tests/circuits.rs +++ b/tooling/cli/tests/circuits.rs @@ -54,7 +54,7 @@ fn supported_sha_circuits_prove_and_verify() { let system = CircuitProofSystem::new(statement).unwrap(); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); - let proof = system.prove(witness, &data).unwrap(); + let proof = system.prove(witness, data).unwrap(); system.verify(&proof).unwrap(); } } @@ -77,7 +77,7 @@ fn sha_compression_matches_abc_and_binds_public_values() { let system = CircuitProofSystem::new(statement.clone()).unwrap(); let witness = system.witness(&inputs).unwrap(); let data = system.commit(&witness).unwrap(); - let proof = system.prove(witness, &data).unwrap(); + let proof = system.prove(witness, data).unwrap(); CircuitProofSystem::new(statement.clone()) .unwrap() .verify(&proof) diff --git a/tooling/cli/tests/end_to_end.rs b/tooling/cli/tests/end_to_end.rs index 7484ce80..857f7b80 100644 --- a/tooling/cli/tests/end_to_end.rs +++ b/tooling/cli/tests/end_to_end.rs @@ -1,5 +1,28 @@ -use bitz_cli::end_to_end::{CircuitProofSystem, CircuitStatement, Error, OpeningPath}; +use bitz_cli::end_to_end::{CircuitProofSystem, CircuitStatement, Error, OpeningPath, Proof}; use circuit::Circuit; +use pcs::VerifyError; + +fn rejects_changed_or_missing_ood( + system: &CircuitProofSystem, + proof: &Proof, +) { + // These Fast-profile fixtures have zero initial grinding bits, so the first + // 16 transcript bytes encode the OOD evaluation. + let mut changed = proof.clone(); + changed.opening.narg_string[0] ^= 1; + assert!(system.verify(&changed).is_err()); + + let mut missing = proof.clone(); + missing.opening.narg_string.drain(..16); + assert!(system.verify(&missing).is_err()); + + let mut truncated = proof.clone(); + truncated.opening.narg_string.truncate(15); + assert!(matches!( + system.verify(&truncated), + Err(Error::OodVerify(VerifyError::MalformedProof)) + )); +} struct PublicBit; @@ -28,11 +51,12 @@ fn generic_driver_accepts_a_non_sha_circuit() { assert_eq!(prepared.stats().committed_bits, 2); let witness = prepared.witness(&[true]).unwrap(); let data = prepared.commit(&witness).unwrap(); - let proof = prepared.prove(witness, &data).unwrap(); + let proof = prepared.prove(witness, data).unwrap(); CircuitProofSystem::new(PublicBit) .unwrap() .verify(&proof) .unwrap(); + rejects_changed_or_missing_ood(&prepared, &proof); let mut changed = proof.clone(); changed.root.0[0] ^= 1; @@ -100,11 +124,12 @@ fn nonidentity_map_uses_virtual_opening_and_checks_xor_relation() { assert_eq!(system.stats().committed_bits, 2); let witness = system.witness(&[true, false]).unwrap(); let data = system.commit(&witness).unwrap(); - let proof = system.prove(witness, &data).unwrap(); + let proof = system.prove(witness, data).unwrap(); CircuitProofSystem::new(PublicXor) .unwrap() .verify(&proof) .unwrap(); + rejects_changed_or_missing_ood(&system, &proof); assert!(matches!( system.witness(&[true, true]), Err(Error::Unsatisfied) From 09b1238b343b4f0ab37bd135d33ad6354a023854 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 30 Sep 2026 11:42:51 +0530 Subject: [PATCH 4/7] fix: exclude commitment from proving time --- crates/tests/examples/dump_bitz.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/tests/examples/dump_bitz.rs b/crates/tests/examples/dump_bitz.rs index 85f6d316..920b97b7 100644 --- a/crates/tests/examples/dump_bitz.rs +++ b/crates/tests/examples/dump_bitz.rs @@ -5,6 +5,7 @@ mod support; use std::io::Write; +use std::time::Instant; use common::Shape; use crypto_primitives::LiftElement; @@ -27,12 +28,12 @@ fn main() -> Result<(), Box> { let (t, s) = (shape.log_rows(), shape.log_columns()); let instance = Instance::honest(shape, seed); - let started = std::time::Instant::now(); let mut transcript = prover_transcript(); let (_, data) = instance .pcs .commit_with_ood(&instance.packed, &mut transcript) .unwrap(); + let started = Instant::now(); instance .prover .prove( From 7471c440e56891aea0efa6cdde78bdab5744313d Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 30 Sep 2026 12:13:45 +0530 Subject: [PATCH 5/7] refactor: let security profiles control initial OOD sampling --- crates/pcs/src/commitment.rs | 146 ++++++++++++------ crates/pcs/src/lib.rs | 106 ++++--------- crates/pcs/src/ligerito.rs | 13 +- crates/pcs/src/opening.rs | 53 +++---- crates/pcs/src/opening/tests.rs | 89 +++++++++-- crates/pcs/tests/round_trip.rs | 220 +++++++++++++++++---------- crates/prover/src/prove.rs | 4 +- crates/tests/examples/dump_bitz.rs | 2 +- crates/tests/examples/dump_commit.rs | 4 +- crates/tests/src/lib.rs | 2 +- crates/tests/tests/host.rs | 2 +- crates/tests/tests/prove.rs | 4 +- crates/tests/tests/virtual_prove.rs | 12 +- crates/verifier/src/verify.rs | 10 +- tooling/cli/src/end_to_end.rs | 9 +- 15 files changed, 397 insertions(+), 279 deletions(-) diff --git a/crates/pcs/src/commitment.rs b/crates/pcs/src/commitment.rs index 2a2ca593..d49af9e9 100644 --- a/crates/pcs/src/commitment.rs +++ b/crates/pcs/src/commitment.rs @@ -18,7 +18,7 @@ use field::F128; pub use flock_core::hash::HashKind; use flock_core::pcs::Commitment as FlockCommitment; use flock_core::pcs::ligerito::LigeritoProfile; -use flock_core::pcs::{PcsParams, ProverData as FlockProverData}; +use flock_core::pcs::{PcsParams, ProverData as FlockProverData, commit}; use transcript::{Encoding, ProverState, VerifierState}; /// Errors from PCS configuration. @@ -46,26 +46,36 @@ pub struct Pcs { packed_len: usize, } -/// Flock state retained between commitment and openings. -/// OOD-aware commitment creation also retains the initial evaluation for each opening. +/// Commitment and private Flock data retained for proving openings. pub struct ProverData { - commitment: FlockCommitment, + commitment: Commitment, flock_prover_data: FlockProverData, - pub(crate) ood: Option, } -/// Commitment and out-of-domain claim read from the verifier transcript. -/// The claim is authenticated only after [`CommitScheme::verify_lin_with_ood`](crate::CommitScheme::verify_lin_with_ood) succeeds. +/// Root, parameters, and optional out-of-domain claim retained after commitment. +/// Both prover and verifier use this state for openings on the commitment transcript. +/// The verifier authenticates the claim when [`CommitScheme::verify_lin`](crate::CommitScheme::verify_lin) succeeds. #[derive(Debug)] -pub struct VerifierData { - pub(crate) root: Root, +pub struct Commitment { + flock: FlockCommitment, pub(crate) ood: Option, } -impl VerifierData { +impl Commitment { /// Returns the public commitment root. pub fn root(&self) -> Root { - self.root + Root(self.flock.root) + } + + pub(crate) fn matches(&self, pcs: &Pcs) -> bool { + let expected = pcs.params(); + let actual = &self.flock.params; + expected.m == actual.m + && expected.log_inv_rate == actual.log_inv_rate + && expected.log_batch_size == actual.log_batch_size + && expected.profile == actual.profile + && expected.merkle_hash == actual.merkle_hash + && self.ood.is_some() == pcs.ood_grinding_bits().is_some() } } @@ -105,10 +115,14 @@ impl Pcs { }) } - /// Commits to the packed codeword without sampling an OOD claim. - /// Use [`Self::commit_with_ood`] for protocols requiring initial OOD sampling. + /// Commits and samples the initial OOD claim when the security profile requires it. + /// Call before witness-dependent challenges and continue with the same transcript. #[tracing::instrument(name = "Commit witness", skip_all)] - pub fn commit(&self, packed_witness: &[F128]) -> Result<(Root, ProverData), CommitError> { + pub fn commit( + &self, + packed_witness: &[F128], + transcript: &mut ProverState, + ) -> Result<(Root, ProverData), CommitError> { // 1. Input Validation if packed_witness.len() != self.packed_len() { return Err(CommitError::PackedWitnessLengthMismatch); @@ -116,53 +130,43 @@ impl Pcs { // 2. Commit Packed Witness let (flock_commitment, flock_prover_data) = - flock_core::pcs::commit(as_flock_f128s(packed_witness), &self.params); + commit(as_flock_f128s(packed_witness), &self.params); // 3. Build Public Commitment - let commitment = Root(flock_commitment.root); + let root = Root(flock_commitment.root); + let ood = prove(self, &root.0, packed_witness, transcript); // 4. Retain Opening Data Ok(( - commitment, + root, ProverData { - commitment: flock_commitment, + commitment: Commitment { + flock: flock_commitment, + ood, + }, flock_prover_data, - ood: None, }, )) } - /// Commits and retains the initial OOD claim for subsequent batched openings. - /// - /// Call before witness-dependent challenges and continue with the same transcript. - /// [`CommitScheme::prove_lin`](crate::CommitScheme::prove_lin) batches the retained claim into each opening. - /// Profiles using unique decoding omit the OOD round. - /// - /// Returns [`CommitError::PackedWitnessLengthMismatch`] before transcript mutation - /// if `packed_witness` does not have the configured length. - #[tracing::instrument(name = "Commit witness with OOD", skip_all)] - pub fn commit_with_ood( - &self, - packed_witness: &[F128], - transcript: &mut ProverState, - ) -> Result<(Root, ProverData), CommitError> { - let (root, mut data) = self.commit(packed_witness)?; - data.ood = prove(self, &root.0, packed_witness, transcript); - Ok((root, data)) - } - /// Receives the OOD claim for the public root before subsequent protocol challenges. /// - /// Mirrors [`Self::commit_with_ood`]. Invalid grinding or a truncated evaluation + /// Mirrors [`Self::commit`]. Invalid grinding or a truncated evaluation /// returns [`VerifyError::MalformedProof`]; authentication of the evaluation is - /// deferred to [`CommitScheme::verify_lin_with_ood`](crate::CommitScheme::verify_lin_with_ood). + /// deferred to [`CommitScheme::verify_lin`](crate::CommitScheme::verify_lin). pub fn receive_commitment( &self, root: Root, transcript: &mut VerifierState<'_>, - ) -> Result { + ) -> Result { let ood = verify(self, &root.0, transcript)?; - Ok(VerifierData { root, ood }) + Ok(Commitment { + flock: FlockCommitment { + root: root.0, + params: self.params.clone(), + }, + ood, + }) } pub fn bit_len(&self) -> usize { @@ -229,14 +233,15 @@ impl ProverData { /// The commitment this data opens against. pub fn root(&self) -> Root { - Root(self.commitment.root) + self.commitment.root() } pub(crate) fn flock_data(&self) -> &FlockProverData { &self.flock_prover_data } - pub(crate) fn commitment(&self) -> &FlockCommitment { + /// Returns the shared commitment without the private proving data. + pub fn commitment(&self) -> &Commitment { &self.commitment } } @@ -246,6 +251,7 @@ mod tests { use flock_core::pcs::pack_witness; use num_traits::ConstZero; use proptest::prelude::*; + use transcript::{build_prover, build_verifier}; use super::*; @@ -253,6 +259,32 @@ mod tests { Shape::new(7, 15).unwrap() } + #[test] + fn commitment_profiles_select_ood_and_preserve_transcript_agreement() { + for profile in [ + LigeritoProfile::Fast, + LigeritoProfile::Slim, + LigeritoProfile::Secure, + ] { + let pcs = Pcs::new(&shape(), profile, HashKind::Blake3).unwrap(); + let witness = vec![F128::ZERO; pcs.packed_len()]; + let mut prover = build_prover(b"commit-test", b"profile"); + let (root, data) = pcs.commit(&witness, &mut prover).unwrap(); + let expected_ood = profile != LigeritoProfile::Secure; + assert_eq!(data.commitment().ood.is_some(), expected_ood); + let next_challenge = prover.verifier_message::(); + let proof = prover.finish(); + assert_eq!(proof.narg_string.is_empty(), !expected_ood); + let mut verifier = build_verifier(b"commit-test", b"profile", &proof); + let received = pcs.receive_commitment(root, &mut verifier).unwrap(); + assert_eq!(received.root(), data.commitment().root()); + assert_eq!(received.ood.is_some(), expected_ood); + assert!(received.matches(&pcs)); + assert_eq!(verifier.verifier_message::(), next_challenge); + verifier.check_eof().unwrap(); + } + } + #[test] fn commitment_is_deterministic_for_packed_boundary_bits() { let scheme = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); @@ -261,11 +293,26 @@ mod tests { packed_witness[1] = F128::new(1, 0); packed_witness.last_mut().unwrap().hi = 1 << 63; - let (commitment, data) = scheme.commit(&packed_witness).unwrap(); - let (second_commitment, _) = scheme.commit(&packed_witness).unwrap(); + let (commitment, data) = scheme + .commit( + &packed_witness, + &mut build_prover(b"commit-test", b"witness"), + ) + .unwrap(); + let (second_commitment, _) = scheme + .commit( + &packed_witness, + &mut build_prover(b"commit-test", b"witness"), + ) + .unwrap(); let mut changed_witness = packed_witness.clone(); changed_witness[0].lo |= 1 << 2; - let (changed_commitment, _) = scheme.commit(&changed_witness).unwrap(); + let (changed_commitment, _) = scheme + .commit( + &changed_witness, + &mut build_prover(b"commit-test", b"witness"), + ) + .unwrap(); assert_eq!(commitment, second_commitment); assert_ne!(commitment, changed_commitment); @@ -329,11 +376,14 @@ mod tests { fn rejects_arbitrary_short_packed_witnesses(len in 0usize..4096) { let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; len]; + let mut transcript = build_prover(b"commit-test", b"short"); prop_assert!(matches!( - pcs.commit(&packed_witness), + pcs.commit(&packed_witness, &mut transcript), Err(CommitError::PackedWitnessLengthMismatch) )); + let proof = transcript.finish(); + prop_assert!(proof.narg_string.is_empty() && proof.hints.is_empty()); } } } diff --git a/crates/pcs/src/lib.rs b/crates/pcs/src/lib.rs index 3ea0528e..5cb613a6 100644 --- a/crates/pcs/src/lib.rs +++ b/crates/pcs/src/lib.rs @@ -29,7 +29,7 @@ //! - [`Pcs`] stores trusted Flock parameters and the expected bit length. //! - [`Root`] is the public Merkle root. //! - [`ProverData`] retains the codeword and Merkle tree after commitment. -//! - [`VerifierData`] retains the root and OOD claim received before opening. +//! - [`Commitment`] retains the root, parameters, and optional OOD claim on both sides. //! - [`OpeningQuery`] contains an MLE point and target, or a `common::LinearClaim`. //! - [`CommitScheme`] connects commitment, proving, and verification to project transcripts. //! - [`ConfigError`] reports configuration failures. @@ -40,9 +40,9 @@ //! It consumes the packed witness and borrows [`ProverData`]. //! The caller must use matching transcript session and instance labels. //! The caller must also call `VerifierState::check_eof` after successful verification. -//! Use [`Pcs::commit_with_ood`] and [`Pcs::receive_commitment`] before any -//! witness-dependent challenges to include the initial OOD claim. Proving batches -//! that retained claim automatically; verification uses [`Pcs::verify_lin_with_ood`]. +//! Use [`Pcs::commit`] and [`Pcs::receive_commitment`] before any +//! witness-dependent challenges. The security profile selects initial OOD sampling; +//! opening proofs authenticate the retained claim automatically. //! //! # Example //! @@ -70,7 +70,7 @@ //! }; //! //! let mut prover = build_prover(b"pcs-example", b"zero-polynomial"); -//! let (commitment, prover_data) = pcs.commit_with_ood(&packed_witness, &mut prover).unwrap(); +//! let (commitment, prover_data) = pcs.commit(&packed_witness, &mut prover).unwrap(); //! pcs.prove_lin( //! &prover_data, //! packed_witness, @@ -83,7 +83,7 @@ //! //! let mut verifier = build_verifier(b"pcs-example", b"zero-polynomial", &proof); //! let commitment = pcs.receive_commitment(commitment, &mut verifier).unwrap(); -//! pcs.verify_lin_with_ood( +//! pcs.verify_lin( //! &commitment, //! &query, //! StatementBinding::Bind, @@ -109,9 +109,10 @@ mod transpose; mod transpose_tests; use field::F128; +use opening::{prove, verify}; use transcript::{ProverState, VerifierState}; -pub use commitment::{CommitError, ConfigError, HashKind, Pcs, ProverData, VerifierData}; +pub use commitment::{CommitError, Commitment, ConfigError, HashKind, Pcs, ProverData}; pub use common::{OpeningQuery, Root}; pub use flock_core::pcs::ligerito::LigeritoProfile; pub use opening::{ProveError, VerifyError}; @@ -138,45 +139,32 @@ pub enum StatementBinding { /// [`OpeningQuery::InnerProduct`] accepts row weights, column weights, and a target over `F128`. /// The post-GKR sumcheck reduces this claim to an MLE claim before the opening protocol. pub trait CommitScheme { - /// The public commitment. + /// Commitment state shared by the prover and verifier. type Commitment; /// Private data retained by the prover after commitment. type ProverData; - /// Commitment and OOD claim retained by the verifier before opening. - type VerifierData; - /// Commits the caller-owned packed witness to `Enc_C(q_pkd)`, where - /// `q_pkd(y) = Σ_{v ∈ {0,1}^7} q(y, v) · basis[v]`. - /// Bit `r` of element `i` must equal logical bit `128 * i + r`. + /// Commits the packed witness and runs the security profile's initial checks. + /// Bit `r` of packed element `i` must equal logical bit `128 * i + r`. + /// Call before witness-dependent challenges and continue the same transcript. + /// Returns the public root and private data containing the retained commitment. fn commit( &self, packed_witness: &[F128], - ) -> Result<(Self::Commitment, Self::ProverData), CommitError>; - - /// Commits and retains the initial OOD claim for subsequent openings. - /// - /// Call before witness-dependent challenges and continue with the same transcript. - /// Profiles without initial OOD sampling omit that round. - fn commit_with_ood( - &self, - packed_witness: &[F128], transcript: &mut ProverState, - ) -> Result<(Self::Commitment, Self::ProverData), CommitError>; + ) -> Result<(Root, Self::ProverData), CommitError>; - /// Receives the OOD claim for the public commitment before protocol challenges. - /// - /// Mirrors [`Self::commit_with_ood`]. The returned claim must be authenticated - /// by [`Self::verify_lin_with_ood`] on the same transcript. + /// Receives commitment state before subsequent protocol challenges. + /// An opening must authenticate the retained OOD claim before accepting the proof. fn receive_commitment( &self, - commitment: Self::Commitment, + root: Root, transcript: &mut VerifierState<'_>, - ) -> Result; + ) -> Result; - /// Consumes the exact packed witness and proves either opening query. - /// - /// Inner-product claims first pass through the post-GKR sumcheck and then the MLE opening protocol. - /// An OOD claim retained by [`Self::commit_with_ood`] is batched into the opening. + /// Consumes the packed witness and proves either opening query. + /// Inner-product claims pass through the post-GKR sumcheck before the MLE opening. + /// Any initial OOD claim retained by `commit` is batched into the opening. fn prove_lin( &self, data: &Self::ProverData, @@ -186,9 +174,9 @@ pub trait CommitScheme { transcript: &mut ProverState, ) -> Result<(), ProveError>; - /// Verifies either opening query against `commitment`. - /// - /// Inner-product claims first pass through the post-GKR sumcheck and then the MLE opening protocol. + /// Verifies either opening query and any retained OOD claim. + /// Continue the transcript used by `receive_commitment`. The commitment is + /// borrowed so multiple openings can authenticate the same OOD claim. fn verify_lin( &self, commitment: &Self::Commitment, @@ -196,46 +184,26 @@ pub trait CommitScheme { statement_binding: StatementBinding, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError>; - - /// Verifies the linear query batched with the retained OOD claim. - /// - /// Continue the transcript used by [`Self::receive_commitment`]. Borrowing - /// the retained state permits multiple openings against the same commitment. - fn verify_lin_with_ood( - &self, - commitment: &Self::VerifierData, - query: &OpeningQuery, - statement_binding: StatementBinding, - transcript: &mut VerifierState<'_>, - ) -> Result<(), VerifyError>; } impl CommitScheme for Pcs { - type Commitment = Root; + type Commitment = Commitment; type ProverData = ProverData; - type VerifierData = VerifierData; fn commit( &self, packed_witness: &[F128], - ) -> Result<(Self::Commitment, Self::ProverData), CommitError> { - Pcs::commit(self, packed_witness) - } - - fn commit_with_ood( - &self, - packed_witness: &[F128], transcript: &mut ProverState, - ) -> Result<(Self::Commitment, Self::ProverData), CommitError> { - Pcs::commit_with_ood(self, packed_witness, transcript) + ) -> Result<(Root, Self::ProverData), CommitError> { + self.commit(packed_witness, transcript) } fn receive_commitment( &self, - commitment: Self::Commitment, + root: Root, transcript: &mut VerifierState<'_>, - ) -> Result { - Pcs::receive_commitment(self, commitment, transcript) + ) -> Result { + self.receive_commitment(root, transcript) } fn prove_lin( @@ -246,7 +214,7 @@ impl CommitScheme for Pcs { statement_binding: StatementBinding, transcript: &mut ProverState, ) -> Result<(), ProveError> { - opening::prove( + prove( self, data, packed_witness, @@ -263,16 +231,6 @@ impl CommitScheme for Pcs { statement_binding: StatementBinding, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError> { - opening::verify(self, commitment, query, statement_binding, None, transcript) - } - - fn verify_lin_with_ood( - &self, - commitment: &Self::VerifierData, - query: &OpeningQuery, - statement_binding: StatementBinding, - transcript: &mut VerifierState<'_>, - ) -> Result<(), VerifyError> { - opening::verify_lin_with_ood(self, commitment, query, statement_binding, transcript) + verify(self, commitment, query, statement_binding, transcript) } } diff --git a/crates/pcs/src/ligerito.rs b/crates/pcs/src/ligerito.rs index e2abc5fd..2c388759 100644 --- a/crates/pcs/src/ligerito.rs +++ b/crates/pcs/src/ligerito.rs @@ -249,14 +249,7 @@ pub(crate) fn validate_verifier_config( } pub(crate) fn validate_prover_data(pcs: &Pcs, data: &ProverData) -> Result<(), ProveError> { - let expected = pcs.params(); - let actual = &data.commitment().params; - if expected.m != actual.m - || expected.log_inv_rate != actual.log_inv_rate - || expected.log_batch_size != actual.log_batch_size - || expected.profile != actual.profile - || expected.merkle_hash != actual.merkle_hash - { + if !data.commitment().matches(pcs) { return Err(ProveError::ProverDataMismatch); } Ok(()) @@ -598,12 +591,12 @@ mod tests { let shape = Shape::new(7, 15).unwrap(); let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (commitment, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, INSTANCE); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); let query = OpeningQuery::Mle { point: vec![F128::from(2u64); 22], target: F128::ZERO, }; - let mut prover = build_prover(SESSION, INSTANCE); pcs.prove_lin( &data, packed_witness, diff --git a/crates/pcs/src/opening.rs b/crates/pcs/src/opening.rs index 95971052..a7b1d542 100644 --- a/crates/pcs/src/opening.rs +++ b/crates/pcs/src/opening.rs @@ -13,7 +13,7 @@ use transcript::{ProverState, PublicTranscript, VerifierState}; use crate::bridge::{as_flock_f128, as_flock_f128s, from_flock_f128}; use crate::ligerito::{self, ReducedProver, validate_prover_data}; use crate::ood::{OodClaim, add_dense_basis, add_succinct_basis, batching_challenge}; -use crate::{OpeningQuery, Pcs, ProverData, Root, StatementBinding, VerifierData, mle}; +use crate::{Commitment, OpeningQuery, Pcs, ProverData, Root, StatementBinding, mle}; const MLE_STATEMENT_LABEL: &[u8] = b"bitz/pcs/mle-opening/v1"; const INNER_PRODUCT_STATEMENT_LABEL: &[u8] = b"bitz/pcs/bit-inner-product/v3"; @@ -106,28 +106,6 @@ impl From for VerifyError { } } -/// Verifies an opening batched with the OOD claim retained at commitment ingestion. -/// -/// Use the state returned by [`Pcs::receive_commitment`] and continue its transcript. -/// The state is borrowed so multiple openings can authenticate the same OOD claim. -/// Profiles without OOD sampling verify the ordinary linear claim. -pub(crate) fn verify_lin_with_ood( - pcs: &Pcs, - commitment: &VerifierData, - query: &OpeningQuery, - statement_binding: StatementBinding, - transcript: &mut VerifierState<'_>, -) -> Result<(), VerifyError> { - verify( - pcs, - &commitment.root, - query, - statement_binding, - commitment.ood.as_ref(), - transcript, - ) -} - #[tracing::instrument(name = "Prove PCS opening", skip_all)] pub(crate) fn prove( pcs: &Pcs, @@ -137,20 +115,28 @@ pub(crate) fn prove( statement_binding: StatementBinding, transcript: &mut ProverState, ) -> Result<(), ProveError> { + let commitment = data.commitment(); + let root = commitment.root(); match query { OpeningQuery::Mle { point, target } => { let ring_switch = mle::RingSwitch::new(point, pcs.params().m)?; let prover = ReducedProver::new(pcs, data, packed_witness)?; if statement_binding == StatementBinding::Bind { - bind_mle_statement(pcs, &data.commitment().root, point, *target, transcript); + bind_mle_statement(pcs, &root.0, point, *target, transcript); } - prove_mle(prover, ring_switch, *target, data.ood.as_ref(), transcript) + prove_mle( + prover, + ring_switch, + *target, + commitment.ood.as_ref(), + transcript, + ) } OpeningQuery::InnerProduct { claim } => { validate_inner_product_claim(pcs, claim)?; validate_prover_data(pcs, data)?; if statement_binding == StatementBinding::Bind { - bind_inner_product_statement(pcs, &data.commitment().root, claim, transcript); + bind_inner_product_statement(pcs, &root.0, claim, transcript); } transcript.public_message(SUMCHECK_LABEL); let reduced = prove_post_gkr(claim, &packed_witness, transcript)?; @@ -172,24 +158,28 @@ pub(crate) fn prove( #[tracing::instrument(name = "Verify PCS opening", skip_all)] pub(crate) fn verify( pcs: &Pcs, - commitment: &Root, + commitment: &Commitment, query: &OpeningQuery, statement_binding: StatementBinding, - ood_claim: Option<&OodClaim>, transcript: &mut VerifierState<'_>, ) -> Result<(), VerifyError> { + if !commitment.matches(pcs) { + return Err(VerifyError::VerificationFailed); + } + let root = commitment.root(); + let ood_claim = commitment.ood.as_ref(); match query { OpeningQuery::Mle { point, target } => { let ring_switch = mle::RingSwitch::new(point, pcs.params().m)?; if statement_binding == StatementBinding::Bind { - bind_mle_statement(pcs, &commitment.0, point, *target, transcript); + bind_mle_statement(pcs, &root.0, point, *target, transcript); } - verify_mle(pcs, commitment, ring_switch, *target, ood_claim, transcript) + verify_mle(pcs, &root, ring_switch, *target, ood_claim, transcript) } OpeningQuery::InnerProduct { claim } => { validate_inner_product_claim(pcs, claim)?; if statement_binding == StatementBinding::Bind { - bind_inner_product_statement(pcs, &commitment.0, claim, transcript); + bind_inner_product_statement(pcs, &root.0, claim, transcript); } transcript.public_message(SUMCHECK_LABEL); let reduced = verify_post_gkr(claim, transcript)?; @@ -198,7 +188,6 @@ pub(crate) fn verify( commitment, &reduced, StatementBinding::Bind, - ood_claim, transcript, ) } diff --git a/crates/pcs/src/opening/tests.rs b/crates/pcs/src/opening/tests.rs index a0e16ab9..d09392e8 100644 --- a/crates/pcs/src/opening/tests.rs +++ b/crates/pcs/src/opening/tests.rs @@ -1,3 +1,4 @@ +use core::mem::size_of; use std::sync::OnceLock; use common::{LinearClaim, Shape}; @@ -16,7 +17,6 @@ const INSTANCE: &[u8] = b"singleton"; struct Fixture { pcs: Pcs, root: Root, - data: ProverData, witness: Vec, claim: LinearClaim, } @@ -28,7 +28,9 @@ fn fixture() -> &'static Fixture { let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let mut witness = vec![F128::ZERO; pcs.packed_len()]; witness[SINGLETON / 128].hi = 1 << (SINGLETON % 128 - 64); - let (root, data) = pcs.commit(&witness).unwrap(); + let (root, _) = pcs + .commit(&witness, &mut build_prover(SESSION, INSTANCE)) + .unwrap(); let rows = (0..shape.rows()) .map(|row| F128::from(row as u64 + 2)) .collect::>(); @@ -40,7 +42,6 @@ fn fixture() -> &'static Fixture { Fixture { pcs, root, - data, witness, claim, } @@ -51,10 +52,11 @@ fn fixture() -> &'static Fixture { fn inner_product_proof_composes_sumcheck_with_a_bound_mle_opening() { let fixture = fixture(); let mut prover = build_prover(SESSION, INSTANCE); + let (_, data) = fixture.pcs.commit(&fixture.witness, &mut prover).unwrap(); bind_inner_product_statement(&fixture.pcs, &fixture.root.0, &fixture.claim, &mut prover); prove( &fixture.pcs, - &fixture.data, + &data, fixture.witness.clone(), &OpeningQuery::InnerProduct { claim: fixture.claim.clone(), @@ -67,15 +69,18 @@ fn inner_product_proof_composes_sumcheck_with_a_bound_mle_opening() { // Independent composition checks stage order and binding of the derived MLE claim. let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + let commitment = fixture + .pcs + .receive_commitment(fixture.root, &mut verifier) + .unwrap(); bind_inner_product_statement(&fixture.pcs, &fixture.root.0, &fixture.claim, &mut verifier); verifier.public_message(SUMCHECK_LABEL); let reduced = verify_post_gkr(&fixture.claim, &mut verifier).unwrap(); verify( &fixture.pcs, - &fixture.root, + &commitment, &reduced, StatementBinding::Bind, - None, &mut verifier, ) .unwrap(); @@ -136,9 +141,10 @@ fn opening_leaves_matching_transcripts_for_following_protocols() { }, ] { let mut prover = build_prover(SESSION, INSTANCE); + let (_, data) = fixture.pcs.commit(&fixture.witness, &mut prover).unwrap(); prove( &fixture.pcs, - &fixture.data, + &data, fixture.witness.clone(), &query, StatementBinding::Bind, @@ -148,12 +154,15 @@ fn opening_leaves_matching_transcripts_for_following_protocols() { let expected = prover.verifier_message::(); let proof = prover.finish(); let mut verifier = build_verifier(SESSION, INSTANCE, &proof); + let commitment = fixture + .pcs + .receive_commitment(fixture.root, &mut verifier) + .unwrap(); verify( &fixture.pcs, - &fixture.root, + &commitment, &query, StatementBinding::Bind, - None, &mut verifier, ) .unwrap(); @@ -161,3 +170,65 @@ fn opening_leaves_matching_transcripts_for_following_protocols() { verifier.check_eof().unwrap(); } } + +#[test] +fn zero_weight_factor_still_requires_the_correct_pcs_witness_evaluation() { + let fixture = fixture(); + let pcs = &fixture.pcs; + let shape = Shape::new(7, M - 7).unwrap(); + assert_eq!(pcs.ood_grinding_bits(), Some(0)); + + for zero_rows in [true, false] { + let mut rows = fixture.claim.row_weights().to_vec(); + let mut columns = fixture.claim.column_weights().to_vec(); + if zero_rows { + rows.fill(F128::ZERO); + } else { + columns.fill(F128::ZERO); + } + let query = OpeningQuery::InnerProduct { + claim: LinearClaim::from_shape(&shape, rows, columns, F128::ZERO).unwrap(), + }; + let mut prover = build_prover(SESSION, b"zero-inner-product-factor"); + let (root, data) = pcs.commit(&fixture.witness, &mut prover).unwrap(); + prove( + pcs, + &data, + fixture.witness.clone(), + &query, + StatementBinding::Bind, + &mut prover, + ) + .unwrap(); + let proof = prover.finish(); + let mut verifier = build_verifier(SESSION, b"zero-inner-product-factor", &proof); + let commitment = pcs.receive_commitment(root, &mut verifier).unwrap(); + verify( + pcs, + &commitment, + &query, + StatementBinding::Bind, + &mut verifier, + ) + .unwrap(); + verifier.check_eof().unwrap(); + + // The OOD evaluation and two coefficients per sumcheck round precede the witness evaluation. + // A zero weight factor leaves that evaluation unconstrained until the PCS opening. + let evaluation_offset = (1 + 2 * M) * size_of::(); + let mut changed_proof = proof; + changed_proof.narg_string[evaluation_offset] ^= 1; + let mut verifier = build_verifier(SESSION, b"zero-inner-product-factor", &changed_proof); + let commitment = pcs.receive_commitment(root, &mut verifier).unwrap(); + assert_eq!( + verify( + pcs, + &commitment, + &query, + StatementBinding::Bind, + &mut verifier + ), + Err(VerifyError::VerificationFailed), + ); + } +} diff --git a/crates/pcs/tests/round_trip.rs b/crates/pcs/tests/round_trip.rs index 561964eb..5e4b3d46 100644 --- a/crates/pcs/tests/round_trip.rs +++ b/crates/pcs/tests/round_trip.rs @@ -4,8 +4,8 @@ use common::{LinearClaim, Shape}; use field::F128; use num_traits::ConstZero; use pcs::{ - CommitScheme, HashKind, LigeritoProfile, OpeningQuery, Pcs, ProveError, Root, StatementBinding, - VerifyError, + CommitScheme, Commitment, HashKind, LigeritoProfile, OpeningQuery, Pcs, ProveError, Root, + StatementBinding, VerifyError, }; use transcript::{Proof, PublicTranscript, VerifierState, build_prover, build_verifier}; @@ -16,6 +16,17 @@ const INSTANCE: &[u8] = b"m22-singleton-opening"; const INNER_PRODUCT_INSTANCE: &[u8] = b"m22-factored-inner-product"; const INNER_PRODUCT_SET_BITS: [usize; 8] = [0, 63, 64, 127, 128, 255, 256, (1 << M) - 1]; +fn verify_opening( + pcs: &Pcs, + root: &Root, + query: &OpeningQuery, + binding: StatementBinding, + transcript: &mut VerifierState<'_>, +) -> Result<(), VerifyError> { + let commitment = pcs.receive_commitment(*root, transcript)?; + pcs.verify_lin(&commitment, query, binding, transcript) +} + fn shape() -> Shape { Shape::new(7, M - 7).unwrap() } @@ -52,8 +63,8 @@ impl RealFixture { point, }; - let (commitment, data) = pcs.commit(&packed_witness).unwrap(); let mut prover = build_prover(SESSION, INSTANCE); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); pcs.prove_lin( &data, packed_witness, @@ -151,16 +162,19 @@ impl InnerProductFixture { let target = inner_product_target(&shape); assert_ne!(target, F128::ZERO); let query = factored_query(&shape, target); - let (commitment, data) = pcs.commit(&witness).unwrap(); let proofs = [StatementBinding::Bind, StatementBinding::AlreadyBound].map(|binding| { let mut prover = build_prover(SESSION, INNER_PRODUCT_INSTANCE); + let (commitment, data) = pcs.commit(&witness, &mut prover).unwrap(); if binding == StatementBinding::AlreadyBound { bind_outer_inner_product_statement(&mut prover, &pcs, &commitment, &query); } pcs.prove_lin(&data, witness.clone(), &query, binding, &mut prover) .unwrap(); - prover.finish() + (commitment, prover.finish()) }); + let [(commitment, bound), (other_root, already_bound)] = proofs; + assert_eq!(commitment, other_root); + let proofs = [bound, already_bound]; Self { pcs, commitment, @@ -179,12 +193,16 @@ impl InnerProductFixture { query: &OpeningQuery, proof: &'proof Proof, binding: StatementBinding, - ) -> VerifierState<'proof> { + ) -> (Commitment, VerifierState<'proof>) { let mut verifier = build_verifier(SESSION, INNER_PRODUCT_INSTANCE, proof); + let received = self + .pcs + .receive_commitment(*commitment, &mut verifier) + .unwrap(); if binding == StatementBinding::AlreadyBound { bind_outer_inner_product_statement(&mut verifier, &self.pcs, commitment, query); } - verifier + (received, verifier) } fn verify( @@ -194,9 +212,9 @@ impl InnerProductFixture { proof: &Proof, binding: StatementBinding, ) -> Result<(), VerifyError> { - let mut verifier = self.verifier(commitment, query, proof, binding); + let (received, mut verifier) = self.verifier(commitment, query, proof, binding); self.pcs - .verify_lin(commitment, query, binding, &mut verifier) + .verify_lin(&received, query, binding, &mut verifier) } } @@ -251,15 +269,14 @@ fn real_pcs_opening_round_trip_succeeds() { let fixture = fixture(); let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); - fixture - .pcs - .verify_lin( - &fixture.commitment, - &fixture.query, - StatementBinding::Bind, - &mut verifier, - ) - .unwrap(); + verify_opening( + &fixture.pcs, + &fixture.commitment, + &fixture.query, + StatementBinding::Bind, + &mut verifier, + ) + .unwrap(); verifier.check_eof().unwrap(); } @@ -278,7 +295,7 @@ fn real_pcs_ood_round_batches_into_opening() { fn ood_round_trip(pcs: &impl CommitScheme, packed_witness: Vec, query: OpeningQuery) { let mut prover = build_prover(SESSION, b"ood-round-trip"); - let (commitment, data) = pcs.commit_with_ood(&packed_witness, &mut prover).unwrap(); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); pcs.prove_lin( &data, packed_witness, @@ -292,7 +309,7 @@ fn ood_round_trip(pcs: &impl CommitScheme, packed_witness: Vec, query: Ope let mut verifier = build_verifier(SESSION, b"ood-round-trip", &proof); let received = pcs.receive_commitment(commitment, &mut verifier).unwrap(); - pcs.verify_lin_with_ood(&received, &query, StatementBinding::Bind, &mut verifier) + pcs.verify_lin(&received, &query, StatementBinding::Bind, &mut verifier) .unwrap(); assert_eq!(verifier.verifier_message::(), next_challenge); verifier.check_eof().unwrap(); @@ -307,7 +324,7 @@ fn real_pcs_ood_round_rejects_a_changed_evaluation() { target: F128::ZERO, }; let mut prover = build_prover(SESSION, b"ood-tampering"); - let (commitment, data) = pcs.commit_with_ood(&packed_witness, &mut prover).unwrap(); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); pcs.prove_lin( &data, packed_witness, @@ -322,7 +339,7 @@ fn real_pcs_ood_round_rejects_a_changed_evaluation() { let mut verifier = build_verifier(SESSION, b"ood-tampering", &proof); let received = pcs.receive_commitment(commitment, &mut verifier).unwrap(); assert!( - pcs.verify_lin_with_ood(&received, &query, StatementBinding::Bind, &mut verifier,) + pcs.verify_lin(&received, &query, StatementBinding::Bind, &mut verifier,) .is_err() ); } @@ -336,7 +353,7 @@ fn factored_inner_product_round_trip_succeeds_for_all_profiles_and_bindings() { ] { let fixture = inner_product_fixture(profile); for binding in [StatementBinding::Bind, StatementBinding::AlreadyBound] { - let mut verifier = fixture.verifier( + let (received, mut verifier) = fixture.verifier( &fixture.commitment, &fixture.query, fixture.proof(binding), @@ -344,7 +361,7 @@ fn factored_inner_product_round_trip_succeeds_for_all_profiles_and_bindings() { ); fixture .pcs - .verify_lin(&fixture.commitment, &fixture.query, binding, &mut verifier) + .verify_lin(&received, &fixture.query, binding, &mut verifier) .unwrap(); verifier.check_eof().unwrap(); } @@ -356,11 +373,11 @@ fn factored_inner_product_prover_rejects_a_false_target() { let shape = inner_product_shape(); let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let witness = inner_product_witness(pcs.packed_len()); - let (commitment, data) = pcs.commit(&witness).unwrap(); let query = factored_query(&shape, inner_product_target(&shape) + F128::from(1u64)); for binding in [StatementBinding::Bind, StatementBinding::AlreadyBound] { let mut prover = build_prover(SESSION, INNER_PRODUCT_INSTANCE); + let (commitment, data) = pcs.commit(&witness, &mut prover).unwrap(); if binding == StatementBinding::AlreadyBound { bind_outer_inner_product_statement(&mut prover, &pcs, &commitment, &query); } @@ -424,11 +441,11 @@ fn factored_inner_product_requires_complete_transcript_consumption() { } else { proof.narg_string.push(0); } - let mut verifier = + let (received, mut verifier) = fixture.verifier(&fixture.commitment, &fixture.query, &proof, binding); fixture .pcs - .verify_lin(&fixture.commitment, &fixture.query, binding, &mut verifier) + .verify_lin(&received, &fixture.query, binding, &mut verifier) .unwrap(); assert!(verifier.check_eof().is_err()); } @@ -439,11 +456,11 @@ fn factored_inner_product_requires_complete_transcript_consumption() { fn factored_inner_product_rejects_wrong_weight_lengths() { let pcs = Pcs::new(&shape(), LigeritoProfile::Secure, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (commitment, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"wrong-inner-product-weight-count"); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); let larger_shape = Shape::new(8, M - 7).unwrap(); let query = factored_query(&larger_shape, F128::ZERO); - let mut prover = build_prover(SESSION, b"wrong-inner-product-weight-count"); assert_eq!( pcs.prove_lin( &data, @@ -458,7 +475,13 @@ fn factored_inner_product_rejects_wrong_weight_lengths() { let proof = Proof::default(); let mut verifier = build_verifier(SESSION, b"wrong-inner-product-weight-count", &proof); assert_eq!( - pcs.verify_lin(&commitment, &query, StatementBinding::Bind, &mut verifier), + verify_opening( + &pcs, + &commitment, + &query, + StatementBinding::Bind, + &mut verifier + ), Err(VerifyError::WeightLengthMismatch), ); } @@ -468,12 +491,12 @@ fn factored_inner_product_rejects_invalid_prover_inputs_before_sumcheck() { let shape = inner_product_shape(); let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (_, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"inner-product-wrong-packed-length"); + let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); let query = factored_query(&shape, F128::ZERO); let mut short_witness = packed_witness.clone(); short_witness.pop(); - let mut prover = build_prover(SESSION, b"inner-product-wrong-packed-length"); assert_eq!( pcs.prove_lin( &data, @@ -504,15 +527,14 @@ fn slim_profile_opening_round_trip_exercises_pow() { let fixture = RealFixture::build(LigeritoProfile::Slim); let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); - fixture - .pcs - .verify_lin( - &fixture.commitment, - &fixture.query, - StatementBinding::Bind, - &mut verifier, - ) - .unwrap(); + verify_opening( + &fixture.pcs, + &fixture.commitment, + &fixture.query, + StatementBinding::Bind, + &mut verifier, + ) + .unwrap(); verifier.check_eof().unwrap(); } @@ -524,9 +546,9 @@ fn real_pcs_accepts_an_already_bound_statement() { point: vec![F128::from(2u64); M], target: F128::from(0u64), }; - let (commitment, data) = pcs.commit(&packed_witness).unwrap(); - let mut prover = build_prover(SESSION, b"already-bound"); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); + bind_outer_statement(&mut prover, &pcs, &commitment, &query); pcs.prove_lin( &data, @@ -539,9 +561,10 @@ fn real_pcs_accepts_an_already_bound_statement() { let proof = prover.finish(); let mut verifier = build_verifier(SESSION, b"already-bound", &proof); + let received = pcs.receive_commitment(commitment, &mut verifier).unwrap(); bind_outer_statement(&mut verifier, &pcs, &commitment, &query); pcs.verify_lin( - &commitment, + &received, &query, StatementBinding::AlreadyBound, &mut verifier, @@ -550,10 +573,13 @@ fn real_pcs_accepts_an_already_bound_statement() { verifier.check_eof().unwrap(); let mut mismatched_verifier = build_verifier(SESSION, b"already-bound", &proof); + let received = pcs + .receive_commitment(commitment, &mut mismatched_verifier) + .unwrap(); bind_outer_statement(&mut mismatched_verifier, &pcs, &commitment, &query); assert!( pcs.verify_lin( - &commitment, + &received, &query, StatementBinding::Bind, &mut mismatched_verifier, @@ -566,13 +592,13 @@ fn real_pcs_accepts_an_already_bound_statement() { fn real_pcs_rejects_point_length_mismatches() { let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (commitment, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"wrong-prover-point"); + let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); let short_query = OpeningQuery::Mle { point: vec![F128::from(2u64); M - 1], target: F128::from(0u64), }; - let mut prover = build_prover(SESSION, b"wrong-prover-point"); assert_eq!( pcs.prove_lin( &data, @@ -592,7 +618,7 @@ fn real_pcs_rejects_point_length_mismatches() { let mut verifier = build_verifier(SESSION, b"wrong-verifier-point", &proof); assert_eq!( pcs.verify_lin( - &commitment, + data.commitment(), &long_query, StatementBinding::Bind, &mut verifier, @@ -605,13 +631,13 @@ fn real_pcs_rejects_point_length_mismatches() { fn real_pcs_rejects_packed_witness_length_mismatches_during_opening() { let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let mut packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (_, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"wrong-packed-length"); + let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); packed_witness.pop(); let query = OpeningQuery::Mle { point: vec![F128::from(2u64); M], target: F128::from(0u64), }; - let mut prover = build_prover(SESSION, b"wrong-packed-length"); assert_eq!( pcs.prove_lin( @@ -629,13 +655,13 @@ fn real_pcs_rejects_packed_witness_length_mismatches_during_opening() { fn real_pcs_rejects_mismatched_prover_parameters() { let source = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; source.packed_len()]; - let (_, data) = source.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"mismatched-parameters"); + let (_, data) = source.commit(&packed_witness, &mut prover).unwrap(); let other = Pcs::new(&shape(), LigeritoProfile::Slim, HashKind::Blake3).unwrap(); let query = OpeningQuery::Mle { point: vec![F128::from(2u64); M], target: F128::from(0u64), }; - let mut prover = build_prover(SESSION, b"mismatched-parameters"); assert_eq!( other.prove_lin( @@ -649,16 +675,41 @@ fn real_pcs_rejects_mismatched_prover_parameters() { ); } +#[test] +fn real_pcs_rejects_commitment_state_from_different_parameters() { + let fixture = fixture(); + for (profile, hash) in [ + (LigeritoProfile::Slim, HashKind::Blake3), + (LigeritoProfile::Fast, HashKind::Sha256), + ] { + let other = Pcs::new(&shape(), profile, hash).unwrap(); + let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); + let commitment = fixture + .pcs + .receive_commitment(fixture.commitment, &mut verifier) + .unwrap(); + assert_eq!( + other.verify_lin( + &commitment, + &fixture.query, + StatementBinding::Bind, + &mut verifier + ), + Err(VerifyError::VerificationFailed), + ); + } +} + #[test] fn real_pcs_prover_rejects_a_false_evaluation_without_consuming_prover_data() { let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (_, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"false-evaluation"); + let (_, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); let query = OpeningQuery::Mle { point: vec![F128::from(2u64); M], target: F128::from(1u64), }; - let mut prover = build_prover(SESSION, b"false-evaluation"); let codeword_len = data.codeword_len(); assert_eq!( @@ -678,7 +729,8 @@ fn real_pcs_prover_rejects_a_false_evaluation_without_consuming_prover_data() { fn real_pcs_rejects_an_opening_for_a_different_packed_witness() { let pcs = Pcs::new(&shape(), LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let packed_witness = vec![F128::ZERO; pcs.packed_len()]; - let (commitment, data) = pcs.commit(&packed_witness).unwrap(); + let mut prover = build_prover(SESSION, b"different-packed-witness"); + let (commitment, data) = pcs.commit(&packed_witness, &mut prover).unwrap(); let mut different_witness = packed_witness; different_witness[0].lo = 1; let point = (0..M) @@ -688,7 +740,6 @@ fn real_pcs_rejects_an_opening_for_a_different_packed_witness() { target: singleton_target(&point, 0), point, }; - let mut prover = build_prover(SESSION, b"different-packed-witness"); pcs.prove_lin( &data, different_witness, @@ -701,7 +752,13 @@ fn real_pcs_rejects_an_opening_for_a_different_packed_witness() { let mut verifier = build_verifier(SESSION, b"different-packed-witness", &proof); assert_eq!( - pcs.verify_lin(&commitment, &query, StatementBinding::Bind, &mut verifier,), + verify_opening( + &pcs, + &commitment, + &query, + StatementBinding::Bind, + &mut verifier, + ), Err(VerifyError::VerificationFailed) ); } @@ -717,7 +774,8 @@ fn real_pcs_rejects_statement_mutations() { *target += F128::from(1u64); let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); assert_eq!( - fixture.pcs.verify_lin( + verify_opening( + &fixture.pcs, &fixture.commitment, &changed_query, StatementBinding::Bind, @@ -731,7 +789,8 @@ fn real_pcs_rejects_statement_mutations() { let changed_commitment = Root(changed_root); let mut verifier = build_verifier(SESSION, INSTANCE, &fixture.proof); assert_eq!( - fixture.pcs.verify_lin( + verify_opening( + &fixture.pcs, &changed_commitment, &fixture.query, StatementBinding::Bind, @@ -749,7 +808,8 @@ fn real_pcs_rejects_malformed_transcript_streams() { truncated_stream.narg_string.truncate(1); let mut verifier = build_verifier(SESSION, INSTANCE, &truncated_stream); assert_eq!( - fixture.pcs.verify_lin( + verify_opening( + &fixture.pcs, &fixture.commitment, &fixture.query, StatementBinding::Bind, @@ -762,7 +822,8 @@ fn real_pcs_rejects_malformed_transcript_streams() { truncated_hint.hints.pop(); let mut verifier = build_verifier(SESSION, INSTANCE, &truncated_hint); assert_eq!( - fixture.pcs.verify_lin( + verify_opening( + &fixture.pcs, &fixture.commitment, &fixture.query, StatementBinding::Bind, @@ -775,7 +836,8 @@ fn real_pcs_rejects_malformed_transcript_streams() { *changed_hint.hints.last_mut().unwrap() ^= 1; let mut verifier = build_verifier(SESSION, INSTANCE, &changed_hint); assert_eq!( - fixture.pcs.verify_lin( + verify_opening( + &fixture.pcs, &fixture.commitment, &fixture.query, StatementBinding::Bind, @@ -792,29 +854,27 @@ fn real_pcs_requires_complete_transcript_consumption() { let mut trailing_narg = fixture.proof.clone(); trailing_narg.narg_string.push(0); let mut verifier = build_verifier(SESSION, INSTANCE, &trailing_narg); - fixture - .pcs - .verify_lin( - &fixture.commitment, - &fixture.query, - StatementBinding::Bind, - &mut verifier, - ) - .unwrap(); + verify_opening( + &fixture.pcs, + &fixture.commitment, + &fixture.query, + StatementBinding::Bind, + &mut verifier, + ) + .unwrap(); assert!(verifier.check_eof().is_err()); let mut trailing_hint = fixture.proof.clone(); trailing_hint.hints.push(0); let mut verifier = build_verifier(SESSION, INSTANCE, &trailing_hint); - fixture - .pcs - .verify_lin( - &fixture.commitment, - &fixture.query, - StatementBinding::Bind, - &mut verifier, - ) - .unwrap(); + verify_opening( + &fixture.pcs, + &fixture.commitment, + &fixture.query, + StatementBinding::Bind, + &mut verifier, + ) + .unwrap(); assert!(verifier.check_eof().is_err()); } diff --git a/crates/prover/src/prove.rs b/crates/prover/src/prove.rs index 3fa89897..49fc2789 100644 --- a/crates/prover/src/prove.rs +++ b/crates/prover/src/prove.rs @@ -42,7 +42,7 @@ pub struct VirtualWitness<'a> { impl BitZProver { /// Proves the caller's linear claim about the committed bits. /// - /// Call `Pcs::commit_with_ood` on this transcript, then pass its retained + /// Call `Pcs::commit` on this transcript, then pass its retained /// `data` and the packed witness. The root is read back off /// `data` rather than passed alongside it, so the two cannot disagree. /// `pcs` must be the scheme that committed, or the opening will not verify. @@ -82,7 +82,7 @@ impl BitZProver { /// Proves a claim on `h = M (1 || f)` against the commitment to `f`. /// - /// Build the setup from `statement.params().claim()`. Use `Pcs::commit_with_ood` + /// Build the setup from `statement.params().claim()`. Use `Pcs::commit` /// on `witness.committed_bits` and this transcript, then pass its `data`. GKR reduces /// the input claim to an inner product on padded virtual bits. This method /// transposes its coefficients before PCS opens the committed bits. diff --git a/crates/tests/examples/dump_bitz.rs b/crates/tests/examples/dump_bitz.rs index 920b97b7..65ebbf00 100644 --- a/crates/tests/examples/dump_bitz.rs +++ b/crates/tests/examples/dump_bitz.rs @@ -31,7 +31,7 @@ fn main() -> Result<(), Box> { let mut transcript = prover_transcript(); let (_, data) = instance .pcs - .commit_with_ood(&instance.packed, &mut transcript) + .commit(&instance.packed, &mut transcript) .unwrap(); let started = Instant::now(); instance diff --git a/crates/tests/examples/dump_commit.rs b/crates/tests/examples/dump_commit.rs index 4605c01c..742d0464 100644 --- a/crates/tests/examples/dump_commit.rs +++ b/crates/tests/examples/dump_commit.rs @@ -9,7 +9,7 @@ use pcs::{HashKind, LigeritoProfile, Pcs}; use rand_chacha::ChaCha8Rng; use rand_core::SeedableRng; use support::{hex, write_binary, write_witness}; -use tests::packed_witness; +use tests::{packed_witness, prover_transcript}; fn main() -> Result<(), Box> { const USAGE: &str = "usage: dump_commit [t [s [seed [out-file]]]]"; @@ -42,7 +42,7 @@ fn main() -> Result<(), Box> { let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3) .map_err(|error| format!("PCS configuration failed: {error:?}"))?; let (root, _data) = pcs - .commit(&packed) + .commit(&packed, &mut prover_transcript()) .map_err(|error| format!("commitment failed: {error:?}"))?; write_binary(out, |output| write_witness(output, &packed))?; let root = hex(&root.0); diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 3de5aa6c..066f4134 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -110,7 +110,7 @@ impl Instance { } = HonestClaim::new(shape, &mut rng); let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); - let (com, data) = pcs.commit(&packed).unwrap(); + let (com, data) = pcs.commit(&packed, &mut prover_transcript()).unwrap(); Self { params, diff --git a/crates/tests/tests/host.rs b/crates/tests/tests/host.rs index 6dfb080e..b1857259 100644 --- a/crates/tests/tests/host.rs +++ b/crates/tests/tests/host.rs @@ -13,7 +13,7 @@ fn shipped(instance: &Instance) -> Vec { let mut transcript = prover_transcript(); let (_, data) = instance .pcs - .commit_with_ood(&instance.packed, &mut transcript) + .commit(&instance.packed, &mut transcript) .unwrap(); instance .prover diff --git a/crates/tests/tests/prove.rs b/crates/tests/tests/prove.rs index 761bea98..6a0d3267 100644 --- a/crates/tests/tests/prove.rs +++ b/crates/tests/tests/prove.rs @@ -13,7 +13,7 @@ fn prove(instance: &Instance) -> Proof { let mut transcript = prover_transcript(); let (_, data) = instance .pcs - .commit_with_ood(&instance.packed, &mut transcript) + .commit(&instance.packed, &mut transcript) .unwrap(); instance .prover @@ -112,7 +112,7 @@ fn an_opening_against_another_commitment_is_refused() { let mut transcript = prover_transcript(); let (_, data) = committed .pcs - .commit_with_ood(&committed.packed, &mut transcript) + .commit(&committed.packed, &mut transcript) .unwrap(); proved .prover diff --git a/crates/tests/tests/virtual_prove.rs b/crates/tests/tests/virtual_prove.rs index 7d4a9048..2e0d8ad2 100644 --- a/crates/tests/tests/virtual_prove.rs +++ b/crates/tests/tests/virtual_prove.rs @@ -75,7 +75,9 @@ impl Instance { virtual_bits[1] = F128::from(2u64); let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); - let (root, data) = pcs.commit(&committed_bits).unwrap(); + let (root, data) = pcs + .commit(&committed_bits, &mut prover_transcript()) + .unwrap(); Self { params, committed_shape, @@ -96,7 +98,7 @@ impl Instance { let mut transcript = prover_transcript(); let (_, data) = self .pcs - .commit_with_ood(&self.committed_bits, &mut transcript) + .commit(&self.committed_bits, &mut transcript) .unwrap(); BitZProver::new(self.params, WINDOW) .prove_virtual( @@ -303,7 +305,7 @@ fn virtual_bits_inconsistent_with_the_map_cannot_be_opened() { let mut transcript = prover_transcript(); let (_, data) = instance .pcs - .commit_with_ood(&instance.committed_bits, &mut transcript) + .commit(&instance.committed_bits, &mut transcript) .unwrap(); assert_eq!( BitZProver::new(instance.params, WINDOW).prove_virtual( @@ -372,9 +374,7 @@ fn sha256_virtual_inner_product_opens_the_committed_bits() { let statement = VirtualStatement::new(params, committed_shape, &map, &claim).unwrap(); let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); let mut transcript = prover_transcript(); - let (root, data) = pcs - .commit_with_ood(&committed_bits, &mut transcript) - .unwrap(); + let (root, data) = pcs.commit(&committed_bits, &mut transcript).unwrap(); BitZProver::new(params, WINDOW) .prove_virtual( &statement, diff --git a/crates/verifier/src/verify.rs b/crates/verifier/src/verify.rs index e90da561..0510f1bb 100644 --- a/crates/verifier/src/verify.rs +++ b/crates/verifier/src/verify.rs @@ -2,7 +2,7 @@ use common::{LinearClaim, OpeningQuery, Root, VirtualMap, VirtualMapError, VirtualStatement}; use field::Fq; -use pcs::{CommitScheme, Pcs, StatementBinding, VerifierData, VerifyError as OpeningVerifyError}; +use pcs::{CommitScheme, Commitment, Pcs, StatementBinding, VerifyError as OpeningVerifyError}; use transcript::VerifierState; use crate::{BitZVerifier, ReceiveError, ReduceError, reduce::gkr_reduce}; @@ -74,7 +74,7 @@ impl BitZVerifier { &self, statement: &VirtualStatement<'_, Q, impl VirtualMap>, pcs: &Pcs, - commitment: &VerifierData, + commitment: &Commitment, mut transcript: VerifierState<'_>, ) -> Result<(), VerifyError> { let params = statement.params(); @@ -93,7 +93,7 @@ impl BitZVerifier { let query = statement .transpose_query(query) .map_err(VerifyError::VirtualMap)?; - pcs.verify_lin_with_ood(commitment, &query, StatementBinding::Bind, &mut transcript) + pcs.verify_lin(commitment, &query, StatementBinding::Bind, &mut transcript) .map_err(VerifyError::Opening)?; transcript .check_eof() @@ -109,7 +109,7 @@ impl BitZVerifier { &self, claim: &LinearClaim>, pcs: &Pcs, - commitment: &VerifierData, + commitment: &Commitment, mut transcript: VerifierState<'_>, ) -> Result<(), VerifyError> { // Step 1: the admissibility and precondition checks have already run -- @@ -124,7 +124,7 @@ impl BitZVerifier { // Step 6: verify the inner-product sumcheck, ring switch, and opening. // Acceptance requires authenticating GKR's terminal claim against the commitment. - pcs.verify_lin_with_ood(commitment, &query, StatementBinding::Bind, &mut transcript) + pcs.verify_lin(commitment, &query, StatementBinding::Bind, &mut transcript) .map_err(VerifyError::Opening)?; // Both streams must be spent. Taking the transcript by value is what diff --git a/tooling/cli/src/end_to_end.rs b/tooling/cli/src/end_to_end.rs index d1be2c78..9e4c9d96 100644 --- a/tooling/cli/src/end_to_end.rs +++ b/tooling/cli/src/end_to_end.rs @@ -216,7 +216,7 @@ impl CircuitProofSystem { let mut transcript = build_prover(SESSION, self.statement.domain()); let (_, data) = self .pcs - .commit_with_ood(&witness.committed, &mut transcript) + .commit(&witness.committed, &mut transcript) .map_err(Error::Commit)?; Ok(CommittedWitness { data, transcript }) } @@ -288,7 +288,7 @@ impl CircuitProofSystem { self.bind(&mut transcript, proof.root); if self.opening_path == OpeningPath::Direct { self.pcs - .verify_lin_with_ood( + .verify_lin( &commitment, &self.constant_query(), StatementBinding::Bind, @@ -470,10 +470,7 @@ mod tests { // A valid opening to zero must not substitute for the required one. let packed = vec![F128::ZERO; 1 << system.committed_shape.log_packed_len()]; let mut transcript = build_prover(SESSION, system.statement.domain()); - let (_, bad_data) = system - .pcs - .commit_with_ood(&packed, &mut transcript) - .unwrap(); + let (_, bad_data) = system.pcs.commit(&packed, &mut transcript).unwrap(); system.bind(&mut transcript, bad_data.root()); let query = OpeningQuery::Mle { point: vec![F128::ZERO; system.committed_shape.log_bits()], From 597f71dbd47eaa41ace4b88e285e21bad4db5749 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 30 Sep 2026 12:23:58 +0530 Subject: [PATCH 6/7] fix: avoid duplicate fixture commitments --- crates/tests/examples/dump_bitz.rs | 12 ++---- crates/tests/src/lib.rs | 5 ++- crates/tests/tests/host.rs | 20 ++++----- crates/tests/tests/prove.rs | 63 ++++++++++++++--------------- crates/tests/tests/virtual_prove.rs | 37 +++++++---------- 5 files changed, 61 insertions(+), 76 deletions(-) diff --git a/crates/tests/examples/dump_bitz.rs b/crates/tests/examples/dump_bitz.rs index 65ebbf00..e873a5ed 100644 --- a/crates/tests/examples/dump_bitz.rs +++ b/crates/tests/examples/dump_bitz.rs @@ -10,7 +10,7 @@ use std::time::Instant; use common::Shape; use crypto_primitives::LiftElement; use support::{hex, write_binary, write_witness}; -use tests::{Instance, Q, prover_transcript, verifier_transcript}; +use tests::{Instance, Q, verifier_transcript}; fn main() -> Result<(), Box> { const USAGE: &str = "usage: dump_bitz "; @@ -26,20 +26,16 @@ fn main() -> Result<(), Box> { Shape::for_log_bits(log_bits).map_err(|error| format!("invalid shape: {error:?}"))?; std::fs::create_dir_all(out)?; let (t, s) = (shape.log_rows(), shape.log_columns()); - let instance = Instance::honest(shape, seed); + let mut instance = Instance::honest(shape, seed); - let mut transcript = prover_transcript(); - let (_, data) = instance - .pcs - .commit(&instance.packed, &mut transcript) - .unwrap(); + let mut transcript = instance.transcript.take().unwrap(); let started = Instant::now(); instance .prover .prove( &instance.claim, &instance.pcs, - &data, + &instance.data, instance.packed.clone(), &mut transcript, ) diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 066f4134..4da32491 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -96,6 +96,7 @@ pub struct Instance { pub pcs: Pcs, pub com: Root, pub data: ProverData, + pub transcript: Option, pub packed: Vec, } @@ -110,7 +111,8 @@ impl Instance { } = HonestClaim::new(shape, &mut rng); let pcs = Pcs::new(&shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); - let (com, data) = pcs.commit(&packed, &mut prover_transcript()).unwrap(); + let mut transcript = prover_transcript(); + let (com, data) = pcs.commit(&packed, &mut transcript).unwrap(); Self { params, @@ -120,6 +122,7 @@ impl Instance { pcs, com, data, + transcript: Some(transcript), packed, } } diff --git a/crates/tests/tests/host.rs b/crates/tests/tests/host.rs index b1857259..39ed8616 100644 --- a/crates/tests/tests/host.rs +++ b/crates/tests/tests/host.rs @@ -6,21 +6,17 @@ //! than shipped, so there is no encoding of it to round-trip. use host::wire_proof; -use tests::{Instance, narrow_shape, prover_transcript, verifier_transcript, wide_shape}; +use tests::{Instance, narrow_shape, verifier_transcript, wide_shape}; /// Runs an honest prover and hands back what a caller would ship. -fn shipped(instance: &Instance) -> Vec { - let mut transcript = prover_transcript(); - let (_, data) = instance - .pcs - .commit(&instance.packed, &mut transcript) - .unwrap(); +fn shipped(instance: &mut Instance) -> Vec { + let mut transcript = instance.transcript.take().unwrap(); instance .prover .prove( &instance.claim, &instance.pcs, - &data, + &instance.data, instance.packed.clone(), &mut transcript, ) @@ -32,8 +28,8 @@ fn shipped(instance: &Instance) -> Vec { #[test] fn a_proof_survives_the_round_trip_through_bytes() { for shape in [narrow_shape(), wide_shape()] { - let instance = Instance::honest(shape, 41); - let proof_bytes = shipped(&instance); + let mut instance = Instance::honest(shape, 41); + let proof_bytes = shipped(&mut instance); let proof = wire_proof::decode(&proof_bytes).expect("its own encoding"); @@ -62,8 +58,8 @@ fn a_tampered_fold_is_left_for_the_verifier_to_catch() { // The container frames but does not authenticate: a tampered fold decodes // cleanly and the sponge refuses it on replay. let shape = wide_shape(); - let instance = Instance::honest(shape, 43); - let proof_bytes = shipped(&instance); + let mut instance = Instance::honest(shape, 43); + let proof_bytes = shipped(&mut instance); let mut tampered = proof_bytes.clone(); let last_fold_byte = 32 + 16 * shape.columns() - 1; diff --git a/crates/tests/tests/prove.rs b/crates/tests/tests/prove.rs index 6a0d3267..07cc90b5 100644 --- a/crates/tests/tests/prove.rs +++ b/crates/tests/tests/prove.rs @@ -5,22 +5,18 @@ use field::{F128, Fq}; use num_traits::{ConstOne, ConstZero}; use pcs::{HashKind, LigeritoProfile, Pcs, VerifyError as PcsVerifyError}; use prover::ProveError; -use tests::{Instance, narrow_shape, prover_transcript, verifier_transcript, wide_shape}; +use tests::{Instance, narrow_shape, verifier_transcript, wide_shape}; use transcript::Proof; use verifier::{ReceiveError, VerifyError}; -fn prove(instance: &Instance) -> Proof { - let mut transcript = prover_transcript(); - let (_, data) = instance - .pcs - .commit(&instance.packed, &mut transcript) - .unwrap(); +fn prove(instance: &mut Instance) -> Proof { + let mut transcript = instance.transcript.take().unwrap(); instance .prover .prove( &instance.claim, &instance.pcs, - &data, + &instance.data, instance.packed.clone(), &mut transcript, ) @@ -31,8 +27,8 @@ fn prove(instance: &Instance) -> Proof { #[test] fn an_honest_proof_verifies_on_both_floor_shapes() { for shape in [narrow_shape(), wide_shape()] { - let instance = Instance::honest(shape, 31); - let proof = prove(&instance); + let mut instance = Instance::honest(shape, 31); + let proof = prove(&mut instance); instance .verifier @@ -48,8 +44,8 @@ fn an_honest_proof_verifies_on_both_floor_shapes() { #[test] fn a_proof_replayed_under_a_different_commitment_is_refused() { - let instance = Instance::honest(narrow_shape(), 32); - let proof = prove(&instance); + let mut instance = Instance::honest(narrow_shape(), 32); + let proof = prove(&mut instance); // Binding a different root changes the fold batching point, so GKR rejects. assert_eq!( @@ -65,8 +61,8 @@ fn a_proof_replayed_under_a_different_commitment_is_refused() { #[test] fn the_statement_is_bound_before_the_first_challenge() { - let instance = Instance::honest(narrow_shape(), 33); - let proof = prove(&instance); + let mut instance = Instance::honest(narrow_shape(), 33); + let proof = prove(&mut instance); // Same folds, same commitment, a claim that differs only in its claimed // value. The fold's own reconstruction rejects it, which is the check the @@ -85,8 +81,8 @@ fn the_statement_is_bound_before_the_first_challenge() { #[test] fn a_proof_with_trailing_bytes_is_refused() { - let instance = Instance::honest(narrow_shape(), 34); - let mut proof = prove(&instance); + let mut instance = Instance::honest(narrow_shape(), 34); + let mut proof = prove(&mut instance); proof.hints.push(0); assert_eq!( @@ -107,19 +103,15 @@ fn an_opening_against_another_commitment_is_refused() { // and the GKR claim is true of that witness. Only the codeword and // the Merkle tree the opening reads belong to a different commitment. let proved = Instance::honest(narrow_shape(), 35); - let committed = Instance::honest(narrow_shape(), 36); + let mut committed = Instance::honest(narrow_shape(), 36); - let mut transcript = prover_transcript(); - let (_, data) = committed - .pcs - .commit(&committed.packed, &mut transcript) - .unwrap(); + let mut transcript = committed.transcript.take().unwrap(); proved .prover .prove( &proved.claim, &proved.pcs, - &data, + &committed.data, proved.packed.clone(), &mut transcript, ) @@ -141,8 +133,8 @@ fn an_opening_against_another_commitment_is_refused() { fn a_tampered_opening_proof_is_refused() { // The opening rides the hint channel, which the sponge never sees, so // nothing upstream of the opening notices this. The opening itself must. - let instance = Instance::honest(narrow_shape(), 37); - let mut proof = prove(&instance); + let mut instance = Instance::honest(narrow_shape(), 37); + let mut proof = prove(&mut instance); let middle = proof.hints.len() / 2; proof.hints[middle] ^= 0xff; @@ -161,14 +153,14 @@ fn a_tampered_opening_proof_is_refused() { fn a_proof_verified_under_a_different_profile_is_refused() { // OOD binds PCS parameters before the first fold challenge, so a different // profile changes the fold transcript and GKR rejects. - let instance = Instance::honest(narrow_shape(), 38); + let mut instance = Instance::honest(narrow_shape(), 38); let slim = Pcs::new( instance.params.shape(), LigeritoProfile::Slim, HashKind::Blake3, ) .unwrap(); - let proof = prove(&instance); + let proof = prove(&mut instance); assert!(matches!( instance.verifier.verify( @@ -182,10 +174,10 @@ fn a_proof_verified_under_a_different_profile_is_refused() { } #[test] -fn a_witness_of_the_wrong_length_is_refused_before_anything_is_written() { - let instance = Instance::honest(narrow_shape(), 39); +fn a_witness_of_the_wrong_length_is_refused_without_changing_the_commitment_transcript() { + let mut instance = Instance::honest(narrow_shape(), 39); - let mut transcript = prover_transcript(); + let mut transcript = instance.transcript.take().unwrap(); assert_eq!( instance.prover.prove( &instance.claim, @@ -197,8 +189,13 @@ fn a_witness_of_the_wrong_length_is_refused_before_anything_is_written() { Err(ProveError::Witness(TableError::BitCountMismatch)) ); - // The shape is checked before the first absorb, so a rejected witness - // leaves no half-written proof behind. + let next_challenge = transcript.verifier_message::(); let proof = transcript.finish(); - assert!(proof.narg_string.is_empty() && proof.hints.is_empty()); + let mut verifier = verifier_transcript(&proof); + instance + .pcs + .receive_commitment(instance.com, &mut verifier) + .unwrap(); + assert_eq!(verifier.verifier_message::(), next_challenge); + verifier.check_eof().unwrap(); } diff --git a/crates/tests/tests/virtual_prove.rs b/crates/tests/tests/virtual_prove.rs index 2e0d8ad2..452637ae 100644 --- a/crates/tests/tests/virtual_prove.rs +++ b/crates/tests/tests/virtual_prove.rs @@ -14,7 +14,7 @@ use num_traits::{ConstOne, ConstZero}; use pcs::{HashKind, LigeritoProfile, Pcs, ProverData}; use prover::{BitZProver, ProveError, VirtualWitness}; use tests::{Q, WINDOW, prover_transcript, verifier_transcript}; -use transcript::Proof; +use transcript::{Proof, ProverState}; use verifier::{BitZVerifier, VerifyError}; /// `h[0] = 1`, `h[1] = f[0]`, `h[128] = f[1]`, `h[129] = f[0] XOR f[1]`. @@ -54,6 +54,7 @@ struct Instance { pcs: Pcs, root: Root, data: ProverData, + transcript: Option, } impl Instance { @@ -75,9 +76,8 @@ impl Instance { virtual_bits[1] = F128::from(2u64); let pcs = Pcs::new(&committed_shape, LigeritoProfile::Fast, HashKind::Blake3).unwrap(); - let (root, data) = pcs - .commit(&committed_bits, &mut prover_transcript()) - .unwrap(); + let mut transcript = prover_transcript(); + let (root, data) = pcs.commit(&committed_bits, &mut transcript).unwrap(); Self { params, committed_shape, @@ -87,6 +87,7 @@ impl Instance { pcs, root, data, + transcript: Some(transcript), } } @@ -94,17 +95,13 @@ impl Instance { VirtualStatement::new(self.params, self.committed_shape, &Map(7), &self.claim).unwrap() } - fn prove(&self) -> Proof { - let mut transcript = prover_transcript(); - let (_, data) = self - .pcs - .commit(&self.committed_bits, &mut transcript) - .unwrap(); + fn prove(&mut self) -> Proof { + let mut transcript = self.transcript.take().unwrap(); BitZProver::new(self.params, WINDOW) .prove_virtual( &self.statement(), &self.pcs, - &data, + &self.data, VirtualWitness { committed_bits: self.committed_bits.clone(), virtual_bits: &self.virtual_bits, @@ -132,7 +129,7 @@ impl Instance { #[test] fn virtual_inner_product_opens_the_committed_bits() { - let instance = Instance::new(); + let mut instance = Instance::new(); let proof = instance.prove(); instance .verify(&instance.statement(), instance.root, &proof) @@ -141,9 +138,9 @@ fn virtual_inner_product_opens_the_committed_bits() { #[test] fn changed_virtual_statements_are_rejected() { - let instance = Instance::new(); - let statement = instance.statement(); + let mut instance = Instance::new(); let proof = instance.prove(); + let statement = instance.statement(); instance.verify(&statement, instance.root, &proof).unwrap(); let mut changed_root = instance.root; @@ -190,7 +187,7 @@ fn changed_virtual_statements_are_rejected() { #[test] fn malformed_virtual_proofs_are_rejected() { - let instance = Instance::new(); + let mut instance = Instance::new(); let proof = instance.prove(); let verify = |proof: &Proof| instance.verify(&instance.statement(), instance.root, proof); verify(&proof).unwrap(); @@ -297,21 +294,17 @@ fn virtual_witness_lengths_and_setup_must_match_the_statement() { #[test] fn virtual_bits_inconsistent_with_the_map_cannot_be_opened() { - let instance = Instance::new(); + let mut instance = Instance::new(); let mut virtual_bits = instance.virtual_bits.clone(); // Move the constant-one bit to row two. The integer sum stays three, // but the virtual witness no longer equals M (1 || f). virtual_bits[0] = F128::from(6u64); - let mut transcript = prover_transcript(); - let (_, data) = instance - .pcs - .commit(&instance.committed_bits, &mut transcript) - .unwrap(); + let mut transcript = instance.transcript.take().unwrap(); assert_eq!( BitZProver::new(instance.params, WINDOW).prove_virtual( &instance.statement(), &instance.pcs, - &data, + &instance.data, VirtualWitness { committed_bits: instance.committed_bits.clone(), virtual_bits: &virtual_bits, From 4f831e68f8648a28295e4aee39c5326587537763 Mon Sep 17 00:00:00 2001 From: shreyas-londhe Date: Wed, 30 Sep 2026 12:47:20 +0530 Subject: [PATCH 7/7] perf: reduce large OOD basis update time --- Cargo.lock | 1 + crates/pcs/Cargo.toml | 1 + crates/pcs/src/ood.rs | 54 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 65fe2035..d5eb1780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1126,6 +1126,7 @@ dependencies = [ "poly", "post_gkr", "proptest", + "rayon", "tracing", "transcript", ] diff --git a/crates/pcs/Cargo.toml b/crates/pcs/Cargo.toml index 2c3c8aa7..7326565c 100644 --- a/crates/pcs/Cargo.toml +++ b/crates/pcs/Cargo.toml @@ -16,6 +16,7 @@ transcript = { workspace = true } tracing = { workspace = true } num-traits = { workspace = true } poly = { workspace = true } +rayon = { workspace = true } [dev-dependencies] divan = { workspace = true } diff --git a/crates/pcs/src/ood.rs b/crates/pcs/src/ood.rs index bb5d718a..47ac1a28 100644 --- a/crates/pcs/src/ood.rs +++ b/crates/pcs/src/ood.rs @@ -14,6 +14,7 @@ use field::F128; use flock_core::field::F128 as FlockF128; use num_traits::ConstOne; use poly::{DenseMultilinearExtension, eq_table}; +use rayon::{current_num_threads, prelude::*}; use transcript::{ProverState, PublicTranscript, VerifierState}; use crate::bridge::{as_flock_f128, from_flock_f128}; @@ -24,6 +25,7 @@ const OOD_ROUND_TAG: &[u8] = b"bitz/pcs/ood/v1"; const OOD_BATCHING_TAG: &[u8] = b"bitz/pcs/ood-batching/v1"; const OOD_POW_TAG: &[u8] = b"bitz/pcs/ood-pow/v1"; const BLOCK_LOG: usize = 12; +const PARALLEL_MIN_LEN: usize = 1 << 18; /// An evaluation of the packed witness MLE, authenticated by the batched opening. #[derive(Debug)] @@ -86,10 +88,18 @@ pub(crate) fn add_dense_basis(basis: &mut [FlockF128], claim: &OodClaim, coeffic let block = 1usize << low; let tail = eq_table(&claim.point[..low]); let head = eq_table(&claim.point[low..]); - for (chunk, &scale) in basis.chunks_exact_mut(block).zip(&head) { + let update = |(chunk, &scale): (&mut [FlockF128], &F128)| { for (basis, &weight) in chunk.iter_mut().zip(&tail) { *basis += as_flock_f128(coefficient * scale * weight); } + }; + if basis.len() >= PARALLEL_MIN_LEN && current_num_threads() >= 4 { + basis + .par_chunks_exact_mut(block) + .zip(head.par_iter()) + .for_each(update); + } else { + basis.chunks_exact_mut(block).zip(&head).for_each(update); } } @@ -162,10 +172,52 @@ fn verify_pow(transcript: &mut VerifierState<'_>, bits: u32) -> Result<(), ()> { #[cfg(test)] mod tests { use num_traits::ConstZero; + use rayon::ThreadPoolBuilder; use transcript::{build_prover, build_verifier}; use super::*; + #[test] + fn dense_basis_updates_match_the_full_equality_table_across_thread_counts() { + let pools: Vec<_> = [1, 2, 4, 8] + .into_iter() + .map(|threads| { + ThreadPoolBuilder::new() + .num_threads(threads) + .build() + .unwrap() + }) + .collect(); + for log_len in [0, 8, 12, 17, 18, 19] { + let len = 1usize << log_len; + let claim = OodClaim { + point: ood_point(F128::new(7, 11), len), + value: F128::ZERO, + }; + let weights = eq_table(&claim.point); + let initial: Vec<_> = (0..len) + .map(|i| FlockF128::new(i as u64 + 1, (i as u64).rotate_left(17) + 3)) + .collect(); + for coefficient in [F128::ZERO, F128::ONE, F128::new(13, 17)] { + let expected: Vec<_> = initial + .iter() + .zip(&weights) + .map(|(&value, &weight)| value + as_flock_f128(coefficient * weight)) + .collect(); + for pool in &pools { + let mut actual = initial.clone(); + pool.install(|| add_dense_basis(&mut actual, &claim, coefficient)); + assert_eq!( + actual, + expected, + "log_len={log_len}, threads={}", + pool.current_num_threads() + ); + } + } + } + } + #[test] fn dense_and_succinct_ood_bases_agree_after_folding() { let point = ood_point(F128::new(7, 11), 1 << 14);