1mod batched_ideal_check;
3mod combined_poly_builder;
4mod structs;
5
6pub use batched_ideal_check::BatchedIdealCheckError;
7pub use structs::*;
8
9use crate::projections::{
10 ColumnMajorTrace, ProjectedScalars, RowMajorTrace, column_major_to_row_major,
11};
12use batched_ideal_check::*;
13use crypto_primitives::{BaseFieldConfig, ProjectPrimitiveIntegersWithConfig};
14use std::marker::PhantomData;
15use thiserror::Error;
16use zinc_poly::{
17 EvaluationError,
18 univariate::dynamic::{DynamicPolynomial, DynamicPolynomialConfig, HasDynamicPolynomialConfig},
19 utils::ArithErrors as PolyArithErrors,
20};
21use zinc_transcript::traits::{ConstTranscribable, Transcript};
22use zinc_uair::{
23 Uair,
24 degree_counter::count_constraint_degrees,
25 ideal::{Ideal, IdealCheck},
26 ideal_collector::{IdealOrZero, collect_ideals},
27};
28use zinc_utils::sub;
29
30#[derive(Default, Clone, Copy)]
41pub struct IdealCheckProtocol<U: Uair>(PhantomData<U>);
42
43impl<U: Uair> IdealCheckProtocol<U> {
44 #[allow(clippy::type_complexity, clippy::arithmetic_side_effects)]
75 pub fn prove_mle_first<C, const DEGREE_PLUS_ONE: usize>(
76 transcript: &mut impl Transcript,
77 trace_matrix: &ColumnMajorTrace<C::Element>,
78 projected_scalars: &ProjectedScalars<U::Scalar, DynamicPolynomial<C::Element>>,
79 family_idx: usize,
80 num_constraints: usize,
81 evaluation_point: &[C::Element],
82 field_cfg: &C,
83 ) -> Result<Proof<C::Element>, IdealCheckError<C::Element>>
84 where
85 C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig,
86 C::Integer: ConstTranscribable,
87 {
88 let ideal_collector = collect_ideals::<U>(num_constraints);
95 let degrees = count_constraint_degrees::<U>();
96
97 let mut has_linear_nonzero: bool = false;
98 let mut nonlinear_zero: Vec<usize> = Vec::new();
99 let mut nonlinear_nonzero: Vec<usize> = Vec::new();
100
101 macro_rules! categorize_ideals {
102 ($ideals:expr, $degrees:expr) => {
103 for (idx, ideal) in $ideals.iter().enumerate() {
104 if $degrees[idx] <= 1 {
105 has_linear_nonzero |= !ideal.is_zero_ideal();
106 } else if ideal.is_zero_ideal() {
107 nonlinear_zero.push(idx);
108 } else {
109 nonlinear_nonzero.push(idx);
110 }
111 }
112 };
113 }
114
115 if family_idx == 0 {
116 categorize_ideals!(ideal_collector.ideals, degrees.q_degrees);
117 } else {
118 let prime_idx = family_idx - 1;
119 categorize_ideals!(
120 ideal_collector.fq_ideals[prime_idx],
121 degrees.fq_degrees[prime_idx]
122 );
123 }
124
125 let mut combined_mle_values: Vec<DynamicPolynomial<C::Element>> = if has_linear_nonzero {
130 let mut res =
131 combined_poly_builder::evaluate_combined_polynomials::<_, U, DEGREE_PLUS_ONE>(
132 trace_matrix,
133 projected_scalars,
134 family_idx,
135 num_constraints,
136 evaluation_point,
137 field_cfg,
138 )?;
139
140 for &i in &nonlinear_zero {
143 res[i] = DynamicPolynomial::ZERO;
144 }
145
146 res
147 } else {
148 vec![DynamicPolynomial::ZERO; num_constraints]
149 };
150
151 if !nonlinear_nonzero.is_empty() {
152 let row_major = column_major_to_row_major(trace_matrix);
156 let values = combined_poly_builder::evaluate_for_constraints::<_, U, DEGREE_PLUS_ONE>(
157 &row_major,
158 projected_scalars,
159 family_idx,
160 num_constraints,
161 field_cfg,
162 &nonlinear_nonzero,
163 evaluation_point,
164 )?;
165 for (&i, v) in nonlinear_nonzero.iter().zip(values) {
166 combined_mle_values[i] = v;
167 }
168 }
169
170 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
171 combined_mle_values.iter().for_each(|cv| {
172 transcript.absorb_field_element_slice(field_cfg, &cv.coeffs, &mut transcription_buf);
173 });
174
175 Ok(Proof {
176 combined_mle_values,
177 })
178 }
179
180 #[allow(clippy::type_complexity)]
202 pub fn prove_combined<C, const DEGREE_PLUS_ONE: usize>(
203 transcript: &mut impl Transcript,
204 trace_matrix: &RowMajorTrace<C::Element>,
205 projected_scalars: &ProjectedScalars<U::Scalar, DynamicPolynomial<C::Element>>,
206 family_idx: usize,
207 num_constraints: usize,
208 evaluation_point: &[C::Element],
209 field_cfg: &C,
210 ) -> Result<Proof<C::Element>, IdealCheckError<C::Element>>
211 where
212 C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig,
213 C::Integer: ConstTranscribable,
214 {
215 macro_rules! get_non_zero_indices {
218 ($ideals:expr) => {
219 $ideals
220 .iter()
221 .enumerate()
222 .filter(|(_, i)| !i.is_zero_ideal())
223 .map(|(idx, _)| idx)
224 .collect::<Vec<usize>>()
225 };
226 }
227 let ideal_collector = collect_ideals::<U>(num_constraints);
228 let non_zero_indices: Vec<usize> = if family_idx == 0 {
229 get_non_zero_indices!(ideal_collector.ideals)
230 } else {
231 let prime_idx = sub!(family_idx, 1);
232 get_non_zero_indices!(ideal_collector.fq_ideals.get(prime_idx).unwrap_or(&vec![]))
233 };
234
235 let mut combined_mle_values = vec![DynamicPolynomial::ZERO; num_constraints];
236 if !non_zero_indices.is_empty() {
237 let computed = combined_poly_builder::evaluate_for_constraints::<_, U, DEGREE_PLUS_ONE>(
238 trace_matrix,
239 projected_scalars,
240 family_idx,
241 num_constraints,
242 field_cfg,
243 &non_zero_indices,
244 evaluation_point,
245 )?;
246
247 for (&ci, val) in non_zero_indices.iter().zip(computed) {
248 combined_mle_values[ci] = val;
249 }
250 };
251
252 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
253
254 combined_mle_values.iter().for_each(|v| {
255 transcript.absorb_field_element_slice(field_cfg, &v.coeffs, &mut transcription_buf);
256 });
257
258 Ok(Proof {
259 combined_mle_values,
260 })
261 }
262
263 #[allow(clippy::type_complexity, clippy::too_many_arguments)]
286 pub fn verify_as_subprotocol<'cfg, C, IdealOverF, IdealOverFFromRef, IdealOverFFromFqRef>(
287 transcript: &mut impl Transcript,
288 proof: Proof<C::Element>,
289 family_idx: usize,
290 num_constraints: usize,
291 evaluation_point: &[C::Element],
292 ideal_over_f_from_ref: IdealOverFFromRef,
293 ideal_over_f_from_fq_ref: IdealOverFFromFqRef,
294 field_cfg: &'cfg C,
295 ) -> Result<VerifierSubclaim<C::Element>, IdealCheckError<C::Element>>
296 where
297 C: BaseFieldConfig,
298 C::Integer: ConstTranscribable,
299 IdealOverF: Ideal + IdealCheck<DynamicPolynomialConfig<'cfg, C>>,
300 IdealOverFFromRef: Fn(&IdealOrZero<U::Ideal>) -> IdealOverF,
301 IdealOverFFromFqRef: Fn(&IdealOrZero<U::FqIdeal>) -> IdealOverF,
302 {
303 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
304
305 let combined_mle_values = proof.combined_mle_values;
306
307 for mle_value in &combined_mle_values {
308 transcript.absorb_field_element_slice(
309 field_cfg,
310 &mle_value.coeffs,
311 &mut transcription_buf,
312 );
313 }
314
315 let ideal_collector = collect_ideals::<U>(num_constraints);
316
317 macro_rules! collect_non_trivial {
318 ($ideals:expr, $ideal_from_ref:ident) => {
319 $ideals
320 .iter()
321 .zip(combined_mle_values.iter())
322 .filter(|(ideal, _)| !ideal.is_zero_ideal())
323 .map(|(ideal, value)| ($ideal_from_ref(ideal), value.clone()))
324 .unzip()
325 };
326 }
327
328 let (non_trivial_ideals, non_trivial_values): (Vec<_>, Vec<_>) = if family_idx == 0 {
334 collect_non_trivial!(ideal_collector.ideals, ideal_over_f_from_ref)
335 } else {
336 let prime_idx = sub!(family_idx, 1);
337 collect_non_trivial!(
338 ideal_collector.fq_ideals.get(prime_idx).unwrap_or(&vec![]),
339 ideal_over_f_from_fq_ref
340 )
341 };
342
343 batched_ideal_check(
344 &field_cfg.dyn_poly_cfg(),
345 &non_trivial_ideals,
346 &non_trivial_values,
347 )?;
348
349 Ok(VerifierSubclaim {
350 evaluation_point: evaluation_point.to_vec(),
351 values: combined_mle_values,
352 })
353 }
354}
355
356#[derive(Clone, Debug, Error)]
357pub enum IdealCheckError<F> {
358 #[error("ideal check prover failed to evaluate an mle: {0}")]
359 MleEvaluationError(#[from] EvaluationError),
360 #[error("mle evaluation ideal check failure: {0}")]
361 IdealCollectorError(#[from] BatchedIdealCheckError<DynamicPolynomial<F>>),
362 #[error("`eq` polynomial construction failure: {0}")]
363 EqPolyConstructionError(#[from] PolyArithErrors),
364}
365
366#[allow(clippy::arithmetic_side_effects, clippy::redundant_clone)]
367#[cfg(test)]
368mod tests {
369 use crate::test_utils::{
370 LIMBS, run_ideal_check_prover_combined, run_ideal_check_prover_linear, test_config,
371 };
372 use crypto_primitives::{
373 crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint,
374 };
375 use rand::rng;
376 use zinc_poly::univariate::{dense::DensePolynomial, dynamic::DynamicPolynomialConfig};
377 use zinc_test_uair::{
378 GenerateRandomTrace, TestUairBitOpsMixedSplice, TestUairNoMultiplication,
379 TestUairSimpleMultiplication,
380 };
381 use zinc_transcript::Blake3Transcript;
382 use zinc_uair::{
383 constraint_counter::count_constraints,
384 ideal::{DegreeOneIdeal, Ideal, IdealCheck},
385 };
386
387 use super::*;
388
389 fn do_test<
394 U,
395 IdealOverF,
396 IdealOverFFromRef,
397 IdealOverFFromFqRef,
398 const DEGREE_PLUS_ONE: usize,
399 >(
400 num_vars: usize,
401 prime_idx: Option<usize>,
402 ideal_over_f_from_ref: IdealOverFFromRef,
403 ideal_over_f_from_fq_ref: IdealOverFFromFqRef,
404 ) where
405 U: Uair<Scalar = DensePolynomial<Int<5>, DEGREE_PLUS_ONE>>
406 + GenerateRandomTrace<DEGREE_PLUS_ONE, PolyCoeff = Int<5>, Int = Int<5>>,
407 IdealOverF: Ideal + for<'a> IdealCheck<DynamicPolynomialConfig<'a, MontyField<LIMBS>>>,
408 IdealOverFFromRef: Fn(&IdealOrZero<U::Ideal>) -> IdealOverF + Copy,
409 IdealOverFFromFqRef: Fn(&IdealOrZero<U::FqIdeal>) -> IdealOverF + Copy,
410 {
411 let mut rng = rng();
412 let field_cfg = test_config();
413
414 let num_constraints = count_constraints::<U>();
415 let num_constraints = prime_idx
416 .map(|i| num_constraints.for_prime(i))
417 .unwrap_or(num_constraints.q);
418 let family_idx = prime_idx.map_or(0, |i| i + 1);
419
420 {
422 let transcript = Blake3Transcript::new();
423 let (proof, evaluation_point, ..) = run_ideal_check_prover_combined::<U, DEGREE_PLUS_ONE>(
424 num_vars,
425 &U::generate_random_trace(num_vars, &mut rng),
426 prime_idx,
427 &mut transcript.clone(),
428 );
429
430 let verifier_result = IdealCheckProtocol::<U>::verify_as_subprotocol(
433 &mut transcript.clone(),
434 proof,
435 family_idx,
436 num_constraints,
437 &evaluation_point,
438 ideal_over_f_from_ref,
439 ideal_over_f_from_fq_ref,
440 &field_cfg,
441 )
442 .expect("Verification failed");
443
444 assert_eq!(evaluation_point, verifier_result.evaluation_point);
445 }
446
447 {
449 let transcript = Blake3Transcript::new();
450 let (proof, evaluation_point, ..) = run_ideal_check_prover_linear::<U, DEGREE_PLUS_ONE>(
451 num_vars,
452 &U::generate_random_trace(num_vars, &mut rng),
453 prime_idx,
454 &mut transcript.clone(),
455 );
456
457 let verifier_result = IdealCheckProtocol::<U>::verify_as_subprotocol(
458 &mut transcript.clone(),
459 proof,
460 family_idx,
461 num_constraints,
462 &evaluation_point,
463 ideal_over_f_from_ref,
464 ideal_over_f_from_fq_ref,
465 &field_cfg,
466 )
467 .expect("Verification failed");
468
469 assert_eq!(evaluation_point, verifier_result.evaluation_point);
470 }
471 }
472
473 #[test]
474 fn test_successful_verification() {
475 let field_cfg = test_config();
476
477 let num_vars = 2;
478
479 do_test::<TestUairNoMultiplication<Int<5>, Uint<LIMBS>>, _, _, _, 32>(
481 num_vars,
482 None,
483 |ideal_over_ring| ideal_over_ring.map(|i| DegreeOneIdeal::project(&field_cfg, i)),
484 |_| unreachable!("F_q[X] should not be used for this UAIR"),
485 );
486
487 do_test::<TestUairSimpleMultiplication<Int<5>, Uint<LIMBS>>, _, _, _, 32>(
489 num_vars,
490 None,
491 |_ideal_over_ring| IdealOrZero::<DegreeOneIdeal<_>>::zero(),
492 |_| unreachable!("F_q[X] should not be used for this UAIR"),
493 );
494
495 do_test::<TestUairBitOpsMixedSplice<Int<5>, Uint<LIMBS>>, _, _, _, 32>(
497 num_vars,
498 None,
499 |ideal_over_ring| ideal_over_ring.map(|i| DegreeOneIdeal::project(&field_cfg, i)),
500 |_| unreachable!("F_q[X] should not be used for this UAIR"),
501 );
502 }
503}