Skip to main content

zinc_piop/
ideal_check.rs

1//! Ideal-check subprotocol.
2mod batched_ideal_check;
3mod combined_poly_builder;
4mod structs;
5
6pub use structs::*;
7
8#[cfg(feature = "parallel")]
9use rayon::prelude::*;
10
11use crate::projections::{ColumnMajorTrace, RowMajorTrace};
12use batched_ideal_check::*;
13use crypto_primitives::PrimeField;
14use num_traits::ConstZero;
15use std::collections::HashMap;
16use thiserror::Error;
17use zinc_poly::{
18    EvaluationError,
19    univariate::dynamic::over_field::DynamicPolynomialF,
20    utils::{ArithErrors as PolyArithErrors, build_eq_x_r_vec},
21};
22use zinc_transcript::traits::{ConstTranscribable, Transcript};
23use zinc_uair::{
24    Uair,
25    ideal::{Ideal, IdealCheck},
26    ideal_collector::{IdealOrZero, collect_ideals},
27};
28use zinc_utils::{cfg_into_iter, inner_transparent_field::InnerTransparentField};
29
30/// Ideal-check subprotocol.
31pub trait IdealCheckProtocol: Uair {
32    /// Prover for linear-only UAIRs using MLE-first evaluation.
33    ///
34    /// Uses column-indexed trace for efficient MLE evaluation:
35    /// evaluates trace columns at the challenge point first,
36    /// then applies constraints to the evaluated values.
37    ///
38    /// # Parameters
39    /// - `transcript`: the Fiat-Shamir transcript.
40    /// - `trace_matrix`: input trace for the UAIR `U` projected to
41    ///   `DynamicPolynomialF<F>`, column-indexed: `trace_matrix[col][row]`.
42    /// - `projected_scalars`: UAIR scalars projected to
43    ///   `DynamicPolynomialF<F>`.
44    /// - `num_constraints`: number of constraints this UAIR encodes.
45    /// - `num_vars`: number of variables in trace MLEs.
46    /// - `field_cfg`: random field configuration sampled on the previous steps
47    ///   of the overall protocol.
48    #[allow(clippy::type_complexity)]
49    fn prove_linear<F>(
50        transcript: &mut impl Transcript,
51        trace_matrix: &ColumnMajorTrace<F>,
52        projected_scalars: &HashMap<Self::Scalar, DynamicPolynomialF<F>>,
53        num_constraints: usize,
54        num_vars: usize,
55        field_cfg: &F::Config,
56    ) -> Result<(Proof<F>, ProverState<F>), IdealCheckError<F, Self::Ideal>>
57    where
58        F: InnerTransparentField,
59        F::Inner: ConstTranscribable,
60        F::Modulus: ConstTranscribable;
61
62    /// Prover for any UAIR using combined polynomial construction.
63    ///
64    /// Uses row-indexed (transposed) trace for efficient row-by-row
65    /// combined polynomial construction.
66    ///
67    /// # Parameters
68    /// - `transcript`: the Fiat-Shamir transcript.
69    /// - `trace_matrix`: input trace for the UAIR `U` projected to
70    ///   `DynamicPolynomialF<F>`, row-indexed: `trace_matrix[row][col]`.
71    /// - `projected_scalars`: UAIR scalars projected to
72    ///   `DynamicPolynomialF<F>`.
73    /// - `num_constraints`: number of constraints this UAIR encodes.
74    /// - `num_vars`: number of variables in trace MLEs.
75    /// - `field_cfg`: random field configuration sampled on the previous steps
76    ///   of the overall protocol.
77    #[allow(clippy::type_complexity)]
78    fn prove_combined<F>(
79        transcript: &mut impl Transcript,
80        trace_matrix: &RowMajorTrace<F>,
81        projected_scalars: &HashMap<Self::Scalar, DynamicPolynomialF<F>>,
82        num_constraints: usize,
83        num_vars: usize,
84        field_cfg: &F::Config,
85    ) -> Result<(Proof<F>, ProverState<F>), IdealCheckError<F, Self::Ideal>>
86    where
87        F: InnerTransparentField,
88        F::Inner: ConstTranscribable,
89        F::Modulus: ConstTranscribable;
90
91    /// The verifier part of the ideal-check subprotocol.
92    ///
93    /// The verifier samples a random field element
94    /// the same way the prover sampled a random field
95    /// element for projecting coefficients but disregards it
96    /// as the verifier does not need to project anything.
97    /// Then it computes the ideals encoded by the UAIR `U`,
98    /// samples a random evaluation point and receives
99    /// the evaluations of the combined polynomials sent by the prover
100    /// and checks they belong to the corresponding ideals defined
101    /// by the UAIR `U`.
102    ///
103    /// # Parameters
104    /// - `transcript`: the Fiat-Shamir transcript.
105    /// - `proof`: a purported proof produced by the prover.
106    /// - `num_constraints`: the number of constraints the UAIR `U` encodes.
107    /// - `num_vars`: the number of variables the trace row MLEs have.
108    /// - `ideal_over_f_from_ref`: since the UAIR `U` is not aware of the field
109    ///   the ideal check is operating on it defines ideals over the ring
110    ///   `IcTypes::Witness`. `ideal_over_f_from_ref` allows to convert the
111    ///   ideals over `IcTypes::Witness` into ideals over the field
112    ///   `IcTypes::F`. Think of this as a projection for ideals.
113    /// - `field_cfg`: random field configuration sampled on the previous steps
114    ///   of the overall protocol.
115    #[allow(clippy::type_complexity)]
116    fn verify_as_subprotocol<F, IdealOverF, IdealOverFFromRef>(
117        transcript: &mut impl Transcript,
118        proof: Proof<F>,
119        num_constraints: usize,
120        num_vars: usize,
121        ideal_over_f_from_ref: IdealOverFFromRef,
122        field_cfg: &F::Config,
123    ) -> Result<VerifierSubclaim<F>, IdealCheckError<F, IdealOverF>>
124    where
125        F: InnerTransparentField,
126        F::Inner: ConstTranscribable,
127        F::Modulus: ConstTranscribable,
128        IdealOverF: Ideal + IdealCheck<DynamicPolynomialF<F>>,
129        IdealOverFFromRef: Fn(&IdealOrZero<Self::Ideal>) -> IdealOverF;
130}
131
132impl<U> IdealCheckProtocol for U
133where
134    U: Uair,
135{
136    #[allow(clippy::type_complexity)]
137    fn prove_linear<F>(
138        transcript: &mut impl Transcript,
139        trace_matrix: &ColumnMajorTrace<F>,
140        projected_scalars: &HashMap<U::Scalar, DynamicPolynomialF<F>>,
141        num_constraints: usize,
142        num_vars: usize,
143        field_cfg: &F::Config,
144    ) -> Result<(Proof<F>, ProverState<F>), IdealCheckError<F, U::Ideal>>
145    where
146        F: InnerTransparentField,
147        F::Inner: ConstTranscribable,
148        F::Modulus: ConstTranscribable,
149    {
150        let evaluation_point = transcript.get_field_challenges(num_vars, field_cfg);
151
152        // Evaluate combined polynomials using MLE-first approach:
153        // evaluate trace columns at the point, then apply constraints.
154        let combined_mle_values = combined_poly_builder::evaluate_combined_polynomials::<_, U>(
155            trace_matrix,
156            projected_scalars,
157            num_constraints,
158            &evaluation_point,
159            field_cfg,
160        )?;
161
162        let mut transcription_buf: Vec<u8> = vec![0; F::Inner::NUM_BYTES];
163
164        combined_mle_values.iter().for_each(|combined_mle_value| {
165            transcript
166                .absorb_random_field_slice(&combined_mle_value.coeffs, &mut transcription_buf);
167        });
168
169        Ok((
170            Proof {
171                combined_mle_values,
172            },
173            ProverState { evaluation_point },
174        ))
175    }
176
177    #[allow(clippy::type_complexity)]
178    fn prove_combined<F>(
179        transcript: &mut impl Transcript,
180        trace_matrix: &RowMajorTrace<F>,
181        projected_scalars: &HashMap<U::Scalar, DynamicPolynomialF<F>>,
182        num_constraints: usize,
183        num_vars: usize,
184        field_cfg: &F::Config,
185    ) -> Result<(Proof<F>, ProverState<F>), IdealCheckError<F, U::Ideal>>
186    where
187        F: InnerTransparentField,
188        F::Inner: ConstTranscribable,
189        F::Modulus: ConstTranscribable,
190    {
191        // Collect ideals to identify assert_zero constraints whose
192        // combined polynomial is zero by construction (for honest provers).
193        let ideal_collector = collect_ideals::<U>(num_constraints);
194        let is_zero_ideal: Vec<bool> = ideal_collector
195            .ideals
196            .iter()
197            .map(|i| i.is_zero_ideal())
198            .collect();
199
200        let evaluation_point = transcript.get_field_challenges(num_vars, field_cfg);
201
202        // Build combined polynomial MLEs row-by-row and evaluate them.
203        let combined_mles = combined_poly_builder::compute_combined_polynomials::<_, U>(
204            trace_matrix,
205            projected_scalars,
206            num_constraints,
207            field_cfg,
208            &is_zero_ideal,
209        );
210
211        let eq_table = build_eq_x_r_vec(&evaluation_point, field_cfg)?;
212
213        // Evaluate coefficient MLEs at the evaluation point.
214        let combined_mle_values: Vec<DynamicPolynomialF<F>> = cfg_into_iter!(combined_mles)
215            .enumerate()
216            .map(|(i, coeff_mles)| {
217                // Skip zero-ideal constraints: their combined polynomial
218                // is zero for an honest prover.
219                if is_zero_ideal[i] {
220                    return DynamicPolynomialF::ZERO;
221                }
222                let coeffs = coeff_mles
223                    .into_iter()
224                    .map(|coeff_mle| {
225                        zinc_poly::utils::mle_eval_with_eq_table(
226                            &coeff_mle.evaluations,
227                            &eq_table,
228                            field_cfg,
229                        )
230                    })
231                    .collect::<Vec<_>>();
232                DynamicPolynomialF::new_trimmed(coeffs)
233            })
234            .collect::<Vec<_>>();
235
236        let mut transcription_buf: Vec<u8> = vec![0; F::Inner::NUM_BYTES];
237
238        combined_mle_values.iter().for_each(|combined_mle_value| {
239            transcript
240                .absorb_random_field_slice(&combined_mle_value.coeffs, &mut transcription_buf);
241        });
242
243        Ok((
244            Proof {
245                combined_mle_values,
246            },
247            ProverState { evaluation_point },
248        ))
249    }
250
251    fn verify_as_subprotocol<F, IdealOverF, IdealOverFFromRef>(
252        transcript: &mut impl Transcript,
253        proof: Proof<F>,
254        num_constraints: usize,
255        num_vars: usize,
256        ideal_over_f_from_ref: IdealOverFFromRef,
257        field_cfg: &F::Config,
258    ) -> Result<VerifierSubclaim<F>, IdealCheckError<F, IdealOverF>>
259    where
260        F: InnerTransparentField,
261        F::Inner: ConstTranscribable,
262        F::Modulus: ConstTranscribable,
263        IdealOverF: Ideal + IdealCheck<DynamicPolynomialF<F>>,
264        IdealOverFFromRef: Fn(&IdealOrZero<U::Ideal>) -> IdealOverF,
265    {
266        let mut transcription_buf: Vec<u8> = vec![0; F::Inner::NUM_BYTES];
267
268        let combined_mle_values = proof.combined_mle_values;
269
270        let evaluation_point = transcript.get_field_challenges(num_vars, field_cfg);
271
272        for mle_value in &combined_mle_values {
273            transcript.absorb_random_field_slice(&mle_value.coeffs, &mut transcription_buf);
274        }
275
276        let ideal_collector = collect_ideals::<U>(num_constraints);
277
278        // Only check non-trivial ideals. For assert_zero constraints
279        // the ideal is the zero ideal and the combined polynomial
280        // value is zero by construction; the sumcheck that follows
281        // verifies consistency of the claimed evaluations with the
282        // actual trace.
283        let (non_trivial_ideals, non_trivial_values): (Vec<_>, Vec<_>) = ideal_collector
284            .ideals
285            .iter()
286            .zip(combined_mle_values.iter())
287            .filter(|(ideal, _)| !ideal.is_zero_ideal())
288            .map(|(ideal, value)| (ideal_over_f_from_ref(ideal), value.clone()))
289            .unzip();
290
291        batched_ideal_check(&non_trivial_ideals, &non_trivial_values)?;
292
293        Ok(VerifierSubclaim {
294            evaluation_point,
295            values: combined_mle_values,
296        })
297    }
298}
299
300#[derive(Clone, Debug, Error)]
301pub enum IdealCheckError<F: PrimeField, I> {
302    #[error("ideal check prover failed to evaluate an mle: {0}")]
303    MleEvaluationError(#[from] EvaluationError),
304    #[error("mle evaluation ideal check failure: {0}")]
305    IdealCollectorError(#[from] BatchedIdealCheckError<DynamicPolynomialF<F>, I>),
306    #[error("`eq` polynomial construction failure: {0}")]
307    EqPolyConstructionError(#[from] PolyArithErrors),
308}
309
310#[cfg(test)]
311mod tests {
312    use crypto_primitives::{crypto_bigint_int::Int, crypto_bigint_monty::MontyField};
313
314    use rand::rng;
315    use zinc_poly::univariate::{dense::DensePolynomial, dynamic::over_field::DynamicPolynomialF};
316    use zinc_test_uair::{
317        GenerateRandomTrace, TestUairNoMultiplication, TestUairSimpleMultiplication,
318    };
319    use zinc_transcript::Blake3Transcript;
320    use zinc_uair::{
321        constraint_counter::count_constraints,
322        ideal::{DegreeOneIdeal, Ideal, IdealCheck},
323    };
324
325    use crate::test_utils::{
326        LIMBS, run_ideal_check_prover_combined, run_ideal_check_prover_linear, test_config,
327    };
328
329    use super::*;
330
331    // TODO(Ilia): These tests are absolute joke.
332    //             Once we have time we need to create a comprehensive test suite
333    //             akin to the one we have for the PCS or the sumcheck.
334
335    fn test_successful_verification_linear<
336        U,
337        IdealOverF,
338        IdealOverFFromRef,
339        const DEGREE_PLUS_ONE: usize,
340    >(
341        num_vars: usize,
342        ideal_over_f_from_ref: IdealOverFFromRef,
343    ) where
344        U: Uair<Scalar = DensePolynomial<Int<5>, DEGREE_PLUS_ONE>>
345            + GenerateRandomTrace<DEGREE_PLUS_ONE, PolyCoeff = Int<5>, Int = Int<5>>
346            + IdealCheckProtocol,
347        IdealOverF: Ideal + IdealCheck<DynamicPolynomialF<MontyField<LIMBS>>>,
348        IdealOverFFromRef: Fn(&IdealOrZero<U::Ideal>) -> IdealOverF,
349    {
350        let mut rng = rng();
351        let transcript = Blake3Transcript::new();
352
353        let (proof, prover_state, ..) = run_ideal_check_prover_linear::<U, DEGREE_PLUS_ONE>(
354            num_vars,
355            &U::generate_random_trace(num_vars, &mut rng),
356            &mut transcript.clone(),
357        );
358
359        let num_constraints = count_constraints::<U>();
360
361        let verifier_result = U::verify_as_subprotocol(
362            &mut transcript.clone(),
363            proof,
364            num_constraints,
365            num_vars,
366            ideal_over_f_from_ref,
367            &test_config(),
368        )
369        .expect("Verification failed");
370
371        assert_eq!(
372            prover_state.evaluation_point,
373            verifier_result.evaluation_point
374        );
375    }
376
377    fn test_successful_verification_combined<
378        U,
379        IdealOverF,
380        IdealOverFFromRef,
381        const DEGREE_PLUS_ONE: usize,
382    >(
383        num_vars: usize,
384        ideal_over_f_from_ref: IdealOverFFromRef,
385    ) where
386        U: Uair<Scalar = DensePolynomial<Int<5>, DEGREE_PLUS_ONE>>
387            + GenerateRandomTrace<DEGREE_PLUS_ONE, PolyCoeff = Int<5>, Int = Int<5>>
388            + IdealCheckProtocol,
389        IdealOverF: Ideal + IdealCheck<DynamicPolynomialF<MontyField<LIMBS>>>,
390        IdealOverFFromRef: Fn(&IdealOrZero<U::Ideal>) -> IdealOverF,
391    {
392        let mut rng = rng();
393        let transcript = Blake3Transcript::new();
394
395        let (proof, prover_state, ..) = run_ideal_check_prover_combined::<U, DEGREE_PLUS_ONE>(
396            num_vars,
397            &U::generate_random_trace(num_vars, &mut rng),
398            &mut transcript.clone(),
399        );
400
401        let num_constraints = count_constraints::<U>();
402
403        let verifier_result = U::verify_as_subprotocol(
404            &mut transcript.clone(),
405            proof,
406            num_constraints,
407            num_vars,
408            ideal_over_f_from_ref,
409            &test_config(),
410        )
411        .expect("Verification failed");
412
413        assert_eq!(
414            prover_state.evaluation_point,
415            verifier_result.evaluation_point
416        );
417    }
418
419    #[test]
420    fn test_successful_verification() {
421        let field_cfg = test_config();
422
423        let num_vars = 2;
424
425        // Linear UAIR - test both approaches
426        test_successful_verification_linear::<TestUairNoMultiplication<Int<5>>, _, _, 32>(
427            num_vars,
428            |ideal_over_ring| ideal_over_ring.map(|i| DegreeOneIdeal::from_with_cfg(i, &field_cfg)),
429        );
430        test_successful_verification_combined::<TestUairNoMultiplication<Int<5>>, _, _, 32>(
431            num_vars,
432            |ideal_over_ring| ideal_over_ring.map(|i| DegreeOneIdeal::from_with_cfg(i, &field_cfg)),
433        );
434
435        // Non-linear UAIR - only combined approach works
436        test_successful_verification_combined::<TestUairSimpleMultiplication<Int<5>>, _, _, 32>(
437            num_vars,
438            |_ideal_over_ring| IdealOrZero::<DegreeOneIdeal<_>>::zero(),
439        );
440    }
441}