diff --git a/slop/crates/basefold-prover/src/prover.rs b/slop/crates/basefold-prover/src/prover.rs index 86c47ff5d1..0c928980b8 100644 --- a/slop/crates/basefold-prover/src/prover.rs +++ b/slop/crates/basefold-prover/src/prover.rs @@ -96,52 +96,21 @@ impl, P: ComputeTcsOpenings, - mle_rounds: Rounds>>, - evaluation_claims: Rounds>, - prover_data: Rounds>, + batched_mle: Mle, + batched_eval_claim: GC::EF, + batched_codeword: RsCodeWord, + prover_datas: Rounds>, challenger: &mut GC::Challenger, ) -> Result, BaseFoldConfigProverError> { let fri_prover = FriCpuProver::(PhantomData); - // Get all the mles from all rounds in order. - let mles = mle_rounds - .iter() - .flat_map(|round| round.clone().into_iter()) - .collect::>>(); - - let encoded_messages = prover_data - .iter() - .flat_map(|data| data.encoded_messages.iter().cloned()) - .collect::>>(); - - let evaluation_claims = evaluation_claims.into_iter().flatten().collect::>(); - - // Grind for batch randomness. - let batch_grinding_witness = challenger.grind(BATCH_GRINDING_BITS); - - // Sample batching coefficients via partial Lagrange basis. - let total_len = mles.iter().map(|mle| mle.num_polynomials()).sum::(); - let num_batching_variables = total_len.next_power_of_two().ilog2(); - let batching_point = challenger.sample_point::(num_batching_variables); - let batching_coefficients = partial_lagrange_blocking(&batching_point); + let mut current_mle = batched_mle; + let mut current_codeword = batched_codeword; - // Batch the mles and codewords. - let (mle_batch, codeword_batch, batched_eval_claim) = fri_prover.batch( - &batching_coefficients, - mles, - encoded_messages, - evaluation_claims, - &self.encoder, - ); - // From this point on, run the BaseFold protocol on the random linear combination codeword, - // the random linear combination multilinear, and the random linear combination of the - // evaluation claims. - let mut current_mle = mle_batch; - let mut current_codeword = codeword_batch; // Initialize the vecs that go into a BaseFoldProof. let log_len = current_mle.num_variables(); let mut univariate_messages: Vec<[GC::EF; 2]> = vec![]; @@ -155,10 +124,12 @@ impl, P: ComputeTcsOpenings, P: ComputeTcsOpenings, P: ComputeTcsOpenings = (0..fri_config.num_queries) .map(|_| challenger.sample_bits(log_len as usize + fri_config.log_blowup())) .collect(); - // Open the original polynomials at the query indices. + // Open each committed polynomial at the query indices. let mut component_polynomials_query_openings_and_proofs = vec![]; - for prover_data in prover_data { + for prover_data in prover_datas { let BasefoldProverData { encoded_messages, tcs_prover_data } = prover_data; let values = self.tcs_prover.compute_openings_at_indices(encoded_messages, &query_indices); @@ -207,8 +182,8 @@ impl, P: ComputeTcsOpenings::TcsCommitError) .unwrap(); - let opening = MerkleTreeOpeningAndProof:: { values, proof }; - component_polynomials_query_openings_and_proofs.push(opening); + component_polynomials_query_openings_and_proofs + .push(MerkleTreeOpeningAndProof:: { values, proof }); } // Provide openings for the FRI query phase. @@ -218,7 +193,7 @@ impl, P: ComputeTcsOpenings>= 1; } - let leaves: Message> = leaves.into(); + let leaves: Message> = leaves.into(); let values = self.tcs_prover.compute_openings_at_indices(leaves, &indices); let proof = self @@ -236,10 +211,68 @@ impl, P: ComputeTcsOpenings, + mle_rounds: Rounds>>, + evaluation_claims: Rounds>, + prover_data: Rounds>, + challenger: &mut GC::Challenger, + ) -> Result, BaseFoldConfigProverError> { + let fri_prover = FriCpuProver::(PhantomData); + // Get all the mles from all rounds in order. + let mles = mle_rounds + .iter() + .flat_map(|round| round.clone().into_iter()) + .collect::>>(); + + let encoded_messages = prover_data + .iter() + .flat_map(|data| data.encoded_messages.iter().cloned()) + .collect::>>(); + + let evaluation_claims = evaluation_claims.into_iter().flatten().collect::>(); + + // Grind for batch randomness. + let batch_grinding_witness = challenger.grind(BATCH_GRINDING_BITS); + + // Sample batching coefficients via partial Lagrange basis. + let total_len = mles.iter().map(|mle| mle.num_polynomials()).sum::(); + let num_batching_variables = total_len.next_power_of_two().ilog2(); + let batching_point = challenger.sample_point::(num_batching_variables); + + let batching_coefficients = partial_lagrange_blocking(&batching_point); + + // Batch the mles and codewords. + let (mle_batch, codeword_batch, batched_eval_claim) = fri_prover.batch( + &batching_coefficients, + mles, + encoded_messages, + evaluation_claims, + &self.encoder, + ); + + // Run the BaseFold protocol on the random linear combination codeword, + // the random linear combination multilinear, and the random linear combination of the + // evaluation claims. + let mut proof = self.prove_from_prebatched_inputs( + eval_point, + mle_batch, + batched_eval_claim, + codeword_batch, + prover_data, + challenger, + )?; + // Add in the true grinding witness; + proof.batch_grinding_witness = batch_grinding_witness; + Ok(proof) + } + pub fn prove_untrusted_evaluations( &self, eval_point: Point, diff --git a/slop/crates/basefold/src/verifier.rs b/slop/crates/basefold/src/verifier.rs index 3c6267034c..bec99b6249 100644 --- a/slop/crates/basefold/src/verifier.rs +++ b/slop/crates/basefold/src/verifier.rs @@ -119,41 +119,28 @@ impl BasefoldVerifier where GC::F: TwoAdicField, { - pub fn verify_mle_evaluations( + /// Verify an already-batched MLE claim. + /// + /// # Parameters + /// - `commitment`: Merkle root of the committed (base-field) tensor + /// - `point`: Evaluation point in the extension field + /// - `eval_claim`: Claimed evaluation of the batched MLE at `point` + /// - `proof`: The Basefold proof to verify + /// - `to_virtual_oracle`: Closure called with the raw per-commitment Merkle openings and + /// the sampled query indices; it returns the virtual oracle values to feed into the + /// FRI query phase. + /// - `challenger`: Fiat-Shamir challenger (state must match the prover's) + pub fn verify_from_prebatched_inputs( &self, commitments: &[GC::Digest], mut point: Point, - evaluation_claims: &[MleEval], + eval_claim: GC::EF, proof: &BasefoldProof, + to_virtual_oracle: impl FnOnce(&[MerkleTreeOpeningAndProof], &[usize]) -> Vec, challenger: &mut GC::Challenger, ) -> Result<(), BaseFoldVerifierError> { - // Check batch grinding witness. - if !challenger.check_witness(BATCH_GRINDING_BITS, proof.batch_grinding_witness) { - return Err(BaseFoldVerifierError::BatchPow); - } - - // Sample the challenge used to batch all the different polynomials. - let total_len = evaluation_claims - .iter() - .map(|batch_claims| batch_claims.num_polynomials()) - .sum::(); - - let num_batching_variables = total_len.next_power_of_two().ilog2(); - let batching_point = challenger.sample_point::(num_batching_variables); - let batching_coefficients = partial_lagrange_blocking(&batching_point); - - // Compute the batched evaluation claim. - let eval_claim = evaluation_claims - .iter() - .flat_map(|batch_claims| batch_claims.iter()) - .zip(batching_coefficients.as_slice()) - .map(|(eval, batch_power)| *eval * *batch_power) - .sum::(); - - if evaluation_claims.len() != commitments.len() - || commitments.len() != proof.component_polynomials_query_openings_and_proofs.len() - || commitments.len() != self.num_expected_commitments - { + // number of oracle opening proofs matches number of oracles + if proof.component_polynomials_query_openings_and_proofs.len() != commitments.len() { return Err(BaseFoldVerifierError::IncorrectShape); } @@ -236,59 +223,47 @@ where .map(|_| challenger.sample_bits(log_len + self.fri_config.log_blowup())) .collect::>(); - // Compute the batch evaluations from the openings of the component polynomials. - let mut batch_evals = vec![GC::EF::zero(); query_indices.len()]; - let mut batch_idx = 0; - for (round_idx, opening_and_proof) in - proof.component_polynomials_query_openings_and_proofs.iter().enumerate() + // Shape-check and verify Merkle openings for each committed polynomial + let log_tensor_height = log_len + self.fri_config.log_blowup(); + for (opening_and_proof, commitment) in + proof.component_polynomials_query_openings_and_proofs.iter().zip_eq(commitments) { - let values = &opening_and_proof.values; - let total_columns = evaluation_claims[round_idx].num_polynomials(); - if values.dimensions.sizes().len() != 2 { + let initial_opening_values = &opening_and_proof.values; + if initial_opening_values.dimensions.sizes().len() != 2 { return Err(BaseFoldVerifierError::IncorrectShape); } - if values.dimensions.sizes()[0] != query_indices.len() { + if initial_opening_values.dimensions.sizes()[0] != query_indices.len() { return Err(BaseFoldVerifierError::IncorrectShape); } - if values.dimensions.sizes()[1] != total_columns { + if opening_and_proof.proof.log_tensor_height != log_tensor_height { return Err(BaseFoldVerifierError::IncorrectShape); } - let round_coefficients = - &batching_coefficients.as_slice()[batch_idx..batch_idx + total_columns]; - for (batch_eval, values) in batch_evals.iter_mut().zip_eq(values.split()) { - for (value, batching_coefficient) in - values.as_slice().iter().zip(round_coefficients) - { - *batch_eval += *batching_coefficient * *value; - } - } - batch_idx += total_columns; - } - // Verify the proof of the claimed values of the original commitments at the query indices. - for (commit, opening_and_proof) in - commitments.iter().zip_eq(proof.component_polynomials_query_openings_and_proofs.iter()) - { - // Sizes is checked to have at least two dimensions above. - let width = opening_and_proof.values.sizes()[1]; self.tcs .verify_tensor_openings( - commit, + commitment, &query_indices, &opening_and_proof.values, - width, - log_len + self.fri_config.log_blowup(), + initial_opening_values.sizes()[1], + log_tensor_height, &opening_and_proof.proof, ) .map_err(BaseFoldVerifierError::TcsError)?; } + // Turn the raw per-commitment openings into virtual oracle values via the + // caller-provided closure. + let virtual_oracle_evals = to_virtual_oracle( + &proof.component_polynomials_query_openings_and_proofs, + &query_indices, + ); + // Check that the query openings are consistent as FRI messages. self.verify_queries( &proof.fri_commitments, &query_indices, proof.final_poly, - batch_evals, + virtual_oracle_evals, &proof.query_phase_openings_and_proofs, &betas, )?; @@ -304,6 +279,76 @@ where Ok(()) } + pub fn verify_mle_evaluations( + &self, + commitments: &[GC::Digest], + point: Point, + evaluation_claims: &[MleEval], + proof: &BasefoldProof, + challenger: &mut GC::Challenger, + ) -> Result<(), BaseFoldVerifierError> { + // Check batch grinding witness. + if !challenger.check_witness(BATCH_GRINDING_BITS, proof.batch_grinding_witness) { + return Err(BaseFoldVerifierError::BatchPow); + } + + // Sample the challenge used to batch all the different polynomials. + let total_len = evaluation_claims + .iter() + .map(|batch_claims| batch_claims.num_polynomials()) + .sum::(); + + let num_batching_variables = total_len.next_power_of_two().ilog2(); + let batching_point = challenger.sample_point::(num_batching_variables); + let batching_coefficients = partial_lagrange_blocking(&batching_point); + + // Compute the batched evaluation claim. + let eval_claim = evaluation_claims + .iter() + .flat_map(|batch_claims| batch_claims.iter()) + .zip(batching_coefficients.as_slice()) + .map(|(eval, batch_power)| *eval * *batch_power) + .sum::(); + + if evaluation_claims.len() != commitments.len() + || commitments.len() != proof.component_polynomials_query_openings_and_proofs.len() + || commitments.len() != self.num_expected_commitments + { + return Err(BaseFoldVerifierError::IncorrectShape); + } + + // How to compute the virtual batch commitment from the openings of the component polynomials. + let to_virtual_oracle = + |openings: &[MerkleTreeOpeningAndProof], _query_indices: &[usize]| -> Vec { + let mut batch_evals = vec![GC::EF::zero(); self.fri_config.num_queries]; + let mut batch_idx = 0; + for (round_idx, opening_and_proof) in openings.iter().enumerate() { + let values = &opening_and_proof.values; + let total_columns = evaluation_claims[round_idx].num_polynomials(); + let round_coefficients = + &batching_coefficients.as_slice()[batch_idx..batch_idx + total_columns]; + for (batch_eval, values) in batch_evals.iter_mut().zip_eq(values.split()) { + for (value, batching_coefficient) in + values.as_slice().iter().zip(round_coefficients) + { + *batch_eval += *batching_coefficient * *value; + } + } + batch_idx += total_columns; + } + batch_evals + }; + + self.verify_from_prebatched_inputs( + commitments, + point, + eval_claim, + proof, + to_virtual_oracle, + challenger, + ) + } + /// The FRI verifier for a single query. We modify this from Plonky3 to be compatible with /// opening only a single vector. fn verify_queries( diff --git a/slop/crates/veil/src/zk/stacked_pcs/basefold_prover_wrapper.rs b/slop/crates/veil/src/zk/stacked_pcs/basefold_prover_wrapper.rs index 432b1eb140..92375fb53c 100644 --- a/slop/crates/veil/src/zk/stacked_pcs/basefold_prover_wrapper.rs +++ b/slop/crates/veil/src/zk/stacked_pcs/basefold_prover_wrapper.rs @@ -1,83 +1,4 @@ -//! Zero-knowledge-aware wrapper for [`BasefoldProver`] with custom batching support. -//! -//! -//! This module provides utilities to work with [`BasefoldProver`] while injecting -//! custom batching logic that operates on extension field MLEs. This is necessary -//! for zero-knowledge protocols where masking polynomials need to be incorporated -//! during the batching phase. -//! -//! # Overview -//! -//! The standard `BasefoldProver::prove_trusted_mle_evaluations` function takes MLEs -//! over the base field `GC::F` and internally performs batching to convert them to -//! extension field `GC::EF` before running the core Basefold protocol. This module -//! factors out that conversion, providing two key utilities: -//! -//! 1. **`ZkBasefoldProver`**: A wrapper struct around `BasefoldProver` that implements -//! both `MultilinearPcsBatchProver` and `ZkMultilinearPcsBatchProver`, providing -//! a clean interface for working with custom batching. -//! -//! 2. **`prove_from_batched_inputs`**: A standalone function that runs the Basefold -//! protocol starting from already-batched extension field inputs. This allows you -//! to implement custom batching logic (e.g., with masking polynomials) while -//! reusing all the Basefold infrastructure. -//! -//! # Usage Example -//! -//! ```ignore -//! use slop_basefold_prover::{BasefoldProver, BasefoldProverComponents}; -//! use veil::{ZkBasefoldProver, ZkMultilinearPcsBatchProver, prove_from_batched_inputs}; -//! -//! // Create your standard BasefoldProver -//! let basefold_prover: BasefoldProver = /* ... */; -//! -//! // Wrap it to get ZK capabilities -//! let zk_prover = ZkBasefoldProver::new(basefold_prover); -//! -//! // The wrapper implements MultilinearPcsBatchProver, so you can use it -//! // for standard commitment operations -//! let (commitment, prover_data) = zk_prover.commit_multilinears(mles)?; -//! -//! // Implement your custom batching logic here -//! let (batched_mle, batched_codeword, batched_eval_claim) = -//! my_custom_zk_batching(data_mles, masking_mles, evaluations, challenger); -//! -//! // Option 1: Use the helper method (equivalent to Option 1) -//! let proof = zk_prover.prove_with_batched_ef_inputs( -//! eval_point, -//! batched_mle, -//! batched_eval_claim, -//! prover_data, -//! challenger, -//! )?; -//! -//! // Option 2: Use the standalone function directly -//! let proof = prove_from_batched_inputs( -//! zk_prover.basefold_prover(), -//! eval_point, -//! batched_mle, -//! batched_eval_claim, -//! prover_data, -//! challenger, -//! )?; -//! ``` -//! -//! # Custom Batching Pattern -//! -//! To implement ZK-aware batching: -//! -//! 1. **Convert to extension field**: Convert your base field MLEs to extension field -//! 2. **Add masking**: Incorporate masking polynomials for zero-knowledge -//! 3. **Random linear combination**: Compute batched MLE as: -//! ```text -//! batched_mle = Σ(α^i · data_mle_i) + Σ(α^(n+j) · mask_mle_j) -//! ``` -//! where α is the batching challenge -//! 4. **Encode**: Reed-Solomon encode the batched MLE to get batched codeword -//! 5. **Compute claim**: Compute the batched evaluation claim similarly -//! -//! See `basefold-prover/src/fri.rs::FriCpuProver::batch` for the standard batching -//! implementation that you can adapt for ZK. +//! Zero-knowledge-aware wrapper for [`BasefoldProver`] with custom encoding support. use crate::zk::inner::{MerkleProverData, ZkIopCtx, ZkMerkleizer}; use itertools::Itertools; @@ -98,143 +19,6 @@ use slop_multilinear::{Mle, Point}; use slop_tensor::Tensor; use std::{marker::PhantomData, sync::Arc}; -/// Core function that proves evaluations starting from already-batched extension field inputs. -/// -/// This function factors out the post-batching logic from `BasefoldProver::prove_trusted_mle_evaluations`, -/// allowing custom batching strategies to be used while reusing the core Basefold proving protocol. -/// -/// # Parameters -/// - `basefold_prover`: Reference to the underlying BasefoldProver -/// - `eval_point`: The evaluation point in the extension field -/// - `batched_mle`: Pre-batched MLE over the extension field GC::EF -/// - `batched_codeword`: Pre-batched Reed-Solomon codeword -/// - `batched_eval_claim`: The batched evaluation claim -/// - `prover_data`: Prover data from committing the unbatched ORIGINAL MLEs -/// - `challenger`: The Fiat-Shamir challenger -/// -/// # Returns -/// A BasefoldProof or an error -/// The commitments in the basefold proof correspond to the Merkleization of the basefield components of the extension field MLE -#[allow(clippy::type_complexity)] -pub fn prove_from_batched_inputs>( - basefold_prover: &BasefoldProver, - mut eval_point: Point, - batched_mle: Mle, - batched_eval_claim: GC::EF, - batched_codeword: RsCodeWord, - prover_datas: Vec>>, - challenger: &mut GC::Challenger, -) -> Result, BaseFoldConfigProverError> { - let fri_prover = FriCpuProver::(PhantomData); - - let mut current_mle = batched_mle; - let mut current_codeword = batched_codeword; - - // Initialize the vecs that go into a BaseFoldProof. - let log_len = current_mle.num_variables(); - let mut univariate_messages: Vec<[GC::EF; 2]> = vec![]; - let mut fri_commitments = vec![]; - let mut commit_phase_data = vec![]; - let mut current_batched_eval_claim = batched_eval_claim; - let mut commit_phase_values = vec![]; - - assert_eq!( - current_mle.num_variables(), - eval_point.dimension() as u32, - "eval point dimension mismatch" - ); - - // Main Basefold reduction loop - for _ in 0..eval_point.dimension() { - // Compute claims for `g(X_0, X_1, ..., X_{d-1}, 0)` and `g(X_0, X_1, ..., X_{d-1}, 1)`. - let last_coord = eval_point.remove_last_coordinate(); - let zero_values = current_mle.fixed_at_zero(&eval_point); - let zero_val = zero_values[0]; - let one_val = (current_batched_eval_claim - zero_val) / last_coord + zero_val; - let uni_poly = [zero_val, one_val]; - univariate_messages.push(uni_poly); - - uni_poly.iter().for_each(|elem| challenger.observe_ext_element(*elem)); - - // Perform a single round of the FRI commit phase, returning the commitment, folded - // codeword, and folding parameter. - let (beta, folded_mle, folded_codeword, commitment, leaves, prover_data_round) = fri_prover - .commit_phase_round( - current_mle, - current_codeword, - &basefold_prover.tcs_prover, - challenger, - ) - .map_err(BasefoldProverError::CommitPhaseError)?; - - fri_commitments.push(commitment); - commit_phase_data.push(prover_data_round); - commit_phase_values.push(leaves); - - current_mle = folded_mle; - current_codeword = folded_codeword; - current_batched_eval_claim = zero_val + beta * one_val; - } - - // Finalize the constant polynomial - let final_poly = fri_prover.final_poly(current_codeword); - challenger.observe_ext_element(final_poly); - - // Proof of work - let fri_config = basefold_prover.encoder.config(); - let pow_bits = fri_config.proof_of_work_bits; - let pow_witness = challenger.grind(pow_bits); - - // FRI Query Phase. - let query_indices: Vec = (0..fri_config.num_queries) - .map(|_| challenger.sample_bits(log_len as usize + fri_config.log_blowup())) - .collect(); - - // Open each committed polynomial at the query indices. - let mut component_polynomials_query_openings_and_proofs = vec![]; - for prover_data in prover_datas { - let BasefoldProverData { encoded_messages, tcs_prover_data } = prover_data; - let values = basefold_prover - .tcs_prover - .compute_openings_at_indices(encoded_messages, &query_indices); - let proof = basefold_prover - .tcs_prover - .prove_openings_at_indices(tcs_prover_data, &query_indices) - .map_err(BaseFoldConfigProverError::::TcsCommitError) - .unwrap(); - component_polynomials_query_openings_and_proofs - .push(MerkleTreeOpeningAndProof:: { values, proof }); - } - - // Provide openings for the FRI query phase. - let mut query_phase_openings_and_proofs = vec![]; - let mut indices = query_indices; - for (leaves, data) in commit_phase_values.into_iter().zip_eq(commit_phase_data) { - for index in indices.iter_mut() { - *index >>= 1; - } - let leaves: Message> = leaves.into(); - let values = basefold_prover.tcs_prover.compute_openings_at_indices(leaves, &indices); - - let proof = basefold_prover - .tcs_prover - .prove_openings_at_indices(data, &indices) - .map_err(BaseFoldConfigProverError::::TcsCommitError)?; - let opening = MerkleTreeOpeningAndProof { values, proof }; - query_phase_openings_and_proofs.push(opening); - } - - Ok(BasefoldProof { - univariate_messages, - fri_commitments, - component_polynomials_query_openings_and_proofs, - query_phase_openings_and_proofs, - final_poly, - pow_witness, - batch_grinding_witness: Default::default(), - }) -} - /// A wrapper around BasefoldProver that enables custom batching strategies. /// /// This wrapper allows zero-knowledge protocols to inject their own batching logic @@ -258,40 +42,6 @@ impl> ZkBasefoldProver { Self { inner } } - /// Prove evaluations using custom pre-batched inputs over the extension field. - /// - /// This is the main entry point for ZK protocols. Instead of taking base field MLEs - /// and batching them internally, this function accepts pre-batched extension field - /// inputs, allowing the caller to implement custom batching logic (e.g., incorporating - /// masking polynomials for zero-knowledge). - /// - /// # Parameters - /// - `eval_point`: The evaluation point in the extension field - /// - `batched_mle`: Pre-batched MLE over GC::EF (may include masking) - /// - `batched_codeword`: Corresponding Reed-Solomon encoded codeword - /// - `batched_eval_claim`: The claimed evaluation - /// - `prover_data`: Prover data from commitment phase - /// - `challenger`: Fiat-Shamir challenger - pub fn prove_with_batched_ef_inputs( - &self, - eval_point: Point, - batched_mle: Mle, - batched_codeword: RsCodeWord, - batched_eval_claim: GC::EF, - prover_datas: Vec>>, - challenger: &mut GC::Challenger, - ) -> Result, BaseFoldConfigProverError> { - prove_from_batched_inputs( - &self.inner, - eval_point, - batched_mle, - batched_eval_claim, - batched_codeword, - prover_datas, - challenger, - ) - } - /// Encode MLEs with an arbitrary log_blowup factor. /// /// This function performs Reed-Solomon encoding on the input MLEs using a custom @@ -401,17 +151,3 @@ impl> ZkBasefoldProver { Ok((commitment, BasefoldProverData { encoded_messages, tcs_prover_data })) } } - -// NOTE: Custom batching logic with masking polynomials should be implemented -// by the user of this module. The general pattern is: -// -// 1. Convert base field MLEs to extension field using field embedding -// 2. Generate masking polynomials for zero-knowledge -// 3. Compute random linear combination using batching challenge: -// batched_mle = sum(challenge^i * data_mle_i) + sum(challenge^(n+j) * mask_mle_j) -// 4. Compute batched evaluation claim similarly -// 5. Encode the batched MLE to get the batched codeword -// 6. Call prove_from_batched_inputs() with these batched values -// -// See basefold-prover/src/fri.rs::FriCpuProver::batch for reference implementation -// of standard (non-ZK) batching. diff --git a/slop/crates/veil/src/zk/stacked_pcs/basefold_verifier_wrapper.rs b/slop/crates/veil/src/zk/stacked_pcs/basefold_verifier_wrapper.rs deleted file mode 100644 index 0eb3d3f814..0000000000 --- a/slop/crates/veil/src/zk/stacked_pcs/basefold_verifier_wrapper.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! Zero-knowledge-aware wrapper for [`BasefoldVerifier`] with custom batch evaluation support. -//! -//! This module provides [`ZkStackedPcsVerifier`], a wrapper around `StackedPcsVerifier` that -//! verifies Basefold proofs for pre-batched, extension-field MLEs. The code is largely -//! copy-pasted from the standard Basefold verifier with two key modifications: -//! -//! 1. **Extension field openings**: The commitment is over a 4-column base-field tensor -//! (representing the base-field components of extension-field coefficients), so the -//! FRI query phase works with extension-field evaluations reconstructed from base-field -//! openings. -//! -//! 2. **Closure-based batch evaluation**: Instead of computing batch evaluations internally, -//! `verify_trusted_ext_mle_evaluation` accepts a `compute_batch_evals` closure. This -//! allows the caller (e.g., `verifier.rs`) to inject protocol-specific correction logic -//! (such as RLC padding correction) without polluting this copy-pasted Basefold code. -//! -//! # Relationship to Other Modules -//! -//! - **Prover side**: Proofs verified here are generated by -//! [`prove_from_batched_inputs`](crate::zk::stacked_pcs::basefold_prover_wrapper::prove_from_batched_inputs) -//! in the prover wrapper. -//! - **Caller**: [`verifier.rs`](crate::zk::stacked_pcs::verifier) constructs the `compute_batch_evals` closure -//! that captures the RLC padding vector and expected evaluations, then passes it to -//! `verify_trusted_ext_mle_evaluation`. -//! -//! # Usage -//! -//! ```ignore -//! let verifier = ZkStackedPcsVerifier::new(stacked_verifier); -//! -//! // The caller constructs a closure that computes corrected batch evaluations -//! // from query indices and the tensor height. -//! let compute_batch_evals = |query_indices: &[usize], log_tensor_height: usize| -> Vec { -//! // ... correction logic using RLC padding, expected evals, etc. -//! }; -//! -//! verifier.verify_trusted_ext_mle_evaluation( -//! &commitment, -//! eval_point, -//! eval_claim, -//! &proof, -//! compute_batch_evals, -//! &mut challenger, -//! )?; -//! ``` - -use crate::zk::inner::{ZkIopCtx, ZkMerkleizer}; -use itertools::Itertools; -use slop_algebra::{AbstractExtensionField, AbstractField, TwoAdicField}; -use slop_basefold::{BaseFoldVerifierError, BasefoldProof, BasefoldVerifier, FriConfig}; -use slop_basefold_prover::BasefoldProver; -use slop_challenger::{CanObserve, CanSampleBits, FieldChallenger, GrindingChallenger, IopCtx}; -use slop_merkle_tree::MerkleTreeTcsError; -use slop_multilinear::Point; -use slop_stacked::StackedPcsVerifier; -use slop_utils::reverse_bits_len; - -use super::basefold_prover_wrapper::ZkBasefoldProver; - -/// Zero-knowledge-aware wrapper around [`StackedPcsVerifier`]. -/// -/// This struct wraps the standard stacked PCS verifier and provides a modified -/// Basefold verification routine that: -/// - Operates on extension-field MLE commitments (stored as base-field tensors) -/// - Accepts a closure for computing corrected batch evaluations during FRI queries -/// -/// The wrapper only adds `verify_trusted_ext_mle_evaluation` and `verify_queries`; -/// all other verification logic lives in the caller (`verifier.rs`). -pub struct ZkStackedPcsVerifier -where - GC: IopCtx, -{ - /// The underlying stacked PCS verifier (contains `BasefoldVerifier` and stacking config) - pub inner: StackedPcsVerifier, -} - -impl ZkStackedPcsVerifier -where - GC::F: TwoAdicField, - GC::EF: TwoAdicField, -{ - pub const fn new(inner: StackedPcsVerifier) -> Self { - Self { inner } - } - - /// Verifies a Basefold proof for a pre-batched, extension-field MLE evaluation. - /// - /// This is the counterpart to - /// [`prove_from_batched_inputs`](crate::basefold_prover_wrapper::prove_from_batched_inputs) - /// on the prover side. The commitment is over a base-field tensor whose columns are the - /// base-field components of the extension-field MLE coefficients. - /// - /// # Parameters - /// - `commitment`: Merkle root of the committed (base-field) tensor - /// - `point`: Evaluation point in the extension field - /// - `eval_claim`: Claimed evaluation of the batched MLE at `point` - /// - `proof`: The Basefold proof to verify - /// - `compute_batch_evals`: Closure called with `(query_indices, log_tensor_height)` that - /// returns corrected batch evaluations for the FRI query phase. This is where the caller - /// injects protocol-specific logic (e.g., subtracting RLC padding contributions). - /// - `challenger`: Fiat-Shamir challenger (state must match the prover's) - /// - /// # Verification Steps - /// 1. Shape checks on the proof structure - /// 2. Sumcheck verification: checks round-by-round consistency of univariate messages - /// 3. Proof-of-work verification (grinding) - /// 4. FRI query phase: samples query indices, calls `compute_batch_evals` to get - /// corrected evaluations, verifies Merkle openings, and checks FRI consistency - /// 5. Final polynomial consistency check - pub fn verify_trusted_ext_mle_evaluation( - &self, - commitments: &[GC::Digest], - mut point: Point, - eval_claim: GC::EF, - proof: &BasefoldProof, - compute_batch_evals: impl FnOnce(&[usize], usize) -> Vec, - challenger: &mut GC::Challenger, - ) -> Result<(), BaseFoldVerifierError> { - let verifier = &self.inner.basefold_verifier; - - if proof.component_polynomials_query_openings_and_proofs.len() != commitments.len() { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - // Assert correctness of shape. - if proof.fri_commitments.len() != proof.univariate_messages.len() - || proof.fri_commitments.len() != point.dimension() - || proof.univariate_messages.is_empty() - { - return Err(BaseFoldVerifierError::SumcheckFriLengthMismatch); - } - - // The prover messages correspond to fixing the last coordinate first, so we reverse the - // underlying point for the verification. - point.reverse(); - - // Sample the challenges used for FRI folding and BaseFold random linear combinations. - let betas = proof - .fri_commitments - .iter() - .zip_eq(proof.univariate_messages.iter()) - .map(|(commitment, poly)| { - poly.iter().copied().for_each(|x| challenger.observe_ext_element(x)); - challenger.observe(*commitment); - challenger.sample_ext_element::() - }) - .collect::>(); - - // Check the consistency of the first univariate message with the claimed evaluation. The - // first_poly is supposed to be `vals(X_0, X_1, ..., X_{d-1}, 0), vals(X_0, X_1, ..., - // X_{d-1}, 1)`. Given this, the claimed evaluation should be `(1 - X_d) * - // first_poly[0] + X_d * first_poly[1]`. - let first_poly = proof.univariate_messages[0]; - if eval_claim != (GC::EF::one() - *point[0]) * first_poly[0] + *point[0] * first_poly[1] { - return Err(BaseFoldVerifierError::Sumcheck); - }; - - // Fold the two messages into a single evaluation claim for the next round, using the - // sampled randomness. - let mut expected_eval = first_poly[0] + betas[0] * first_poly[1]; - - // Check round-by-round consistency between the successive sumcheck univariate messages. - for (i, (poly, beta)) in - proof.univariate_messages[1..].iter().zip_eq(betas[1..].iter()).enumerate() - { - // The check is similar to the one for `first_poly`. - let i = i + 1; - if expected_eval != (GC::EF::one() - *point[i]) * poly[0] + *point[i] * poly[1] { - return Err(BaseFoldVerifierError::Sumcheck); - } - - // Fold the two pieces of the message. - expected_eval = poly[0] + *beta * poly[1]; - } - - challenger.observe_ext_element(proof.final_poly); - - // Check proof of work (grinding to find a number that hashes to have - // `verifier.fri_config.proof_of_work_bits` zeroes at the beginning). - if !challenger.check_witness(verifier.fri_config.proof_of_work_bits, proof.pow_witness) { - return Err(BaseFoldVerifierError::Pow); - } - - let log_len = proof.fri_commitments.len(); - - if log_len + verifier.fri_config.log_blowup() > GC::F::TWO_ADICITY { - return Err(BaseFoldVerifierError::TwoAdicityOverflow); - } - - // Sample query indices for the FRI query IOPP part of BaseFold. This part is very similar - // to the corresponding part in the univariate FRI verifier. - let query_indices = (0..verifier.fri_config.num_queries) - .map(|_| challenger.sample_bits(log_len + verifier.fri_config.log_blowup())) - .collect::>(); - - // Shape-check and verify Merkle openings for each committed polynomial. - let log_tensor_height = log_len + verifier.fri_config.log_blowup(); - for (opening_and_proof, commitment) in - proof.component_polynomials_query_openings_and_proofs.iter().zip_eq(commitments) - { - let initial_opening_values = &opening_and_proof.values; - if initial_opening_values.dimensions.sizes().len() != 2 { - return Err(BaseFoldVerifierError::IncorrectShape); - } - if initial_opening_values.dimensions.sizes()[0] != query_indices.len() { - return Err(BaseFoldVerifierError::IncorrectShape); - } - if opening_and_proof.proof.log_tensor_height != log_tensor_height { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - verifier - .tcs - .verify_tensor_openings( - commitment, - &query_indices, - &opening_and_proof.values, - initial_opening_values.sizes()[1], - log_tensor_height, - &opening_and_proof.proof, - ) - .map_err(BaseFoldVerifierError::TcsError)?; - } - - // Compute corrected batch evaluations via the caller-provided closure. - let batch_evals_corrected = compute_batch_evals(&query_indices, log_tensor_height); - - // Check that the query openings are consistent as FRI messages. - self.verify_queries( - &proof.fri_commitments, - &query_indices, - proof.final_poly, - batch_evals_corrected, - &proof.query_phase_openings_and_proofs, - &betas, - )?; - - if proof.final_poly - != proof.univariate_messages.last().unwrap()[0] - + *betas.last().unwrap() * proof.univariate_messages.last().unwrap()[1] - { - return Err(BaseFoldVerifierError::SumcheckFinalPolyMismatch); - } - - Ok(()) - } - - /// Verifies the FRI query phase for all sampled query indices. - /// - /// Starting from the `reduced_openings` (corrected batch evaluations at the leaf level), - /// this function iteratively folds evaluations through each FRI round, checking that: - /// - Each folded evaluation is consistent with the Merkle-opened sibling value - /// - Each round's Merkle opening verifies against the corresponding commitment - /// - All queries converge to the declared `final_poly` constant - /// - /// This is adapted from the standard Plonky3 FRI verifier to work with a single - /// committed vector (rather than multiple). - fn verify_queries( - &self, - commitments: &[GC::Digest], - indices: &[usize], - final_poly: GC::EF, - reduced_openings: Vec, - query_openings: &[slop_merkle_tree::MerkleTreeOpeningAndProof], - betas: &[GC::EF], - ) -> Result<(), BaseFoldVerifierError> { - let verifier = &self.inner.basefold_verifier; - let log_max_height = commitments.len() + verifier.fri_config.log_blowup(); - - let mut folded_evals = reduced_openings; - let mut indices = indices.to_vec(); - - let mut xis = indices - .iter() - .map(|index| { - GC::F::two_adic_generator(log_max_height) - .exp_u64(reverse_bits_len(*index, log_max_height) as u64) - }) - .collect::>(); - - if commitments.len() != query_openings.len() || commitments.len() != betas.len() { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - // Loop over the FRI queries. - for (round_idx, ((commitment, query_opening), beta)) in (verifier.fri_config.log_blowup() - ..log_max_height) - .rev() - .zip_eq(commitments.iter().zip_eq(query_openings.iter()).zip_eq(betas)) - { - let openings = &query_opening.values; - if openings.dimensions.sizes().len() != 2 { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - if indices.len() != folded_evals.len() - || indices.len() != openings.dimensions.sizes()[0] - || indices.len() != xis.len() - { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - for (((index, folded_eval), opening), x) in indices - .iter_mut() - .zip_eq(folded_evals.iter_mut()) - .zip_eq(openings.split()) - .zip_eq(xis.iter_mut()) - { - let index_sibling = *index ^ 1; - let index_pair = *index >> 1; - - if opening.total_len() != 2 * >::D { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - let evals: [GC::EF; 2] = opening - .as_slice() - .chunks_exact(GC::EF::D) - .map(GC::EF::from_base_slice) - .collect::>() - .try_into() - .unwrap(); - - // Check that the folded evaluation is consistent with the FRI query proof opening. - if evals[*index % 2] != *folded_eval { - return Err(BaseFoldVerifierError::QueryValueMismatch); - } - - let mut xs = [*x; 2]; - xs[index_sibling % 2] *= GC::F::two_adic_generator(1); - - // interpolate and evaluate at beta - *folded_eval = evals[0] - + (*beta - xs[0].into()) * (evals[1] - evals[0]) / GC::EF::from(xs[1] - xs[0]); - - *index = index_pair; - *x = x.square(); - } - - // The magic constant 2 here is the folding factor we use for FRI. - if round_idx != query_opening.proof.log_tensor_height - || query_opening.proof.width != GC::EF::D * 2 - { - return Err(BaseFoldVerifierError::IncorrectShape); - } - - // Check that the opening is consistent with the commitment. - verifier - .tcs - .verify_tensor_openings( - commitment, - &indices, - &query_opening.values, - GC::EF::D * 2, - round_idx, - &query_opening.proof, - ) - .map_err(BaseFoldVerifierError::TcsError)?; - } - - for folded_eval in folded_evals { - if folded_eval != final_poly { - return Err(BaseFoldVerifierError::QueryFinalPolyMismatch); - } - } - - Ok(()) - } -} - -/// Creates both a `ZkBasefoldProver` and `ZkStackedPcsVerifier` with default FRI configuration. -/// -/// This is a convenience function that simplifies the typical setup where you need both -/// a prover and verifier. The underlying `BasefoldVerifier` is shared (prover borrows it -/// before verifier takes ownership). -/// -/// The `num_encoding_variables` value is fixed here and all subsequent -/// [`commit_mle`](crate::zk::ZkProverCtx::commit_mle) and -/// [`read_oracle`](crate::compiler::ReadingCtx::read_oracle) calls must use a matching -/// `num_encoding_variables` (inferred from the MLE size for `commit_mle`, or passed -/// directly for `read_oracle`). -/// -/// # Arguments -/// * `num_expected_commitments` — upper bound on the number of MLE commitments that -/// will be made during the protocol. -/// * `num_encoding_variables` — number of variables per stacked polynomial (encoding -/// width). Each committed MLE will be stacked into a tensor whose rows have -/// `2^num_encoding_variables` entries. -/// -/// # Example -/// ```ignore -/// let (pcs_prover, pcs_verifier) = -/// initialize_zk_prover_and_verifier::(1, NUM_ENCODING_VARIABLES); -/// ``` -pub fn initialize_zk_prover_and_verifier>( - num_expected_commitments: usize, - num_encoding_variables: u32, -) -> (ZkBasefoldProver, ZkStackedPcsVerifier) -where - GC::F: TwoAdicField, -{ - let fri_config = FriConfig::default_fri_config(); - let basefold_verifier = BasefoldVerifier::::new(fri_config, num_expected_commitments); - let basefold_prover = BasefoldProver::new(&basefold_verifier); - let zk_basefold_prover = ZkBasefoldProver::new(basefold_prover); - let stacked_verifier = StackedPcsVerifier::new(basefold_verifier, num_encoding_variables); - let zk_stacked_verifier = ZkStackedPcsVerifier::new(stacked_verifier); - (zk_basefold_prover, zk_stacked_verifier) -} diff --git a/slop/crates/veil/src/zk/stacked_pcs/mod.rs b/slop/crates/veil/src/zk/stacked_pcs/mod.rs index 06b4cef0e0..3df618992c 100644 --- a/slop/crates/veil/src/zk/stacked_pcs/mod.rs +++ b/slop/crates/veil/src/zk/stacked_pcs/mod.rs @@ -1,5 +1,4 @@ pub mod basefold_prover_wrapper; -pub mod basefold_verifier_wrapper; pub mod prover; pub mod utils; pub mod verifier; @@ -7,15 +6,52 @@ pub mod verifier; #[cfg(test)] mod tests; -pub use basefold_verifier_wrapper::*; pub use prover::*; pub use verifier::*; -use crate::zk::verifier_ctx::ZkIopCtx; +use crate::zk::inner::{ZkIopCtx, ZkMerkleizer}; +use basefold_prover_wrapper::ZkBasefoldProver; +use slop_algebra::TwoAdicField; +use slop_basefold::{BasefoldVerifier, FriConfig}; +use slop_basefold_prover::BasefoldProver; use slop_koala_bear::KoalaBearDegree4Duplex; +use slop_stacked::StackedPcsVerifier; impl ZkIopCtx for KoalaBearDegree4Duplex { type PcsProof = ZkStackedPcsProof; - type PcsVerifier = ZkStackedPcsVerifier; + type PcsVerifier = StackedPcsVerifier; +} + +/// Creates both a `ZkBasefoldProver` and a `StackedPcsVerifier` with default FRI configuration. +/// +/// This is a convenience function that simplifies the typical setup where you need both +/// a prover and verifier. The underlying `BasefoldVerifier` is shared (prover borrows it +/// before verifier takes ownership). +/// +/// The `num_encoding_variables` value is fixed here and all subsequent +/// [`commit_mle`](crate::zk::ZkProverCtx::commit_mle) and +/// [`read_oracle`](crate::compiler::ReadingCtx::read_oracle) calls must use a matching +/// `num_encoding_variables` (inferred from the MLE size for `commit_mle`, or passed +/// directly for `read_oracle`). +/// +/// # Arguments +/// * `num_expected_commitments` — upper bound on the number of MLE commitments that +/// will be made during the protocol. +/// * `num_encoding_variables` — number of variables per stacked polynomial (encoding +/// width). Each committed MLE will be stacked into a tensor whose rows have +/// `2^num_encoding_variables` entries. +pub fn initialize_zk_prover_and_verifier>( + num_expected_commitments: usize, + num_encoding_variables: u32, +) -> (ZkBasefoldProver, StackedPcsVerifier) +where + GC::F: TwoAdicField, +{ + let fri_config = FriConfig::default_fri_config(); + let basefold_verifier = BasefoldVerifier::::new(fri_config, num_expected_commitments); + let basefold_prover = BasefoldProver::new(&basefold_verifier); + let zk_basefold_prover = ZkBasefoldProver::new(basefold_prover); + let stacked_verifier = StackedPcsVerifier::new(basefold_verifier, num_encoding_variables); + (zk_basefold_prover, stacked_verifier) } diff --git a/slop/crates/veil/src/zk/stacked_pcs/prover.rs b/slop/crates/veil/src/zk/stacked_pcs/prover.rs index 62d41bcbb5..3dc15c9e91 100644 --- a/slop/crates/veil/src/zk/stacked_pcs/prover.rs +++ b/slop/crates/veil/src/zk/stacked_pcs/prover.rs @@ -363,15 +363,16 @@ impl> ZkBasefoldProver { let rlc_mle_extension = Mle::new(RowMajorMatrix::new(combined_mle_vec, 1).into()); let rlc_eval_proof = { let mut challenger = zkbuilder.challenger(); - self.prove_with_batched_ef_inputs( - eval_point_inner, - rlc_mle_extension, - rlc_codeword, - rlc_eval_claim, - full_pcs_datas, - &mut challenger, - ) - .map_err(ZkStackedPcsProverError::BasefoldError)? + self.inner + .prove_from_prebatched_inputs( + eval_point_inner, + rlc_mle_extension, + rlc_eval_claim, + rlc_codeword, + full_pcs_datas.into_iter().collect(), + &mut challenger, + ) + .map_err(ZkStackedPcsProverError::BasefoldError)? }; // Build constraint data (shared with verifier) diff --git a/slop/crates/veil/src/zk/stacked_pcs/verifier.rs b/slop/crates/veil/src/zk/stacked_pcs/verifier.rs index 59ed99fd00..1fe908f0de 100644 --- a/slop/crates/veil/src/zk/stacked_pcs/verifier.rs +++ b/slop/crates/veil/src/zk/stacked_pcs/verifier.rs @@ -8,12 +8,12 @@ use rayon::prelude::*; use slop_algebra::{AbstractExtensionField, AbstractField, TwoAdicField}; use slop_basefold::BaseFoldVerifierError; use slop_challenger::{FieldChallenger, IopCtx}; -use slop_merkle_tree::MerkleTreeTcsError; +use slop_merkle_tree::{MerkleTreeOpeningAndProof, MerkleTreeTcsError}; use slop_multilinear::{partial_lagrange_blocking, Point}; +use slop_stacked::StackedPcsVerifier; use slop_utils::reverse_bits_len; use thiserror::Error; -use super::basefold_verifier_wrapper::ZkStackedPcsVerifier; use super::ZkStackedPcsProof; /// Type alias for `VerifierValue` when using the ZK stacked PCS. @@ -40,199 +40,169 @@ pub enum ZkStackedVerifierError { IncorrectShape(String), } -impl ZkStackedPcsVerifier +/// Verifies a batched ZK stacked PCS proof for multiple evaluation claims at the same point. +/// +/// Each entry in `commitments_and_claims` is a `(commitment, claim_expr)` pair. +/// All commitments must share the same `log_num_polys` and `mle_num_vars`. +/// +/// Returns the constraint data on success. The caller is responsible for adding constraints +/// to the context. +pub fn verify_zk_stacked_pcs_batched>( + stacked_verifier: &StackedPcsVerifier, + commitments_and_claims: &[(GC::Digest, C::Expr)], + point: &Point, + proof: ZkStackedPcsProof, + context: &mut C, +) -> Result, ZkStackedVerifierError> where GC::F: TwoAdicField, { - /// Verifies a ZK stacked PCS proof for a single evaluation claim. - /// - /// Thin wrapper around [`verify_zk_stacked_pcs_batched`] for the single-commitment case. - pub fn verify_zk_stacked_pcs>( - &self, - commitment: &GC::Digest, - point: &Point, - claim_expr: &C::Expr, - proof: ZkStackedPcsProof, - context: &mut C, - ) -> Result, ZkStackedVerifierError> { - self.verify_zk_stacked_pcs_batched( - &[(*commitment, claim_expr.clone())], - point, - proof, - context, - ) - } + let num_claims = commitments_and_claims.len(); + assert!(num_claims > 0, "must have at least one claim"); - /// Verifies a batched ZK stacked PCS proof for multiple evaluation claims at the same point. - /// - /// Each entry in `commitments_and_claims` is a `(commitment, claim_expr)` pair. - /// All commitments must share the same `log_num_polys` and `mle_num_vars`. - /// - /// Returns the constraint data on success. - /// The caller is responsible for adding constraints to the context. - pub fn verify_zk_stacked_pcs_batched>( - &self, - commitments_and_claims: &[(GC::Digest, C::Expr)], - point: &Point, - proof: ZkStackedPcsProof, - context: &mut C, - ) -> Result, ZkStackedVerifierError> { - let num_claims = commitments_and_claims.len(); - assert!(num_claims > 0, "must have at least one claim"); - - let ZkStackedPcsProof { rlc_eval_proof, rlc_eval_claim, rlc_padding_vec, log_num_polys } = - proof; - - let verifier = &self.inner.basefold_verifier; - let num_encoding_variables = self.inner.log_stacking_height as usize; - let num_polys = (1 << log_num_polys) + GC::EF::D; // +deg(EF/F) for mask - - // Shape check: point must have the right dimension - if log_num_polys + num_encoding_variables != point.dimension() { - return Err(ZkStackedVerifierError::IncorrectShape("Inconsistent dimensions".into())); - } + let ZkStackedPcsProof { rlc_eval_proof, rlc_eval_claim, rlc_padding_vec, log_num_polys } = + proof; - // Shape check: one opening per commitment in the proof - if rlc_eval_proof.component_polynomials_query_openings_and_proofs.len() != num_claims { - return Err(ZkStackedVerifierError::IncorrectShape( - "Number of openings doesn't match number of commitments".into(), - )); - } + let verifier = &stacked_verifier.basefold_verifier; + let num_encoding_variables = stacked_verifier.log_stacking_height as usize; + let num_polys = (1 << log_num_polys) + GC::EF::D; // +deg(EF/F) for mask - // Enough padding for the needed query count - let query_count = verifier.fri_config.num_queries; - if query_count > rlc_padding_vec.len() { - return Err(ZkStackedVerifierError::IncorrectShape( - "Not enough padding for RLC eval".into(), - )); - } + // Shape check: point must have the right dimension + if log_num_polys + num_encoding_variables != point.dimension() { + return Err(ZkStackedVerifierError::IncorrectShape("Inconsistent dimensions".into())); + } - // Step 1: Read evals from context for each commitment. - // Only commitment 0 includes mask column evaluations; - // the others only have data column evaluations. - let mut per_claim_evals = Vec::with_capacity(num_claims); - for j in 0..num_claims { - let num_to_read = if j == 0 { num_polys } else { 1 << log_num_polys }; - let Some(evals) = context.read_next(num_to_read) else { - return Err(ZkStackedVerifierError::IncorrectShape("Failed to get evals".into())); - }; - per_claim_evals.push(evals); - } + // Shape check: one opening per commitment in the proof + if rlc_eval_proof.component_polynomials_query_openings_and_proofs.len() != num_claims { + return Err(ZkStackedVerifierError::IncorrectShape( + "Number of openings doesn't match number of commitments".into(), + )); + } - // Step 2: Sample shared RLC point (dimension = log_num_polys) - let rlc_point = { - let mut challenger = context.challenger(); - let coords: Vec = - (0..log_num_polys).map(|_| challenger.sample_ext_element()).collect(); - Point::new(coords.into()) - }; + // Enough padding for the needed query count + let query_count = verifier.fri_config.num_queries; + if query_count > rlc_padding_vec.len() { + return Err(ZkStackedVerifierError::IncorrectShape( + "Not enough padding for RLC eval".into(), + )); + } - // Step 3: Sample batching challenge α - let batching_challenge: GC::EF = { - let mut challenger = context.challenger(); - challenger.sample_ext_element() + // Step 1: Read evals from context for each commitment. + // Only commitment 0 includes mask column evaluations; + // the others only have data column evaluations. + let mut per_claim_evals = Vec::with_capacity(num_claims); + for j in 0..num_claims { + let num_to_read = if j == 0 { num_polys } else { 1 << log_num_polys }; + let Some(evals) = context.read_next(num_to_read) else { + return Err(ZkStackedVerifierError::IncorrectShape("Failed to get evals".into())); }; + per_claim_evals.push(evals); + } - // Step 4: Observe combined padding and eval claim - context.challenger().observe_ext_element_slice(&rlc_padding_vec); - context.challenger().observe_ext_element(rlc_eval_claim); - - // Precompute α powers and eq evals - let alpha_powers: Vec = batching_challenge.powers().take(num_claims + 1).collect(); - - let eq_evals = partial_lagrange_blocking(&rlc_point).into_buffer().into_vec(); - let num_original = 1 << log_num_polys; - - // Step 5: Compute expected combined evals from all commitments' query openings - // For each query index q: - // combined_eval[q] = Σ_j α^j * data_rlc_j[q] + α^k * mask_0[q] - let num_queries = - rlc_eval_proof.component_polynomials_query_openings_and_proofs[0].values.sizes()[0]; - let expected_combined_eval: Vec = (0..num_queries) - .into_par_iter() - .map(|q| { - let mut combined = GC::EF::zero(); - for (j, opening_and_proof) in rlc_eval_proof - .component_polynomials_query_openings_and_proofs - .iter() - .enumerate() - { - let opening_tensor = &opening_and_proof.values; - let row_width = opening_tensor.sizes()[1]; - let row = &opening_tensor.as_slice()[q * row_width..(q + 1) * row_width]; - - let eq_sum: GC::EF = eq_evals - .iter() - .zip_eq(row[..num_original].iter()) - .map(|(eq_val, &mle_val)| *eq_val * GC::EF::from(mle_val)) - .sum(); - combined += alpha_powers[j] * eq_sum; - - // Only include the mask from commitment 0 - if j == 0 { - combined += alpha_powers[num_claims] - * GC::EF::from_base_slice(&row[num_original..]); - } - } - combined - }) - .collect(); - - // Step 6: Define virtual oracle with combined padding correction - let (eval_point, _) = point.split_at(point.dimension() - log_num_polys); - let point_dim = eval_point.dimension(); - let compute_batch_evals = |query_indices: &[usize], log_tensor_height: usize| { + // Step 2: Sample shared RLC point (dimension = log_num_polys) + let rlc_point = { + let mut challenger = context.challenger(); + let coords: Vec = + (0..log_num_polys).map(|_| challenger.sample_ext_element()).collect(); + Point::new(coords.into()) + }; + + // Step 3: Sample batching challenge α + let batching_challenge: GC::EF = { + let mut challenger = context.challenger(); + challenger.sample_ext_element() + }; + + // Step 4: Observe combined padding and eval claim + context.challenger().observe_ext_element_slice(&rlc_padding_vec); + context.challenger().observe_ext_element(rlc_eval_claim); + + // Precompute α powers and eq evals + let alpha_powers: Vec = batching_challenge.powers().take(num_claims + 1).collect(); + + let eq_evals = partial_lagrange_blocking(&rlc_point).into_buffer().into_vec(); + let num_original = 1 << log_num_polys; + + // Step 5: Define the virtual oracle. For each query index q, this combines the raw + // per-commitment openings into + // combined_eval[q] = Σ_j α^j * data_rlc_j[q] + α^k * mask_0[q] + // and subtracts the RLC padding correction to produce the value of the virtual + // oracle at the corresponding FRI domain point. + let (eval_point, _) = point.split_at(point.dimension() - log_num_polys); + let point_dim = eval_point.dimension(); + let to_virtual_oracle = + |openings: &[MerkleTreeOpeningAndProof], query_indices: &[usize]| -> Vec { + let log_tensor_height = openings[0].proof.log_tensor_height; let root = GC::EF::two_adic_generator(log_tensor_height); - let corrections = query_indices.iter().map(|&i| { - let x = root.exp_u64(reverse_bits_len(i, log_tensor_height) as u64); - let padding_eval = rlc_padding_vec - .iter() - .rev() - .fold(GC::EF::zero(), |acc, &coeff| acc * x + coeff); - let x_to_unpadded_size = x.exp_u64(1 << point_dim); - padding_eval * x_to_unpadded_size - }); - expected_combined_eval - .into_iter() - .zip(corrections) - .map(|(eval, correction)| eval - correction) - .collect() - }; + query_indices + .par_iter() + .enumerate() + .map(|(q, &query_idx)| { + let mut combined = GC::EF::zero(); + for (j, opening_and_proof) in openings.iter().enumerate() { + let opening_tensor = &opening_and_proof.values; + let row_width = opening_tensor.sizes()[1]; + let row = &opening_tensor.as_slice()[q * row_width..(q + 1) * row_width]; + + let eq_sum: GC::EF = eq_evals + .iter() + .zip_eq(row[..num_original].iter()) + .map(|(eq_val, &mle_val)| *eq_val * GC::EF::from(mle_val)) + .sum(); + combined += alpha_powers[j] * eq_sum; + + // Only include the mask from commitment 0 + if j == 0 { + combined += alpha_powers[num_claims] + * GC::EF::from_base_slice(&row[num_original..]); + } + } - // Step 7: Verify basefold proof with all commitments - let commitments: Vec = commitments_and_claims.iter().map(|(c, _)| *c).collect(); - if let Err(e) = self.verify_trusted_ext_mle_evaluation( - &commitments, - eval_point, - rlc_eval_claim, - &rlc_eval_proof, - compute_batch_evals, - &mut context.challenger(), - ) { - return Err(ZkStackedVerifierError::PcsError(e)); - } + let x = root.exp_u64(reverse_bits_len(query_idx, log_tensor_height) as u64); + let padding_eval = rlc_padding_vec + .iter() + .rev() + .fold(GC::EF::zero(), |acc, &coeff| acc * x + coeff); + let correction = padding_eval * x.exp_u64(1 << point_dim); - // Build constraint data - let claim_datas: Vec<_> = commitments_and_claims - .iter() - .zip(per_claim_evals) - .map(|((_, claim_expr), evals)| ZkStackedPcsClaimData { - point: point.clone(), - orig_eval_index: claim_expr.clone(), - evals, - }) - .collect(); - - let constraint_data = ZkStackedPcsConstraintData { - log_num_cols: log_num_polys, - rlc_point, - batching_challenge, - combined_rlc_eval_claim: rlc_eval_claim, - claims: claim_datas, + combined - correction + }) + .collect() }; - Ok(constraint_data) + // Step 6: Verify basefold proof with all commitments + let commitments: Vec = commitments_and_claims.iter().map(|(c, _)| *c).collect(); + if let Err(e) = verifier.verify_from_prebatched_inputs( + &commitments, + eval_point, + rlc_eval_claim, + &rlc_eval_proof, + to_virtual_oracle, + &mut context.challenger(), + ) { + return Err(ZkStackedVerifierError::PcsError(e)); } + + // Build constraint data + let claim_datas: Vec<_> = commitments_and_claims + .iter() + .zip(per_claim_evals) + .map(|((_, claim_expr), evals)| ZkStackedPcsClaimData { + point: point.clone(), + orig_eval_index: claim_expr.clone(), + evals, + }) + .collect(); + + let constraint_data = ZkStackedPcsConstraintData { + log_num_cols: log_num_polys, + rlc_point, + batching_challenge, + combined_rlc_eval_claim: rlc_eval_claim, + claims: claim_datas, + }; + + Ok(constraint_data) } /// Per-claim constraint data for a single evaluation claim within a batched proof. @@ -319,7 +289,7 @@ impl> ZkProtocolProof // ZkPcsVerifier trait implementation // ============================================================================ -impl>> ZkPcsVerifier for ZkStackedPcsVerifier +impl>> ZkPcsVerifier for StackedPcsVerifier where GC::F: TwoAdicField, { @@ -348,14 +318,14 @@ where .collect::, ZkPcsVerificationError>>()?; // Verify the batched stacked PCS proof - let constraint_data = self - .verify_zk_stacked_pcs_batched( - &commitments_and_claims, - &claim.point, - proof.clone(), - ctx, - ) - .map_err(|e| ZkPcsVerificationError::VerificationFailed(e.to_string()))?; + let constraint_data = verify_zk_stacked_pcs_batched( + self, + &commitments_and_claims, + &claim.point, + proof.clone(), + ctx, + ) + .map_err(|e| ZkPcsVerificationError::VerificationFailed(e.to_string()))?; // Build constraints from the constraint data constraint_data.build_constraints(); diff --git a/slop/crates/veil/src/zk/sumcheck_integration_tests/tests.rs b/slop/crates/veil/src/zk/sumcheck_integration_tests/tests.rs index 3659026f82..138f597780 100644 --- a/slop/crates/veil/src/zk/sumcheck_integration_tests/tests.rs +++ b/slop/crates/veil/src/zk/sumcheck_integration_tests/tests.rs @@ -10,7 +10,6 @@ type MK = Poseidon2KoalaBear16Prover; use crate::zk::stacked_pcs::{ basefold_prover_wrapper::ZkBasefoldProver, initialize_zk_prover_and_verifier, prover::StackedPcsZkProverContext, verifier::StackedPcsZkVerificationContext, - ZkStackedPcsVerifier, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha20Rng; @@ -19,6 +18,7 @@ use slop_jagged::{HadamardProduct, LongMle}; use slop_koala_bear::KoalaBearDegree4Duplex; use slop_matrix::dense::RowMajorMatrix; use slop_multilinear::Mle; +use slop_stacked::StackedPcsVerifier; use super::{ verifier::ZkPartialSumcheckParameters, zk_reduce_sumcheck_to_evaluation, @@ -164,7 +164,7 @@ fn test_zk_sumcheck() { build_all_constraints(sumcheck_data, &mut context); // Verify (no PCS used, but need to specify verifier type) - context.verify::>(None).unwrap(); + context.verify::>(None).unwrap(); eprintln!("Verification time {:?}", now.elapsed()); } }