1mod folder;
4mod structs;
5
6pub use structs::*;
7
8use crate::{
9 CombFn,
10 combined_poly_resolver::{
11 folder::ConstraintFolder,
12 structs::{Proof as CprProof, ProverState as CprProverState},
13 },
14 ideal_check,
15 projections::ProjectedScalars,
16 sumcheck::{
17 SumCheckError, multi_degree::MultiDegreeSumcheckGroup,
18 prover::ProverState as SumcheckProverState,
19 },
20};
21use crypto_primitives::{
22 BaseFieldConfig, ProjectPrimitiveIntegersWithConfig, SetConfig, SetElement,
23};
24use itertools::Itertools;
25#[cfg(feature = "parallel")]
26use rayon::prelude::*;
27use std::{marker::PhantomData, slice};
28use thiserror::Error;
29use zinc_poly::{
30 EvaluationError,
31 mle::DenseMultilinearExtension,
32 univariate::dynamic::{DynamicPolynomial, HasDynamicPolynomialConfig},
33 utils::{ArithErrors, build_eq_x_r, eq_eval},
34};
35use zinc_transcript::traits::{ConstTranscribable, Transcript};
36use zinc_uair::{TraceRow, Uair, ideal::ImpossibleIdeal};
37use zinc_utils::{
38 UNCHECKED, add, cfg_iter,
39 from_ref::FromRef,
40 inner_product::{InnerProduct, NativeInnerProduct},
41 powers,
42};
43
44pub struct CombinedPolyResolver<C: SetConfig>(PhantomData<C>);
63
64impl<C> CombinedPolyResolver<C>
65where
66 C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
67 C::Integer: ConstTranscribable,
68{
69 #[allow(clippy::arithmetic_side_effects, clippy::too_many_arguments)]
106 pub fn prepare_sumcheck_group<U>(
107 trace_matrix: Vec<DenseMultilinearExtension<C::Element>>,
108 bit_op_down_mles: Vec<DenseMultilinearExtension<C::Element>>,
109 evaluation_point: &[C::Element],
110 projected_scalars: &ProjectedScalars<U::Scalar, C::Element>,
111 family_idx: usize,
112 num_constraints: usize,
113 num_vars: usize,
114 max_degree: usize,
115 folding_challenge: &C::Element,
116 field_cfg: &C,
117 ) -> Result<
118 (MultiDegreeSumcheckGroup<C>, CprProverAncillary),
119 CombinedPolyResolverError<C::Element>,
120 >
121 where
122 U::Scalar: 'static,
123 U: Uair,
124 {
125 debug_assert_ne!(
126 num_vars, 1,
127 "The protocol is not needed when the number of variables is 1 :)"
128 );
129
130 let zero = field_cfg.zero();
131 let one = field_cfg.one();
132
133 let uair_sig = U::signature();
139
140 assert_eq!(
141 bit_op_down_mles.len(),
142 uair_sig.bit_op_specs().len(),
143 "bit_op_down_mles count must match UairSignature::bit_op_specs().len()",
144 );
145
146 let n = 1usize << num_vars;
147 let shift_mles: Vec<DenseMultilinearExtension<C::Element>> = cfg_iter!(uair_sig.shifts())
148 .map(|spec| {
149 let mut evals = trace_matrix[spec.source_col()][spec.shift_amount()..].to_vec();
150 evals.resize(n, zero.clone());
151 DenseMultilinearExtension {
152 evaluations: evals,
153 num_vars,
154 }
155 })
156 .collect();
157
158 let bit_op_down_offset = uair_sig.bit_op_down_offset();
169 let num_shift_down = shift_mles.len();
170 let num_bit_op_specs = bit_op_down_mles.len();
171 let mut down: Vec<DenseMultilinearExtension<C::Element>> =
172 Vec::with_capacity(num_shift_down + num_bit_op_specs);
173 let mut shift_iter = shift_mles.into_iter();
174 for _ in 0..bit_op_down_offset {
175 down.push(shift_iter.next().expect("offset within shift_mles range"));
176 }
177 down.extend(bit_op_down_mles);
178 down.extend(shift_iter);
179
180 let eq_r = build_eq_x_r(field_cfg, evaluation_point)?;
181 let last_row_selector = DenseMultilinearExtension {
185 num_vars,
186 evaluations: {
187 let mut evals = vec![zero; 1 << num_vars];
188 evals[(1 << num_vars) - 1] = one.clone();
189 evals
190 },
191 };
192
193 let folding_challenge_powers: Vec<C::Element> =
198 powers(field_cfg, folding_challenge, num_constraints);
199
200 let num_cols = trace_matrix.len();
201 let num_down_cols = down.len();
202 let mles: Vec<DenseMultilinearExtension<C::Element>> = {
203 let mut mles = Vec::with_capacity(2 + num_cols + num_down_cols);
204
205 mles.push(last_row_selector);
206 mles.push(eq_r);
207
208 mles.extend(trace_matrix);
209 mles.extend(down);
210
211 mles
212 };
213
214 let projected_scalars = projected_scalars.clone();
215 let comb_cfg = field_cfg.clone();
216 let comb_fn: CombFn<C::Element> = Box::new(move |mle_values: &[C::Element]| {
217 let uair_sig = U::signature();
218 let up_layout = uair_sig.total_cols().as_column_layout();
219 let down_layout = uair_sig.down_cols().as_column_layout();
220
221 let selector = &mle_values[0];
222 let eq_r = &mle_values[1];
223
224 let mut folder =
225 ConstraintFolder::new(family_idx, &folding_challenge_powers, &comb_cfg);
226
227 let project = |scalar: &U::Scalar| {
228 projected_scalars
229 .get(scalar)
230 .expect("all scalars should have been projected at this point")
231 };
232
233 U::constrain_general(
234 &mut folder,
235 &comb_cfg,
236 TraceRow::from_slice_with_layout(&mle_values[2..num_cols + 2], up_layout),
237 TraceRow::from_slice_with_layout(&mle_values[num_cols + 2..], down_layout),
238 project,
239 |x, y| Some(comb_cfg.mul(x, &project(y))),
240 ImpossibleIdeal::from_ref,
241 ImpossibleIdeal::from_ref,
242 );
243
244 comb_cfg.mul(
245 &comb_cfg.mul(&folder.folded_constraints, &comb_cfg.sub(&one, selector)),
246 eq_r,
247 )
248 });
249
250 Ok((
251 MultiDegreeSumcheckGroup::new(max_degree + 2, mles, comb_fn),
252 CprProverAncillary {
253 num_cols,
254 num_down_cols: num_shift_down,
255 num_vars,
256 },
257 ))
258 }
259
260 #[allow(clippy::arithmetic_side_effects, clippy::type_complexity)]
270 pub fn finalize_prover<U>(
271 transcript: &mut impl Transcript,
272 sumcheck_prover_state: SumcheckProverState<C>,
273 ancillary: CprProverAncillary,
274 field_cfg: &C,
275 ) -> Result<
276 (CprProof<C::Element>, CprProverState<C::Element>),
277 CombinedPolyResolverError<C::Element>,
278 >
279 where
280 U: Uair,
281 {
282 debug_assert!(
288 sumcheck_prover_state
289 .mles
290 .iter()
291 .all(|mle| mle.num_vars == 1)
292 );
293
294 let last_sumcheck_challenge = sumcheck_prover_state
295 .randomness
296 .last()
297 .expect("sumcheck could not have had 0 rounds");
298
299 let mut mles = sumcheck_prover_state.mles;
300 let evals: Vec<C::Element> = mles
301 .drain(2..)
302 .map(|mle| mle.evaluate(field_cfg, slice::from_ref(last_sumcheck_challenge)))
303 .try_collect()?;
304
305 let uair_sig = U::signature();
306 let num_bit_op_specs = uair_sig.bit_op_specs().len();
307 let bit_op_down_offset = uair_sig.bit_op_down_offset();
308
309 debug_assert_eq!(
310 evals.len(),
311 ancillary.num_cols + ancillary.num_down_cols + num_bit_op_specs,
312 );
313
314 let up_end = ancillary.num_cols;
321 let bit_op_start = up_end + bit_op_down_offset;
322 let bit_op_end = bit_op_start + num_bit_op_specs;
323
324 let up_evals = evals[..up_end].to_vec();
325 let bit_op_evals = evals[bit_op_start..bit_op_end].to_vec();
326 let mut down_evals = Vec::with_capacity(ancillary.num_down_cols);
327 down_evals.extend_from_slice(&evals[up_end..bit_op_start]);
328 down_evals.extend_from_slice(&evals[bit_op_end..]);
329
330 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
331 transcript.absorb_field_element_slice(field_cfg, &up_evals, &mut transcription_buf);
332 transcript.absorb_field_element_slice(field_cfg, &down_evals, &mut transcription_buf);
333 transcript.absorb_field_element_slice(field_cfg, &bit_op_evals, &mut transcription_buf);
334 Ok((
335 CprProof {
336 up_evals,
337 down_evals,
338 bit_op_evals,
339 },
340 CprProverState {
341 evaluation_point: sumcheck_prover_state.randomness,
342 },
343 ))
344 }
345
346 #[allow(clippy::too_many_arguments)]
365 pub fn prepare_verifier<U>(
366 proof: &CprProof<C::Element>,
367 claimed_sum: C::Element,
368 ic_check_subclaim: &ideal_check::VerifierSubclaim<C::Element>,
369 num_constraints: usize,
370 num_vars: usize,
371 projecting_element: &C::Element,
372 folding_challenge: &C::Element,
373 field_cfg: &C,
374 ) -> Result<CprVerifierAncillary<C::Element>, CombinedPolyResolverError<C::Element>>
375 where
376 U: Uair,
377 {
378 let uair_sig = U::signature();
379 proof.validate_evaluation_sizes(
380 uair_sig.total_cols().cols(),
381 uair_sig.shifts().len(),
382 uair_sig.bit_op_specs().len(),
383 )?;
384
385 let zero = field_cfg.zero();
386 let poly_cfg = field_cfg.dyn_poly_cfg();
387
388 let projection_powers: Vec<C::Element> = {
390 let max_coeffs_len = ic_check_subclaim
391 .values
392 .iter()
393 .map(|poly| poly_cfg.degree(poly).map_or(0, |d| add!(d, 1)))
394 .max()
395 .unwrap_or(0)
396 .max(1);
397 powers(field_cfg, projecting_element, max_coeffs_len)
398 };
399
400 let folding_challenge_powers: Vec<C::Element> =
403 powers(field_cfg, folding_challenge, num_constraints);
404
405 let expected_sum = ic_check_subclaim
408 .values
409 .iter()
410 .zip(&folding_challenge_powers)
411 .map(|(claimed_value, random_coeff)| {
412 let deg = poly_cfg.degree(claimed_value).map_or(0, |d| add!(d, 1));
413 let value = NativeInnerProduct::inner_product::<UNCHECKED>(
414 field_cfg,
415 &claimed_value.coeffs[..deg],
416 &projection_powers[..deg],
417 zero.clone(),
418 )
419 .expect("inner product cannot fail here");
420 field_cfg.mul(&value, random_coeff)
421 })
422 .fold(zero.clone(), |acc, term| field_cfg.add(&acc, &term));
423
424 if claimed_sum != expected_sum {
425 return Err(CombinedPolyResolverError::WrongSumcheckSum {
426 got: claimed_sum,
427 expected: expected_sum,
428 });
429 }
430
431 Ok(CprVerifierAncillary {
432 folding_challenge_powers,
433 ic_evaluation_point: ic_check_subclaim.evaluation_point.clone(),
434 num_vars,
435 })
436 }
437
438 #[allow(clippy::too_many_arguments)]
457 pub fn finalize_verifier<U>(
458 transcript: &mut impl Transcript,
459 proof: CprProof<C::Element>,
460 shared_point: Vec<C::Element>,
461 expected_evaluation: C::Element,
462 ancillary: CprVerifierAncillary<C::Element>,
463 projected_scalars: &ProjectedScalars<U::Scalar, C::Element>,
464 family_idx: usize,
465 field_cfg: &C,
466 ) -> Result<VerifierSubclaim<C::Element>, CombinedPolyResolverError<C::Element>>
467 where
468 U: Uair,
469 {
470 let uair_sig = U::signature();
471 let down_layout = uair_sig.down_cols().as_column_layout();
472 let one = field_cfg.one();
473
474 let eq_r_value = eq_eval(field_cfg, &shared_point, &ancillary.ic_evaluation_point)?;
475 let selector_value = eq_eval(
476 field_cfg,
477 &shared_point,
478 &vec![one.clone(); ancillary.num_vars],
479 )?;
480
481 let mut folder =
482 ConstraintFolder::new(family_idx, &ancillary.folding_challenge_powers, field_cfg);
483
484 let project = |scalar: &U::Scalar| {
485 projected_scalars
486 .get(scalar)
487 .expect("all scalars should have been projected at this point")
488 };
489
490 let bit_op_down_offset = uair_sig.bit_op_down_offset();
495 let mut full_down_evals =
496 Vec::with_capacity(add!(proof.down_evals.len(), proof.bit_op_evals.len()));
497 full_down_evals.extend_from_slice(&proof.down_evals[..bit_op_down_offset]);
498 full_down_evals.extend_from_slice(&proof.bit_op_evals);
499 full_down_evals.extend_from_slice(&proof.down_evals[bit_op_down_offset..]);
500
501 U::constrain_general(
502 &mut folder,
503 field_cfg,
504 TraceRow::from_slice_with_layout(
505 &proof.up_evals,
506 uair_sig.total_cols().as_column_layout(),
507 ),
508 TraceRow::from_slice_with_layout(&full_down_evals, down_layout),
509 project,
510 |x, y| Some(field_cfg.mul(x, &project(y))),
511 ImpossibleIdeal::from_ref,
512 ImpossibleIdeal::from_ref,
513 );
514
515 let expected_claim_value = field_cfg.mul(
517 &field_cfg.mul(&eq_r_value, &field_cfg.sub(&one, &selector_value)),
518 &folder.folded_constraints,
519 );
520
521 if expected_claim_value != expected_evaluation {
522 return Err(CombinedPolyResolverError::ClaimValueDoesNotMatch {
523 got: expected_evaluation,
524 expected: expected_claim_value,
525 });
526 }
527
528 let mut transcription_buf: Vec<u8> = vec![0; <C::Integer as ConstTranscribable>::NUM_BYTES];
529 transcript.absorb_field_element_slice(field_cfg, &proof.up_evals, &mut transcription_buf);
530 transcript.absorb_field_element_slice(field_cfg, &proof.down_evals, &mut transcription_buf);
531 transcript.absorb_field_element_slice(
532 field_cfg,
533 &proof.bit_op_evals,
534 &mut transcription_buf,
535 );
536
537 Ok(VerifierSubclaim {
538 up_evals: proof.up_evals,
539 down_evals: proof.down_evals,
540 bit_op_evals: proof.bit_op_evals,
541 evaluation_point: shared_point,
542 })
543 }
544}
545
546#[derive(Debug, Error)]
547pub enum CombinedPolyResolverError<F: SetElement> {
548 #[error("failed to build eq_r: {0}")]
549 EqrError(ArithErrors),
550 #[error("error evaluating MLE: {0}")]
551 MleEvaluationError(EvaluationError),
552 #[error("error projecting polynomial {0:?} by point {1:?}: {2}")]
553 ProjectionError(DynamicPolynomial<F>, F, EvaluationError),
554 #[error("wrong trace columns evaluations number: got {got}, expected {expected}")]
555 WrongUpEvalsNumber { got: usize, expected: usize },
556 #[error("wrong shifted trace columns evaluations number: got {got}, expected {expected}")]
557 WrongDownEvalsNumber { got: usize, expected: usize },
558 #[error("wrong bit-op virtual columns evaluations number: got {got}, expected {expected}")]
559 WrongBitOpEvalsNumber { got: usize, expected: usize },
560 #[error("sumcheck verification failed: {0}")]
561 SumcheckError(SumCheckError<F>),
562 #[error("wrong sumcheck claimed sum: received {got:?}, expected {expected:?}")]
563 WrongSumcheckSum { got: F, expected: F },
564 #[error("resulting claim value does not match: received {got:?}, expected {expected:?}")]
565 ClaimValueDoesNotMatch { got: F, expected: F },
566}
567
568impl<F: SetElement> From<EvaluationError> for CombinedPolyResolverError<F> {
569 fn from(eval_error: EvaluationError) -> Self {
570 Self::MleEvaluationError(eval_error)
571 }
572}
573
574impl<F: SetElement> From<ArithErrors> for CombinedPolyResolverError<F> {
575 fn from(arith_error: ArithErrors) -> Self {
576 Self::EqrError(arith_error)
577 }
578}
579
580impl<F: SetElement> From<SumCheckError<F>> for CombinedPolyResolverError<F> {
581 fn from(sumcheck_error: SumCheckError<F>) -> Self {
582 Self::SumcheckError(sumcheck_error)
583 }
584}
585
586#[cfg(test)]
587#[allow(
588 clippy::arithmetic_side_effects,
589 clippy::cast_possible_truncation,
590 clippy::cast_precision_loss,
591 clippy::cast_sign_loss,
592 clippy::clone_on_copy,
593 clippy::redundant_clone
594)]
595mod tests {
596 use super::*;
597 use crate::{
598 ideal_check::IdealCheckProtocol,
599 projections::{ProjectedTrace, evaluate_trace_to_column_mles, project_scalars_to_field},
600 sumcheck::multi_degree::MultiDegreeSumcheck,
601 test_utils::{LIMBS, run_ideal_check_prover_combined, test_config},
602 };
603 use crypto_primitives::{
604 crypto_bigint_int::Int,
605 crypto_bigint_monty::{MontyField, MontyFieldElement},
606 crypto_bigint_uint::Uint,
607 };
608 use rand::rng;
609 use zinc_poly::univariate::{dense::DensePolynomial, dynamic::DynamicPolynomialConfig};
610 use zinc_test_uair::{
611 GenerateRandomTrace, TestUairNoMultiplication, TestUairSimpleMultiplication,
612 };
613 use zinc_transcript::Blake3Transcript;
614 use zinc_uair::{
615 constraint_counter::count_constraints,
616 degree_counter::count_max_degree,
617 ideal::{DegreeOneIdeal, Ideal, IdealCheck},
618 ideal_collector::IdealOrZero,
619 };
620
621 fn test_successful_verification_generic<
626 U,
627 IdealOverF,
628 IdealOverFFromRef,
629 const DEGREE_PLUS_ONE: usize,
630 >(
631 num_vars: usize,
632 prime_idx: Option<usize>,
633 ideal_over_f_from_ref: IdealOverFFromRef,
634 ) where
635 U: Uair<Scalar = DensePolynomial<Int<5>, DEGREE_PLUS_ONE>>
636 + GenerateRandomTrace<DEGREE_PLUS_ONE, PolyCoeff = Int<5>, Int = Int<5>>,
637 IdealOverF: Ideal + for<'a> IdealCheck<DynamicPolynomialConfig<'a, MontyField<LIMBS>>>,
638 IdealOverFFromRef: Fn(&IdealOrZero<U::Ideal>) -> IdealOverF,
639 {
640 let mut rng = rng();
641 let field_cfg = test_config();
642
643 let mut prover_transcript = Blake3Transcript::new();
644 let mut verifier_transcript = prover_transcript.clone();
645
646 let trace = U::generate_random_trace(num_vars, &mut rng);
647
648 let (ic_proof, evaluation_point, projected_scalars, projected_trace) =
649 run_ideal_check_prover_combined::<U, DEGREE_PLUS_ONE>(
650 num_vars,
651 &trace,
652 prime_idx,
653 &mut prover_transcript,
654 );
655
656 let num_constraints = count_constraints::<U>();
657
658 let ic_evaluation_point: Vec<MontyFieldElement<LIMBS>> =
663 verifier_transcript.get_field_challenges(num_vars, &field_cfg);
664
665 let ic_check_subclaim = IdealCheckProtocol::<U>::verify_as_subprotocol(
666 &mut verifier_transcript,
667 ic_proof,
668 0,
669 num_constraints.q,
670 &ic_evaluation_point,
671 ideal_over_f_from_ref,
672 |_| panic!("F_q[X] not supported here!"),
673 &field_cfg,
674 )
675 .expect("Verification failed");
676
677 let max_degree = count_max_degree::<U>();
678
679 let projecting_element: MontyFieldElement<4> =
680 prover_transcript.get_field_challenge(&field_cfg);
681
682 let projected_scalars =
683 project_scalars_to_field(&field_cfg, projected_scalars, &projecting_element).unwrap();
684
685 let folding_challenge: MontyFieldElement<LIMBS> =
688 prover_transcript.get_field_challenge(&field_cfg);
689 let (cpr_group, cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::<U>(
690 evaluate_trace_to_column_mles(
691 &field_cfg,
692 &ProjectedTrace::RowMajor(projected_trace),
693 &projecting_element,
694 ),
695 Vec::new(),
696 &evaluation_point,
697 &projected_scalars,
698 0,
699 num_constraints.q,
700 num_vars,
701 max_degree,
702 &folding_challenge,
703 &field_cfg,
704 )
705 .expect("CPR prepare failed");
706
707 let mut sumcheck_outputs = MultiDegreeSumcheck::prove_as_subprotocol(
708 &mut prover_transcript,
709 vec![(vec![cpr_group], &field_cfg)],
710 num_vars,
711 &field_cfg,
712 );
713 let (md_proof, states) = sumcheck_outputs.pop().expect("single family");
714
715 let (proof, _) = CombinedPolyResolver::finalize_prover::<U>(
716 &mut prover_transcript,
717 states.into_iter().next().unwrap(),
718 cpr_ancillary,
719 &field_cfg,
720 )
721 .expect("CPR finalize failed");
722
723 let projecting_element: MontyFieldElement<LIMBS> =
724 verifier_transcript.get_field_challenge(&field_cfg);
725 let folding_challenge: MontyFieldElement<LIMBS> =
726 verifier_transcript.get_field_challenge(&field_cfg);
727
728 let cpr_verifier_ancillary = CombinedPolyResolver::prepare_verifier::<U>(
730 &proof,
731 md_proof.claimed_sums()[0].clone(),
732 &ic_check_subclaim,
733 num_constraints.q,
734 num_vars,
735 &projecting_element,
736 &folding_challenge,
737 &field_cfg,
738 )
739 .expect("CPR prepare_verifier failed");
740
741 let md_subclaims = MultiDegreeSumcheck::verify_as_subprotocol(
742 &mut verifier_transcript,
743 num_vars,
744 &[(&md_proof, &field_cfg)],
745 &field_cfg,
746 )
747 .expect("MultiDegreeSumcheck verify failed")
748 .pop()
749 .expect("single family");
750
751 assert!(
752 CombinedPolyResolver::finalize_verifier::<U>(
753 &mut verifier_transcript,
754 proof,
755 md_subclaims.point().to_vec(),
756 md_subclaims.expected_evaluations()[0].clone(),
757 cpr_verifier_ancillary,
758 &projected_scalars,
759 0,
760 &field_cfg,
761 )
762 .is_ok()
763 );
764 }
765
766 #[test]
767 fn test_successful_verification() {
768 let field_cfg = test_config();
769
770 let num_vars = 2;
771
772 test_successful_verification_generic::<
773 TestUairNoMultiplication<Int<5>, Uint<LIMBS>>,
774 _,
775 _,
776 32,
777 >(num_vars, None, |ideal_over_ring| {
778 ideal_over_ring.map(|i| DegreeOneIdeal::project(&field_cfg, i))
779 });
780 test_successful_verification_generic::<
781 TestUairSimpleMultiplication<Int<5>, Uint<LIMBS>>,
782 _,
783 _,
784 32,
785 >(num_vars, None, |_ideal_over_ring| {
786 IdealOrZero::<DegreeOneIdeal<_>>::zero()
787 });
788 }
789}