Skip to main content

zinc_piop/
combined_poly_resolver.rs

1//! Combined polynomial resolver subprotocol.
2
3mod 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
44/// Combined polynomial resolver.
45///
46/// Builds one sumcheck group over either the Q[X]-projected trace and the
47/// $Q[X]$ family of constraints, or the $F_{q_i}[X]$ family
48/// (for a specific $q_i$ prime). The `MultiDegreeSumcheck` at the protocol
49/// layer batches them all together.
50///
51/// Food for thought:
52/// The n+1 CPR groups could in principle be collapsed into a single
53/// `MultiDegreeSumcheck` group sharing one evaluation-point trajectory, with
54/// per-family soundness factor $q_i / q^*$. The shared-integer challenges
55/// (folding $\alpha$, projecting $\psi$) are already in place; what remains is
56/// fusing the per-family combination functions and writing down the
57/// corresponding soundness lemma. The trade-off is loss of per-family
58/// arithmetic locality — each $F_{q_i}[X]$ family currently does its
59/// sumcheck work in its native (cheap) field, which a merged comb_fn would have
60/// to give up or dispatch internally. Worth revisiting only if proof size /
61/// verifier simplicity outweighs prover cost.
62pub struct CombinedPolyResolver<C: SetConfig>(PhantomData<C>);
63
64impl<C> CombinedPolyResolver<C>
65where
66    C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
67    C::Integer: ConstTranscribable,
68{
69    /// Build the CPR sumcheck group for use in the multi-degree sumcheck.
70    ///
71    /// Pre-sumcheck half of the CPR prover. Samples the folding challenge `α`,
72    /// builds the MLE vector and combination function with the constraint
73    /// polynomial identity:
74    ///
75    /// $$
76    /// \sum_{b \in H} (f_0(b, x_0[b],...,x_n[b], x_0ˆdown[b],...,x_nˆdown[b])
77    ///                 + \alpha f_1(...) + ... + \alpha^k f_k(...)) = v_0 +
78    ///                   \alpha * v_1 + ... + \alphaˆk * v_k,
79    /// $$
80    /// where $f_i(b, x_0[b],...,x_n[b], x_0ˆdown[b],...,x_nˆdown[b])
81    ///         = eq(r, b) * (1 - eq(r, 1,...1))
82    ///             * g_i(x_0[b],...,x_n[b], x_0ˆdown[b],...,x_nˆdown[b])$
83    /// and `g_i` is a constraint polynomial given by the UAIR `U`.
84    /// `v_0,...,v_k` are the claimed evaluations of the combined polynomials.
85    ///
86    /// # Parameters
87    /// - `trace_matrix`: The trace that have been projected to F.
88    /// - `bit_op_down_mles`: MLEs of the bit-op virtual columns, projected to
89    ///   `F::Inner`, in `UairSignature::bit_op_specs()` order. The caller is
90    ///   responsible for applying the bit-op (ROTR / SHR) entry-wise on the
91    ///   *unprojected* binary_poly source column *before* projection — see
92    ///   Lemma 2.3 of the Zinc+ paper. The length must equal the signature's
93    ///   `bit_op_specs().len()`.
94    /// - `evaluation_point`: The evaluation point for the claims.
95    /// - `projected_scalars`: The UAIR scalars projected to `F`.
96    /// - `family_idx`: which constraint family to fold. `0` -> $Q[X]$; `i >= 1`
97    ///   -> $F_{q_{i-1}}[X]$.
98    /// - `num_constraints`: The number of constraint polynomials in the UAIR
99    ///   `U`.
100    /// - `num_vars`: The number of variables of the trace MLEs.
101    /// - `max_degree`: The degree of the UAIR `U`.
102    /// - `folding_challenge`: pre-sampled batching challenge $\alpha$ used to
103    ///   batch the constraint claims.
104    /// - `field_cfg`: The random field config.
105    #[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        // Shifted trace: for each ShiftSpec, take the source column,
134        // drop the first `shift_amount` rows, and zero-pad to the full
135        // domain size so the MLE keeps the correct `num_vars`.
136        // TODO consider working with pointers since down cols are virtual cols until
137        // folded in sumcheck - virtual MLE trait needed in sumcheck.
138        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        // Down-row layout (see UairSignature::with_bit_op_specs):
159        //
160        //     [shifted_binary..., bit_op_binary...,
161        //      shifted_arbitrary..., shifted_int...]
162        //
163        // Shifts are sorted by source_col, so binary-source shifts come first.
164        // We splice the bit-op MLEs in between the binary and non-binary
165        // shift groups so that the resulting `down` vector is consistent with
166        // the down ColumnLayout (binary_poly_cols + arbitrary_poly_cols +
167        // int_cols).
168        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        // To get the constraints on the last row ignored
182        // we multiply each constraint polynomial
183        // by the selector (1 - eq(1,...,1, x))
184        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        // The batching challenge $\alpha$ is supplied by the caller. The
194        // protocol layer samples one shared integer in $[0, q^*)$ once
195        // and lifts it into each family's field, so the Q[X] CPR and
196        // per-prime CPRs reuse the same underlying integer.
197        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    /// Finalize the CPR proof after the multi-degree sumcheck completes.
261    ///
262    /// # Parameters
263    /// - `transcript`: FS-transcript (absorbs `up_evals` and `down_evals`).
264    /// - `sumcheck_prover_state`: The CPR group's `ProverState` from
265    ///   `MultiDegreeSumcheck::prove_as_subprotocol` (states\[0\]).
266    /// - `ancillary`: Produced by [`prepare_sumcheck_group`]; carries column
267    ///   counts and `num_vars` needed to split the flat eval vector.
268    /// - `field_cfg`: Field configuration.
269    #[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        // Sumcheck prover stops evaluating MLEs
283        // at the second-to-last challenge
284        // leaving all MLEs in num_vars=1
285        // state. We need to evaluate them up
286        // and send to the verifier.
287        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        // The post-sumcheck evals are laid out as
315        //   [up_evals..., shifted_binary..., bit_op_evals..., shifted_non_binary...]
316        // matching the order in which `prepare_sumcheck_group` packed them
317        // into the MLE vector. We split them into three on-wire vectors:
318        // `up_evals`, `down_evals` (shifts only, in their UAIR-signature
319        // order), and `bit_op_evals` (in `bit_op_specs()` order).
320        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    /// Pre-sumcheck half of the CPR verifier.
347    ///
348    /// Must run before [`MultiDegreeSumcheck::verify_as_subprotocol`].
349    /// Pure check: takes the pre-sampled $\psi$ projecting element and the
350    /// pre-sampled batching challenge $\alpha$; does not touch the transcript.
351    ///
352    /// # Parameters
353    /// - `proof`: The CPR proof (`up_evals`, `down_evals`).
354    /// - `claimed_sum`: The claimed sum from
355    ///   `combined_sumcheck.claimed_sums()[0]`.
356    /// - `ic_check_subclaim`: Subclaim from the ideal check; provides the
357    ///   evaluation point and claimed values used to verify the sumcheck sum.
358    /// - `num_constraints`: Number of constraint polynomials in `U`.
359    /// - `num_vars`: Number of variables of the trace MLEs.
360    /// - `projecting_element`: The random challenge used to project `F[X] → F`.
361    /// - `folding_challenge`: pre-sampled batching challenge $\alpha$ used to
362    ///   batch the constraint claims.
363    /// - `field_cfg`: Field configuration.
364    #[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        // Precompute powers of the projecting element for batch evaluation.
389        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        // The batching challenge $\alpha$ is supplied by the caller; see
401        // [`prepare_sumcheck_group`] for the shared-challenge rationale.
402        let folding_challenge_powers: Vec<C::Element> =
403            powers(field_cfg, folding_challenge, num_constraints);
404
405        // TODO(Alex): investigate if parallelising this is beneficial.
406        // Compute v_0 + \alpha * v_1 + ... + \alpha ^ k * v_k.
407        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    /// Post-sumcheck half of the CPR verifier.
439    ///
440    /// Runs after [`MultiDegreeSumcheck::verify_as_subprotocol`] produces the
441    /// shared evaluation point.
442    ///
443    /// # Parameters
444    /// - `transcript`: FS-transcript (absorbs `up_evals` and `down_evals`).
445    /// - `proof`: The CPR proof (consumed to produce the subclaim).
446    /// - `shared_point`: The shared evaluation point `r*` from the multi-degree
447    ///   sumcheck.
448    /// - `expected_evaluation`: `md_subclaims.expected_evaluations()[0]` — the
449    ///   expected value of the CPR combination function at `r*`.
450    /// - `ancillary`: Produced by [`prepare_verifier`]; carries folding
451    ///   challenge powers, ideal-check evaluation point, and `num_vars`.
452    /// - `projected_scalars`: UAIR scalars projected to `F`.
453    /// - `family_idx`: which constraint family. `0` -> $Q[X]$; `i >= 1` ->
454    ///   $F_{q_{i-1}}[X]$.
455    /// - `field_cfg`: Field configuration.
456    #[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        // Build the full down-row eval vec by splicing bit-op evals into the
491        // binary_poly slot, matching the down ColumnLayout enforced by
492        // `UairSignature::with_bit_op_specs`:
493        //   [shifted_binary..., bit_op_evals..., shifted_arbitrary..., shifted_int...]
494        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        // eq * (1 - selector) * folded
516        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    // TODO(Ilia): These tests are absolute joke.
622    //             Once we have time we need to create a comprehensive test suite
623    //             akin to the one we have for the PCS or the sumcheck.
624
625    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        // Mirror the prover's evaluation-point sampling:
659        // the test helper (`run_ideal_check_prover_combined`) squeezed `num_vars` field
660        // challenges from the prover transcript before invoking `prove_combined`, so do
661        // the same here for the verifier.
662        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            /* family_idx = */ 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        // Prover: prepare -> MultiDegreeSumcheck → finalize.
686        // $\alpha$ is sampled by the caller per the shared-challenge API.
687        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            /* family_idx = */ 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        // Verifier: prepare → MultiDegreeSumcheck → finalize
729        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                /* family_idx = */ 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}