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