Skip to main content

zinc_protocol/
verifier.rs

1use super::*;
2use itertools::Itertools;
3use std::io::Cursor;
4use zinc_piop::{
5    combined_poly_resolver::CombinedPolyResolver,
6    ideal_check::{self, IdealCheckProtocol},
7    lookup::booleanity::{BoolVerifierAncillary, BooleanityChecker, BooleanityProof},
8    multipoint_eval::{self, MultipointEval, MultipointEvalFamilyInputs},
9    projections::{
10        ProjectedScalars, ProjectedTrace, project_scalars, project_scalars_to_field,
11        project_trace_coeffs_row_major,
12    },
13    sumcheck::multi_degree::MultiDegreeSumcheck,
14};
15use zinc_poly::univariate::dynamic::{DynamicPolynomialConfig, HasDynamicPolynomialConfig};
16use zinc_transcript::{
17    Blake3Transcript,
18    traits::{ConstTranscribable, Transcript},
19};
20use zinc_uair::{
21    Uair, UairSignature, UairTrace,
22    constraint_counter::count_constraints,
23    ideal::{Ideal, IdealCheck},
24    ideal_collector::IdealOrZero,
25};
26use zinc_utils::{add, powers, projectable_to_field::ProjectableToField, sub};
27use zip_plus::{
28    pcs::structs::{ZipPlus, ZipPlusParams, ZipTypes},
29    pcs_transcript::PcsVerifierTranscript,
30};
31
32//
33// Shared base
34//
35
36/// Persistent verifier infrastructure carried across every step.
37#[derive(Clone, Debug)]
38pub struct VerifierBase<'a, Zt: ZincTypes<D, FD>, const D: usize, const FD: usize> {
39    num_vars: usize,
40    uair_signature: UairSignature<Zt::Fmod>,
41    pcs_transcript: PcsVerifierTranscript,
42    public_trace: &'a UairTrace<'a, Zt::Int, Zt::Int, D, D>,
43
44    // Commitment info
45    vp_bin: &'a ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
46    vp_arb: &'a ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
47    vp_int: &'a ZipPlusParams<Zt::IntZt, Zt::IntLc>,
48}
49
50//
51// Type-state structs
52//
53
54/// After step 0 (transcript reconstruction).
55#[derive(Clone, Debug)]
56pub struct VerifierTranscriptReconstructed<
57    'a,
58    Zt: ZincTypes<D, FD>,
59    U: Uair,
60    C: BaseFieldConfig,
61    IdealOverF,
62    const D: usize,
63    const FD: usize,
64> {
65    base: VerifierBase<'a, Zt, D, FD>,
66
67    // Proof leftovers, still in wire form (canonical lifted integers)
68    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
69    proof_ideal_check: IdealCheckProof<Zt::Fmod>,
70    proof_cpr: CombinedPolyResolverProof<Zt::Fmod>,
71    proof_combined_sumcheck: MultiDegreeSumcheckProof<Zt::Fmod>,
72    proof_multipoint_eval: MultipointEvalProof<Zt::Fmod>,
73    /// Per-constraint-family witness-only lifted MLE evals. Layout:
74    /// `[0]` = Q-family ($q_0$), `[i]` = declared prime $i - 1$ for
75    /// $i = 1, \ldots, n$. Length = `1 + n_fq`.
76    proof_witness_lifted_evals: Vec<Vec<DynamicPolynomial<Zt::Fmod>>>,
77    /// Witness-only lifted MLE evals for $q''$ prime.
78    /// `None` when there's no $F_q[X]$ constraints, in which case we assume
79    /// $q'' := q_0$ and thus `proof_witness_lifted_evals[0]` is used for
80    /// PCS verification.
81    proof_witness_lifted_evals_pp: Option<Vec<DynamicPolynomial<Zt::Fmod>>>,
82    proof_lookup_proof: Option<BatchedLookupProof<Zt::Fmod>>,
83    proof_booleanity: Option<BooleanityProof<Zt::Fmod>>,
84    proof_affine_booleanity: Option<BooleanityProof<Zt::Fmod>>,
85    proof_ideal_checks_fq: Vec<IdealCheckProof<Zt::Fmod>>,
86    proof_cpr_fq: Vec<CombinedPolyResolverProof<Zt::Fmod>>,
87    proof_combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<Zt::Fmod>>,
88    proof_multipoint_evals_fq: Vec<MultipointEvalProof<Zt::Fmod>>,
89    _phantom: PhantomData<(U, C, IdealOverF)>,
90}
91
92/// After step 1 (prime projection).
93#[derive(Clone, Debug)]
94pub struct VerifierPrimeProjected<
95    'a,
96    Zt: ZincTypes<D, FD>,
97    U: Uair,
98    C: BaseFieldConfig,
99    IdealOverF,
100    const D: usize,
101    const FD: usize,
102> {
103    base: VerifierBase<'a, Zt, D, FD>,
104    field_cfg: C,
105
106    // Proof leftovers
107    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
108    proof_ideal_check: IdealCheckProof<C::Element>,
109    proof_cpr: CombinedPolyResolverProof<C::Element>,
110    proof_combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
111    proof_multipoint_eval: MultipointEvalProof<C::Element>,
112    /// Per-constraint-family witness-only lifted MLE evals (see
113    /// [`VerifierTranscriptReconstructed`] doc). Length = `1 + n_fq`.
114    proof_witness_lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
115    /// Witness-only lifted MLE evals for $q''$ prime (see
116    /// [`VerifierTranscriptReconstructed`] doc)
117    proof_witness_lifted_evals_pp: Option<Vec<DynamicPolynomial<Zt::Fmod>>>,
118    proof_lookup_proof: Option<BatchedLookupProof<C::Element>>,
119    proof_booleanity: Option<BooleanityProof<C::Element>>,
120    proof_affine_booleanity: Option<BooleanityProof<C::Element>>,
121    proof_ideal_checks_fq: Vec<IdealCheckProof<C::Element>>,
122    proof_cpr_fq: Vec<CombinedPolyResolverProof<C::Element>>,
123    proof_combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
124    proof_multipoint_evals_fq: Vec<MultipointEvalProof<C::Element>>,
125    _phantom: PhantomData<(U, IdealOverF)>,
126}
127
128/// After step 2 (ideal check). `project_ideal` has been consumed.
129#[derive(Clone, Debug)]
130pub struct VerifierIdealChecked<
131    'a,
132    Zt: ZincTypes<D, FD>,
133    U: Uair,
134    C: BaseFieldConfig,
135    IdealOverF,
136    const D: usize,
137    const FD: usize,
138> {
139    base: VerifierBase<'a, Zt, D, FD>,
140    field_cfg: C,
141    /// Per-family field configs (`[0]` = $Q[X]$, `[i >= 1]` =
142    /// $F_{q_{i-1}}[X]$), kept for the next step's shared $\psi$
143    /// projecting element.
144    all_field_cfgs: Vec<C>,
145    /// Index of $q^*$ in `all_field_cfgs`, computed once in step 2 and
146    /// threaded forward so step 3 can recover `q_star_cfg` by indexing.
147    q_star_idx: usize,
148    /// Per-family IC subclaims (length `n + 1`). `[0]` is the Q[X]
149    /// family's subclaim; `[i + 1]` is the per-prime $F_{q_i}[X]$
150    /// subclaim. Previously only the Q[X] subclaim was kept; the per-prime
151    /// subclaims were squeezed and discarded. They are now retained so
152    /// step 4 (CPR verify) can drive a per-prime `prepare_verifier` call
153    /// for each fq family.
154    ic_subclaims: Vec<ideal_check::VerifierSubclaim<C::Element>>,
155
156    // Proof leftovers
157    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
158    proof_cpr: CombinedPolyResolverProof<C::Element>,
159    proof_combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
160    proof_multipoint_eval: MultipointEvalProof<C::Element>,
161    /// Per-constraint-family witness-only lifted MLE evals (see
162    /// [`VerifierTranscriptReconstructed`] doc). Length = `1 + n_fq`.
163    proof_witness_lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
164    /// Witness-only lifted MLE evals for $q''$ prime (see
165    /// [`VerifierTranscriptReconstructed`] doc)
166    proof_witness_lifted_evals_pp: Option<Vec<DynamicPolynomial<Zt::Fmod>>>,
167    proof_lookup_proof: Option<BatchedLookupProof<C::Element>>,
168    proof_booleanity: Option<BooleanityProof<C::Element>>,
169    proof_affine_booleanity: Option<BooleanityProof<C::Element>>,
170    /// Per-prime CPR proofs (one per declared prime).
171    proof_cpr_fq: Vec<CombinedPolyResolverProof<C::Element>>,
172    /// Per-prime multi-degree sumcheck proofs (one per declared prime).
173    proof_combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
174    /// Per-prime multipoint-eval proofs (one per declared prime).
175    proof_multipoint_evals_fq: Vec<MultipointEvalProof<C::Element>>,
176    _phantom: PhantomData<(U, IdealOverF)>,
177}
178
179/// After step 3 (eval projection). `project_scalar` has been consumed.
180#[derive(Clone, Debug)]
181pub struct VerifierEvalProjected<
182    'a,
183    Zt: ZincTypes<D, FD>,
184    U: Uair,
185    C: BaseFieldConfig,
186    IdealOverF,
187    const D: usize,
188    const FD: usize,
189> {
190    base: VerifierBase<'a, Zt, D, FD>,
191    field_cfg: C,
192    /// Per-family field configs (`[0]` = $Q[X]$, `[i >= 1]` =
193    /// $F_{q_{i-1}}[X]$), kept for later per-prime CPR/MP/PCS steps.
194    all_field_cfgs: Vec<C>,
195    /// Index of $q^*$ in `all_field_cfgs`, kept for later steps that need
196    /// to re-sample shared challenges in $[0, q^*)$.
197    q_star_idx: usize,
198    /// Per-family IC subclaims (length `n + 1`). `[0]` feeds the Q[X] CPR
199    /// `prepare_verifier`; `[i + 1]` will feed each per-prime CPR
200    /// `prepare_verifier` in step 4.
201    ic_subclaims: Vec<ideal_check::VerifierSubclaim<C::Element>>,
202    /// Per-family $\psi$-projecting elements: integer sampled mod $q^*$
203    /// and projected onto each of `all_field_cfgs`.
204    projecting_elements: Vec<C::Element>,
205    /// Q[X] family's $\psi$-projected scalars.
206    projected_scalars_f: ProjectedScalars<U::Scalar, C::Element>,
207    /// Per-prime $\psi$-projected scalars (one per declared prime). Built
208    /// in step 3 from each family's `projecting_elements[i + 1]` and the
209    /// UAIR-author-supplied `project_scalar` closure (applied with
210    /// `all_field_cfgs[i + 1]`). Consumed in step 4 (CPR finalize per
211    /// family).
212    projected_scalars_f_fq: Vec<ProjectedScalars<U::Scalar, C::Element>>,
213
214    // Proof leftovers
215    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
216    proof_cpr: CombinedPolyResolverProof<C::Element>,
217    proof_combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
218    proof_multipoint_eval: MultipointEvalProof<C::Element>,
219    /// Per-constraint-family witness-only lifted MLE evals (see
220    /// [`VerifierTranscriptReconstructed`] doc). Length = `1 + n_fq`.
221    proof_witness_lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
222    /// Witness-only lifted MLE evals for $q''$ prime (see
223    /// [`VerifierTranscriptReconstructed`] doc)
224    proof_witness_lifted_evals_pp: Option<Vec<DynamicPolynomial<Zt::Fmod>>>,
225    proof_lookup_proof: Option<BatchedLookupProof<C::Element>>,
226    proof_booleanity: Option<BooleanityProof<C::Element>>,
227    proof_affine_booleanity: Option<BooleanityProof<C::Element>>,
228    /// Per-prime CPR proofs (one per declared prime).
229    proof_cpr_fq: Vec<CombinedPolyResolverProof<C::Element>>,
230    /// Per-prime multi-degree sumcheck proofs (one per declared prime),
231    /// consumed in step 4 by the lockstep verifier driver.
232    proof_combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
233    /// Per-prime multipoint-eval proofs (one per declared prime),
234    /// consumed in step 5 by the lockstep multipoint-eval verifier.
235    proof_multipoint_evals_fq: Vec<MultipointEvalProof<C::Element>>,
236    _phantom: PhantomData<(U, IdealOverF)>,
237}
238
239/// After step 4 (sumcheck verify).
240#[derive(Clone, Debug)]
241pub struct VerifierSumchecked<
242    'a,
243    Zt: ZincTypes<D, FD>,
244    C: BaseFieldConfig,
245    IdealOverF,
246    const D: usize,
247    const FD: usize,
248> {
249    base: VerifierBase<'a, Zt, D, FD>,
250    field_cfg: C,
251    /// Per-family field configs (carried for downstream steps).
252    all_field_cfgs: Vec<C>,
253    /// Index of $q^*$ in `all_field_cfgs` (carried for downstream steps).
254    q_star_idx: usize,
255    /// Per-family $\psi$-projecting elements: integer sampled mod $q^*$
256    /// and projected onto each of `all_field_cfgs`.
257    projecting_elements: Vec<C::Element>,
258    /// CPR subclaim's evaluation point ($r^\star$)
259    cpr_eval_point: Vec<C::Element>,
260    cpr_up_evals: Vec<C::Element>,
261    cpr_bit_op_evals: Vec<C::Element>,
262    cpr_down_evals: Vec<C::Element>,
263    /// Per-prime CPR subclaim evaluation points (r*, lifted into
264    /// each family's field). Length `n_fq`. Empty for UAIRs with no
265    /// declared fq primes.
266    cpr_eval_points_fq: Vec<Vec<C::Element>>,
267    /// Per-prime CPR subclaim `up_evals`. Length `n_fq`. Consumed in step 5.
268    cpr_up_evals_fq: Vec<Vec<C::Element>>,
269    /// Per-prime CPR subclaim `bit_op_evals`. Length `n_fq`. Consumed in step
270    /// 5.
271    cpr_bit_op_evals_fq: Vec<Vec<C::Element>>,
272    /// Per-prime CPR subclaim `down_evals`. Length `n_fq`. Consumed in step 5.
273    cpr_down_evals_fq: Vec<Vec<C::Element>>,
274    /// `bit_slice_evals` carried over from booleanity's `finalize_verifier`,
275    /// to be collapsed into the appended `up_evals` entries
276    /// $c_j = \sum_i b_{j,i}\,(\alpha')^i$.
277    ///
278    /// `None` iff there are no witness binary-poly columns.
279    bool_bit_slice_evals: Option<Vec<C::Element>>,
280    /// Affine-virtual bit-slice evaluations carried separately from the
281    /// committed-column Booleanity proof.
282    affine_bool_bit_slice_evals: Option<Vec<C::Element>>,
283    /// Fresh challenge sampled after `bit_slice_evals` were absorbed by
284    /// booleanity's `finalize_verifier`. Consumed in step 5 (bridge
285    /// scalars) and step 6 (appended $\alpha'$-projected open_evals).
286    ///
287    /// `None` iff there are neither witness binary-poly columns nor affine
288    /// virtual booleanity targets.
289    alpha_prime_f: Option<C::Element>,
290
291    // Proof leftovers
292    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
293    proof_multipoint_eval: MultipointEvalProof<C::Element>,
294    proof_multipoint_evals_fq: Vec<MultipointEvalProof<C::Element>>,
295    /// Per-constraint-family witness-only lifted MLE evals (see
296    /// [`VerifierTranscriptReconstructed`] doc). Length = `1 + n_fq`.
297    proof_witness_lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
298    /// Witness-only lifted MLE evals for $q''$ prime (see
299    /// [`VerifierTranscriptReconstructed`] doc)
300    proof_witness_lifted_evals_pp: Option<Vec<DynamicPolynomial<Zt::Fmod>>>,
301    proof_lookup_proof: Option<BatchedLookupProof<C::Element>>,
302    _phantom: PhantomData<IdealOverF>,
303}
304
305/// After step 5 (multi-point eval).
306#[derive(Clone, Debug)]
307pub struct VerifierMultipointEvaled<
308    'a,
309    Zt: ZincTypes<D, FD>,
310    C: BaseFieldConfig,
311    IdealOverF,
312    const D: usize,
313    const FD: usize,
314> {
315    base: VerifierBase<'a, Zt, D, FD>,
316    field_cfg: C,
317    /// Per-family field configs (carried for downstream steps).
318    all_field_cfgs: Vec<C>,
319    /// Per-family $\psi$-projecting elements: integer sampled mod $q^*$
320    /// and projected onto each of `all_field_cfgs`.
321    projecting_elements: Vec<C::Element>,
322    // See VerifierSumchecked::alpha_prime_f
323    alpha_prime_f: Option<C::Element>,
324    mp_subclaim: multipoint_eval::Subclaim<C::Element>,
325    /// Per-prime multipoint-eval subclaims, one per declared prime, produced
326    /// by step 5's lockstep multipoint-eval verifier. Empty for UAIRs with
327    /// no declared fq primes. Consumed in step 6.
328    mp_subclaims_fq: Vec<multipoint_eval::Subclaim<C::Element>>,
329
330    // Proof leftovers
331    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
332    /// Per-constraint-family witness-only lifted MLE evals (see
333    /// [`VerifierTranscriptReconstructed`] doc). Length = `1 + n_fq`.
334    proof_witness_lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
335    /// Witness-only lifted MLE evals for $q''$ prime (see
336    /// [`VerifierTranscriptReconstructed`] doc)
337    proof_witness_lifted_evals_pp: Option<Vec<DynamicPolynomial<Zt::Fmod>>>,
338    proof_lookup_proof: Option<BatchedLookupProof<C::Element>>,
339    _phantom: PhantomData<IdealOverF>,
340}
341
342/// After step 6 (lifted evals verification).
343#[derive(Clone, Debug)]
344pub struct VerifierLiftedEvalsChecked<
345    'a,
346    Zt: ZincTypes<D, FD>,
347    C: BaseFieldConfig,
348    IdealOverF,
349    const D: usize,
350    const FD: usize,
351> {
352    base: VerifierBase<'a, Zt, D, FD>,
353    /// `q''`-family witness-only lifted MLE evaluations at `r* =  r_0 mod q''`.
354    /// Consumed by PCS verify.
355    lifted_evals_pp: Vec<DynamicPolynomial<C::Element>>,
356    /// PCS-only prime cfg sampled at step 6 start (mirror of prover step 7).
357    q_pp_cfg: C,
358    /// $r^\star = r_0 \bmod q''$ — the PCS evaluation point.
359    r_star: Vec<C::Element>,
360
361    // Proof leftovers
362    proof_commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
363    /// Lookup proof component, threaded through the verifier state
364    /// machine. Not yet consumed: the `BatchedLookupProof` verifier chain
365    /// is not implemented (see step 5 / step 6 TODOs). Retained so the
366    /// proof shape stays stable once lookup verification lands.
367    #[allow(dead_code)]
368    proof_lookup_proof: Option<BatchedLookupProof<C::Element>>,
369    _phantom: PhantomData<IdealOverF>,
370}
371
372/// After step 7 (PCS verify). Ready for
373/// [`finish`](VerifierPcsVerified::finish).
374#[derive(Clone, Debug)]
375pub struct VerifierPcsVerified<IdealOverF> {
376    pcs_transcript: PcsVerifierTranscript,
377    _phantom: PhantomData<IdealOverF>,
378}
379
380fn prepare_booleanity_verifier_group<C>(
381    transcript: &mut Blake3Transcript,
382    claimed_sums: &[C::Element],
383    group_idx: &mut usize,
384    num_cols: usize,
385    bit_width: usize,
386    num_vars: usize,
387    field_cfg: &C,
388) -> Result<Option<BoolVerifierAncillary<C::Element>>, ProtocolError<C::Element>>
389where
390    C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
391    C::Integer: ConstTranscribable,
392{
393    if num_cols == 0 {
394        return Ok(None);
395    }
396
397    let claimed_sum = claimed_sums
398        .get(*group_idx)
399        .ok_or(ProtocolError::BooleanityProofMissing)?;
400    let ancillary = BooleanityChecker::<C>::prepare_verifier(
401        transcript,
402        claimed_sum,
403        num_cols,
404        bit_width,
405        num_vars,
406        field_cfg,
407    )
408    .map_err(ProtocolError::Booleanity)?;
409    *group_idx = add!(*group_idx, 1);
410    Ok(Some(ancillary))
411}
412
413fn finalize_booleanity_verifier_group<C>(
414    transcript: &mut Blake3Transcript,
415    proof: Option<BooleanityProof<C::Element>>,
416    shared_point: &[C::Element],
417    expected_evaluations: &[C::Element],
418    group_idx: &mut usize,
419    ancillary: Option<BoolVerifierAncillary<C::Element>>,
420    field_cfg: &C,
421) -> Result<Option<Vec<C::Element>>, ProtocolError<C::Element>>
422where
423    C: BaseFieldConfig + ProjectPrimitiveIntegersWithConfig + 'static,
424    C::Integer: ConstTranscribable,
425{
426    let (proof, ancillary) = match (proof, ancillary) {
427        (None, None) => return Ok(None),
428        (Some(proof), Some(ancillary)) => (proof, ancillary),
429        _ => return Err(ProtocolError::BooleanityProofMissing),
430    };
431    let expected_eval = expected_evaluations
432        .get(*group_idx)
433        .ok_or(ProtocolError::BooleanityProofMissing)?;
434    *group_idx = add!(*group_idx, 1);
435
436    let subclaim = BooleanityChecker::<C>::finalize_verifier(
437        transcript,
438        proof,
439        shared_point.to_vec(),
440        expected_eval,
441        ancillary,
442        field_cfg,
443    )
444    .map_err(ProtocolError::Booleanity)?;
445    Ok(Some(subclaim.bit_slice_evals))
446}
447
448//
449// Step implementations
450//
451
452impl<Zt, U, C, const D: usize, const FD: usize> ZincPlusPiop<Zt, U, C, D, FD>
453where
454    Zt: ZincTypes<D, FD>,
455    U: Uair<Prime = Zt::Fmod>,
456    C: BaseFieldConfig<Integer = Zt::Fmod>,
457{
458    /// Step 0: Verifier entry point.
459    /// Reconstruct Fiat-Shamir transcript from commitments and public data.
460    #[allow(clippy::type_complexity)]
461    pub fn step0_reconstruct_transcript<'a, IdealOverF>(
462        (vp_bin, vp_arb, vp_int): &'a (
463            ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
464            ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
465            ZipPlusParams<Zt::IntZt, Zt::IntLc>,
466        ),
467        mut proof: Proof<Zt::Fmod>,
468        public_trace: &'a UairTrace<'a, Zt::Int, Zt::Int, D, D>,
469        num_vars: usize,
470    ) -> Result<
471        VerifierTranscriptReconstructed<'a, Zt, U, C, IdealOverF, D, FD>,
472        ProtocolError<C::Element>,
473    >
474    where
475        IdealOverF: Ideal,
476    {
477        assert!(
478            num_vars > 0,
479            "Attempt to verify a constant: num_vars must be > 0"
480        );
481        let uair_signature = U::signature();
482        let zip_proof = std::mem::take(&mut proof.zip);
483        let mut base = VerifierBase {
484            num_vars,
485            uair_signature,
486            public_trace,
487            pcs_transcript: PcsVerifierTranscript {
488                fs_transcript: Blake3Transcript::default(),
489                stream: Cursor::new(zip_proof),
490            },
491            vp_bin,
492            vp_arb,
493            vp_int,
494        };
495
496        for comm in [
497            &proof.commitments.0,
498            &proof.commitments.1,
499            &proof.commitments.2,
500        ] {
501            base.pcs_transcript.fs_transcript.absorb_bytes(&comm.root);
502        }
503
504        absorb_public_columns(
505            &mut base.pcs_transcript.fs_transcript,
506            &base.public_trace.binary_poly,
507        );
508        absorb_public_columns(
509            &mut base.pcs_transcript.fs_transcript,
510            &base.public_trace.arbitrary_poly,
511        );
512        absorb_public_columns(
513            &mut base.pcs_transcript.fs_transcript,
514            &base.public_trace.int,
515        );
516
517        Ok(VerifierTranscriptReconstructed {
518            base,
519            proof_commitments: proof.commitments,
520            proof_ideal_check: proof.ideal_check,
521            proof_cpr: proof.cpr_proof,
522            proof_combined_sumcheck: proof.combined_sumcheck,
523            proof_multipoint_eval: proof.multipoint_eval,
524            proof_witness_lifted_evals: proof.witness_lifted_evals,
525            proof_witness_lifted_evals_pp: proof.witness_lifted_evals_pp,
526            proof_lookup_proof: proof.lookup_proof,
527            proof_booleanity: proof.booleanity_proof,
528            proof_affine_booleanity: proof.affine_booleanity_proof,
529            proof_ideal_checks_fq: proof.ideal_checks_fq,
530            proof_cpr_fq: proof.cpr_proofs_fq,
531            proof_combined_sumchecks_fq: proof.combined_sumchecks_fq,
532            proof_multipoint_evals_fq: proof.multipoint_evals_fq,
533            _phantom: PhantomData,
534        })
535    }
536}
537
538impl<'a, Zt, U, C, IdealOverF, const D: usize, const FD: usize>
539    VerifierTranscriptReconstructed<'a, Zt, U, C, IdealOverF, D, FD>
540where
541    Zt: ZincTypes<D, FD>,
542    C: BaseFieldConfig<Integer = Zt::Fmod> + Clone + Send + Sync + 'static,
543    U: Uair,
544    IdealOverF: Ideal,
545{
546    /// Step 1: Prime projection. Samples the random field configuration.
547    #[allow(clippy::type_complexity)]
548    pub fn step1_prime_projection(
549        mut self,
550    ) -> Result<VerifierPrimeProjected<'a, Zt, U, C, IdealOverF, D, FD>, ProtocolError<C::Element>>
551    {
552        let field_cfg = self
553            .base
554            .pcs_transcript
555            .fs_transcript
556            .get_random_field_cfg::<C, Zt::Fmod, Zt::PrimeTest>();
557
558        // Project the wire proof (canonical lifted integers) into the
559        // per-family fields, now that every constraint-family config is
560        // known ($q_0$ sampled above, declared primes from the signature).
561        // Rejects non-canonical encodings. The $q''$-family section stays
562        // in wire form until step 7 samples $q''$.
563        let all_field_cfgs = build_all_cfgs::<C>(&self.base.uair_signature, field_cfg.clone());
564        macro_rules! project {
565            ($cfg:expr, $section:expr) => {
566                $section.try_map(|int| project_canonical($cfg, int))
567            };
568        }
569        macro_rules! project_fq_vec {
570            ($section:expr) => {
571                $section
572                    .iter()
573                    .zip(&all_field_cfgs[1..])
574                    .map(|(p, cfg)| project!(cfg, p))
575                    .try_collect()
576            };
577        }
578        let n_families = all_field_cfgs.len();
579        if self.proof_witness_lifted_evals.len() != n_families {
580            return Err(ProtocolError::FamilyCountMismatch {
581                got: self.proof_witness_lifted_evals.len(),
582                expected: n_families,
583            });
584        }
585        for fq_len in [
586            self.proof_ideal_checks_fq.len(),
587            self.proof_cpr_fq.len(),
588            self.proof_combined_sumchecks_fq.len(),
589            self.proof_multipoint_evals_fq.len(),
590        ] {
591            if fq_len != sub!(n_families, 1) {
592                return Err(ProtocolError::FamilyCountMismatch {
593                    got: fq_len,
594                    expected: sub!(n_families, 1),
595                });
596            }
597        }
598        let proof_witness_lifted_evals = self
599            .proof_witness_lifted_evals
600            .iter()
601            .zip(&all_field_cfgs)
602            .map(|(polys, cfg)| {
603                polys
604                    .iter()
605                    .map(|poly| poly.try_map(|int| project_canonical(cfg, int)))
606                    .collect::<Result<Vec<_>, _>>()
607            })
608            .collect::<Result<Vec<_>, _>>()?;
609
610        Ok(VerifierPrimeProjected {
611            base: self.base,
612            proof_commitments: self.proof_commitments,
613            proof_ideal_check: project!(&field_cfg, self.proof_ideal_check)?,
614            proof_cpr: project!(&field_cfg, self.proof_cpr)?,
615            proof_combined_sumcheck: project!(&field_cfg, self.proof_combined_sumcheck)?,
616            proof_multipoint_eval: project!(&field_cfg, self.proof_multipoint_eval)?,
617            proof_witness_lifted_evals,
618            proof_witness_lifted_evals_pp: self.proof_witness_lifted_evals_pp,
619            proof_lookup_proof: match &self.proof_lookup_proof {
620                Some(p) => Some(project!(&field_cfg, p)?),
621                None => None,
622            },
623            proof_booleanity: match &self.proof_booleanity {
624                Some(p) => Some(project!(&field_cfg, p)?),
625                None => None,
626            },
627            proof_affine_booleanity: match &self.proof_affine_booleanity {
628                Some(p) => Some(project!(&field_cfg, p)?),
629                None => None,
630            },
631            proof_ideal_checks_fq: project_fq_vec!(self.proof_ideal_checks_fq)?,
632            proof_cpr_fq: project_fq_vec!(self.proof_cpr_fq)?,
633            proof_combined_sumchecks_fq: project_fq_vec!(self.proof_combined_sumchecks_fq)?,
634            proof_multipoint_evals_fq: project_fq_vec!(self.proof_multipoint_evals_fq)?,
635            field_cfg,
636            _phantom: PhantomData,
637        })
638    }
639}
640
641impl<'a, Zt, U, C, IdealOverF, const D: usize, const FD: usize>
642    VerifierPrimeProjected<'a, Zt, U, C, IdealOverF, D, FD>
643where
644    Zt: ZincTypes<D, FD>,
645    Zt::Int: ProjectableToField<C>,
646    <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<C>,
647    C: BaseFieldConfig<Integer = Zt::Fmod>
648        + ProjectPrimitiveIntegersWithConfig
649        + ProjectElementWithConfig<Zt::Int>
650        + ProjectElementWithConfig<Zt::CombR>
651        + ProjectElementWithConfig<Zt::Chal>
652        + Clone
653        + Send
654        + Sync
655        + 'static,
656    U: Uair + 'static,
657    IdealOverF: Ideal + for<'cfg> IdealCheck<DynamicPolynomialConfig<'cfg, C>>,
658{
659    /// Step 2: Ideal check verification.
660    ///
661    /// **Shared evaluation point.**
662    ///
663    /// Mirrors the prover: sample one shared $r \in [0, q^*)^\mu$ from the
664    /// transcript and lift it into each family's field via
665    /// `cfg.project`, then run the per-family verifications in
666    /// `family_idx` order.
667    ///
668    /// `project_fq_ideal` is only invoked when the UAIR declares at least
669    /// one prime; UAIRs with $Q[X]$ only constraints can pass
670    /// `|_, _| unreachable!()`.
671    #[allow(clippy::type_complexity)]
672    pub fn step2_ideal_check(
673        mut self,
674        project_ideal: impl Fn(&IdealOrZero<U::Ideal>, &C) -> IdealOverF,
675        project_fq_ideal: impl Fn(&IdealOrZero<U::FqIdeal>, &C) -> IdealOverF,
676    ) -> Result<VerifierIdealChecked<'a, Zt, U, C, IdealOverF, D, FD>, ProtocolError<C::Element>>
677    {
678        let num_constraints = count_constraints::<U>();
679        let primes = self.base.uair_signature.primes().to_vec();
680
681        if primes.len() != self.proof_ideal_checks_fq.len() {
682            // Honest prover always emits one proof per declared prime
683            return Err(ProtocolError::FqIdealCheck {
684                prime_idx: self.proof_ideal_checks_fq.len(),
685                q: "<Length mismatch>".to_owned(),
686                source: IdealCheckError::IdealCollectorError(
687                    ideal_check::BatchedIdealCheckError::LengthMismatch {
688                        num_ideals: primes.len(),
689                        provided_values: self.proof_ideal_checks_fq.len(),
690                    },
691                ),
692            });
693        }
694
695        // Rebuild per-family field configs, then sample the
696        // shared evaluation point from the transcript exactly as the
697        // prover does. Family 0 = Q[X] (random sampled prime); families
698        // i >= 1 = declared primes in `primes()` order.
699        let all_field_cfgs = build_all_cfgs::<C>(&self.base.uair_signature, self.field_cfg.clone());
700        let q_star_idx = shared_challenge::compute_q_star_idx::<C>(&all_field_cfgs);
701        let q_star_cfg = &all_field_cfgs[q_star_idx];
702        let shared_eval_points: Vec<Vec<C::Element>> =
703            shared_challenge::sample_shared_field_challenges::<C>(
704                &mut self.base.pcs_transcript.fs_transcript,
705                self.base.num_vars,
706                q_star_cfg,
707                &all_field_cfgs,
708            );
709
710        // Q[X]-family ideal check verification.
711        let mut ic_subclaims: Vec<ideal_check::VerifierSubclaim<C::Element>> =
712            Vec::with_capacity(all_field_cfgs.len());
713        let q_subclaim = IdealCheckProtocol::<U>::verify_as_subprotocol::<_, IdealOverF, _, _>(
714            &mut self.base.pcs_transcript.fs_transcript,
715            self.proof_ideal_check,
716            /* family_idx = */ 0,
717            num_constraints.q,
718            &shared_eval_points[0],
719            |ideal| project_ideal(ideal, &self.field_cfg),
720            |_| unreachable!("Q[X] family"),
721            &self.field_cfg,
722        )?;
723        ic_subclaims.push(q_subclaim);
724
725        // Per-prime F_q[X] ideal-check verifications.
726        // Subclaims are collected and threaded forward to step 4's per-prime CPR
727        // `prepare_verifier`.
728        for (prime_idx, (cfg_q_i, fq_proof)) in all_field_cfgs[1..]
729            .iter()
730            .zip(self.proof_ideal_checks_fq)
731            .enumerate()
732        {
733            let family_idx = add!(prime_idx, 1);
734            let fq_subclaim =
735                IdealCheckProtocol::<U>::verify_as_subprotocol::<_, IdealOverF, _, _>(
736                    &mut self.base.pcs_transcript.fs_transcript,
737                    fq_proof,
738                    family_idx,
739                    num_constraints.for_prime(prime_idx),
740                    &shared_eval_points[family_idx],
741                    |_| unreachable!("F_q[X] family"),
742                    |ideal| project_fq_ideal(ideal, cfg_q_i),
743                    cfg_q_i,
744                )
745                .map_err(|source| ProtocolError::FqIdealCheck {
746                    prime_idx,
747                    q: cfg_q_i.modulus().to_string(),
748                    source,
749                })?;
750            ic_subclaims.push(fq_subclaim);
751        }
752
753        Ok(VerifierIdealChecked {
754            base: self.base,
755            field_cfg: self.field_cfg,
756            all_field_cfgs,
757            q_star_idx,
758            ic_subclaims,
759            proof_commitments: self.proof_commitments,
760            proof_cpr: self.proof_cpr,
761            proof_combined_sumcheck: self.proof_combined_sumcheck,
762            proof_multipoint_eval: self.proof_multipoint_eval,
763            proof_witness_lifted_evals: self.proof_witness_lifted_evals,
764            proof_witness_lifted_evals_pp: self.proof_witness_lifted_evals_pp,
765            proof_lookup_proof: self.proof_lookup_proof,
766            proof_booleanity: self.proof_booleanity,
767            proof_affine_booleanity: self.proof_affine_booleanity,
768            proof_cpr_fq: self.proof_cpr_fq,
769            proof_combined_sumchecks_fq: self.proof_combined_sumchecks_fq,
770            proof_multipoint_evals_fq: self.proof_multipoint_evals_fq,
771            _phantom: PhantomData,
772        })
773    }
774}
775
776impl<'a, Zt, U, C, IdealOverF, const D: usize, const FD: usize>
777    VerifierIdealChecked<'a, Zt, U, C, IdealOverF, D, FD>
778where
779    Zt: ZincTypes<D, FD>,
780    C: BaseFieldConfig<Integer = Zt::Fmod>
781        + ProjectElementWithConfig<Zt::Chal>
782        + Clone
783        + Send
784        + Sync
785        + 'static,
786    U: Uair<Prime = Zt::Fmod> + 'static,
787    IdealOverF: Ideal,
788{
789    /// Step 3: Evaluation projection. Consumes `project_scalar`.
790    ///
791    /// **Shared projecting element.**
792    ///
793    /// Mirrors the prover: sample a shared integer $a \in [0, q^*)$ and lift it
794    /// into each family's field. The $Q[X]$ family consumes
795    /// `projecting_elements[0]`; per-prime families consume
796    /// `projecting_elements[i + 1]`.
797    ///
798    /// Also builds per-prime $\psi$-projected scalars from
799    /// `project_scalar(., all_field_cfgs[i + 1])` and threads them
800    /// forward as `projected_scalars_f_fq` for the per-prime CPR
801    /// `finalize_verifier` in step 4.
802    pub fn step3_eval_projection(
803        mut self,
804        project_scalar: impl Fn(&U::Scalar, &C) -> DynamicPolynomial<C::Element>,
805    ) -> Result<VerifierEvalProjected<'a, Zt, U, C, IdealOverF, D, FD>, ProtocolError<C::Element>>
806    {
807        let q_star_cfg = &self.all_field_cfgs[self.q_star_idx];
808        let projecting_elements: Vec<C::Element> =
809            shared_challenge::sample_shared_field_challenge::<C>(
810                &mut self.base.pcs_transcript.fs_transcript,
811                q_star_cfg,
812                &self.all_field_cfgs,
813            );
814
815        // Q[X] family.
816        let projected_scalars_fx =
817            project_scalars::<C, U>(&self.field_cfg, |s| project_scalar(s, &self.field_cfg));
818        let projected_scalars_f = project_scalars_to_field(
819            &self.field_cfg,
820            projected_scalars_fx,
821            &projecting_elements[0],
822        )
823        .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?;
824
825        // Per-prime F_q[X] families.
826        let projected_scalars_f_fq: Result<Vec<ProjectedScalars<U::Scalar, C::Element>>, _> = self
827            .all_field_cfgs
828            .iter()
829            .zip(projecting_elements.iter())
830            .skip(1) // Skip Q[X] family
831            .map(|(cfg_i, projecting_element)| {
832                let projected_scalars_fx_i =
833                    project_scalars::<C, U>(cfg_i, |s| project_scalar(s, cfg_i));
834                project_scalars_to_field(cfg_i, projected_scalars_fx_i, projecting_element)
835                    .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))
836            })
837            .collect();
838        let projected_scalars_f_fq = projected_scalars_f_fq?;
839
840        Ok(VerifierEvalProjected {
841            base: self.base,
842            field_cfg: self.field_cfg,
843            all_field_cfgs: self.all_field_cfgs,
844            q_star_idx: self.q_star_idx,
845            ic_subclaims: self.ic_subclaims,
846            projecting_elements,
847            projected_scalars_f,
848            projected_scalars_f_fq,
849            proof_commitments: self.proof_commitments,
850            proof_cpr: self.proof_cpr,
851            proof_combined_sumcheck: self.proof_combined_sumcheck,
852            proof_multipoint_eval: self.proof_multipoint_eval,
853            proof_witness_lifted_evals: self.proof_witness_lifted_evals,
854            proof_witness_lifted_evals_pp: self.proof_witness_lifted_evals_pp,
855            proof_lookup_proof: self.proof_lookup_proof,
856            proof_booleanity: self.proof_booleanity,
857            proof_affine_booleanity: self.proof_affine_booleanity,
858            proof_cpr_fq: self.proof_cpr_fq,
859            proof_combined_sumchecks_fq: self.proof_combined_sumchecks_fq,
860            proof_multipoint_evals_fq: self.proof_multipoint_evals_fq,
861            _phantom: PhantomData,
862        })
863    }
864}
865
866impl<'a, Zt, U, C, IdealOverF, const D: usize, const FD: usize>
867    VerifierEvalProjected<'a, Zt, U, C, IdealOverF, D, FD>
868where
869    Zt: ZincTypes<D, FD>,
870    Zt::Int: ProjectableToField<C>,
871    <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<C>,
872    C: BaseFieldConfig<Integer = Zt::Fmod>
873        + ProjectPrimitiveIntegersWithConfig
874        + ProjectElementWithConfig<Zt::Int>
875        + ProjectElementWithConfig<Zt::CombR>
876        + ProjectElementWithConfig<Zt::Chal>
877        + Clone
878        + Send
879        + Sync
880        + 'static,
881    U: Uair<Prime = Zt::Fmod> + 'static,
882    IdealOverF: Ideal,
883{
884    /// Step 4: Sumcheck verification (CPR + optional booleanity +
885    /// future lookup groups), followed by the squeeze of the bridge
886    /// challenge $\alpha'$ when booleanity ran.
887    pub fn step4_sumcheck_verify(
888        mut self,
889    ) -> Result<VerifierSumchecked<'a, Zt, C, IdealOverF, D, FD>, ProtocolError<C::Element>> {
890        let num_constraints = count_constraints::<U>();
891
892        // Per-prime sub-proof length sanity check
893        let n_fq = self.all_field_cfgs.len().saturating_sub(1);
894        if self.proof_cpr_fq.len() != n_fq || self.proof_combined_sumchecks_fq.len() != n_fq {
895            return Err(ProtocolError::FqIdealCheck {
896                prime_idx: self.proof_cpr_fq.len(),
897                q: "<fq sub-proof length mismatch>".to_owned(),
898                source: IdealCheckError::IdealCollectorError(
899                    ideal_check::BatchedIdealCheckError::LengthMismatch {
900                        num_ideals: n_fq,
901                        provided_values: self.proof_cpr_fq.len(),
902                    },
903                ),
904            });
905        }
906
907        // Sample one shared CPR batching challenge \alpha in [0, q*)
908        let q_star_cfg = &self.all_field_cfgs[self.q_star_idx];
909        let folding_challenges: Vec<C::Element> =
910            shared_challenge::sample_shared_field_challenge::<C>(
911                &mut self.base.pcs_transcript.fs_transcript,
912                q_star_cfg,
913                &self.all_field_cfgs,
914            );
915
916        // -------- Q[X] family: CPR pre-sumcheck ------------------------
917        let q_cpr_verifier_ancillary = CombinedPolyResolver::prepare_verifier::<U>(
918            &self.proof_cpr,
919            self.proof_combined_sumcheck.claimed_sums()[0].clone(),
920            &self.ic_subclaims[0],
921            num_constraints.q,
922            self.base.num_vars,
923            &self.projecting_elements[0],
924            &folding_challenges[0],
925            &self.field_cfg,
926        )?;
927
928        // Booleanity pre-sumcheck: squeezes the zerocheck point `r`
929        // (num_vars field elements) and the batching challenge `alpha`,
930        // in that order.
931        let sig = self.base.uair_signature.clone();
932        let num_pub_bin = sig.public_cols().num_binary_poly_cols();
933        let num_total_bin = sig.total_cols().num_binary_poly_cols();
934        let num_wit_bin = num_total_bin.saturating_sub(num_pub_bin);
935        let num_affine_virtuals = sig.affine_virtual_specs().len();
936        let mut bool_group_idx = 1;
937
938        let bool_verifier_ancillary = prepare_booleanity_verifier_group(
939            &mut self.base.pcs_transcript.fs_transcript,
940            self.proof_combined_sumcheck.claimed_sums(),
941            &mut bool_group_idx,
942            num_wit_bin,
943            D,
944            self.base.num_vars,
945            &self.field_cfg,
946        )?;
947        let affine_bool_verifier_ancillary = prepare_booleanity_verifier_group(
948            &mut self.base.pcs_transcript.fs_transcript,
949            self.proof_combined_sumcheck.claimed_sums(),
950            &mut bool_group_idx,
951            num_affine_virtuals,
952            D,
953            self.base.num_vars,
954            &self.field_cfg,
955        )?;
956
957        // -------- Per-prime families: CPR pre-sumcheck ------------------
958        let mut fq_cpr_ancillaries: Vec<_> = Vec::with_capacity(n_fq);
959        for prime_idx in 0..n_fq {
960            let family_idx = add!(prime_idx, 1);
961            let cfg_i = &self.all_field_cfgs[family_idx];
962            let anc_i = CombinedPolyResolver::prepare_verifier::<U>(
963                &self.proof_cpr_fq[prime_idx],
964                self.proof_combined_sumchecks_fq[prime_idx].claimed_sums()[0].clone(),
965                &self.ic_subclaims[family_idx],
966                num_constraints.for_prime(prime_idx),
967                self.base.num_vars,
968                &self.projecting_elements[family_idx],
969                &folding_challenges[family_idx],
970                cfg_i,
971            )?;
972            fq_cpr_ancillaries.push(anc_i);
973        }
974
975        // -------- Lockstep multi-degree sumcheck verify ----------------
976        // Family 0 = Q[X] (CPR + optional booleanity);
977        // Families i >= 1 = per-prime CPR.
978        let mut family_proofs: Vec<(&MultiDegreeSumcheckProof<C::Element>, &C)> =
979            Vec::with_capacity(add!(n_fq, 1));
980        family_proofs.push((&self.proof_combined_sumcheck, &self.field_cfg));
981        for prime_idx in 0..n_fq {
982            let family_idx = add!(prime_idx, 1);
983            family_proofs.push((
984                &self.proof_combined_sumchecks_fq[prime_idx],
985                &self.all_field_cfgs[family_idx],
986            ));
987        }
988        let all_md_subclaims = MultiDegreeSumcheck::verify_as_subprotocol(
989            &mut self.base.pcs_transcript.fs_transcript,
990            self.base.num_vars,
991            &family_proofs,
992            q_star_cfg,
993        )
994        .map_err(CombinedPolyResolverError::SumcheckError)?;
995
996        // -------- Q[X] family finalize ---------------------------------
997        let q_md_subclaims = all_md_subclaims
998            .first()
999            .expect("Q[X] family subclaim always present");
1000
1001        let cpr_subclaim = CombinedPolyResolver::finalize_verifier::<U>(
1002            &mut self.base.pcs_transcript.fs_transcript,
1003            self.proof_cpr,
1004            q_md_subclaims.point().to_vec(),
1005            q_md_subclaims.expected_evaluations()[0].clone(),
1006            q_cpr_verifier_ancillary,
1007            &self.projected_scalars_f,
1008            /* family_idx = */ 0,
1009            &self.field_cfg,
1010        )?;
1011
1012        // Booleanity -> multipoint-eval `alpha_prime` bridge.
1013        // After `finalize_verifier` (residue check + transcript absorption of
1014        // `bit_slice_evals`), squeeze `alpha_prime` and carry both forward to the
1015        // next step.
1016        let mut bool_group_idx = 1;
1017        let bool_bit_slice_evals = finalize_booleanity_verifier_group(
1018            &mut self.base.pcs_transcript.fs_transcript,
1019            self.proof_booleanity.take(),
1020            q_md_subclaims.point(),
1021            q_md_subclaims.expected_evaluations(),
1022            &mut bool_group_idx,
1023            bool_verifier_ancillary,
1024            &self.field_cfg,
1025        )?;
1026        let affine_bool_bit_slice_evals = finalize_booleanity_verifier_group(
1027            &mut self.base.pcs_transcript.fs_transcript,
1028            self.proof_affine_booleanity.take(),
1029            q_md_subclaims.point(),
1030            q_md_subclaims.expected_evaluations(),
1031            &mut bool_group_idx,
1032            affine_bool_verifier_ancillary,
1033            &self.field_cfg,
1034        )?;
1035
1036        // -------- Per-prime family finalize ---------------------------
1037        // Mirror the prover's loop: pop next subclaim per family, call
1038        // `finalize_verifier` under that family's cfg + projected scalars.
1039        // Capture per-family CPR subclaims so step 5 (lockstep MP-eval)
1040        // can feed them per family.
1041        let n_fq = self.all_field_cfgs.len().saturating_sub(1);
1042        let mut cpr_eval_points_fq: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1043        let mut cpr_up_evals_fq: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1044        let mut cpr_bit_op_evals_fq: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1045        let mut cpr_down_evals_fq: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1046        for (prime_idx, ((cpr_proof_i, cpr_ancillary_i), md_subclaims_i)) in self
1047            .proof_cpr_fq
1048            .into_iter()
1049            .zip(fq_cpr_ancillaries)
1050            .zip(all_md_subclaims.iter().skip(1))
1051            .enumerate()
1052        {
1053            let family_idx = add!(prime_idx, 1);
1054            let cfg_i = &self.all_field_cfgs[family_idx];
1055            let cpr_subclaim_i = CombinedPolyResolver::finalize_verifier::<U>(
1056                &mut self.base.pcs_transcript.fs_transcript,
1057                cpr_proof_i,
1058                md_subclaims_i.point().to_vec(),
1059                md_subclaims_i.expected_evaluations()[0].clone(),
1060                cpr_ancillary_i,
1061                &self.projected_scalars_f_fq[prime_idx],
1062                family_idx,
1063                cfg_i,
1064            )?;
1065            cpr_eval_points_fq.push(cpr_subclaim_i.evaluation_point);
1066            cpr_up_evals_fq.push(cpr_subclaim_i.up_evals);
1067            cpr_bit_op_evals_fq.push(cpr_subclaim_i.bit_op_evals);
1068            cpr_down_evals_fq.push(cpr_subclaim_i.down_evals);
1069        }
1070
1071        // Squeeze alpha_prime in the same transcript order as the prover.
1072        let alpha_prime_f: Option<C::Element> =
1073            (bool_bit_slice_evals.is_some() || affine_bool_bit_slice_evals.is_some()).then(|| {
1074                self.base
1075                    .pcs_transcript
1076                    .fs_transcript
1077                    .get_field_challenge(&self.field_cfg)
1078            });
1079
1080        let cpr_eval_point = cpr_subclaim.evaluation_point;
1081
1082        assert!(
1083            self.proof_lookup_proof.is_none(),
1084            "Arbitrary lookup argument is not supported yet!"
1085        );
1086
1087        Ok(VerifierSumchecked {
1088            base: self.base,
1089            field_cfg: self.field_cfg,
1090            all_field_cfgs: self.all_field_cfgs,
1091            q_star_idx: self.q_star_idx,
1092            projecting_elements: self.projecting_elements,
1093            cpr_eval_point,
1094            cpr_up_evals: cpr_subclaim.up_evals,
1095            cpr_bit_op_evals: cpr_subclaim.bit_op_evals,
1096            cpr_down_evals: cpr_subclaim.down_evals,
1097            cpr_eval_points_fq,
1098            cpr_up_evals_fq,
1099            cpr_bit_op_evals_fq,
1100            cpr_down_evals_fq,
1101            bool_bit_slice_evals,
1102            affine_bool_bit_slice_evals,
1103            alpha_prime_f,
1104            proof_commitments: self.proof_commitments,
1105            proof_multipoint_eval: self.proof_multipoint_eval,
1106            proof_multipoint_evals_fq: self.proof_multipoint_evals_fq,
1107            proof_witness_lifted_evals: self.proof_witness_lifted_evals,
1108            proof_witness_lifted_evals_pp: self.proof_witness_lifted_evals_pp,
1109            proof_lookup_proof: self.proof_lookup_proof,
1110            _phantom: PhantomData,
1111        })
1112    }
1113}
1114
1115impl<'a, Zt, C, IdealOverF, const D: usize, const FD: usize>
1116    VerifierSumchecked<'a, Zt, C, IdealOverF, D, FD>
1117where
1118    Zt: ZincTypes<D, FD>,
1119    C: BaseFieldConfig<Integer = Zt::Fmod>
1120        + ProjectPrimitiveIntegersWithConfig
1121        + Clone
1122        + Send
1123        + Sync
1124        + 'static,
1125    IdealOverF: Ideal,
1126{
1127    /// Step 5: Multi-point evaluation sumcheck.
1128    ///
1129    /// When the booleanity argument ran, this step appends one extra
1130    /// scalar up_eval $c_j = \sum_i b_{j,i}\,(\alpha')^i$ per witness
1131    /// binary-poly column (derived from the in-flight `bit_slice_evals`).
1132    /// These collapse the bit-slice claims at $r^\star$ into the
1133    /// multipoint-eval consistency equation; the matching
1134    /// $\alpha'$-projected `open_evals` are produced in step 6.
1135    /// `down_evals` are passed through unchanged.
1136    #[allow(clippy::arithmetic_side_effects)]
1137    pub fn step5_multipoint_eval<U: Uair>(
1138        mut self,
1139    ) -> Result<VerifierMultipointEvaled<'a, Zt, C, IdealOverF, D, FD>, ProtocolError<C::Element>>
1140    {
1141        // Length-mismatch guard
1142        let n_fq = self.cpr_eval_points_fq.len();
1143        if self.proof_multipoint_evals_fq.len() != n_fq {
1144            return Err(ProtocolError::FqIdealCheck {
1145                prime_idx: self.proof_multipoint_evals_fq.len(),
1146                q: "<fq mp-eval sub-proof length mismatch>".to_owned(),
1147                source: IdealCheckError::IdealCollectorError(
1148                    ideal_check::BatchedIdealCheckError::LengthMismatch {
1149                        num_ideals: n_fq,
1150                        provided_values: self.proof_multipoint_evals_fq.len(),
1151                    },
1152                ),
1153            });
1154        }
1155
1156        // Q[X] family up_evals, extended only by the committed-column
1157        // Booleanity bridge. Affine virtuals stay separate and are checked
1158        // directly against their source-column projections below.
1159        let sig = self.base.uair_signature.clone();
1160        let num_pub_bin = sig.public_cols().num_binary_poly_cols();
1161        let num_total_bin = sig.total_cols().num_binary_poly_cols();
1162        let num_wit_bin = num_total_bin.saturating_sub(num_pub_bin);
1163        let num_affine_virtuals = sig.affine_virtual_specs().len();
1164        let alpha_prime = self.alpha_prime_f.as_ref();
1165
1166        let witness_bridge_evals = if num_wit_bin == 0 {
1167            Vec::new()
1168        } else {
1169            collapse_bit_slice_evals::<C, D>(
1170                self.bool_bit_slice_evals
1171                    .as_ref()
1172                    .ok_or(ProtocolError::BooleanityProofMissing)?,
1173                num_wit_bin,
1174                alpha_prime.ok_or(ProtocolError::BooleanityProofMissing)?,
1175                &self.field_cfg,
1176            )
1177        };
1178
1179        if num_affine_virtuals > 0 {
1180            let alpha_prime = alpha_prime.ok_or(ProtocolError::BooleanityProofMissing)?;
1181            let alpha_powers = powers(&self.field_cfg, alpha_prime, D);
1182            let mut source_bridge_evals: Vec<C::Element> = self
1183                .base
1184                .public_trace
1185                .binary_poly
1186                .iter()
1187                .map(|col| {
1188                    project_binary_col_at_field::<C, D>(col, &alpha_powers, &self.field_cfg)
1189                        .evaluate(&self.field_cfg, &self.cpr_eval_point)
1190                        .map_err(ProtocolError::AffineVirtualSourceProjection)
1191                })
1192                .collect::<Result<_, _>>()?;
1193            debug_assert_eq!(source_bridge_evals.len(), num_pub_bin);
1194            source_bridge_evals.extend(witness_bridge_evals.iter().cloned());
1195
1196            let affine_bridge_evals = collapse_bit_slice_evals::<C, D>(
1197                self.affine_bool_bit_slice_evals
1198                    .as_ref()
1199                    .ok_or(ProtocolError::BooleanityProofMissing)?,
1200                num_affine_virtuals,
1201                alpha_prime,
1202                &self.field_cfg,
1203            );
1204            let expected_affine_evals = expected_affine_virtual_bridge_evals::<C, _, D>(
1205                &sig,
1206                &source_bridge_evals,
1207                alpha_prime,
1208                &self.field_cfg,
1209            );
1210            for (spec_index, (got, expected)) in affine_bridge_evals
1211                .into_iter()
1212                .zip(expected_affine_evals)
1213                .enumerate()
1214            {
1215                if got != expected {
1216                    return Err(ProtocolError::AffineVirtualBridgeMismatch {
1217                        spec_index,
1218                        got,
1219                        expected,
1220                    });
1221                }
1222            }
1223        }
1224
1225        let q_up_evals: Vec<C::Element> = self
1226            .cpr_up_evals
1227            .iter()
1228            .cloned()
1229            .chain(witness_bridge_evals)
1230            .collect();
1231
1232        // Per-prime families: zero-pad up_evals to match Q's column count
1233        //
1234        // Witness binary-poly columns live in Q[X] only.
1235        // Pad F_q[X] evals with zeros to share one lockstep `gammas` vector and one
1236        // shared r_0 across all families.
1237        let fq_up_evals_ext: Vec<Vec<C::Element>> = (0..n_fq)
1238            .map(|prime_idx| {
1239                let family_idx = add!(prime_idx, 1);
1240                let zero_i = self.all_field_cfgs[family_idx].zero();
1241                let mut up_i = self.cpr_up_evals_fq[prime_idx].clone();
1242                up_i.extend((0..num_wit_bin).map(|_| zero_i.clone()));
1243                up_i
1244            })
1245            .collect();
1246
1247        let shifts = self.base.uair_signature.shifts();
1248        let num_vars = self.base.num_vars;
1249        let q_star_cfg = self.all_field_cfgs[self.q_star_idx].clone();
1250
1251        // Single (n+1)-family lockstep MP-eval verifier
1252        let mut all_proofs: Vec<multipoint_eval::Proof<C::Element>> =
1253            Vec::with_capacity(add!(n_fq, 1));
1254        all_proofs.push(self.proof_multipoint_eval);
1255        all_proofs.extend(self.proof_multipoint_evals_fq);
1256
1257        let mut all_families: Vec<MultipointEvalFamilyInputs<'_, C>> =
1258            Vec::with_capacity(add!(n_fq, 1));
1259        // The verifier-side MP precheck only consumes the claimed eval vectors;
1260        // bit-op MLE bodies exist on the prover side only.
1261        all_families.push(MultipointEvalFamilyInputs {
1262            field_cfg: &self.field_cfg,
1263            trace_mles: &[],
1264            bit_op_mles: &[],
1265            eval_point: &self.cpr_eval_point,
1266            up_evals: &q_up_evals,
1267            bit_op_evals: &self.cpr_bit_op_evals,
1268            down_evals: &self.cpr_down_evals,
1269        });
1270
1271        for (prime_idx, up_evals) in fq_up_evals_ext.iter().enumerate() {
1272            let family_idx = add!(prime_idx, 1);
1273            all_families.push(MultipointEvalFamilyInputs {
1274                field_cfg: &self.all_field_cfgs[family_idx],
1275                trace_mles: &[],
1276                bit_op_mles: &[],
1277                eval_point: &self.cpr_eval_points_fq[prime_idx],
1278                up_evals,
1279                bit_op_evals: &self.cpr_bit_op_evals_fq[prime_idx],
1280                down_evals: &self.cpr_down_evals_fq[prime_idx],
1281            });
1282        }
1283
1284        let mut subclaims_iter = MultipointEval::verify_as_subprotocol(
1285            &mut self.base.pcs_transcript.fs_transcript,
1286            all_proofs,
1287            all_families,
1288            shifts,
1289            num_vars,
1290            &q_star_cfg,
1291        )?
1292        .into_iter();
1293
1294        let mp_subclaim = subclaims_iter.next().expect("Q-family subclaim present");
1295        let mp_subclaims_fq: Vec<multipoint_eval::Subclaim<C::Element>> = subclaims_iter.collect();
1296        debug_assert_eq!(mp_subclaims_fq.len(), n_fq);
1297
1298        Ok(VerifierMultipointEvaled {
1299            base: self.base,
1300            field_cfg: self.field_cfg,
1301            all_field_cfgs: self.all_field_cfgs,
1302            projecting_elements: self.projecting_elements,
1303            alpha_prime_f: self.alpha_prime_f,
1304            mp_subclaim,
1305            mp_subclaims_fq,
1306            proof_commitments: self.proof_commitments,
1307            proof_witness_lifted_evals: self.proof_witness_lifted_evals,
1308            proof_witness_lifted_evals_pp: self.proof_witness_lifted_evals_pp,
1309            proof_lookup_proof: self.proof_lookup_proof,
1310            _phantom: PhantomData,
1311        })
1312    }
1313}
1314
1315impl<'a, Zt, C, IdealOverF, const D: usize, const FD: usize>
1316    VerifierMultipointEvaled<'a, Zt, C, IdealOverF, D, FD>
1317where
1318    Zt: ZincTypes<D, FD>,
1319    Zt::Int: ProjectableToField<C>,
1320    <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<C>,
1321    C: BaseFieldConfig<Integer = Zt::Fmod>
1322        + ProjectPrimitiveIntegersWithConfig
1323        + ProjectElementWithConfig<Zt::Int>
1324        + ProjectElementWithConfig<Zt::Chal>
1325        + Clone
1326        + Send
1327        + Sync
1328        + 'static,
1329    IdealOverF: Ideal,
1330{
1331    /// Step 6: Per-family lifted-eval consistency check.
1332    ///
1333    /// **When $F_q[X]$ constraints are present:**
1334    ///
1335    /// 1. **Sample $q''$** (mirror of prover step 7 start).
1336    /// 2. For each family $i \in \{0, ..., n\}$ (Q + declared primes):
1337    ///    - Recompute the public-column lifted evals under family $i$'s cfg,
1338    ///      interleave with the sent witness-only lifted evals to get the full
1339    ///      all-column lifted-eval list `all_lifted_evals[i]`.
1340    ///    - Project each lifted eval at `projecting_elements[i]` to get
1341    ///      `open_evals_i`.
1342    ///    - For Q-family (i = 0) when booleanity ran: append $\alpha'$
1343    ///      projections of witness-bin lifts.
1344    ///    - For F_q families (i >= 1): append zero entries (matching the
1345    ///      zero-padded MP `up_evals` on F_q families).
1346    ///    - Call `MultipointEval::verify_subclaim` against the matching MP
1347    ///      subclaim, family by family.
1348    /// 3. Also recompute the $q''$-family lifted evals and absorb every
1349    ///    family's coefficients into the FS transcript in the same order as the
1350    ///    prover.
1351    ///
1352    /// **When only $Q[X]$ constraints are present:**
1353    ///
1354    /// We alias $q'' := q_0$ and $r^* := r_0$ instead of sampling $q''$
1355    /// randomly. The rest proceeds as described above, but we use
1356    /// `proof_witness_lifted_evals[0]` instead of
1357    /// `proof_witness_lifted_evals_pp`.
1358    ///
1359    /// **Soundness**: per-family arithmetic in single prime fields. With
1360    /// the shared $r_0$ integer endpoint, the $n + 1$ constraint-family
1361    /// `verify_subclaim` calls each bind their family's lifted eval to
1362    /// that family's residue of the committed trace; the $q''$-family
1363    /// lifted eval is bound independently by the PCS open in step 7.
1364    #[allow(clippy::arithmetic_side_effects, clippy::too_many_lines)]
1365    pub fn step6_lifted_evals<U: Uair>(
1366        mut self,
1367    ) -> Result<VerifierLiftedEvalsChecked<'a, Zt, C, IdealOverF, D, FD>, ProtocolError<C::Element>>
1368    {
1369        let n_fq = self.mp_subclaims_fq.len();
1370        // Expected length of `proof_witness_lifted_evals`: one entry per
1371        // constraint family (Q-family at index 0 plus the `n_fq` declared
1372        // primes at indices 1..=n_fq).
1373        let n_families = add!(n_fq, 1);
1374
1375        // Length-mismatch guard.
1376        if self.proof_witness_lifted_evals.len() != n_families {
1377            return Err(ProtocolError::FqIdealCheck {
1378                prime_idx: self.proof_witness_lifted_evals.len(),
1379                q: "<witness-lifted-evals family count mismatch>".to_owned(),
1380                source: IdealCheckError::IdealCollectorError(
1381                    ideal_check::BatchedIdealCheckError::LengthMismatch {
1382                        num_ideals: n_families,
1383                        provided_values: self.proof_witness_lifted_evals.len(),
1384                    },
1385                ),
1386            });
1387        }
1388
1389        let pub_cols = self.base.uair_signature.public_cols();
1390        let num_pub_bin = pub_cols.num_binary_poly_cols();
1391        let num_pub_arb = pub_cols.num_arbitrary_poly_cols();
1392        let num_pub_int = pub_cols.num_int_cols();
1393
1394        let wit_cols = self.base.uair_signature.witness_cols();
1395        let num_wit_bin = wit_cols.num_binary_poly_cols();
1396        let num_wit_arb = wit_cols.num_arbitrary_poly_cols();
1397        let num_wit_int = wit_cols.num_int_cols();
1398
1399        // Since the entire vector is absorbed into the FS transcript below,
1400        // prevent a malicious prover from adding entries not tied to a
1401        // commitment.
1402        let num_wit_total = add!(add!(num_wit_bin, num_wit_arb), num_wit_int);
1403
1404        for (family_idx, witness_lifted_i) in self.proof_witness_lifted_evals.iter().enumerate() {
1405            if witness_lifted_i.len() != num_wit_total {
1406                return Err(ProtocolError::WitnessLiftedEvalsLengthMismatch {
1407                    family_idx,
1408                    got: witness_lifted_i.len(),
1409                    expected: num_wit_total,
1410                });
1411            }
1412        }
1413
1414        let expected_pp_len = if n_fq == 0 { 0 } else { num_wit_total };
1415        let actual_pp_len = self
1416            .proof_witness_lifted_evals_pp
1417            .as_ref()
1418            .map(|v| v.len())
1419            .unwrap_or(0);
1420        if actual_pp_len != expected_pp_len {
1421            return Err(ProtocolError::WitnessLiftedEvalsPpLengthMismatch {
1422                got: actual_pp_len,
1423                expected: expected_pp_len,
1424            });
1425        }
1426
1427        // Sample q'' (mirror of prover step 7 start)
1428        //
1429        // With no F_q[X] constraints, q'' is aliased to q_0 and r* = r0.
1430        let q_pp_cfg = if n_fq == 0 {
1431            self.field_cfg.clone()
1432        } else {
1433            self.base
1434                .pcs_transcript
1435                .fs_transcript
1436                .get_random_field_cfg::<C, Zt::Fmod, Zt::PrimeTest>()
1437        };
1438
1439        // $q''$ is now known: project the last wire-form proof section,
1440        // rejecting non-canonical encodings.
1441        let proof_witness_lifted_evals_pp = self
1442            .proof_witness_lifted_evals_pp
1443            .as_ref()
1444            .map(|polys| {
1445                polys
1446                    .iter()
1447                    .map(|poly| poly.try_map(|int| project_canonical(&q_pp_cfg, int)))
1448                    .collect::<Result<Vec<_>, _>>()
1449            })
1450            .transpose()?;
1451        // r* = r0 mod q'' (the underlying integer of mp_subclaim.r0 is
1452        // the shared challenge in [0, q*); lift into q'' cfg).
1453        // Aliased to r0 when q'' = q0.
1454        let r_star: Vec<C::Element> = if n_fq == 0 {
1455            self.mp_subclaim.r0.clone()
1456        } else {
1457            self.mp_subclaim
1458                .r0
1459                .iter()
1460                .map(|x| q_pp_cfg.project(&self.field_cfg.lift(x)))
1461                .collect()
1462        };
1463
1464        // Helper: assemble all-column lifted evals from per-family public
1465        // (recomputed) + sent witness lifts.
1466        let assemble_all = |public_lifted: &[DynamicPolynomial<C::Element>],
1467                            witness_lifted: &[DynamicPolynomial<C::Element>]|
1468         -> Vec<DynamicPolynomial<C::Element>> {
1469            public_lifted[..num_pub_bin]
1470                .iter()
1471                .chain(&witness_lifted[..num_wit_bin])
1472                .chain(&public_lifted[num_pub_bin..add!(num_pub_bin, num_pub_arb)])
1473                .chain(&witness_lifted[num_wit_bin..add!(num_wit_bin, num_wit_arb)])
1474                .chain(&public_lifted[add!(num_pub_bin, num_pub_arb)..])
1475                .chain(&witness_lifted[add!(num_wit_bin, num_wit_arb)..])
1476                .cloned()
1477                .collect()
1478        };
1479
1480        // Helper: recompute public-only lifted evals under family i's cfg
1481        // at family i's r_0 endpoint.
1482        let recompute_public_lifted =
1483            |family_r0: &[C::Element], family_cfg: &C| -> Vec<DynamicPolynomial<C::Element>> {
1484                if add!(add!(num_pub_bin, num_pub_arb), num_pub_int) == 0 {
1485                    return Vec::new();
1486                }
1487                let projected_public = project_trace_coeffs_row_major::<C, Zt::Int, Zt::Int, D, D>(
1488                    self.base.public_trace,
1489                    family_cfg,
1490                );
1491                compute_lifted_evals::<C, D>(
1492                    family_r0,
1493                    &self.base.public_trace.binary_poly,
1494                    &ProjectedTrace::RowMajor(projected_public),
1495                    family_cfg,
1496                )
1497            };
1498
1499        // Q-family (i = 0)
1500        let q_r_0 = self.mp_subclaim.r0.clone();
1501        let q_public_lifted = recompute_public_lifted(&q_r_0, &self.field_cfg);
1502        let q_witness_lifted = self.proof_witness_lifted_evals[0].clone();
1503        let q_all_lifted = assemble_all(&q_public_lifted, &q_witness_lifted);
1504
1505        let q_poly_cfg = self.field_cfg.dyn_poly_cfg();
1506        let mut q_open_evals: Vec<C::Element> = q_all_lifted
1507            .iter()
1508            .map(|bar_u| {
1509                q_poly_cfg
1510                    .evaluate_at_point(bar_u, &self.projecting_elements[0])
1511                    .map_err(ProtocolError::LiftedEvalProjection)
1512            })
1513            .collect::<Result<Vec<_>, _>>()?;
1514
1515        // Booleanity bridge: append $\alpha'$-projected witness-bin lifts.
1516        if let Some(alpha_prime) = &self.alpha_prime_f {
1517            for bar_u in &q_witness_lifted[..num_wit_bin] {
1518                q_open_evals.push(
1519                    q_poly_cfg
1520                        .evaluate_at_point(bar_u, alpha_prime)
1521                        .map_err(ProtocolError::LiftedEvalProjection)?,
1522                );
1523            }
1524        }
1525
1526        MultipointEval::verify_subclaim(
1527            &self.mp_subclaim,
1528            &q_open_evals,
1529            &derive_bit_op_open_evals::<C, D>(
1530                self.base.uair_signature.bit_op_specs(),
1531                &q_all_lifted,
1532                &self.projecting_elements[0],
1533                &self.field_cfg,
1534            )?,
1535            self.base.uair_signature.shifts(),
1536            &self.field_cfg,
1537        )?;
1538
1539        // Per-prime families (i >= 1)
1540        for prime_idx in 0..n_fq {
1541            let family_idx = add!(prime_idx, 1);
1542            let cfg_i = &self.all_field_cfgs[family_idx];
1543            let r_0_i = self.mp_subclaims_fq[prime_idx].r0.clone();
1544            let witness_lifted_i = &self.proof_witness_lifted_evals[family_idx];
1545            let public_lifted_i = recompute_public_lifted(&r_0_i, cfg_i);
1546            let all_lifted_i = assemble_all(&public_lifted_i, witness_lifted_i);
1547
1548            let mut open_evals_i: Vec<C::Element> = all_lifted_i
1549                .iter()
1550                .map(|bar_u| {
1551                    cfg_i
1552                        .dyn_poly_cfg()
1553                        .evaluate_at_point(bar_u, &self.projecting_elements[family_idx])
1554                        .map_err(ProtocolError::LiftedEvalProjection)
1555                })
1556                .collect::<Result<Vec<_>, _>>()?;
1557
1558            // Booleanity-bridge slots on fq families are zero-padded
1559            // (witness-bin lives in Q[X] only).
1560            if self.alpha_prime_f.is_some() {
1561                let zero_i = cfg_i.zero();
1562                open_evals_i.extend((0..num_wit_bin).map(|_| zero_i.clone()));
1563            }
1564
1565            MultipointEval::verify_subclaim(
1566                &self.mp_subclaims_fq[prime_idx],
1567                &open_evals_i,
1568                &derive_bit_op_open_evals::<C, D>(
1569                    self.base.uair_signature.bit_op_specs(),
1570                    &all_lifted_i,
1571                    &self.projecting_elements[family_idx],
1572                    cfg_i,
1573                )?,
1574                self.base.uair_signature.shifts(),
1575                cfg_i,
1576            )?;
1577        }
1578
1579        // Absorb all families' coefficients into the FS transcript in the same uniform
1580        // order as the prover
1581        let mut transcription_buf: Vec<u8> = vec![0; C::Integer::NUM_BYTES];
1582        debug_assert_eq!(
1583            self.all_field_cfgs.len(),
1584            self.proof_witness_lifted_evals.len()
1585        );
1586        for (cfg_i, witness_lifted_i) in self
1587            .all_field_cfgs
1588            .iter()
1589            .zip(&self.proof_witness_lifted_evals)
1590        {
1591            for bar_u in witness_lifted_i {
1592                self.base
1593                    .pcs_transcript
1594                    .fs_transcript
1595                    .absorb_field_element_slice(cfg_i, &bar_u.coeffs, &mut transcription_buf);
1596            }
1597        }
1598        if let Some(ref lifted_evals_pp) = proof_witness_lifted_evals_pp {
1599            for bar_u in lifted_evals_pp.iter() {
1600                self.base
1601                    .pcs_transcript
1602                    .fs_transcript
1603                    .absorb_field_element_slice(&q_pp_cfg, &bar_u.coeffs, &mut transcription_buf);
1604            }
1605        }
1606
1607        // When q'' was aliased to q0, the PCS check reuses Q[X] family's witness lift.
1608        let lifted_evals_pp = if n_fq == 0 {
1609            self.proof_witness_lifted_evals[0].clone()
1610        } else {
1611            let Some(lifted_evals_pp) = proof_witness_lifted_evals_pp else {
1612                return Err(ProtocolError::WitnessLiftedEvalsPpLengthMismatch {
1613                    got: 0,
1614                    expected: expected_pp_len,
1615                });
1616            };
1617            lifted_evals_pp
1618        };
1619
1620        Ok(VerifierLiftedEvalsChecked {
1621            base: self.base,
1622            lifted_evals_pp,
1623            q_pp_cfg,
1624            r_star,
1625            proof_commitments: self.proof_commitments,
1626            proof_lookup_proof: self.proof_lookup_proof,
1627            _phantom: PhantomData,
1628        })
1629    }
1630}
1631
1632impl<'a, Zt, C, IdealOverF, const D: usize, const FD: usize>
1633    VerifierLiftedEvalsChecked<'a, Zt, C, IdealOverF, D, FD>
1634where
1635    Zt: ZincTypes<D, FD>,
1636    C: BaseFieldConfig<Integer = Zt::Fmod>
1637        + ProjectPrimitiveIntegersWithConfig
1638        + ProjectElementWithConfig<Zt::CombR>
1639        + ProjectElementWithConfig<Zt::Chal>
1640        + Clone
1641        + Send
1642        + Sync
1643        + 'static,
1644    IdealOverF: Ideal,
1645{
1646    /// Step 7: PCS verification at $r^\star := r_0 \bmod q''$,
1647    /// using the $q''$-family lifted evals sent by the prover and
1648    /// recovered into `lifted_evals_pp` during step 6.
1649    ///
1650    /// $q''$ was sampled at the start of step 6.
1651    /// Here we sample binary folding challenges under $q''$ and run the
1652    /// per-commitment PCS verify.
1653    ///
1654    /// Per-poly claims for `verify_with_alphas` are computed directly from
1655    /// `lifted_evals_pp[witness_range]` — no per-coefficient $\phi_{q''}$
1656    /// lift is needed because the prover already sent each $\bar
1657    /// u_j^{(q'')} \in F_{q''}[X]$.
1658    pub fn step7_pcs_verify<U: Uair, const CHECK_FOR_OVERFLOW: bool>(
1659        mut self,
1660    ) -> Result<VerifierPcsVerified<IdealOverF>, ProtocolError<C::Element>> {
1661        let commitments = &self.proof_commitments;
1662
1663        let wit_cols = self.base.uair_signature.witness_cols();
1664        let num_wit_bin = wit_cols.num_binary_poly_cols();
1665        let num_wit_arb = wit_cols.num_arbitrary_poly_cols();
1666
1667        let pcs_transcript = &mut self.base.pcs_transcript;
1668        let lifted_evals_pp = &self.lifted_evals_pp;
1669        let q_pp_cfg = &self.q_pp_cfg;
1670        let r_star = &self.r_star;
1671
1672        let zero = q_pp_cfg.zero();
1673
1674        macro_rules! verify_pcs_batch {
1675            // Non-folded variant
1676            ($Zt:ty, $Lc:ty, $vp:expr, $idx:tt, $pt:expr, [$evals_range:expr]) => {{
1677                verify_pcs_batch!(
1678                    $Zt,
1679                    $Lc,
1680                    $vp,
1681                    $idx,
1682                    $pt,
1683                    [$evals_range],
1684                    |bar_u: &DynamicPolynomial<C::Element>, alphas: &[_]| {
1685                        let mut eval_j = zero.clone();
1686                        for (coeff, alpha) in bar_u.coeffs.iter().zip(alphas.iter()) {
1687                            // bar_u is already in F_{q''}; just batch with alphas.
1688                            let term = q_pp_cfg.mul(&q_pp_cfg.project(alpha), coeff);
1689                            q_pp_cfg.add_assign(&mut eval_j, &term);
1690                        }
1691                        eval_j
1692                    }
1693                )
1694            }};
1695
1696            // Universal variant with custom eval_j computation (used for folded columns)
1697            ($Zt:ty, $Lc:ty, $vp:expr, $idx:tt, $pt:expr, [$evals_range:expr], $compute_eval_j:expr) => {{
1698                let comm = &commitments.$idx;
1699                if comm.batch_size > 0 {
1700                    let per_poly_alphas = ZipPlus::<$Zt, $Lc>::sample_alphas(
1701                        &mut pcs_transcript.fs_transcript,
1702                        comm.batch_size,
1703                    );
1704                    let mut eval_f = zero.clone();
1705                    for (bar_u, alphas) in lifted_evals_pp[$evals_range]
1706                        .iter()
1707                        .zip(per_poly_alphas.iter())
1708                    {
1709                        let eval_j = $compute_eval_j(bar_u, alphas);
1710                        q_pp_cfg.add_assign(&mut eval_f, &eval_j);
1711                    }
1712                    ZipPlus::<$Zt, $Lc>::verify_with_alphas::<C, CHECK_FOR_OVERFLOW>(
1713                        pcs_transcript,
1714                        $vp,
1715                        comm,
1716                        q_pp_cfg,
1717                        $pt,
1718                        &eval_f,
1719                        &per_poly_alphas,
1720                    )
1721                    .map_err(|e| ProtocolError::PcsVerification($idx, e))?;
1722                }
1723            }};
1724        }
1725
1726        // Folded witness columns are proved using the extended evaluation
1727        // point `r_star_ext = r_star || folding_challenges`.
1728        // Folding challenges are sampled fresh under q''.
1729        let num_folding_challenges = Zt::BinaryFold::FOLDING_FACTOR.ilog2();
1730        let folding_challenges = (0..num_folding_challenges)
1731            .map(|_| {
1732                let g_chal: Zt::Chal = pcs_transcript.fs_transcript.get_challenge();
1733                q_pp_cfg.project(&g_chal)
1734            })
1735            .collect_vec();
1736        let mut r_star_ext = r_star.clone();
1737        r_star_ext.extend_from_slice(&folding_challenges);
1738
1739        // Witness-only ranges inside `lifted_evals_pp` (layout
1740        // `[wit_bin..., wit_arb..., wit_int...]`, same as `witness_only`
1741        // in prover step 7).
1742        verify_pcs_batch!(
1743            Zt::BinaryZt,
1744            Zt::BinaryLc,
1745            self.base.vp_bin,
1746            0,
1747            &r_star_ext,
1748            [0..num_wit_bin],
1749            |bar_u: &DynamicPolynomial<C::Element>, alphas: &[_]| {
1750                // bar_u is already in F_{q''}; use coeffs directly.
1751                Zt::BinaryFold::fold_eval_claim(
1752                    &bar_u.coeffs,
1753                    alphas,
1754                    &folding_challenges,
1755                    q_pp_cfg,
1756                )
1757            }
1758        );
1759        verify_pcs_batch!(
1760            Zt::ArbitraryZt,
1761            Zt::ArbitraryLc,
1762            self.base.vp_arb,
1763            1,
1764            r_star,
1765            [num_wit_bin..add!(num_wit_bin, num_wit_arb)]
1766        );
1767        verify_pcs_batch!(
1768            Zt::IntZt,
1769            Zt::IntLc,
1770            self.base.vp_int,
1771            2,
1772            r_star,
1773            [add!(num_wit_bin, num_wit_arb)..]
1774        );
1775
1776        Ok(VerifierPcsVerified {
1777            pcs_transcript: self.base.pcs_transcript,
1778            _phantom: PhantomData,
1779        })
1780    }
1781}
1782
1783impl<IdealOverF: Ideal> VerifierPcsVerified<IdealOverF> {
1784    /// Complete verification.
1785    ///
1786    /// Asserts that the proof stream has been fully consumed.
1787    pub fn finish<E: SetElement>(self) -> Result<(), ProtocolError<E>> {
1788        self.pcs_transcript.check_eof()?;
1789        Ok(())
1790    }
1791}
1792
1793fn derive_bit_op_open_evals<C: BaseFieldConfig, const D: usize>(
1794    specs: &[zinc_uair::BitOpSpec],
1795    all_lifted: &[DynamicPolynomial<C::Element>],
1796    projecting_element: &C::Element,
1797    field_cfg: &C,
1798) -> Result<Vec<C::Element>, ProtocolError<C::Element>> {
1799    specs
1800        .iter()
1801        .map(|spec| {
1802            let source = &all_lifted[spec.source_col()];
1803            let transformed = spec.op().transform::<C, D>(source, field_cfg);
1804            field_cfg
1805                .dyn_poly_cfg()
1806                .evaluate_at_point(&transformed, projecting_element)
1807                .map_err(ProtocolError::LiftedEvalProjection)
1808        })
1809        .collect()
1810}
1811
1812//
1813// verify() wrapper
1814//
1815
1816impl<Zt, U, C, const D: usize, const FD: usize> ZincPlusPiop<Zt, U, C, D, FD>
1817where
1818    Zt: ZincTypes<D, FD>,
1819    Zt::Int: ProjectableToField<C>,
1820    <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<C>,
1821    C: BaseFieldConfig<Integer = Zt::Fmod>
1822        + ProjectPrimitiveIntegersWithConfig
1823        + ProjectElementWithConfig<Zt::Int>
1824        + ProjectElementWithConfig<Zt::CombR>
1825        + ProjectElementWithConfig<Zt::Chal>
1826        + Clone
1827        + Send
1828        + Sync
1829        + 'static,
1830    U: Uair<Prime = Zt::Fmod> + 'static,
1831{
1832    /// Zinc+ full PIOP verifier.
1833    ///
1834    /// Runs all verification steps in sequence and returns `Ok(())` on
1835    /// success. For per-step control, start with
1836    /// [`Self::step0_reconstruct_transcript`] and chain the individual
1837    /// `stepN_*` methods.
1838    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1839    pub fn verify<IdealOverF, const CHECK_FOR_OVERFLOW: bool>(
1840        vp: &(
1841            ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
1842            ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
1843            ZipPlusParams<Zt::IntZt, Zt::IntLc>,
1844        ),
1845        proof: Proof<Zt::Fmod>,
1846        public_trace: &UairTrace<Zt::Int, Zt::Int, D, D>,
1847        num_vars: usize,
1848        project_scalar: impl Fn(&U::Scalar, &C) -> DynamicPolynomial<C::Element>,
1849        project_ideal: impl Fn(&IdealOrZero<U::Ideal>, &C) -> IdealOverF,
1850        project_fq_ideal: impl Fn(&IdealOrZero<U::FqIdeal>, &C) -> IdealOverF,
1851    ) -> Result<(), ProtocolError<C::Element>>
1852    where
1853        IdealOverF: Ideal + for<'cfg> IdealCheck<DynamicPolynomialConfig<'cfg, C>>,
1854    {
1855        ZincPlusPiop::<Zt, U, C, D, FD>::step0_reconstruct_transcript::<IdealOverF>(
1856            vp,
1857            proof,
1858            public_trace,
1859            num_vars,
1860        )?
1861        .step1_prime_projection()?
1862        .step2_ideal_check(project_ideal, project_fq_ideal)?
1863        .step3_eval_projection(project_scalar)?
1864        .step4_sumcheck_verify()?
1865        .step5_multipoint_eval::<U>()?
1866        .step6_lifted_evals::<U>()?
1867        .step7_pcs_verify::<U, CHECK_FOR_OVERFLOW>()?
1868        .finish::<C::Element>()
1869    }
1870}
1871
1872/// Test-only accessors for internal state, needed for tampering tests.
1873#[cfg(test)]
1874pub mod test_helpers {
1875    use super::*;
1876
1877    #[cfg(test)]
1878    impl<'a, Zt: ZincTypes<D, FD>, const D: usize, const FD: usize> VerifierBase<'a, Zt, D, FD> {
1879        #[cfg(test)]
1880        pub fn fs_transcript_mut(&mut self) -> &mut Blake3Transcript {
1881            &mut self.pcs_transcript.fs_transcript
1882        }
1883    }
1884
1885    #[cfg(test)]
1886    impl<'a, Zt, U, C, IdealOverF, const D: usize, const FD: usize>
1887        VerifierEvalProjected<'a, Zt, U, C, IdealOverF, D, FD>
1888    where
1889        Zt: ZincTypes<D, FD>,
1890        C: BaseFieldConfig,
1891        U: Uair,
1892    {
1893        pub fn projecting_element_f(&self) -> &C::Element {
1894            &self.projecting_elements[0]
1895        }
1896
1897        pub fn field_cfg(&self) -> &C {
1898            &self.field_cfg
1899        }
1900
1901        pub fn fs_transcript_mut(&mut self) -> &mut Blake3Transcript {
1902            self.base.fs_transcript_mut()
1903        }
1904
1905        pub fn proof_combined_sumcheck(&self) -> &MultiDegreeSumcheckProof<C::Element> {
1906            &self.proof_combined_sumcheck
1907        }
1908
1909        pub fn ic_subclaim(&self) -> &ideal_check::VerifierSubclaim<C::Element> {
1910            &self.ic_subclaims[0]
1911        }
1912
1913        pub fn proof_cpr(&self) -> &CombinedPolyResolverProof<C::Element> {
1914            &self.proof_cpr
1915        }
1916
1917        pub fn num_vars(&self) -> usize {
1918            self.base.num_vars
1919        }
1920
1921        pub fn uair_signature(&self) -> &UairSignature<Zt::Fmod> {
1922            &self.base.uair_signature
1923        }
1924    }
1925}