Skip to main content

zinc_protocol/
prover.rs

1use super::*;
2use itertools::Itertools;
3use std::{
4    borrow::Cow,
5    fmt::{Debug, Display},
6};
7use zinc_piop::{
8    combined_poly_resolver::CombinedPolyResolver,
9    ideal_check::{IdealCheckProtocol, Proof as IdealCheckProof},
10    lookup::booleanity::{BoolProverAncillary, BooleanityChecker, BooleanityProof},
11    multipoint_eval::{MultipointEval, MultipointEvalFamilyInputs, Proof as MultipointEvalProof},
12    projections::{
13        ColumnMajorTrace, ProjectedScalars, ProjectedTrace, RowMajorTrace,
14        build_bit_op_virtual_mle, evaluate_trace_to_column_mles, project_scalars,
15        project_scalars_to_field, project_trace_coeffs_column_major,
16        project_trace_coeffs_row_major,
17    },
18    sumcheck::multi_degree::{MultiDegreeSumcheck, MultiDegreeSumcheckGroup},
19};
20use zinc_poly::{mle::DenseMultilinearExtension, univariate::dynamic::DynamicPolynomial};
21use zinc_transcript::traits::{ConstTranscribable, Transcript};
22use zinc_uair::{
23    Uair, UairSignature, UairTrace, constraint_counter::count_constraints,
24    degree_counter::count_max_degree,
25};
26use zinc_utils::{
27    add, cfg_iter, cfg_join, mul_by_scalar::MulByScalar, powers,
28    projectable_to_field::ProjectableToField,
29};
30use zip_plus::{
31    pcs::structs::{ZipPlus, ZipPlusHint, ZipPlusParams, ZipTypes},
32    pcs_transcript::PcsProverTranscript,
33};
34
35//
36// Type-state structs
37//
38
39/// Initial prover state, before commitment: the UAIR signature, the
40/// caller-provided original trace, and the folded witness trace.
41#[derive(Clone, Debug)]
42pub struct ProverFolded<
43    'a,
44    Zt: ZincTypes<D, FD>,
45    U: Uair,
46    C: BaseFieldConfig,
47    const D: usize,
48    const FD: usize,
49> {
50    uair_signature: UairSignature<Zt::Fmod>,
51    original_trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D, D>,
52    folded_witness_trace: UairTrace<'a, Zt::Int, Zt::Int, FD, D>,
53
54    _phantom: PhantomData<(&'a u8, U, C)>,
55}
56
57/// Persistent prover infrastructure carried across every subsequent
58/// step: the Fiat-Shamir transcript, PCS parameters/hints/commitments,
59/// and trace reference.
60/// Obtained after step 1 via [`step1_commit`](ProverFolded::step1_commit).
61#[derive(Clone, Debug)]
62pub struct ProverCommitted<
63    'a,
64    Zt: ZincTypes<D, FD>,
65    U: Uair,
66    C: BaseFieldConfig,
67    const D: usize,
68    const FD: usize,
69> {
70    num_vars: usize,
71    uair_signature: UairSignature<Zt::Fmod>,
72    original_trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D, D>,
73    folded_witness_trace: UairTrace<'a, Zt::Int, Zt::Int, FD, D>,
74    pcs_transcript: PcsProverTranscript,
75
76    // Commitment info
77    pp_bin: &'a ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
78    pp_arb: &'a ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
79    pp_int: &'a ZipPlusParams<Zt::IntZt, Zt::IntLc>,
80    hint_bin: Option<ZipPlusHint<<Zt::BinaryZt as ZipTypes>::Cw>>,
81    hint_arb: Option<ZipPlusHint<<Zt::ArbitraryZt as ZipTypes>::Cw>>,
82    hint_int: Option<ZipPlusHint<<Zt::IntZt as ZipTypes>::Cw>>,
83    commitment_bin: ZipPlusCommitment,
84    commitment_arb: ZipPlusCommitment,
85    commitment_int: ZipPlusCommitment,
86
87    _phantom: PhantomData<(U, C)>,
88}
89
90/// After step 2 via [`step2_combined`](ProverCommitted::step2_combined)
91/// (row-major / "combined" projection).
92#[derive(Clone, Debug)]
93pub struct ProverProjectedCombined<
94    'a,
95    Zt: ZincTypes<D, FD>,
96    U: Uair,
97    C: BaseFieldConfig,
98    const D: usize,
99    const FD: usize,
100> {
101    base: ProverCommitted<'a, Zt, U, C, D, FD>,
102    field_cfg: C,
103    projected_trace: RowMajorTrace<C::Element>,
104    projected_scalars_fx: ProjectedScalars<U::Scalar, DynamicPolynomial<C::Element>>,
105    /// Field configs for all constraint families, starting with randomly
106    /// sampled `field_cfg` (for $Q[X]$ constraints, always present) followed by
107    /// config for each $q_i$ for $F_{q_i}[X]$ constraints.
108    all_field_cfgs: Vec<C>,
109    /// Index of $q^* := \min_i q_i$ in `all_field_cfgs`.
110    q_star_idx: usize,
111    /// Per-prime $F_{q_i}[X]$ projections (one entry per prime in
112    /// `UairSignature::primes()`), pre-staged in step 2 so step 3's per-prime
113    /// ideal check can read them. Empty for UAIRs with $Q[X]$ only constraints.
114    ///
115    /// TODO(perf): the row-major projection is duplicated -- once for the
116    ///   Q[X] family and once per prime here. A future optimization could
117    ///   emit all projections in one trace sweep.
118    fq_staging: Vec<FqProjStaging<U, C::Element>>,
119}
120
121/// After step 2 via [`step2_mle_first`](ProverCommitted::step2_mle_first)
122/// (column-major / MLE-first projection).
123#[derive(Clone, Debug)]
124pub struct ProverProjectedMleFirst<
125    'a,
126    Zt: ZincTypes<D, FD>,
127    U: Uair,
128    C: BaseFieldConfig,
129    const D: usize,
130    const FD: usize,
131> {
132    base: ProverCommitted<'a, Zt, U, C, D, FD>,
133    field_cfg: C,
134    projected_trace: ColumnMajorTrace<C::Element>,
135    projected_scalars_fx: ProjectedScalars<U::Scalar, DynamicPolynomial<C::Element>>,
136    /// Field configs for all constraint families, starting with randomly
137    /// sampled `field_cfg` (for $Q[X]$ constraints, always present) followed by
138    /// config for each $q_i$ for $F_{q_i}[X]$ constraints.
139    all_field_cfgs: Vec<C>,
140    /// Index of $q^* := \min_i q_i$ in `all_field_cfgs`.
141    q_star_idx: usize,
142    /// Per-prime $F_{q_i}[X]$ projections, column-major layout
143    /// counterpart of [`ProverProjectedCombined::fq_staging`].
144    ///
145    /// TODO(perf): the column-major projection is duplicated -- once for the
146    ///   Q[X] family and once per prime here. A future optimization could
147    ///   emit all projections in one trace sweep.
148    fq_staging: Vec<FqProjStaging<U, C::Element>>,
149}
150
151/// Per-prime $\phi_{q_i}$ projection of the integer trace and UAIR scalars,
152/// pre-built at step 2 for step 3's per-prime ideal check (and threaded
153/// forward through step 4 into the per-prime CPR / sumcheck / MP-eval
154/// chain). The trace layout (row- vs column-major) matches the variant
155/// chosen at step 2 and is carried inside [`ProjectedTrace`].
156///
157/// Field config is stored separately on the parent state (see
158/// `all_field_cfgs`) and is not needed here.
159#[derive(Clone, Debug)]
160pub struct FqProjStaging<U: Uair, F: Clone> {
161    projected_trace: ProjectedTrace<F>,
162    projected_scalars_fx: ProjectedScalars<U::Scalar, DynamicPolynomial<F>>,
163}
164
165/// After step 3 (ideal check).
166#[derive(Clone, Debug)]
167pub struct ProverIdealChecked<
168    'a,
169    Zt: ZincTypes<D, FD>,
170    U: Uair,
171    C: BaseFieldConfig,
172    const D: usize,
173    const FD: usize,
174> {
175    base: ProverCommitted<'a, Zt, U, C, D, FD>,
176    field_cfg: C,
177    /// Field configs for all constraint families, starting with randomly
178    /// sampled `field_cfg` (for $Q[X]$ constraints, always present) followed by
179    /// config for each $q_i$ for $F_{q_i}[X]$ constraints.
180    all_field_cfgs: Vec<C>,
181    /// Index of $q^* := \min_i q_i$ in `all_field_cfgs`.
182    q_star_idx: usize,
183    projected_trace: ProjectedTrace<C::Element>,
184    projected_scalars_fx: ProjectedScalars<U::Scalar, DynamicPolynomial<C::Element>>,
185    /// Per-prime $\phi_{q_i}$ projections from step 2, threaded forward so
186    /// step 4 can build per-prime $\psi$-projected trace/scalars and step 5
187    /// can drive the per-prime CPR sumcheck families. Empty for UAIRs
188    /// with no declared fq primes.
189    fq_staging: Vec<FqProjStaging<U, C::Element>>,
190
191    // New
192    ic_proof: IdealCheckProof<C::Element>,
193    /// Per-family IC evaluation points (full `Vec<Vec<C::Element>>` of size
194    /// `n + 1`, sampled once at step 3 via `sample_shared_field_challenges`
195    /// and lifted into each family's field). `[0]` is consumed by the
196    /// Q[X] CPR in step 5; `[i + 1]` will drive the per-prime CPR.
197    ic_eval_points: Vec<Vec<C::Element>>,
198    /// Per-prime $F_{q_i}[X]$ ideal-check proofs, one per declared
199    /// prime in `base.uair_signature.primes()`, in order.
200    ic_proof_fq: Vec<IdealCheckProof<C::Element>>,
201}
202
203/// After step 4 (eval projection). `projected_scalars_fx` has been consumed.
204#[derive(Clone, Debug)]
205pub struct ProverEvalProjected<
206    'a,
207    Zt: ZincTypes<D, FD>,
208    U: Uair,
209    C: BaseFieldConfig,
210    const D: usize,
211    const FD: usize,
212> {
213    base: ProverCommitted<'a, Zt, U, C, D, FD>,
214    field_cfg: C,
215    /// Per-family field configs, kept for the per-prime CPR/sumcheck/MP
216    /// chain in later phases. `[0]` = $Q[X]$ family.
217    all_field_cfgs: Vec<C>,
218    /// Index of $q^* := \min_i q_i$ in `all_field_cfgs`.
219    q_star_idx: usize,
220    projected_trace: ProjectedTrace<C::Element>,
221    /// Per-prime $\phi_{q_i}$-projected coefficient traces, threaded
222    /// forward from step 2's `fq_staging`. Reused by step 7
223    /// (`step7_lift_and_project`) to avoid re-projecting the integer trace
224    /// per declared prime. Empty for UAIRs with no declared fq primes.
225    projected_trace_fq: Vec<ProjectedTrace<C::Element>>,
226    ic_proof: IdealCheckProof<C::Element>,
227    /// Per-family IC evaluation points (full $\text{n+1} \times \mu$
228    /// matrix). `[0]` feeds the Q[X] CPR; `[i + 1]` feeds the per-prime
229    /// CPRs in step 5.
230    ic_eval_points: Vec<Vec<C::Element>>,
231    ic_proof_fq: Vec<IdealCheckProof<C::Element>>,
232
233    // New
234    projected_trace_f: Vec<DenseMultilinearExtension<C::Element>>,
235    /// Q[X] family bit-op virtual MLEs, in `UairSignature::bit_op_specs()`
236    /// order.
237    bit_op_mles: Vec<DenseMultilinearExtension<C::Element>>,
238    projected_scalars_f: ProjectedScalars<U::Scalar, C::Element>,
239    /// Per-prime $\psi$-projected trace MLEs (one entry per declared prime
240    /// in `UairSignature::primes()`). Built in step 4 from each
241    /// `fq_staging[i].projected_trace` using `projecting_elements[i + 1]`.
242    /// Consumed by per-prime CPR `prepare_sumcheck_group` in step 5.
243    /// Empty for UAIRs with no declared fq primes.
244    projected_trace_f_fq: Vec<Vec<DenseMultilinearExtension<C::Element>>>,
245    /// Per-prime family bit-op virtual MLEs, one vector per declared prime,
246    /// each in `UairSignature::bit_op_specs()` order.
247    bit_op_mles_fq: Vec<Vec<DenseMultilinearExtension<C::Element>>>,
248    /// Per-prime $\psi$-projected scalars (one entry per declared prime).
249    /// Built in step 4 from each `fq_staging[i].projected_scalars_fx`
250    /// using `projecting_elements[i + 1]`. Consumed in step 5 by the
251    /// per-prime CPR. Empty for UAIRs with no declared fq primes.
252    projected_scalars_f_fq: Vec<ProjectedScalars<U::Scalar, C::Element>>,
253}
254
255/// After step 5 (sumcheck).
256#[allow(clippy::type_complexity)]
257#[derive(Clone, Debug)]
258pub struct ProverSumchecked<
259    'a,
260    Zt: ZincTypes<D, FD>,
261    U: Uair,
262    C: BaseFieldConfig,
263    const D: usize,
264    const FD: usize,
265> {
266    base: ProverCommitted<'a, Zt, U, C, D, FD>,
267    field_cfg: C,
268    /// Per-family field configs (carried for downstream steps).
269    all_field_cfgs: Vec<C>,
270    /// Index of $q^*$ in `all_field_cfgs` (carried for downstream steps).
271    q_star_idx: usize,
272    projected_trace: ProjectedTrace<C::Element>,
273    /// Per-prime $\phi_{q_i}$-projected coefficient traces, threaded
274    /// forward from step 4 for reuse by step 7's lifted-eval computation.
275    /// Empty for UAIRs with no declared fq primes.
276    projected_trace_fq: Vec<ProjectedTrace<C::Element>>,
277    ic_proof: IdealCheckProof<C::Element>,
278    ic_proof_fq: Vec<IdealCheckProof<C::Element>>,
279    /// Trace MLEs at the original $\psi_a$ projecting element, as built
280    /// by `evaluate_trace_to_column_mles` in a previous step.
281    ///
282    /// Carried forward so that the next step can prepend them when assembling
283    /// the multipoint-eval inputs (and optionally append $\alpha'$-projected
284    /// witness-bin MLEs as the Schwartz-Zippel bridge).
285    projected_trace_f: Vec<DenseMultilinearExtension<C::Element>>,
286    /// Q[X] family bit-op virtual MLEs, carried to multipoint eval.
287    bit_op_mles: Vec<DenseMultilinearExtension<C::Element>>,
288    /// Per-prime $\psi$-projected trace MLEs (one entry per declared
289    /// prime). Threaded forward from step 4 by `step5_sumcheck` so that
290    /// step 6's lockstep multipoint-eval can build per-prime MP families.
291    /// Empty for UAIRs with no declared fq primes.
292    projected_trace_f_fq: Vec<Vec<DenseMultilinearExtension<C::Element>>>,
293    /// Per-prime bit-op virtual MLEs, carried to multipoint eval.
294    bit_op_mles_fq: Vec<Vec<DenseMultilinearExtension<C::Element>>>,
295
296    // New
297    cpr_proof: CombinedPolyResolverProof<C::Element>,
298    cpr_eval_point: Vec<C::Element>,
299    combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
300    /// Per-prime CPR proofs (one per declared prime in
301    /// `UairSignature::primes()`), produced by each per-prime CPR finalize
302    /// in step 5. Empty for UAIRs with no declared fq primes.
303    cpr_proofs_fq: Vec<CombinedPolyResolverProof<C::Element>>,
304    /// Per-prime CPR sumcheck endpoints `r^*_i`, lifted into each family's
305    /// field. Empty for UAIRs with no declared fq primes. Consumed by
306    /// step 6's lockstep multipoint-eval.
307    cpr_eval_points_fq: Vec<Vec<C::Element>>,
308    /// Per-prime multi-degree sumcheck proofs (one per declared prime).
309    /// Empty for UAIRs with no declared fq primes.
310    combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
311    lookup_proof: Option<BatchedLookupProof<C::Element>>,
312    booleanity_proof: Option<BooleanityProof<C::Element>>,
313    affine_booleanity_proof: Option<BooleanityProof<C::Element>>,
314    /// Fresh challenge sampled after `bit_slice_evals` were absorbed by
315    /// booleanity's `finalize_prover`. Used by the next step to (a) build the
316    /// extra $\alpha'$-projected witness-bin trace MLEs and (b) compute
317    /// the per-column bridge scalars $c_j = \sum_i (\alpha')^i b_{j,i}$
318    /// appended to multipoint-eval's `up_evals`.
319    ///
320    /// `None` iff there are neither witness binary-poly columns nor affine
321    /// virtual booleanity targets.
322    alpha_prime_f: Option<C::Element>,
323}
324
325/// After step 6 (multipoint eval).
326#[derive(Clone, Debug)]
327pub struct ProverMultipointEvaled<
328    'a,
329    Zt: ZincTypes<D, FD>,
330    U: Uair,
331    C: BaseFieldConfig,
332    const D: usize,
333    const FD: usize,
334> {
335    base: ProverCommitted<'a, Zt, U, C, D, FD>,
336    field_cfg: C,
337    /// Per-family field configs (carried for downstream steps that need
338    /// the per-prime cfgs).
339    all_field_cfgs: Vec<C>,
340    projected_trace: ProjectedTrace<C::Element>,
341    /// Per-prime $\phi_{q_i}$-projected coefficient traces, threaded
342    /// forward from step 5 for reuse by step 7's lifted-eval computation.
343    /// Empty for UAIRs with no declared fq primes.
344    projected_trace_fq: Vec<ProjectedTrace<C::Element>>,
345    ic_proof: IdealCheckProof<C::Element>,
346    ic_proof_fq: Vec<IdealCheckProof<C::Element>>,
347    cpr_proof: CombinedPolyResolverProof<C::Element>,
348    combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
349    /// Per-prime CPR proofs threaded forward from `ProverSumchecked`.
350    cpr_proofs_fq: Vec<CombinedPolyResolverProof<C::Element>>,
351    /// Per-prime multi-degree sumcheck proofs threaded forward.
352    combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
353    lookup_proof: Option<BatchedLookupProof<C::Element>>,
354    booleanity_proof: Option<BooleanityProof<C::Element>>,
355    affine_booleanity_proof: Option<BooleanityProof<C::Element>>,
356
357    // New
358    mp_proof: MultipointEvalProof<C::Element>,
359    r_0: Vec<C::Element>,
360    /// Per-prime multipoint-eval proofs (one per declared prime in
361    /// `UairSignature::primes()`), produced by step 6's lockstep
362    /// multipoint-eval. Empty for UAIRs with no declared fq primes.
363    mp_proofs_fq: Vec<MultipointEvalProof<C::Element>>,
364    /// Per-prime sumcheck output points $r_0$ (one per declared prime,
365    /// lifted into each family's field — the underlying integer is shared
366    /// with the Q-family `r_0` thanks to the lockstep sumcheck). Empty for
367    /// UAIRs with no declared fq primes. Consumed in step 7.
368    r_0_fq: Vec<Vec<C::Element>>,
369}
370
371/// After step 7 (lift-and-project).
372#[derive(Clone, Debug)]
373pub struct ProverLifted<
374    'a,
375    Zt: ZincTypes<D, FD>,
376    U: Uair,
377    C: BaseFieldConfig,
378    const D: usize,
379    const FD: usize,
380> {
381    base: ProverCommitted<'a, Zt, U, C, D, FD>,
382    field_cfg: C,
383    ic_proof: IdealCheckProof<C::Element>,
384    ic_proof_fq: Vec<IdealCheckProof<C::Element>>,
385    cpr_proof: CombinedPolyResolverProof<C::Element>,
386    combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
387    cpr_proofs_fq: Vec<CombinedPolyResolverProof<C::Element>>,
388    combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
389    lookup_proof: Option<BatchedLookupProof<C::Element>>,
390    booleanity_proof: Option<BooleanityProof<C::Element>>,
391    affine_booleanity_proof: Option<BooleanityProof<C::Element>>,
392    mp_proof: MultipointEvalProof<C::Element>,
393    /// Per-prime multipoint-eval proofs threaded forward from
394    /// `ProverMultipointEvaled`.
395    mp_proofs_fq: Vec<MultipointEvalProof<C::Element>>,
396
397    /// Per-constraint-family **witness-only** lifted MLE evaluations at
398    /// $r_0$ (or family-specific $r_0^{(i)}$). Layout: index `0` is the
399    /// Q-family ($q_0$); indices `1..=n` are the declared primes in
400    /// `UairSignature::primes()` order. Length = `1 + r_0_fq.len()`.
401    /// The verifier recomputes the public-column half per family.
402    lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
403    /// $q''$-family lifted MLE evaluations at $r_0 \bmod q''$, witness
404    /// columns only. Used directly by step8's PCS open as the
405    /// $\phi_{q''}$-projected claim. Tracked separately from
406    /// `lifted_evals` because the $q''$-family is PCS-only (no
407    /// per-family constraint check).
408    /// If no $F_q[X]$ constraints are present, this will be `None` to indicate
409    /// `q'' := q0` and this is identical to `lifted_evals`.
410    lifted_evals_pp: Option<Vec<DynamicPolynomial<C::Element>>>,
411    /// PCS-only prime cfg sampled at step 7 start.
412    q_pp_cfg: C,
413    /// $r^\star = r_0 \bmod q''$ — the PCS evaluation
414    /// point for step 8.
415    r_star: Vec<C::Element>,
416}
417
418/// After step 8 (PCS open). No new fields are added here, but the PCS
419/// transcript has been updated with the opening proof.
420/// Ready for generating the final proof object in
421/// [`finish`](ProverPcsOpened::finish).
422#[derive(Clone, Debug)]
423pub struct ProverPcsOpened<
424    'a,
425    Zt: ZincTypes<D, FD>,
426    U: Uair,
427    C: BaseFieldConfig,
428    const D: usize,
429    const FD: usize,
430> {
431    base: ProverCommitted<'a, Zt, U, C, D, FD>,
432    field_cfg: C,
433    /// PCS-only prime cfg sampled at step 7 start, needed to lift the
434    /// $q''$-family section into wire integers.
435    q_pp_cfg: C,
436    ic_proof: IdealCheckProof<C::Element>,
437    ic_proof_fq: Vec<IdealCheckProof<C::Element>>,
438    cpr_proof: CombinedPolyResolverProof<C::Element>,
439    combined_sumcheck: MultiDegreeSumcheckProof<C::Element>,
440    cpr_proofs_fq: Vec<CombinedPolyResolverProof<C::Element>>,
441    combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>>,
442    lookup_proof: Option<BatchedLookupProof<C::Element>>,
443    booleanity_proof: Option<BooleanityProof<C::Element>>,
444    affine_booleanity_proof: Option<BooleanityProof<C::Element>>,
445    mp_proof: MultipointEvalProof<C::Element>,
446    /// Per-prime multipoint-eval proofs threaded forward.
447    mp_proofs_fq: Vec<MultipointEvalProof<C::Element>>,
448    /// Per-constraint-family witness-only lifted evals. Index `0` is the
449    /// Q-family; indices `1..=n` are the declared primes (in
450    /// `UairSignature::primes()` order). Length = `1 + mp_proofs_fq.len()`.
451    lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>>,
452    /// $q''$-family witness-only lifted evals (PCS-only family).
453    lifted_evals_pp: Option<Vec<DynamicPolynomial<C::Element>>>,
454}
455
456//
457// Step implementations
458//
459
460/// Prover uses common type bounds across all steps, so we use a helper macro to
461/// define them
462macro_rules! impl_with_type_bounds {
463    ($type_name:ident { $($code:tt)* }) => {
464        impl<'a, Zt, U, C, const D: usize, const FD: usize> $type_name<'a, Zt, U, C, D, FD>
465        where
466            Zt: ZincTypes<D, FD>,
467            Zt::Int: ProjectableToField<C>,
468            Zt::CombR: MulByScalar<Zt::Chal>,
469            <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<C>,
470            U: Uair<Prime = Zt::Fmod> + 'static,
471            C: BaseFieldConfig<Integer = Zt::Fmod>
472                + ProjectPrimitiveIntegersWithConfig
473                + ProjectElementWithConfig<Zt::Int>
474                + ProjectElementWithConfig<Zt::CombR>
475                + ProjectElementWithConfig<Zt::Chal>
476                + 'static,
477            C::Integer: ConstTranscribable,
478        {
479            $($code)*
480        }
481    };
482}
483
484impl<Zt, U, C, const D: usize, const FD: usize> ZincPlusPiop<Zt, U, C, D, FD>
485where
486    Zt: ZincTypes<D, FD>,
487    U: Uair<Prime = Zt::Fmod>,
488    C: BaseFieldConfig,
489    C::Integer: ConstTranscribable,
490{
491    /// Step 0: Folding the trace.
492    #[allow(clippy::type_complexity)]
493    pub fn step0_fold<'a>(
494        trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D, D>,
495    ) -> Result<ProverFolded<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
496        let uair_signature = U::signature();
497        let witness_trace = trace.witness(&uair_signature);
498
499        let folded_bin_witness_trace = cfg_iter!(witness_trace.binary_poly)
500            .map(Zt::BinaryFold::fold_trace_mle)
501            .collect();
502
503        let folded_witness_trace = UairTrace {
504            binary_poly: Cow::Owned(folded_bin_witness_trace),
505            arbitrary_poly: witness_trace.arbitrary_poly.clone(),
506            int: witness_trace.int.clone(),
507        };
508
509        Ok(ProverFolded {
510            uair_signature,
511            original_trace: trace,
512            folded_witness_trace,
513            _phantom: PhantomData,
514        })
515    }
516}
517
518impl_with_type_bounds!(ProverFolded
519{
520    /// Step 1: Commitment.
521    /// Commit *witness* columns via Zip+ PCS, absorb roots and public
522    /// data into the Fiat-Shamir transcript.
523    #[allow(clippy::type_complexity)]
524    pub fn step1_commit(
525        self,
526        (pp_bin, pp_arb, pp_int): &'a (
527            ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
528            ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
529            ZipPlusParams<Zt::IntZt, Zt::IntLc>,
530        ),
531        num_vars: usize,
532    ) -> Result<ProverCommitted<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
533        let sig = &self.uair_signature;
534        let public_trace = self.original_trace.public(sig);
535
536        let (res_bin, (res_arb, res_int)) = cfg_join!(
537            commit_optionally(pp_bin, &self.folded_witness_trace.binary_poly),
538            commit_optionally(pp_arb, &self.folded_witness_trace.arbitrary_poly),
539            commit_optionally(pp_int, &self.folded_witness_trace.int),
540        );
541        let (hint_bin, commitment_bin) = res_bin?;
542        let (hint_arb, commitment_arb) = res_arb?;
543        let (hint_int, commitment_int) = res_int?;
544
545        let mut pcs_transcript = PcsProverTranscript::new_from_commitments(
546            [&commitment_bin, &commitment_arb, &commitment_int].into_iter(),
547        );
548
549        absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.binary_poly);
550        absorb_public_columns(
551            &mut pcs_transcript.fs_transcript,
552            &public_trace.arbitrary_poly,
553        );
554        absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.int);
555
556        Ok(ProverCommitted {
557            num_vars,
558            uair_signature: self.uair_signature,
559            original_trace: self.original_trace,
560            folded_witness_trace: self.folded_witness_trace,
561            pcs_transcript,
562            pp_bin,
563            pp_arb,
564            pp_int,
565            hint_bin,
566            hint_arb,
567            hint_int,
568            commitment_bin,
569            commitment_arb,
570            commitment_int,
571            _phantom: PhantomData,
572        })
573    }
574});
575
576impl_with_type_bounds!(ProverCommitted
577{
578    #[allow(clippy::type_complexity)]
579    fn project_common<S: Fn(&U::Scalar, &C) -> DynamicPolynomial<C::Element>>(
580        &mut self,
581        project_scalar: S,
582    ) -> Result<(C, ProjectedScalars<U::Scalar, DynamicPolynomial<C::Element>>), ProtocolError<C::Element>>
583    {
584        let field_cfg = self
585            .pcs_transcript
586            .fs_transcript
587            .get_random_field_cfg::<C, Zt::Fmod, Zt::PrimeTest>();
588
589        let projected_scalars_fx = project_scalars::<C, U>(&field_cfg, |s| project_scalar(s, &field_cfg));
590        Ok((field_cfg, projected_scalars_fx))
591    }
592
593    /// Step 2 (combined / row-major): Prime projection
594    /// (`\phi_q`: `Z[X] -> F_q[X]`). Samples a random prime, projects the
595    /// full trace and scalars using the row-major layout.
596    /// Works for both linear and non-linear constraints.
597    pub fn step2_combined<S: Fn(&U::Scalar, &C) -> DynamicPolynomial<C::Element> + Copy>(
598        mut self,
599        project_scalar: S,
600    ) -> Result<ProverProjectedCombined<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
601        let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?;
602        let all_field_cfgs = build_all_cfgs::<C>(&self.uair_signature, field_cfg.clone());
603
604        let projected_trace = project_trace_coeffs_row_major(self.original_trace, &field_cfg);
605
606        // Per-prime F_q[X] staging: project trace + scalars under each
607        // `phi_{q_i}` deterministically. `project_scalar` is reused with the
608        // per-prime cfg.
609        let fq_cfgs = &all_field_cfgs[1..];
610        let mut fq_staging: Vec<FqProjStaging<U, C::Element>> = Vec::with_capacity(fq_cfgs.len());
611        for cfg_q_i in fq_cfgs.iter() {
612            let projected_trace_i =
613                project_trace_coeffs_row_major(self.original_trace, cfg_q_i);
614            let projected_scalars_i = project_scalars::<C, U>(cfg_q_i, |s| project_scalar(s, cfg_q_i));
615            fq_staging.push(FqProjStaging {
616                projected_trace: ProjectedTrace::RowMajor(projected_trace_i),
617                projected_scalars_fx: projected_scalars_i,
618            });
619        }
620
621        let q_star_idx = shared_challenge::compute_q_star_idx::<C>(&all_field_cfgs);
622
623        Ok(ProverProjectedCombined {
624            base: self,
625            field_cfg,
626            projected_trace,
627            projected_scalars_fx,
628            all_field_cfgs,
629            q_star_idx,
630            fq_staging,
631        })
632    }
633
634    /// Step 2 (MLE-first / column-major): Prime projection
635    /// (`\phi_q`: `Z[X] -> F_q[X]`). Samples a random prime, projects the
636    /// full trace and scalars using the column-major layout.
637    pub fn step2_mle_first<S: Fn(&U::Scalar, &C) -> DynamicPolynomial<C::Element> + Copy>(
638        mut self,
639        project_scalar: S,
640    ) -> Result<ProverProjectedMleFirst<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
641        let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?;
642        let all_field_cfgs = build_all_cfgs::<C>(&self.uair_signature, field_cfg.clone());
643
644        let projected_trace = project_trace_coeffs_column_major(self.original_trace, &field_cfg);
645
646        let fq_cfgs = &all_field_cfgs[1..];
647        let mut fq_staging: Vec<FqProjStaging<U, C::Element>> = Vec::with_capacity(fq_cfgs.len());
648        for cfg_q_i in fq_cfgs.iter() {
649            let projected_trace_i =
650                project_trace_coeffs_column_major(self.original_trace, cfg_q_i);
651            let projected_scalars_i = project_scalars::<C, U>(cfg_q_i, |s| project_scalar(s, cfg_q_i));
652            fq_staging.push(FqProjStaging {
653                projected_trace: ProjectedTrace::ColumnMajor(projected_trace_i),
654                projected_scalars_fx: projected_scalars_i,
655            });
656        }
657
658        let q_star_idx = shared_challenge::compute_q_star_idx::<C>(&all_field_cfgs);
659
660        Ok(ProverProjectedMleFirst {
661            base: self,
662            field_cfg,
663            projected_trace,
664            projected_scalars_fx,
665            all_field_cfgs,
666            q_star_idx,
667            fq_staging,
668        })
669    }
670});
671
672impl_with_type_bounds!(ProverProjectedCombined
673{
674    /// Step 3 (combined): Ideal check via `prove_combined` on the row-major
675    /// trace. Works for both linear and non-linear constraints.
676    ///
677    /// Also runs one per-prime $F_{q_i}[X]$ ideal check per
678    /// declared prime in `UairSignature::primes()`, in order. The per-prime
679    /// trace and scalars are projected deterministically with `q_i`'s
680    /// `field_cfg`.
681    ///
682    /// **Shared evaluation point.** All $n + 1$ families share a single
683    /// MLE evaluation point $r \in [0, q^*)^\mu$ sampled once from
684    /// the transcript at the start of this step. Each family lifts the
685    /// shared integer vector into its own field via $C::Element::from\_with\_cfg$.
686    /// Since each shared integer is strictly less than every $q_i$, the
687    /// lift is a type cast: all families agree on the underlying integer.
688    pub fn step3_ideal_check(
689        mut self,
690    ) -> Result<ProverIdealChecked<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
691        let num_constraints = count_constraints::<U>();
692
693        // Sample one shared evaluation point in `[0, q*)^mu`
694        // up-front and lift it into each family's field.
695        let q_star_cfg = &self.all_field_cfgs[self.q_star_idx];
696        let shared_eval_points: Vec<Vec<C::Element>> =
697            shared_challenge::sample_shared_field_challenges::<C>(
698                &mut self.base.pcs_transcript.fs_transcript,
699                self.base.num_vars,
700                q_star_cfg,
701                &self.all_field_cfgs,
702            );
703
704        let ic_proof = IdealCheckProtocol::<U>::prove_combined::<_, D>(
705            &mut self.base.pcs_transcript.fs_transcript,
706            &self.projected_trace,
707            &self.projected_scalars_fx,
708            /* family_idx = */ 0,
709            num_constraints.q,
710            &shared_eval_points[0],
711            &self.field_cfg,
712        )?;
713
714        // Per-prime F_q[X] ideal checks, in `primes()` order. Uses the
715        // per-prime trace/scalar projections pre-built in step 2.
716        let fq_cfgs = &self.all_field_cfgs[1..];
717        let mut ic_proof_fq: Vec<IdealCheckProof<C::Element>> = Vec::with_capacity(fq_cfgs.len());
718        for (prime_idx, (cfg_q_i, staging)) in
719            fq_cfgs.iter().zip(self.fq_staging.iter()).enumerate()
720        {
721            let family_idx = add!(prime_idx, 1);
722            let ProjectedTrace::RowMajor(ref trace_row) = staging.projected_trace else {
723                unreachable!("should be row-major staging")
724            };
725            let ic_proof_i = IdealCheckProtocol::<U>::prove_combined::<_, D>(
726                &mut self.base.pcs_transcript.fs_transcript,
727                trace_row,
728                &staging.projected_scalars_fx,
729                family_idx,
730                num_constraints.for_prime(prime_idx),
731                &shared_eval_points[family_idx],
732                cfg_q_i,
733            )
734            .map_err(|source| ProtocolError::FqIdealCheck {
735                prime_idx,
736                q: cfg_q_i.modulus().to_string(),
737                source,
738            })?;
739
740            ic_proof_fq.push(ic_proof_i);
741        }
742
743        Ok(ProverIdealChecked {
744            base: self.base,
745            field_cfg: self.field_cfg,
746            all_field_cfgs: self.all_field_cfgs,
747            q_star_idx: self.q_star_idx,
748            projected_trace: ProjectedTrace::RowMajor(self.projected_trace),
749            projected_scalars_fx: self.projected_scalars_fx,
750            fq_staging: self.fq_staging,
751            ic_proof,
752            ic_eval_points: shared_eval_points,
753            ic_proof_fq,
754        })
755    }
756});
757
758impl_with_type_bounds!(ProverProjectedMleFirst
759{
760    /// Step 3 (MLE-first): Ideal check via `prove_mle_first` on the
761    /// column-major trace. Works for any UAIR: linear non-zero-ideal
762    /// constraints go through the column-major MLE-first path, non-linear
763    /// non-zero-ideal constraints fall back to the row-major path (with an
764    /// internal transpose), and zero-ideal constraints are short-circuited
765    /// to zero.
766    ///
767    /// **Shared evaluation point.** See the row-major
768    /// [`step3_ideal_check`](ProverProjectedCombined::step3_ideal_check)
769    /// for the shared $r \in [0, q^*)^\mu$ design; same shape here.
770    pub fn step3_ideal_check(
771        mut self,
772    ) -> Result<ProverIdealChecked<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
773        // The Q[X]-family ideal check only consumes Q[X] constraints; F_q[X]
774        // constraints are handled by the per-prime family below.
775        let num_constraints = count_constraints::<U>();
776
777        // Shared evaluation point in `[0, q*)^mu`, lifted per
778        // family. Mirror of the row-major variant.
779        let q_star_cfg = &self.all_field_cfgs[self.q_star_idx];
780        let shared_eval_points: Vec<Vec<C::Element>> =
781            shared_challenge::sample_shared_field_challenges::<C>(
782                &mut self.base.pcs_transcript.fs_transcript,
783                self.base.num_vars,
784                q_star_cfg,
785                &self.all_field_cfgs,
786            );
787
788        let ic_proof = IdealCheckProtocol::<U>::prove_mle_first::<_, D>(
789            &mut self.base.pcs_transcript.fs_transcript,
790            &self.projected_trace,
791            &self.projected_scalars_fx,
792            /* family_idx = */ 0,
793            num_constraints.q,
794            &shared_eval_points[0],
795            &self.field_cfg,
796        )?;
797
798        // Per-prime F_q[X] ideal checks (MLE-first / column-major), in
799        // `primes()` order. Uses the per-prime trace/scalar projections
800        // pre-built in step 2.
801        let fq_cfgs = &self.all_field_cfgs[1..];
802        let mut ic_proof_fq: Vec<IdealCheckProof<C::Element>> = Vec::with_capacity(fq_cfgs.len());
803        for (prime_idx, (cfg_q_i, staging)) in
804            fq_cfgs.iter().zip(self.fq_staging.iter()).enumerate()
805        {
806            let family_idx = add!(prime_idx, 1);
807            let ProjectedTrace::ColumnMajor(ref trace_col) = staging.projected_trace else {
808                unreachable!("should be column-major staging")
809            };
810            let ic_proof_i = IdealCheckProtocol::<U>::prove_mle_first::<_, D>(
811                &mut self.base.pcs_transcript.fs_transcript,
812                trace_col,
813                &staging.projected_scalars_fx,
814                family_idx,
815                num_constraints.for_prime(prime_idx),
816                &shared_eval_points[family_idx],
817                cfg_q_i,
818            )
819            .map_err(|source| ProtocolError::FqIdealCheck {
820                prime_idx,
821                q: cfg_q_i.modulus().to_string(),
822                source,
823            })?;
824
825            ic_proof_fq.push(ic_proof_i);
826        }
827
828        Ok(ProverIdealChecked {
829            base: self.base,
830            field_cfg: self.field_cfg,
831            all_field_cfgs: self.all_field_cfgs,
832            q_star_idx: self.q_star_idx,
833            projected_trace: ProjectedTrace::ColumnMajor(self.projected_trace),
834            projected_scalars_fx: self.projected_scalars_fx,
835            fq_staging: self.fq_staging,
836            ic_proof,
837            ic_eval_points: shared_eval_points,
838            ic_proof_fq,
839        })
840    }
841});
842
843impl_with_type_bounds!(ProverIdealChecked
844{
845    /// Step 4: Evaluation projection ($\psi_a$: $F_q[X] \to F_q$).
846    ///
847    /// **Shared projecting element.**
848    ///
849    /// Sample one shared integer $a \in [0, q^*)$ once via [`shared_challenge::sample_shared_field_challenge`]
850    /// and lift it into each family's field. The $Q[X]$ family consumes
851    /// `projecting_elements[0]`; per-prime families consume
852    /// `projecting_elements[i + 1]`.
853    ///
854    /// Also builds the per-prime $\psi$-projected trace MLEs / scalars from
855    /// each `fq_staging[i]` using `projecting_elements[i + 1]`, and threads
856    /// them forward as `projected_trace_f_fq` / `projected_scalars_f_fq`
857    /// for the per-prime CPR sumcheck in step 5.
858    pub fn step4_eval_projection(
859        mut self,
860    ) -> Result<ProverEvalProjected<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
861        let q_star_cfg = &self.all_field_cfgs[self.q_star_idx];
862        let projecting_elements: Vec<C::Element> = shared_challenge::sample_shared_field_challenge::<C>(
863            &mut self.base.pcs_transcript.fs_transcript,
864            q_star_cfg,
865            &self.all_field_cfgs,
866        );
867
868        // Q[X] family: $\psi_a$-projected trace MLEs + projected scalars.
869        let projected_trace_f = evaluate_trace_to_column_mles(
870            &self.field_cfg,
871            &self.projected_trace,
872            &projecting_elements[0],
873        );
874
875        let bit_op_specs = self.base.uair_signature.bit_op_specs().to_vec();
876        let bit_op_mles = bit_op_specs
877            .iter()
878            .map(|spec| {
879                build_bit_op_virtual_mle::<C, D>(
880                    &self.projected_trace,
881                    spec,
882                    &projecting_elements[0],
883                    &self.field_cfg,
884                )
885            })
886            .collect();
887
888        let projected_scalars_f = project_scalars_to_field(
889            &self.field_cfg,
890            self.projected_scalars_fx,
891            &projecting_elements[0],
892        )
893        .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?;
894
895        // Per-prime $F_{q_i}[X]$ families: same construction with each
896        // family's $\psi$ projecting element. The per-prime
897        // $\phi_{q_i}$-projected coefficient traces are retained and
898        // threaded forward (`projected_trace_fq`) so step 7's
899        // lifted-eval computation can reuse them instead of
900        // re-projecting from the integer trace.
901        let n_fq = self.fq_staging.len();
902        let mut projected_trace_fq: Vec<ProjectedTrace<C::Element>> = Vec::with_capacity(n_fq);
903        let mut projected_trace_f_fq: Vec<Vec<DenseMultilinearExtension<C::Element>>> =
904            Vec::with_capacity(n_fq);
905        let mut bit_op_mles_fq: Vec<Vec<DenseMultilinearExtension<C::Element>>> =
906            Vec::with_capacity(n_fq);
907        let mut projected_scalars_f_fq: Vec<ProjectedScalars<U::Scalar, C::Element>> =
908            Vec::with_capacity(n_fq);
909        for (prime_idx, staging) in self.fq_staging.into_iter().enumerate() {
910            let family_idx = add!(prime_idx, 1);
911            let FqProjStaging {
912                projected_trace: projected_trace_i,
913                projected_scalars_fx: scalars_fx_i,
914            } = staging;
915            let trace_f_i = evaluate_trace_to_column_mles(
916                &self.all_field_cfgs[family_idx],
917                &projected_trace_i,
918                &projecting_elements[family_idx],
919            );
920            let bit_op_mles_i = bit_op_specs
921                .iter()
922                .map(|spec| {
923                    build_bit_op_virtual_mle::<C, D>(
924                        &projected_trace_i,
925                        spec,
926                        &projecting_elements[family_idx],
927                        &self.all_field_cfgs[family_idx],
928                    )
929                })
930                .collect();
931            let scalars_f_i = project_scalars_to_field(
932                &self.all_field_cfgs[family_idx],
933                scalars_fx_i,
934                &projecting_elements[family_idx],
935            )
936            .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?;
937            projected_trace_fq.push(projected_trace_i);
938            projected_trace_f_fq.push(trace_f_i);
939            bit_op_mles_fq.push(bit_op_mles_i);
940            projected_scalars_f_fq.push(scalars_f_i);
941        }
942
943        Ok(ProverEvalProjected {
944            base: self.base,
945            field_cfg: self.field_cfg,
946            all_field_cfgs: self.all_field_cfgs,
947            q_star_idx: self.q_star_idx,
948            projected_trace: self.projected_trace,
949            projected_trace_fq,
950            ic_proof: self.ic_proof,
951            ic_eval_points: self.ic_eval_points,
952            ic_proof_fq: self.ic_proof_fq,
953            projected_trace_f,
954            bit_op_mles,
955            projected_scalars_f,
956            projected_trace_f_fq,
957            bit_op_mles_fq,
958            projected_scalars_f_fq,
959        })
960    }
961});
962
963impl_with_type_bounds!(ProverEvalProjected
964{
965    /// Step 5: Combined CPR + Booleanity + Lookup multi-degree sumcheck over F_q.
966    /// Batches the CPR constraint claim (degree `max_deg+2`), the booleanity
967    /// argument (degree 3), and lookup groups (one per table type) into a
968    /// single sumcheck sharing one evaluation point `r*`. Produces
969    /// `up_evals`/`down_evals` (CPR), `bit_slice_evals` (booleanity), and
970    /// lookup auxiliary witnesses at `r*`.
971    ///
972    /// After booleanity's `finalize_prover` absorbs all committed and affine
973    /// `bit_slice_evals` into
974    /// the transcript, this step squeezes a fresh challenge $\alpha'$ and
975    /// stores it on `ProverSumchecked`. The actual Schwartz-Zippel bridge
976    /// is installed in `step6_multipoint_eval`, which appends one extra
977    /// $\alpha'$-projected witness-bin column MLE (and matching up_eval
978    /// $c_j = \sum_i b_{j,i} (\alpha')^i$) per witness binary-poly column
979    /// to the multipoint-eval inputs. Unshifted affine virtuals are collapsed
980    /// at the same $\alpha'$ and checked directly by the verifier against
981    /// their source projections at $r^*$. Shifts continue to reference the
982    /// original $\psi_a$-projected witness-bin slot, so `down_evals` are
983    /// untouched: shifted booleanity is inherited from un-shifted
984    /// booleanity (same committed column).
985    ///
986    /// The PCS chain (`step6_multipoint_eval` + lifted-evals + Zip+ open)
987    /// closes the bridge: at the random sumcheck output $r_0$,
988    /// $\overline{u_j}(\alpha') = \widetilde{g_j}(r_0)$ pins the appended
989    /// column's multilinear extension to the true $\alpha'$-projection
990    /// $g_j$ of the committed $u_j$, replacing the previous
991    /// underconstrained $\psi_a$ linear pin-down (sound only for $D=1$).
992    pub fn step5_sumcheck(
993        mut self,
994    ) -> Result<ProverSumchecked<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
995        let num_constraints = count_constraints::<U>();
996        let max_degree = count_max_degree::<U>();
997
998        // Sample one shared CPR batching challenge $\alpha$ in
999        // $[0, q^*)$ and lift it into each family's field. The Q[X] family
1000        // consumes `folding_challenges[0]`; per-prime families consume
1001        // `folding_challenges[i + 1]`.
1002        let q_star_cfg_owned = self.all_field_cfgs[self.q_star_idx].clone();
1003        let folding_challenges: Vec<C::Element> = shared_challenge::sample_shared_field_challenge::<C>(
1004            &mut self.base.pcs_transcript.fs_transcript,
1005            &q_star_cfg_owned,
1006            &self.all_field_cfgs,
1007        );
1008
1009        // ------------- Q[X] family groups -----------------
1010        let (q_cpr_group, q_cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::<U>(
1011            self.projected_trace_f.clone(),
1012            self.bit_op_mles.clone(),
1013            &self.ic_eval_points[0],
1014            &self.projected_scalars_f,
1015            /* family_idx = */ 0,
1016            num_constraints.q,
1017            self.base.num_vars,
1018            max_degree,
1019            &folding_challenges[0],
1020            &self.field_cfg,
1021        )?;
1022
1023        let mut q_groups = vec![q_cpr_group];
1024
1025        // Booleanity: prepare optional group over witness binary-poly cols.
1026        // Lives in the Q[X] family only.
1027        let sig = &self.base.uair_signature;
1028        let num_pub_bin = sig.public_cols().num_binary_poly_cols();
1029        let num_total_bin = sig.total_cols().num_binary_poly_cols();
1030        let trace_wit_bin_poly = &self.base.original_trace.binary_poly[num_pub_bin..num_total_bin];
1031
1032        let bool_ancillary = if !trace_wit_bin_poly.is_empty() {
1033            let (bool_group, anc) = BooleanityChecker::prepare_sumcheck_group::<D>(
1034                &mut self.base.pcs_transcript.fs_transcript,
1035                trace_wit_bin_poly,
1036                self.base.num_vars,
1037                &self.field_cfg,
1038            )
1039            .map_err(ProtocolError::Booleanity)?;
1040            q_groups.push(bool_group);
1041            Some(anc)
1042        } else {
1043            None
1044        };
1045
1046        // Affine virtuals are a separate generic group. Their residual cells
1047        // are not binary, so the committed-column round-1 fast path cannot
1048        // represent them.
1049        let affine_bool_ancillary = if sig.affine_virtual_specs().is_empty() {
1050            None
1051        } else {
1052            let (affine_group, anc) =
1053                BooleanityChecker::prepare_affine_virtual_sumcheck_group::<D>(
1054                    &mut self.base.pcs_transcript.fs_transcript,
1055                    &self.base.original_trace.binary_poly,
1056                    sig.affine_virtual_specs(),
1057                    self.base.num_vars,
1058                    &self.field_cfg,
1059                )
1060                .map_err(ProtocolError::Booleanity)?;
1061            q_groups.push(affine_group);
1062            Some(anc)
1063        };
1064
1065        // TODO: for each LookupGroup from group_lookup_specs(lookup_specs):
1066        //   - call prepare_batched_lookup_group(transcript, instance, &field_cfg)
1067        //   - push triple into groups, collect pending proofs + metas
1068
1069        // ------------- Per-prime $F_{q_i}[X]$ family groups --------------
1070        // One CPR group per declared prime. No booleanity, no lookups in
1071        // the fq families (by design — binary witnesses live in Q[X]).
1072        let n_fq = self.projected_trace_f_fq.len();
1073        let mut fq_cpr_ancillaries: Vec<_> = Vec::with_capacity(n_fq);
1074        let mut fq_family_groups: Vec<Vec<MultiDegreeSumcheckGroup<C>>> =
1075            Vec::with_capacity(n_fq);
1076        for prime_idx in 0..n_fq {
1077            let family_idx = add!(prime_idx, 1);
1078            let cfg_i = &self.all_field_cfgs[family_idx];
1079            let trace_f_i = self.projected_trace_f_fq[prime_idx].clone();
1080            let scalars_f_i = &self.projected_scalars_f_fq[prime_idx];
1081            let eval_point_i = &self.ic_eval_points[family_idx];
1082            let folding_i = &folding_challenges[family_idx];
1083            let (cpr_group_i, cpr_ancillary_i) =
1084                CombinedPolyResolver::prepare_sumcheck_group::<U>(
1085                    trace_f_i,
1086                    self.bit_op_mles_fq[prime_idx].clone(),
1087                    eval_point_i,
1088                    scalars_f_i,
1089                    family_idx,
1090                    num_constraints.for_prime(prime_idx),
1091                    self.base.num_vars,
1092                    max_degree,
1093                    folding_i,
1094                    cfg_i,
1095                )?;
1096            fq_family_groups.push(vec![cpr_group_i]);
1097            fq_cpr_ancillaries.push(cpr_ancillary_i);
1098        }
1099
1100        // ------------- Lockstep multi-degree sumcheck --------------------
1101        // Family 0 = Q[X] with CPR + optional booleanity; families i >= 1
1102        // = per-prime CPR. Shared per-round challenges in $[0, q^*)$.
1103        let mut md_sc_families: Vec<(Vec<MultiDegreeSumcheckGroup<C>>, &C)> =
1104            Vec::with_capacity(add!(n_fq, 1));
1105        md_sc_families.push((q_groups, &self.field_cfg));
1106        for (prime_idx, groups) in fq_family_groups.into_iter().enumerate() {
1107            let family_idx = add!(prime_idx, 1);
1108            md_sc_families.push((groups, &self.all_field_cfgs[family_idx]));
1109        }
1110
1111        let mut sumcheck_outputs = MultiDegreeSumcheck::prove_as_subprotocol(
1112            &mut self.base.pcs_transcript.fs_transcript,
1113            md_sc_families,
1114            self.base.num_vars,
1115            &q_star_cfg_owned,
1116        )
1117        .into_iter();
1118
1119        // ------------- Q[X] family finalize ------------------------------
1120        let (combined_sumcheck, md_states) =
1121            sumcheck_outputs.next().expect("Q[X] family always present");
1122        let mut md_iter = md_states.into_iter();
1123
1124        let (cpr_proof, cpr_prover_state) = CombinedPolyResolver::finalize_prover::<U>(
1125            &mut self.base.pcs_transcript.fs_transcript,
1126            md_iter.next().expect("CPR group always present"),
1127            q_cpr_ancillary,
1128            &self.field_cfg,
1129        )?;
1130
1131        let mut finalize_booleanity_group = |ancillary: Option<BoolProverAncillary>| {
1132            ancillary
1133                .map(|ancillary| {
1134                    BooleanityChecker::finalize_prover(
1135                        &mut self.base.pcs_transcript.fs_transcript,
1136                        md_iter.next().expect("booleanity group present"),
1137                        ancillary,
1138                        &self.field_cfg,
1139                    )
1140                    .map_err(ProtocolError::Booleanity)
1141                })
1142                .transpose()
1143        };
1144        let booleanity_proof = finalize_booleanity_group(bool_ancillary)?;
1145        let affine_booleanity_proof = finalize_booleanity_group(affine_bool_ancillary)?;
1146        debug_assert!(md_iter.next().is_none());
1147
1148        // TODO: build BatchedLookupProof from collected lookup_proofs + lookup_metas
1149        let lookup_proof = None;
1150
1151        // ------------- Per-prime family finalize -------------------------
1152        // For each fq family: the multi-degree sumcheck handed back a
1153        // `Vec<SumcheckProverState>` with exactly one entry (just the CPR
1154        // group). Finalize CPR per family under that family's cfg.
1155        let mut cpr_proofs_fq: Vec<CombinedPolyResolverProof<C::Element>> = Vec::with_capacity(n_fq);
1156        let mut cpr_eval_points_fq: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1157        let mut combined_sumchecks_fq: Vec<MultiDegreeSumcheckProof<C::Element>> =
1158            Vec::with_capacity(n_fq);
1159        for (prime_idx, cpr_ancillary_i) in fq_cpr_ancillaries.into_iter().enumerate() {
1160            let family_idx = add!(prime_idx, 1);
1161            let cfg_i = &self.all_field_cfgs[family_idx];
1162            let (sumcheck_i, states_i) =
1163                sumcheck_outputs.next().expect("fq family sumcheck output");
1164            let mut states_iter_i = states_i.into_iter();
1165            let (cpr_proof_i, cpr_state_i) = CombinedPolyResolver::finalize_prover::<U>(
1166                &mut self.base.pcs_transcript.fs_transcript,
1167                states_iter_i.next().expect("CPR group always present"),
1168                cpr_ancillary_i,
1169                cfg_i,
1170            )?;
1171            combined_sumchecks_fq.push(sumcheck_i);
1172            cpr_proofs_fq.push(cpr_proof_i);
1173            cpr_eval_points_fq.push(cpr_state_i.evaluation_point);
1174        }
1175
1176        // Booleanity bridges: squeeze alpha' after the committed and affine
1177        // bit-slice evaluations were absorbed by their finalize calls.
1178        let alpha_prime_f: Option<C::Element> =
1179            (booleanity_proof.is_some() || affine_booleanity_proof.is_some()).then(|| {
1180                self.base
1181                    .pcs_transcript
1182                    .fs_transcript
1183                    .get_field_challenge(&self.field_cfg)
1184            });
1185
1186        Ok(ProverSumchecked {
1187            base: self.base,
1188            field_cfg: self.field_cfg,
1189            all_field_cfgs: self.all_field_cfgs,
1190            q_star_idx: self.q_star_idx,
1191            projected_trace: self.projected_trace,
1192            projected_trace_fq: self.projected_trace_fq,
1193            ic_proof: self.ic_proof,
1194            ic_proof_fq: self.ic_proof_fq,
1195            projected_trace_f: self.projected_trace_f,
1196            bit_op_mles: self.bit_op_mles,
1197            projected_trace_f_fq: self.projected_trace_f_fq,
1198            bit_op_mles_fq: self.bit_op_mles_fq,
1199            cpr_proof,
1200            cpr_eval_point: cpr_prover_state.evaluation_point,
1201            combined_sumcheck,
1202            cpr_proofs_fq,
1203            cpr_eval_points_fq,
1204            combined_sumchecks_fq,
1205            lookup_proof,
1206            booleanity_proof,
1207            affine_booleanity_proof,
1208            alpha_prime_f,
1209        })
1210    }
1211});
1212
1213impl_with_type_bounds!(ProverSumchecked
1214{
1215    /// Step 6: Multi-point evaluation sumcheck. Combines `up_evals` and
1216    /// `down_evals` at `r*` into a single evaluation point `r_0`.
1217    /// Only the sumcheck proof is sent; scalar evaluations at `r_0` are derived from the
1218    /// polynomial-valued `lifted_evals` in Step 7.
1219    ///
1220    /// When the booleanity argument ran (witness binary-poly columns
1221    /// present), the multipoint-eval inputs are *extended* with one extra
1222    /// $\alpha'$-projected column MLE and one extra scalar up_eval
1223    /// $c_j = \sum_i b_{j,i}\,(\alpha')^{i}$ per witness binary-poly column,
1224    /// placed at indices `[num_total_cols, num_total_cols + num_wit_bin)`.
1225    /// On the Q-family these carry the real bit-slice bridge claim; on the
1226    /// per-prime families they are **zero-padded** so all families share
1227    /// the same column layout (UAIR rule D6: binary witness columns live
1228    /// only in Q[X], so per-prime families have no booleanity-bridge
1229    /// claim to make there). The shared layout lets a single $(n+1)$-family
1230    /// lockstep MP-eval produce a single shared $r_0$ across all families,
1231    /// which Phases H/I rely on for the lift-to-$Z$ + $q''$-anchored
1232    /// PCS open. No `ShiftSpec` references those indices, so
1233    /// `down_evals`/`shifts` are untouched and shifted booleanity is
1234    /// inherited from the un-shifted column (which continues to live at
1235    /// its original $\psi_a$-projected slot).
1236    /// See the module-level `BooleanityChecker` docs for the soundness
1237    /// argument (Schwartz-Zippel at $\alpha'$ + MP/PCS chain at $r_0$).
1238    #[allow(clippy::arithmetic_side_effects, clippy::too_many_lines)]
1239    pub fn step6_multipoint_eval(
1240        mut self,
1241    ) -> Result<ProverMultipointEvaled<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
1242        let n_fq = self.projected_trace_f_fq.len();
1243        let q_star_cfg = self.all_field_cfgs[self.q_star_idx].clone();
1244        let shifts = self.base.uair_signature.shifts();
1245        let num_vars = self.base.num_vars;
1246
1247        // --- Q[X] family (booleanity-bridge appended iff alpha_prime present)
1248        //
1249        // When booleanity ran, the Q-family gets extra columns appended:
1250        //   - `extra_trace_mles[j]`: the $\alpha'$-projection of witness
1251        //     binary column $j$, as a DenseMultilinearExtension over Q[X]'s
1252        //     inner type.
1253        //   - `extra_up_evals[j] = c_j = \sum_i b_{j,i}\,(\alpha')^i$: the
1254        //     $\alpha'$-batched bit_slice_evals at the booleanity endpoint.
1255        // These extensions tie booleanity's $r^*$-anchored bit-slice claims
1256        // into the MP-eval sumcheck so that the MP endpoint $r_0$ also binds
1257        // them; the verifier's downstream lifted-eval projection at
1258        // $\alpha'$ closes the loop.
1259        let mut projected_trace_f = self.projected_trace_f;
1260        let (q_up_evals, num_wit_bin) = if let Some(alpha_prime) =
1261            &self.alpha_prime_f
1262        {
1263            let sig = &self.base.uair_signature;
1264            let num_pub_bin = sig.public_cols().num_binary_poly_cols();
1265            let num_total_bin = sig.total_cols().num_binary_poly_cols();
1266            let num_wit_bin = num_total_bin.saturating_sub(num_pub_bin);
1267
1268            // Project the witness binary-poly columns at \alpha' directly from the
1269            // committed BinaryPoly<D> data:
1270            // More efficient than the generic `evaluate_trace_to_column_mles` path.
1271            let alpha_powers: Vec<C::Element> = powers(&self.field_cfg, alpha_prime, D);
1272            let bin_cols = &self.base.original_trace.binary_poly[num_pub_bin..num_total_bin];
1273            let extra_trace_mles: Vec<DenseMultilinearExtension<C::Element>> = cfg_iter!(bin_cols)
1274                .map(|col| project_binary_col_at_field::<C, D>(col, &alpha_powers, &self.field_cfg))
1275                .collect();
1276            debug_assert_eq!(extra_trace_mles.len(), num_wit_bin);
1277
1278            let extra_up_evals = if num_wit_bin == 0 {
1279                debug_assert!(self.booleanity_proof.is_none());
1280                Vec::new()
1281            } else {
1282                let proof = self
1283                    .booleanity_proof
1284                    .as_ref()
1285                    .expect("witness binary columns require a booleanity proof");
1286                collapse_bit_slice_evals::<C, D>(
1287                    &proof.bit_slice_evals,
1288                    num_wit_bin,
1289                    alpha_prime,
1290                    &self.field_cfg,
1291                )
1292            };
1293
1294            projected_trace_f.extend(extra_trace_mles);
1295
1296            let mut up_evals = self.cpr_proof.up_evals.clone();
1297            up_evals.extend(extra_up_evals);
1298            (up_evals, num_wit_bin)
1299        } else {
1300            (self.cpr_proof.up_evals.clone(), 0)
1301        };
1302
1303        // --- Per-prime families (zero-padded to match Q-family column count)
1304        //
1305        // Each fq family's trace MLEs and up_evals are extended with
1306        // `num_wit_bin` zero entries, padding to the same column count as
1307        // the (booleanity-extended) Q-family. This preserves the lockstep
1308        // shape (shared `gammas` and one $r_0$ across all families) without
1309        // making any non-trivial claim on the per-prime families: zero on
1310        // both sides of the MP-eval sumcheck contributes zero.
1311        let extension_size = 1usize << num_vars;
1312        let mut fq_trace_mles_padded: Vec<Vec<DenseMultilinearExtension<C::Element>>> =
1313            Vec::with_capacity(n_fq);
1314        let mut fq_up_evals_padded: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1315        for (prime_idx, mut trace_i) in self.projected_trace_f_fq.into_iter().enumerate() {
1316            let family_idx = add!(prime_idx, 1);
1317            let cfg_i = &self.all_field_cfgs[family_idx];
1318            let zero_i = cfg_i.zero();
1319
1320            if num_wit_bin > 0 {
1321                let zero_mle = DenseMultilinearExtension::from_evaluations_vec(
1322                    num_vars,
1323                    vec![zero_i.clone(); extension_size],
1324                    zero_i.clone(),
1325                );
1326                trace_i.extend((0..num_wit_bin).map(|_| zero_mle.clone()));
1327            }
1328            fq_trace_mles_padded.push(trace_i);
1329
1330            let mut up_i = self.cpr_proofs_fq[prime_idx].up_evals.clone();
1331            up_i.extend((0..num_wit_bin).map(|_| zero_i.clone()));
1332            fq_up_evals_padded.push(up_i);
1333        }
1334
1335        // --- Single (n+1)-family lockstep MP-eval ---------------------
1336        //
1337        // Family 0 = Q[X] (with booleanity-bridge cols when applicable);
1338        // families i >= 1 = per-prime F_q[X] families (zero-padded to the
1339        // same column count). All families share one r_0 in [0, q*).
1340        let mut all_families: Vec<MultipointEvalFamilyInputs<'_, C>> =
1341            Vec::with_capacity(add!(n_fq, 1));
1342        all_families.push(MultipointEvalFamilyInputs {
1343            field_cfg: &self.field_cfg,
1344            trace_mles: &projected_trace_f,
1345            bit_op_mles: &self.bit_op_mles,
1346            eval_point: &self.cpr_eval_point,
1347            up_evals: &q_up_evals,
1348            bit_op_evals: &self.cpr_proof.bit_op_evals,
1349            down_evals: &self.cpr_proof.down_evals,
1350        });
1351        for prime_idx in 0..n_fq {
1352            let family_idx = add!(prime_idx, 1);
1353            all_families.push(MultipointEvalFamilyInputs {
1354                field_cfg: &self.all_field_cfgs[family_idx],
1355                trace_mles: &fq_trace_mles_padded[prime_idx],
1356                bit_op_mles: &self.bit_op_mles_fq[prime_idx],
1357                eval_point: &self.cpr_eval_points_fq[prime_idx],
1358                up_evals: &fq_up_evals_padded[prime_idx],
1359                bit_op_evals: &self.cpr_proofs_fq[prime_idx].bit_op_evals,
1360                down_evals: &self.cpr_proofs_fq[prime_idx].down_evals,
1361            });
1362        }
1363
1364        let mut outputs_iter = MultipointEval::prove_as_subprotocol(
1365            &mut self.base.pcs_transcript.fs_transcript,
1366            all_families,
1367            shifts,
1368            &q_star_cfg,
1369        )?
1370        .into_iter();
1371
1372        let (mp_proof_q, q_state) = outputs_iter.next().expect("Q-family present");
1373        let r_0_q = q_state.eval_point;
1374
1375        let mut mp_proofs_fq: Vec<MultipointEvalProof<C::Element>> = Vec::with_capacity(n_fq);
1376        let mut r_0_fq: Vec<Vec<C::Element>> = Vec::with_capacity(n_fq);
1377        for (proof_i, state_i) in outputs_iter {
1378            mp_proofs_fq.push(proof_i);
1379            r_0_fq.push(state_i.eval_point);
1380        }
1381
1382        Ok(ProverMultipointEvaled {
1383            base: self.base,
1384            field_cfg: self.field_cfg,
1385            all_field_cfgs: self.all_field_cfgs,
1386            projected_trace: self.projected_trace,
1387            projected_trace_fq: self.projected_trace_fq,
1388            ic_proof: self.ic_proof,
1389            ic_proof_fq: self.ic_proof_fq,
1390            cpr_proof: self.cpr_proof,
1391            combined_sumcheck: self.combined_sumcheck,
1392            cpr_proofs_fq: self.cpr_proofs_fq,
1393            combined_sumchecks_fq: self.combined_sumchecks_fq,
1394            lookup_proof: self.lookup_proof,
1395            booleanity_proof: self.booleanity_proof,
1396            affine_booleanity_proof: self.affine_booleanity_proof,
1397            mp_proof: mp_proof_q,
1398            r_0: r_0_q,
1399            mp_proofs_fq,
1400            r_0_fq,
1401        })
1402    }
1403});
1404
1405impl_with_type_bounds!(ProverMultipointEvaled
1406{
1407    /// Step 7: Lift-and-project.
1408    ///
1409    /// 1. **Sample $q''$.** A fresh PCS-only prime, decoupled from the
1410    ///    constraint primes $q_0, q_1, \dots, q_n$. Sampled here (start of
1411    ///    step 7) — before any lifted evals are produced, so that the
1412    ///    $q''$-family lift can be computed and sent in this step. This
1413    ///    is post-commitments and post-$r_0$, so soundness is preserved
1414    ///    (the prover cannot influence $q''$).
1415    /// 2. **Compute per-family lifted MLE evaluations** at $r_0$:
1416    ///    - Q-family ($q_0$): all columns (public + witness) interleaved by
1417    ///      UAIR column layout, stored locally; only witness columns make
1418    ///      it into the proof.
1419    ///    - Per declared prime $q_i$ (i ≥ 1): witness-only lifted evals
1420    ///      under that family's field cfg. The $\phi_{q_i}$-projected
1421    ///      coefficient trace was already built in step 2 (`fq_staging`)
1422    ///      and threaded through `projected_trace_fq`; this step runs
1423    ///      `compute_lifted_evals` on it at $r_0$ lifted into family
1424    ///      $i$'s field.
1425    ///    - $q''$-family: witness-only lifted evals under $q''$. Same
1426    ///      pattern as fq families; the $r_0$ lifted into $F_{q''}$
1427    ///      gives $r^\star = r_0 \bmod q''$ which doubles
1428    ///      as the PCS evaluation point in step 8.
1429    /// 3. **Absorb** each family's coefficients into the FS transcript in
1430    ///    a deterministic order: Q-family first, then each declared prime
1431    ///    in `primes()` order, then $q''$.
1432    ///
1433    /// **Soundness**: with $r_0$ shared across all constraint families
1434    /// (a consequence of the lockstep sumcheck), the per-family MP-eval
1435    /// consistency check in `step6_lifted_evals` (verifier) binds each
1436    /// $\bar u_j^{(i)}$ to
1437    /// the prover's actual $q_i$-projected trace at $r_0$. The
1438    /// $q''$-family lift is independently bound to the trace by the PCS
1439    /// open at $r^\star$ in step 8.
1440    #[allow(clippy::arithmetic_side_effects)]
1441    pub fn step7_lift_and_project(
1442        mut self,
1443    ) -> Result<ProverLifted<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
1444        let n_fq = self.r_0_fq.len();
1445
1446        // --- Sample q'' (PCS-only prime) ---
1447        //
1448        // The fresh PCS-only prime exists to decouple the witness-trace
1449        // commitment from *which* of several constraint primes opens it. With
1450        // no F_q[X] constraints, there is only q_0, so this decoupling buys
1451        // nothing: we alias q'' := q_0; r* := r_0 and skip q'' lift.
1452        let (q_pp_cfg, r_star) = if n_fq == 0 {
1453            (self.field_cfg.clone(), self.r_0.clone())
1454        } else {
1455            let cfg = self.base
1456                .pcs_transcript
1457                .fs_transcript
1458                .get_random_field_cfg::<C, Zt::Fmod, Zt::PrimeTest>();
1459            let r_star = self
1460                .r_0
1461                .iter()
1462                .map(|x| self.field_cfg.lift(x))
1463                .map(|x| cfg.project(&x))
1464                .collect();
1465            (cfg, r_star)
1466        };
1467
1468        // Witness-col extraction helper. UAIR's column layout interleaves
1469        // public and witness blocks per type (bin / arb / int), so we slice
1470        // out the witness sub-blocks and concatenate.
1471        let sig = self.base.uair_signature.clone();
1472        let pub_cols = sig.public_cols();
1473        let num_pub_bin = pub_cols.num_binary_poly_cols();
1474        let num_pub_arb = pub_cols.num_arbitrary_poly_cols();
1475        let num_pub_int = pub_cols.num_int_cols();
1476        let total = sig.total_cols();
1477        let num_total_bin = total.num_binary_poly_cols();
1478        let num_total_arb = total.num_arbitrary_poly_cols();
1479        let witness = sig.witness_cols();
1480        let witness_arb_offset = add!(num_total_bin, num_pub_arb);
1481        let witness_arb_end = add!(witness_arb_offset, witness.num_arbitrary_poly_cols());
1482        let witness_int_offset = add!(add!(num_total_bin, num_total_arb), num_pub_int);
1483
1484        let witness_only =
1485            |all: &[DynamicPolynomial<C::Element>]| -> Vec<DynamicPolynomial<C::Element>> {
1486                all[num_pub_bin..num_total_bin]
1487                    .iter()
1488                    .chain(&all[witness_arb_offset..witness_arb_end])
1489                    .chain(&all[witness_int_offset..])
1490                    .cloned()
1491                    .collect()
1492            };
1493
1494        // --- Per-constraint-family witness-only lifted evals ---
1495        // Index 0: Q-family (q_0) at r_0. Indices 1..=n: declared primes
1496        // at family-specific r_0_fq[i-1]. Length = 1 + n_fq.
1497        let mut lifted_evals: Vec<Vec<DynamicPolynomial<C::Element>>> =
1498            Vec::with_capacity(add!(n_fq, 1));
1499
1500        // Q-family (index 0): compute all-col lifted evals, then keep
1501        // witness-only. We need the all-col version momentarily for the
1502        // `compute_lifted_evals` call signature (which projects from the
1503        // already-projected trace), but only the witness slice is sent.
1504        let q_lifted_all = compute_lifted_evals(
1505            &self.r_0,
1506            &self.base.original_trace.binary_poly,
1507            &self.projected_trace,
1508            &self.field_cfg,
1509        );
1510        lifted_evals.push(witness_only(&q_lifted_all));
1511
1512        // Declared-prime families (indices 1..=n). Reuse the per-prime
1513        // $\phi_{q_i}$-projected coefficient traces threaded forward from
1514        // step 2's `fq_staging` (via steps 4--6) — same layout as the
1515        // Q-family's `projected_trace`, just under each $q_i$'s cfg.
1516        debug_assert_eq!(self.projected_trace_fq.len(), n_fq);
1517        for (prime_idx, projected_trace_i) in self.projected_trace_fq.iter().enumerate() {
1518            let family_idx = add!(prime_idx, 1);
1519            let cfg_i = &self.all_field_cfgs[family_idx];
1520            let r_0_i = &self.r_0_fq[prime_idx];
1521            let lifted_evals_i = compute_lifted_evals(
1522                r_0_i,
1523                &self.base.original_trace.binary_poly,
1524                projected_trace_i,
1525                cfg_i,
1526            );
1527            lifted_evals.push(witness_only(&lifted_evals_i));
1528        }
1529
1530        // q'' family: witness-only lifted evals (PCS-only)
1531        // Compute the q''-projected witness lift at r*.
1532        // When q'' is aliased to q_0 (no F_q[X] constraints), this lift is
1533        // identical to the Q-family lift already computed, so we reuse it
1534        // while avoiding duplication.
1535        let lifted_evals_pp = if n_fq == 0 {
1536            None
1537        } else {
1538            let projected_trace_pp = project_trace_coeffs_row_major::<C, Zt::Int, Zt::Int, D, D>(
1539                self.base.original_trace,
1540                &q_pp_cfg,
1541            );
1542            let lifted_evals_pp_full = compute_lifted_evals(
1543                &r_star,
1544                &self.base.original_trace.binary_poly,
1545                &ProjectedTrace::RowMajor(projected_trace_pp),
1546                &q_pp_cfg,
1547            );
1548            Some(witness_only(&lifted_evals_pp_full))
1549        };
1550
1551        // --- Absorb all per-family coefficients into the transcript ---
1552        // Uniform order: each constraint family's witness-only lifted
1553        // evals, then the q'' family. Mirrored in step6_lifted_evals.
1554        let mut transcription_buf: Vec<u8> = vec![0; C::Integer::NUM_BYTES];
1555        debug_assert_eq!(self.all_field_cfgs.len(), lifted_evals.len());
1556        for (cfg_i, lifted_i) in self.all_field_cfgs.iter().zip(&lifted_evals) {
1557            for bar_u in lifted_i {
1558                self.base
1559                    .pcs_transcript
1560                    .fs_transcript
1561                    .absorb_field_element_slice(cfg_i, &bar_u.coeffs, &mut transcription_buf);
1562            }
1563        }
1564        if let Some(ref lifted_pp) = lifted_evals_pp {
1565            for bar_u in lifted_pp.iter() {
1566                self.base
1567                    .pcs_transcript
1568                    .fs_transcript
1569                    .absorb_field_element_slice(&q_pp_cfg, &bar_u.coeffs, &mut transcription_buf);
1570            }
1571        }
1572
1573        Ok(ProverLifted {
1574            base: self.base,
1575            field_cfg: self.field_cfg,
1576            ic_proof: self.ic_proof,
1577            ic_proof_fq: self.ic_proof_fq,
1578            cpr_proof: self.cpr_proof,
1579            combined_sumcheck: self.combined_sumcheck,
1580            cpr_proofs_fq: self.cpr_proofs_fq,
1581            combined_sumchecks_fq: self.combined_sumchecks_fq,
1582            lookup_proof: self.lookup_proof,
1583            booleanity_proof: self.booleanity_proof,
1584            affine_booleanity_proof: self.affine_booleanity_proof,
1585            mp_proof: self.mp_proof,
1586            mp_proofs_fq: self.mp_proofs_fq,
1587            lifted_evals,
1588            lifted_evals_pp,
1589            q_pp_cfg,
1590            r_star,
1591        })
1592    }
1593});
1594
1595impl_with_type_bounds!(ProverLifted
1596{
1597    /// Step 8: PCS open at $r^\star := r_0 \bmod q''$, where
1598    /// $q''$ was sampled at the start of step 7.
1599    ///
1600    /// The PCS opening prime $q''$ is decoupled from the constraint primes
1601    /// ($q_0$ and the declared $q_1, \dots, q_n$). This anchors the
1602    /// witness-polynomial commitments to a single fresh prime, so PCS
1603    /// soundness is governed entirely by $q''$ and is independent of the
1604    /// constraint moduli.
1605    ///
1606    /// **Transcript ordering**: $q''$ was already sampled at the start of
1607    /// step 7 (so that the $q''$-family lifted evals could be computed and
1608    /// sent there). Step 8 only samples the binary folding challenges
1609    /// under $q''$, then calls the PCS opens. Mirrored in
1610    /// [`step7_pcs_verify`](crate::ZincPlusPiop::step7_pcs_verify).
1611    pub fn step8_pcs_open<const CHECK_FOR_OVERFLOW: bool>(
1612        mut self,
1613    ) -> Result<ProverPcsOpened<'a, Zt, U, C, D, FD>, ProtocolError<C::Element>> {
1614        let witness_trace = &self.base.folded_witness_trace;
1615        let q_pp_cfg = &self.q_pp_cfg;
1616        let r_star = &self.r_star;
1617
1618        // Folded witness columns are proved using the extended evaluation
1619        // point `r_star_ext = r_star || folding_challenges`. Folding
1620        // challenges are sampled fresh under $q''$.
1621        let mut r_star_ext = r_star.clone();
1622        let num_folding_challenges = Zt::BinaryFold::FOLDING_FACTOR.ilog2();
1623        (0..num_folding_challenges).for_each(|_| {
1624            let g_chal: Zt::Chal = self.base.pcs_transcript.fs_transcript.get_challenge();
1625            let gamma = q_pp_cfg.project(&g_chal);
1626            r_star_ext.push(gamma);
1627        });
1628
1629        if let Some(hint_bin) = &self.base.hint_bin {
1630            let _ = ZipPlus::<Zt::BinaryZt, Zt::BinaryLc>::prove_f::<_, CHECK_FOR_OVERFLOW>(
1631                &mut self.base.pcs_transcript,
1632                self.base.pp_bin,
1633                &witness_trace.binary_poly,
1634                &r_star_ext,
1635                hint_bin,
1636                q_pp_cfg,
1637            )?;
1638        }
1639        if let Some(hint_arb) = &self.base.hint_arb {
1640            let _ = ZipPlus::<Zt::ArbitraryZt, Zt::ArbitraryLc>::prove_f::<_, CHECK_FOR_OVERFLOW>(
1641                &mut self.base.pcs_transcript,
1642                self.base.pp_arb,
1643                &witness_trace.arbitrary_poly,
1644                r_star,
1645                hint_arb,
1646                q_pp_cfg,
1647            )?;
1648        }
1649        if let Some(hint_int) = &self.base.hint_int {
1650            let _ = ZipPlus::<Zt::IntZt, Zt::IntLc>::prove_f::<_, CHECK_FOR_OVERFLOW>(
1651                &mut self.base.pcs_transcript,
1652                self.base.pp_int,
1653                &witness_trace.int,
1654                r_star,
1655                hint_int,
1656                q_pp_cfg,
1657            )?;
1658        }
1659
1660        Ok(ProverPcsOpened {
1661            base: self.base,
1662            field_cfg: self.field_cfg,
1663            q_pp_cfg: self.q_pp_cfg,
1664            ic_proof: self.ic_proof,
1665            ic_proof_fq: self.ic_proof_fq,
1666            cpr_proof: self.cpr_proof,
1667            combined_sumcheck: self.combined_sumcheck,
1668            cpr_proofs_fq: self.cpr_proofs_fq,
1669            combined_sumchecks_fq: self.combined_sumchecks_fq,
1670            lookup_proof: self.lookup_proof,
1671            booleanity_proof: self.booleanity_proof,
1672            affine_booleanity_proof: self.affine_booleanity_proof,
1673            mp_proof: self.mp_proof,
1674            mp_proofs_fq: self.mp_proofs_fq,
1675            lifted_evals: self.lifted_evals,
1676            lifted_evals_pp: self.lifted_evals_pp,
1677        })
1678    }
1679});
1680
1681impl_with_type_bounds!(ProverPcsOpened
1682{
1683    /// Assemble the final proof from accumulated state, lifting every field
1684    /// element into its canonical integer
1685    pub fn finish(self) -> Result<Proof<Zt::Fmod>, ProtocolError<C::Element>> {
1686        let zip_proof = self.base.pcs_transcript.stream.into_inner();
1687        let commitments = (
1688            self.base.commitment_bin,
1689            self.base.commitment_arb,
1690            self.base.commitment_int,
1691        );
1692        let all_cfgs = build_all_cfgs::<C>(&self.base.uair_signature, self.field_cfg.clone());
1693
1694        // Helpers
1695        macro_rules! lift {
1696            ($cfg:expr, $section:expr) => {
1697                $section.try_map(|e| Ok::<Zt::Fmod, ProtocolError<C::Element>>($cfg.lift(e)))
1698            };
1699        }
1700        macro_rules! lift_fq_vec {
1701            ($section:expr) => {
1702                 $section
1703                    .iter()
1704                    .enumerate()
1705                    .map(|(i, p)| lift!(all_cfgs[add!(i, 1)], p))
1706                    .try_collect()
1707            };
1708        }
1709        let lift_polys = |cfg: &C,
1710                          polys: &[DynamicPolynomial<C::Element>]|
1711         -> Result<Vec<DynamicPolynomial<Zt::Fmod>>, ProtocolError<C::Element>> {
1712            polys
1713                .iter()
1714                .map(|p| p.try_map(|e| Ok(cfg.lift(e))))
1715                .collect()
1716        };
1717
1718        let witness_lifted_evals = self
1719            .lifted_evals
1720            .iter()
1721            .enumerate()
1722            .map(|(i, polys)| lift_polys(&all_cfgs[i], polys))
1723            .collect::<Result<Vec<_>, _>>()?;
1724        let witness_lifted_evals_pp = self
1725            .lifted_evals_pp
1726            .as_ref()
1727            .map(|polys| lift_polys(&self.q_pp_cfg, polys))
1728            .transpose()?;
1729
1730        Ok(Proof {
1731            commitments,
1732            ideal_check: lift!(self.field_cfg, self.ic_proof)?,
1733            cpr_proof: lift!(self.field_cfg, self.cpr_proof)?,
1734            combined_sumcheck: lift!(self.field_cfg, self.combined_sumcheck)?,
1735            multipoint_eval: lift!(self.field_cfg, self.mp_proof)?,
1736            zip: zip_proof,
1737            witness_lifted_evals,
1738            lookup_proof: match &self.lookup_proof {
1739                Some(p) => Some(lift!(self.field_cfg, p)?),
1740                None => None,
1741            },
1742            booleanity_proof: match &self.booleanity_proof {
1743                Some(p) => Some(lift!(self.field_cfg, p)?),
1744                None => None,
1745            },
1746            affine_booleanity_proof: match &self.affine_booleanity_proof {
1747                Some(p) => Some(lift!(self.field_cfg, p)?),
1748                None => None,
1749            },
1750            ideal_checks_fq: lift_fq_vec!(self.ic_proof_fq)?,
1751            cpr_proofs_fq: lift_fq_vec!(self.cpr_proofs_fq)?,
1752            combined_sumchecks_fq: lift_fq_vec!(self.combined_sumchecks_fq)?,
1753            multipoint_evals_fq: lift_fq_vec!(self.mp_proofs_fq)?,
1754            witness_lifted_evals_pp,
1755        })
1756    }
1757});
1758
1759//
1760// prove() wrapper
1761//
1762
1763impl<Zt, U, C, const D: usize, const FD: usize> ZincPlusPiop<Zt, U, C, D, FD>
1764where
1765    Zt: ZincTypes<D, FD>,
1766    Zt::Int: ProjectableToField<C>,
1767    Zt::CombR: MulByScalar<Zt::Chal>,
1768    <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<C>,
1769    C: BaseFieldConfig<Integer = Zt::Fmod>
1770        + ProjectPrimitiveIntegersWithConfig
1771        + ProjectElementWithConfig<Zt::Int>
1772        + ProjectElementWithConfig<Zt::CombR>
1773        + ProjectElementWithConfig<Zt::Chal>
1774        + Clone
1775        + Send
1776        + Sync
1777        + 'static,
1778    C::Integer: Display,
1779    U: Uair<Prime = Zt::Fmod> + 'static,
1780{
1781    /// Zinc+ full PIOP prover.
1782    ///
1783    /// Runs all protocol steps in sequence and returns the assembled proof.
1784    /// For per-step control, start with [`Self::step0_fold`] and chain the
1785    /// individual `stepN_*` methods.
1786    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
1787    pub fn prove<const MLE_FIRST: bool, const CHECK_FOR_OVERFLOW: bool>(
1788        pp: &(
1789            ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
1790            ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
1791            ZipPlusParams<Zt::IntZt, Zt::IntLc>,
1792        ),
1793        trace: &UairTrace<'static, Zt::Int, Zt::Int, D, D>,
1794        num_vars: usize,
1795        project_scalar: impl Fn(&U::Scalar, &C) -> DynamicPolynomial<C::Element> + Copy,
1796    ) -> Result<Proof<Zt::Fmod>, ProtocolError<C::Element>> {
1797        let committed = Self::step0_fold(trace)?.step1_commit(pp, num_vars)?;
1798
1799        let ideal_checked = if MLE_FIRST {
1800            committed
1801                .step2_mle_first(project_scalar)?
1802                .step3_ideal_check()?
1803        } else {
1804            committed
1805                .step2_combined(project_scalar)?
1806                .step3_ideal_check()?
1807        };
1808
1809        ideal_checked
1810            .step4_eval_projection()?
1811            .step5_sumcheck()?
1812            .step6_multipoint_eval()?
1813            .step7_lift_and_project()?
1814            .step8_pcs_open::<CHECK_FOR_OVERFLOW>()?
1815            .finish()
1816    }
1817}
1818
1819#[allow(clippy::type_complexity)]
1820fn commit_optionally<Zt: ZipTypes, Lc: LinearCode<Zt>>(
1821    pp: &ZipPlusParams<Zt, Lc>,
1822    trace: &[DenseMultilinearExtension<Zt::Eval>],
1823) -> Result<(Option<ZipPlusHint<Zt::Cw>>, ZipPlusCommitment), ZipError> {
1824    if trace.is_empty() {
1825        Ok((
1826            None,
1827            ZipPlusCommitment {
1828                root: Default::default(),
1829                batch_size: 0,
1830            },
1831        ))
1832    } else {
1833        let (hint, commitment) = ZipPlus::commit(pp, trace)?;
1834        Ok((Some(hint), commitment))
1835    }
1836}