Skip to main content

zinc_protocol/
prover.rs

1use super::*;
2use crypto_primitives::{ConstIntSemiring, FromPrimitiveWithConfig, FromWithConfig};
3use num_traits::Zero;
4use std::{borrow::Cow, collections::HashMap, fmt::Debug};
5use zinc_piop::{
6    combined_poly_resolver::CombinedPolyResolver,
7    ideal_check::IdealCheckProtocol,
8    lookup::booleanity::{self, BooleanityChecker, BooleanityProof},
9    multipoint_eval::{MultipointEval, Proof as MultipointEvalProof},
10    projections::{
11        ColumnMajorTrace, ProjectedTrace, RowMajorTrace, evaluate_trace_to_column_mles,
12        project_scalars, project_scalars_to_field, project_trace_coeffs_column_major,
13        project_trace_coeffs_row_major,
14    },
15    sumcheck::multi_degree::MultiDegreeSumcheck,
16};
17use zinc_poly::univariate::dynamic::over_field::DynamicPolynomialF;
18use zinc_transcript::traits::{ConstTranscribable, Transcript};
19use zinc_uair::{
20    Uair, UairSignature, UairTrace, constraint_counter::count_constraints,
21    degree_counter::count_max_degree,
22};
23use zinc_utils::{
24    add, cfg_join, from_ref::FromRef, inner_transparent_field::InnerTransparentField,
25    mul_by_scalar::MulByScalar, projectable_to_field::ProjectableToField,
26};
27use zip_plus::{
28    pcs::structs::{ZipPlus, ZipPlusHint, ZipPlusParams, ZipTypes},
29    pcs_transcript::PcsProverTranscript,
30};
31
32//
33// Type-state structs
34//
35
36/// Persistent prover infrastructure carried across every step: the
37/// Fiat-Shamir transcript, PCS parameters/hints/commitments, and trace
38/// reference.
39#[derive(Clone, Debug)]
40pub struct ProverFolded<
41    'a,
42    Zt: ZincTypes<D, FD>,
43    U: Uair,
44    F: PrimeField,
45    const D: usize,
46    const FD: usize,
47> {
48    uair_signature: UairSignature,
49    original_trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D, D>,
50    folded_witness_trace: UairTrace<'a, Zt::Int, Zt::Int, FD, D>,
51
52    _phantom: PhantomData<(&'a u8, U, F)>,
53}
54
55/// Persistent prover infrastructure carried across every step: the
56/// Fiat-Shamir transcript, PCS parameters/hints/commitments, and trace
57/// reference.
58/// Obtained after step 1 via [`step1_commit`](ProverFolded::step1_commit).
59#[derive(Clone, Debug)]
60pub struct ProverCommitted<
61    'a,
62    Zt: ZincTypes<D, FD>,
63    U: Uair,
64    F: PrimeField,
65    const D: usize,
66    const FD: usize,
67> {
68    num_vars: usize,
69    uair_signature: UairSignature,
70    original_trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D, D>,
71    folded_witness_trace: UairTrace<'a, Zt::Int, Zt::Int, FD, D>,
72    pcs_transcript: PcsProverTranscript,
73
74    // Commitment info
75    pp_bin: &'a ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
76    pp_arb: &'a ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
77    pp_int: &'a ZipPlusParams<Zt::IntZt, Zt::IntLc>,
78    hint_bin: Option<ZipPlusHint<<Zt::BinaryZt as ZipTypes>::Cw>>,
79    hint_arb: Option<ZipPlusHint<<Zt::ArbitraryZt as ZipTypes>::Cw>>,
80    hint_int: Option<ZipPlusHint<<Zt::IntZt as ZipTypes>::Cw>>,
81    commitment_bin: ZipPlusCommitment,
82    commitment_arb: ZipPlusCommitment,
83    commitment_int: ZipPlusCommitment,
84
85    _phantom: PhantomData<(U, F)>,
86}
87
88/// After step 2 via [`step2_combined`](ProverCommitted::step2_combined)
89/// (row-major / "combined" projection).
90#[derive(Clone, Debug)]
91pub struct ProverProjectedCombined<
92    'a,
93    Zt: ZincTypes<D, FD>,
94    U: Uair,
95    F: PrimeField,
96    const D: usize,
97    const FD: usize,
98> {
99    base: ProverCommitted<'a, Zt, U, F, D, FD>,
100    field_cfg: F::Config,
101    projected_trace: RowMajorTrace<F>,
102    projected_scalars_fx: HashMap<U::Scalar, DynamicPolynomialF<F>>,
103}
104
105/// After step 2 via [`step2_mle_first`](ProverCommitted::step2_mle_first)
106/// (column-major / MLE-first projection).
107#[derive(Clone, Debug)]
108pub struct ProverProjectedMleFirst<
109    'a,
110    Zt: ZincTypes<D, FD>,
111    U: Uair,
112    F: PrimeField,
113    const D: usize,
114    const FD: usize,
115> {
116    base: ProverCommitted<'a, Zt, U, F, D, FD>,
117    field_cfg: F::Config,
118    projected_trace: ColumnMajorTrace<F>,
119    projected_scalars_fx: HashMap<U::Scalar, DynamicPolynomialF<F>>,
120}
121
122/// After step 3 (ideal check).
123#[derive(Clone, Debug)]
124pub struct ProverIdealChecked<
125    'a,
126    Zt: ZincTypes<D, FD>,
127    U: Uair,
128    F: PrimeField,
129    const D: usize,
130    const FD: usize,
131> {
132    base: ProverCommitted<'a, Zt, U, F, D, FD>,
133    field_cfg: F::Config,
134    projected_trace: ProjectedTrace<F>,
135    projected_scalars_fx: HashMap<U::Scalar, DynamicPolynomialF<F>>,
136
137    // New
138    ic_proof: IdealCheckProof<F>,
139    ic_eval_point: Vec<F>,
140}
141
142/// After step 4 (eval projection). `projected_scalars_fx` has been consumed.
143#[derive(Clone, Debug)]
144pub struct ProverEvalProjected<
145    'a,
146    Zt: ZincTypes<D, FD>,
147    U: Uair,
148    F: PrimeField,
149    const D: usize,
150    const FD: usize,
151> {
152    base: ProverCommitted<'a, Zt, U, F, D, FD>,
153    field_cfg: F::Config,
154    projected_trace: ProjectedTrace<F>,
155    ic_proof: IdealCheckProof<F>,
156    ic_eval_point: Vec<F>,
157
158    // New
159    projected_trace_f: Vec<DenseMultilinearExtension<F::Inner>>,
160    projected_scalars_f: HashMap<U::Scalar, F>,
161}
162
163/// After step 5 (sumcheck).
164#[allow(clippy::type_complexity)]
165#[derive(Clone, Debug)]
166pub struct ProverSumchecked<
167    'a,
168    Zt: ZincTypes<D, FD>,
169    U: Uair,
170    F: PrimeField,
171    const D: usize,
172    const FD: usize,
173> {
174    base: ProverCommitted<'a, Zt, U, F, D, FD>,
175    field_cfg: F::Config,
176    projected_trace: ProjectedTrace<F>,
177    ic_proof: IdealCheckProof<F>,
178    projected_trace_f: Vec<DenseMultilinearExtension<F::Inner>>,
179
180    // New
181    cpr_proof: CombinedPolyResolverProof<F>,
182    cpr_eval_point: Vec<F>,
183    combined_sumcheck: MultiDegreeSumcheckProof<F>,
184    lookup_proof: Option<BatchedLookupProof<F>>,
185    booleanity_proof: Option<BooleanityProof<F>>,
186}
187
188/// After step 6 (multipoint eval).
189#[derive(Clone, Debug)]
190pub struct ProverMultipointEvaled<
191    'a,
192    Zt: ZincTypes<D, FD>,
193    U: Uair,
194    F: PrimeField,
195    const D: usize,
196    const FD: usize,
197> {
198    base: ProverCommitted<'a, Zt, U, F, D, FD>,
199    field_cfg: F::Config,
200    projected_trace: ProjectedTrace<F>,
201    ic_proof: IdealCheckProof<F>,
202    cpr_proof: CombinedPolyResolverProof<F>,
203    combined_sumcheck: MultiDegreeSumcheckProof<F>,
204    lookup_proof: Option<BatchedLookupProof<F>>,
205    booleanity_proof: Option<BooleanityProof<F>>,
206
207    // New
208    mp_proof: MultipointEvalProof<F>,
209    r_0: Vec<F>,
210}
211
212/// After step 7 (lift-and-project).
213#[derive(Clone, Debug)]
214pub struct ProverLifted<
215    'a,
216    Zt: ZincTypes<D, FD>,
217    U: Uair,
218    F: PrimeField,
219    const D: usize,
220    const FD: usize,
221> {
222    base: ProverCommitted<'a, Zt, U, F, D, FD>,
223    field_cfg: F::Config,
224    ic_proof: IdealCheckProof<F>,
225    cpr_proof: CombinedPolyResolverProof<F>,
226    combined_sumcheck: MultiDegreeSumcheckProof<F>,
227    lookup_proof: Option<BatchedLookupProof<F>>,
228    booleanity_proof: Option<BooleanityProof<F>>,
229    mp_proof: MultipointEvalProof<F>,
230    r_0: Vec<F>,
231
232    // New
233    lifted_evals: Vec<DynamicPolynomialF<F>>,
234}
235
236/// After step 8 (PCS open). No new fields are added here, but the PCS
237/// transcript has been updated with the opening proof.
238/// Ready for generating the final proof object in
239/// [`finish`](ProverPcsOpened::finish).
240#[derive(Clone, Debug)]
241pub struct ProverPcsOpened<
242    'a,
243    Zt: ZincTypes<D, FD>,
244    U: Uair,
245    F: PrimeField,
246    const D: usize,
247    const FD: usize,
248> {
249    base: ProverCommitted<'a, Zt, U, F, D, FD>,
250    ic_proof: IdealCheckProof<F>,
251    cpr_proof: CombinedPolyResolverProof<F>,
252    combined_sumcheck: MultiDegreeSumcheckProof<F>,
253    lookup_proof: Option<BatchedLookupProof<F>>,
254    booleanity_proof: Option<BooleanityProof<F>>,
255    mp_proof: MultipointEvalProof<F>,
256    lifted_evals: Vec<DynamicPolynomialF<F>>,
257}
258
259//
260// Step implementations
261//
262
263/// Prover uses common type bounds across all steps, so we use a helper macro to
264/// define them
265macro_rules! impl_with_type_bounds {
266    ($type_name:ident { $($code:tt)* }) => {
267        impl<'a, Zt, U, F, const D: usize, const FD: usize> $type_name<'a, Zt, U, F, D, FD>
268        where
269            Zt: ZincTypes<D, FD>,
270            Zt::Int: ProjectableToField<F>,
271            <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<F>,
272            U: Uair + 'static,
273            F: InnerTransparentField
274                + FromPrimitiveWithConfig
275                + for<'b> FromWithConfig<&'b Zt::Int>
276                + for<'b> FromWithConfig<&'b Zt::CombR>
277                + for<'b> FromWithConfig<&'b Zt::Chal>
278                + for<'b> MulByScalar<&'b F>
279                + FromRef<F>
280                + Send
281                + Sync
282                + 'static,
283            F::Inner:
284                ConstIntSemiring + ConstTranscribable + FromRef<Zt::Fmod> + Send + Sync + Zero + Default,
285            F::Modulus: ConstTranscribable + FromRef<Zt::Fmod>,
286        {
287            $($code)*
288        }
289    };
290}
291
292impl<Zt, U, F, const D: usize, const FD: usize> ZincPlusPiop<Zt, U, F, D, FD>
293where
294    Zt: ZincTypes<D, FD>,
295    U: Uair,
296    F: PrimeField,
297    F::Inner: ConstTranscribable,
298{
299    /// Step 1: Folding the trace.
300    #[allow(clippy::type_complexity)]
301    pub fn step0_fold<'a>(
302        trace: &'a UairTrace<'static, Zt::Int, Zt::Int, D, D>,
303    ) -> Result<ProverFolded<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
304        let uair_signature = U::signature();
305        let witness_trace = trace.witness(&uair_signature);
306
307        let folded_bin_witness_trace = cfg_iter!(witness_trace.binary_poly)
308            .map(Zt::BinaryFold::fold_trace_mle)
309            .collect();
310
311        let folded_witness_trace = UairTrace {
312            binary_poly: Cow::Owned(folded_bin_witness_trace),
313            arbitrary_poly: witness_trace.arbitrary_poly.clone(),
314            int: witness_trace.int.clone(),
315        };
316
317        Ok(ProverFolded {
318            uair_signature,
319            original_trace: trace,
320            folded_witness_trace,
321            _phantom: PhantomData,
322        })
323    }
324}
325
326impl_with_type_bounds!(ProverFolded
327{
328    /// Step 1: Commitment.
329    /// Commit *witness* columns via Zip+ PCS, absorb roots and public
330    /// data into the Fiat-Shamir transcript.
331    #[allow(clippy::type_complexity)]
332    pub fn step1_commit(
333        self,
334        (pp_bin, pp_arb, pp_int): &'a (
335            ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
336            ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
337            ZipPlusParams<Zt::IntZt, Zt::IntLc>,
338        ),
339        num_vars: usize,
340    ) -> Result<ProverCommitted<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
341        let sig = &self.uair_signature;
342        let public_trace = self.original_trace.public(sig);
343
344        let (res_bin, (res_arb, res_int)) = cfg_join!(
345            commit_optionally(pp_bin, &self.folded_witness_trace.binary_poly),
346            commit_optionally(pp_arb, &self.folded_witness_trace.arbitrary_poly),
347            commit_optionally(pp_int, &self.folded_witness_trace.int),
348        );
349        let (hint_bin, commitment_bin) = res_bin?;
350        let (hint_arb, commitment_arb) = res_arb?;
351        let (hint_int, commitment_int) = res_int?;
352
353        let mut pcs_transcript = PcsProverTranscript::new_from_commitments(
354            [&commitment_bin, &commitment_arb, &commitment_int].into_iter(),
355        );
356
357        absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.binary_poly);
358        absorb_public_columns(
359            &mut pcs_transcript.fs_transcript,
360            &public_trace.arbitrary_poly,
361        );
362        absorb_public_columns(&mut pcs_transcript.fs_transcript, &public_trace.int);
363
364        Ok(ProverCommitted {
365            num_vars,
366            uair_signature: self.uair_signature,
367            original_trace: self.original_trace,
368            folded_witness_trace: self.folded_witness_trace,
369            pcs_transcript,
370            pp_bin,
371            pp_arb,
372            pp_int,
373            hint_bin,
374            hint_arb,
375            hint_int,
376            commitment_bin,
377            commitment_arb,
378            commitment_int,
379            _phantom: PhantomData,
380        })
381    }
382});
383
384impl_with_type_bounds!(ProverCommitted
385{
386    #[allow(clippy::type_complexity)]
387    fn project_common<S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF<F>>(
388        &mut self,
389        project_scalar: S,
390    ) -> Result<(F::Config, HashMap<U::Scalar, DynamicPolynomialF<F>>), ProtocolError<F, U::Ideal>>
391    {
392        let field_cfg = self
393            .pcs_transcript
394            .fs_transcript
395            .get_random_field_cfg::<F, Zt::Fmod, Zt::PrimeTest>();
396
397        let projected_scalars_fx = project_scalars::<F, U>(|s| project_scalar(s, &field_cfg));
398        Ok((field_cfg, projected_scalars_fx))
399    }
400
401    /// Step 2 (combined / row-major): Prime projection
402    /// (`\phi_q`: `Z[X] -> F_q[X]`). Samples a random prime, projects the
403    /// full trace and scalars using the row-major layout.
404    /// Works for both linear and non-linear constraints.
405    pub fn step2_combined<S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF<F>>(
406        mut self,
407        project_scalar: S,
408    ) -> Result<ProverProjectedCombined<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
409        let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?;
410
411        let projected_trace = project_trace_coeffs_row_major(self.original_trace, &field_cfg);
412        Ok(ProverProjectedCombined {
413            base: self,
414            field_cfg,
415            projected_trace,
416            projected_scalars_fx,
417        })
418    }
419
420    /// Step 2 (MLE-first / column-major): Prime projection
421    /// (`\phi_q`: `Z[X] -> F_q[X]`). Samples a random prime, projects the
422    /// full trace and scalars using the column-major layout.
423    pub fn step2_mle_first<S: Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF<F>>(
424        mut self,
425        project_scalar: S,
426    ) -> Result<ProverProjectedMleFirst<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
427        let (field_cfg, projected_scalars_fx) = self.project_common(project_scalar)?;
428
429        let projected_trace = project_trace_coeffs_column_major(self.original_trace, &field_cfg);
430        Ok(ProverProjectedMleFirst {
431            base: self,
432            field_cfg,
433            projected_trace,
434            projected_scalars_fx,
435        })
436    }
437});
438
439impl_with_type_bounds!(ProverProjectedCombined
440{
441    /// Step 3 (combined): Ideal check via `prove_combined` on the row-major
442    /// trace. Works for both linear and non-linear constraints.
443    pub fn step3_ideal_check(
444        mut self,
445    ) -> Result<ProverIdealChecked<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
446        let num_constraints = count_constraints::<U>();
447
448        let (ic_proof, ic_prover_state) = U::prove_combined(
449            &mut self.base.pcs_transcript.fs_transcript,
450            &self.projected_trace,
451            &self.projected_scalars_fx,
452            num_constraints,
453            self.base.num_vars,
454            &self.field_cfg,
455        )?;
456
457        Ok(ProverIdealChecked {
458            base: self.base,
459            field_cfg: self.field_cfg,
460            projected_trace: ProjectedTrace::RowMajor(self.projected_trace),
461            projected_scalars_fx: self.projected_scalars_fx,
462            ic_proof,
463            ic_eval_point: ic_prover_state.evaluation_point,
464        })
465    }
466});
467
468impl_with_type_bounds!(ProverProjectedMleFirst
469{
470    /// Step 3 (MLE-first): Ideal check via `prove_mle_first` on the
471    /// column-major trace. Works for any UAIR: linear non-zero-ideal
472    /// constraints go through the column-major MLE-first path, non-linear
473    /// non-zero-ideal constraints fall back to the row-major path (with an
474    /// internal transpose), and zero-ideal constraints are short-circuited
475    /// to zero.
476    pub fn step3_ideal_check(
477        mut self,
478    ) -> Result<ProverIdealChecked<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
479        let num_constraints = count_constraints::<U>();
480
481        let (ic_proof, ic_prover_state) = U::prove_mle_first(
482            &mut self.base.pcs_transcript.fs_transcript,
483            &self.projected_trace,
484            &self.projected_scalars_fx,
485            num_constraints,
486            self.base.num_vars,
487            &self.field_cfg,
488        )?;
489
490        Ok(ProverIdealChecked {
491            base: self.base,
492            field_cfg: self.field_cfg,
493            projected_trace: ProjectedTrace::ColumnMajor(self.projected_trace),
494            projected_scalars_fx: self.projected_scalars_fx,
495            ic_proof,
496            ic_eval_point: ic_prover_state.evaluation_point,
497        })
498    }
499});
500
501impl_with_type_bounds!(ProverIdealChecked
502{
503    /// Step 4: Evaluation projection (`\psi_a`: `F_q[X] -> F_q`). Samples
504    /// `a in F_q`, evaluates polynomials at `X = a`.
505    pub fn step4_eval_projection(
506        mut self,
507    ) -> Result<ProverEvalProjected<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
508        let projecting_element: Zt::Chal = self.base.pcs_transcript.fs_transcript.get_challenge();
509        let projecting_element_f: F = F::from_with_cfg(&projecting_element, &self.field_cfg);
510
511        let projected_trace_f =
512            evaluate_trace_to_column_mles(&self.projected_trace, &projecting_element_f);
513
514        let projected_scalars_f =
515            project_scalars_to_field(self.projected_scalars_fx, &projecting_element_f)
516                .map_err(|(_s, _f, e)| ProtocolError::ScalarProjection(e))?;
517
518        Ok(ProverEvalProjected {
519            base: self.base,
520            field_cfg: self.field_cfg,
521            projected_trace: self.projected_trace,
522            ic_proof: self.ic_proof,
523            ic_eval_point: self.ic_eval_point,
524            projected_trace_f,
525            projected_scalars_f,
526        })
527    }
528});
529
530impl_with_type_bounds!(ProverEvalProjected
531{
532    /// Step 5: Combined CPR + Booleanity + Lookup multi-degree sumcheck over F_q.
533    /// Batches the CPR constraint claim (degree `max_deg+2`), the booleanity
534    /// argument (degree 3), and lookup groups (one per table type) into a
535    /// single sumcheck sharing one evaluation point `r*`. Produces
536    /// `up_evals`/`down_evals` (CPR), `bit_slice_evals` (booleanity), and
537    /// lookup auxiliary witnesses at `r*`.
538    pub fn step5_sumcheck(
539        mut self,
540    ) -> Result<ProverSumchecked<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
541        let num_constraints = count_constraints::<U>();
542        let max_degree = count_max_degree::<U>();
543
544        let (cpr_group, cpr_ancillary) = CombinedPolyResolver::prepare_sumcheck_group::<U>(
545            &mut self.base.pcs_transcript.fs_transcript,
546            self.projected_trace_f.clone(),
547            &self.ic_eval_point,
548            &self.projected_scalars_f,
549            num_constraints,
550            self.base.num_vars,
551            max_degree,
552            &self.field_cfg,
553        )?;
554
555        let mut groups = vec![cpr_group];
556
557        // Booleanity: prepare optional group over witness binary-poly cols.
558        let trace_wit_bin_poly = {
559            let sig = &self.base.uair_signature;
560            let num_pub_bin = sig.public_cols().num_binary_poly_cols();
561            let num_total_bin = sig.total_cols().num_binary_poly_cols();
562            &self.base.original_trace.binary_poly[num_pub_bin..num_total_bin]
563        };
564
565        let bool_ancillary = if !trace_wit_bin_poly.is_empty() {
566            let (bool_group, anc) = BooleanityChecker::prepare_sumcheck_group::<D>(
567                &mut self.base.pcs_transcript.fs_transcript,
568                trace_wit_bin_poly,
569                self.base.num_vars,
570                &self.field_cfg,
571            )
572            .map_err(ProtocolError::Booleanity)?;
573            groups.push(bool_group);
574            Some(anc)
575        } else {
576            None
577        };
578
579        // TODO: for each LookupGroup from group_lookup_specs(lookup_specs):
580        //   - call prepare_batched_lookup_group(transcript, instance, &field_cfg)
581        //   - push triple into groups, collect pending proofs + metas
582
583        // Multi-degree sumcheck over CPR (+ booleanity + lookup groups).
584        let (combined_sumcheck, md_states) = MultiDegreeSumcheck::prove_as_subprotocol(
585            &mut self.base.pcs_transcript.fs_transcript,
586            groups,
587            self.base.num_vars,
588            &self.field_cfg,
589        );
590
591        let mut md_iter = md_states.into_iter();
592
593        let (cpr_proof, cpr_prover_state) = CombinedPolyResolver::finalize_prover(
594            &mut self.base.pcs_transcript.fs_transcript,
595            md_iter.next().expect("CPR group always present"),
596            cpr_ancillary,
597            &self.field_cfg,
598        )?;
599
600        let booleanity_proof = if let Some(anc) = bool_ancillary {
601            let state = md_iter.next().expect("booleanity group present");
602            Some(
603                BooleanityChecker::finalize_prover(
604                    &mut self.base.pcs_transcript.fs_transcript,
605                    state,
606                    anc,
607                    &self.field_cfg,
608                )
609                .map_err(ProtocolError::Booleanity)?,
610            )
611        } else {
612            None
613        };
614
615        // TODO: build BatchedLookupProof from collected lookup_proofs + lookup_metas
616        let lookup_proof = None;
617
618        Ok(ProverSumchecked {
619            base: self.base,
620            field_cfg: self.field_cfg,
621            projected_trace: self.projected_trace,
622            ic_proof: self.ic_proof,
623            projected_trace_f: self.projected_trace_f,
624            cpr_proof,
625            cpr_eval_point: cpr_prover_state.evaluation_point,
626            combined_sumcheck,
627            lookup_proof,
628            booleanity_proof,
629        })
630    }
631});
632
633impl_with_type_bounds!(ProverSumchecked
634{
635    /// Step 6: Multi-point evaluation sumcheck. Combines `up_evals` and
636    /// `down_evals` at `r'` into a single evaluation point `r_0`.
637    /// Only the sumcheck proof is sent; scalar evaluations at `r_0` are derived from the
638    /// polynomial-valued `lifted_evals` in Step 7. When the booleanity argument
639    /// is present, its bit-slice MLEs and `bit_slice_evals` are appended to
640    /// the `trace_mles` / `up_evals` passed to `MultipointEval`; the
641    /// corresponding `r_0`-side scalar claims are discharged for free by the
642    /// verifier against `lifted_evals.coeffs` in step 6.
643    pub fn step6_multipoint_eval(
644        mut self,
645    ) -> Result<ProverMultipointEvaled<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
646        // Build extended trace_mles and up_evals: append witness bit-slice
647        // MLEs and their evaluations at r*, in the canonical order produced
648        // by `build_witness_bit_slice_mles` (j-major, i-minor).
649        let (extended_trace_mles, extended_up_evals) = if let Some(bp) = &self.booleanity_proof {
650            let trace_wit_bin_poly = {
651                let sig = &self.base.uair_signature;
652                let num_pub_bin = sig.public_cols().num_binary_poly_cols();
653                let num_total_bin = sig.total_cols().num_binary_poly_cols();
654                &self.base.original_trace.binary_poly[num_pub_bin..num_total_bin]
655            };
656
657            let mut trace_mles = self.projected_trace_f.clone();
658            trace_mles.extend(booleanity::build_witness_bit_slice_mles::<F, D>(
659                trace_wit_bin_poly,
660                &self.field_cfg,
661            ));
662
663            let mut up_evals = self.cpr_proof.up_evals.clone();
664            up_evals.extend_from_slice(&bp.bit_slice_evals);
665            (trace_mles, up_evals)
666        } else {
667            (self.projected_trace_f.clone(), self.cpr_proof.up_evals.clone())
668        };
669
670        let (mp_proof, mp_prover_state) = MultipointEval::prove_as_subprotocol(
671            &mut self.base.pcs_transcript.fs_transcript,
672            &extended_trace_mles,
673            &self.cpr_eval_point,
674            &extended_up_evals,
675            &self.cpr_proof.down_evals,
676            self.base.uair_signature.shifts(),
677            &self.field_cfg,
678        )?;
679
680        Ok(ProverMultipointEvaled {
681            base: self.base,
682            field_cfg: self.field_cfg,
683            projected_trace: self.projected_trace,
684            ic_proof: self.ic_proof,
685            cpr_proof: self.cpr_proof,
686            combined_sumcheck: self.combined_sumcheck,
687            lookup_proof: self.lookup_proof,
688            booleanity_proof: self.booleanity_proof,
689            mp_proof,
690            r_0: mp_prover_state.eval_point,
691        })
692    }
693});
694
695impl_with_type_bounds!(ProverMultipointEvaled
696{
697    /// Step 7: Lift-and-project. Computes per-column polynomial MLE
698    /// evaluations at `r_0` in `F_q[X]` and absorbs them into the transcript.
699    pub fn step7_lift_and_project(
700        mut self,
701    ) -> Result<ProverLifted<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
702        // Compute per-column polynomial MLE evaluations at r_0 in F_q[X]
703        // (after \phi_q but before \psi_a). The verifier derives the scalar
704        // open_evals via \psi_a for the sumcheck consistency check, and
705        // supplies these to the Zip+ PCS for alpha-projection.
706        let lifted_evals = compute_lifted_evals(
707            &self.r_0,
708            &self.base.original_trace.binary_poly,
709            &self.projected_trace,
710            &self.field_cfg,
711        );
712
713        let mut transcription_buf: Vec<u8> = vec![0; F::Inner::NUM_BYTES];
714        for bar_u in &lifted_evals {
715            self.base
716                .pcs_transcript
717                .fs_transcript
718                .absorb_random_field_slice(&bar_u.coeffs, &mut transcription_buf);
719        }
720
721        Ok(ProverLifted {
722            base: self.base,
723            field_cfg: self.field_cfg,
724            ic_proof: self.ic_proof,
725            cpr_proof: self.cpr_proof,
726            combined_sumcheck: self.combined_sumcheck,
727            lookup_proof: self.lookup_proof,
728            booleanity_proof: self.booleanity_proof,
729            mp_proof: self.mp_proof,
730            r_0: self.r_0,
731            lifted_evals,
732        })
733    }
734});
735
736impl_with_type_bounds!(ProverLifted
737{
738    /// Step 8: PCS open at `r_0` (witness columns only).
739    pub fn step8_pcs_open<const CHECK_FOR_OVERFLOW: bool>(
740        mut self,
741    ) -> Result<ProverPcsOpened<'a, Zt, U, F, D, FD>, ProtocolError<F, U::Ideal>> {
742        let witness_trace = &self.base.folded_witness_trace;
743
744        // Folded witness columns are proved using the extended evaluation point
745        // `r_0_ext = r_0 || folding_challenges`.
746        let mut r_0_ext = self.r_0.clone();
747        let num_folding_challenges = Zt::BinaryFold::FOLDING_FACTOR.ilog2();
748        (0..num_folding_challenges).for_each(|_| {
749            let g_chal: Zt::Chal = self.base.pcs_transcript.fs_transcript.get_challenge();
750            let gamma = F::from_with_cfg(&g_chal, &self.field_cfg);
751            r_0_ext.push(gamma);
752        });
753
754        if let Some(hint_bin) = &self.base.hint_bin {
755            let _ = ZipPlus::<Zt::BinaryZt, Zt::BinaryLc>::prove_f::<_, CHECK_FOR_OVERFLOW>(
756                &mut self.base.pcs_transcript,
757                self.base.pp_bin,
758                &witness_trace.binary_poly,
759                &r_0_ext,
760                hint_bin,
761                &self.field_cfg,
762            )?;
763        }
764        if let Some(hint_arb) = &self.base.hint_arb {
765            let _ = ZipPlus::<Zt::ArbitraryZt, Zt::ArbitraryLc>::prove_f::<_, CHECK_FOR_OVERFLOW>(
766                &mut self.base.pcs_transcript,
767                self.base.pp_arb,
768                &witness_trace.arbitrary_poly,
769                &self.r_0,
770                hint_arb,
771                &self.field_cfg,
772            )?;
773        }
774        if let Some(hint_int) = &self.base.hint_int {
775            let _ = ZipPlus::<Zt::IntZt, Zt::IntLc>::prove_f::<_, CHECK_FOR_OVERFLOW>(
776                &mut self.base.pcs_transcript,
777                self.base.pp_int,
778                &witness_trace.int,
779                &self.r_0,
780                hint_int,
781                &self.field_cfg,
782            )?;
783        }
784
785        Ok(ProverPcsOpened {
786            base: self.base,
787            ic_proof: self.ic_proof,
788            cpr_proof: self.cpr_proof,
789            combined_sumcheck: self.combined_sumcheck,
790            lookup_proof: self.lookup_proof,
791            booleanity_proof: self.booleanity_proof,
792            mp_proof: self.mp_proof,
793            lifted_evals: self.lifted_evals,
794        })
795    }
796});
797
798impl_with_type_bounds!(ProverPcsOpened
799{
800    /// Assemble the final proof from accumulated state.
801    pub fn finish(self) -> Result<Proof<F>, ProtocolError<F, U::Ideal>> {
802        let sig = self.base.uair_signature;
803        let zip_proof = self.base.pcs_transcript.stream.into_inner();
804        let commitments = (
805            self.base.commitment_bin,
806            self.base.commitment_arb,
807            self.base.commitment_int,
808        );
809
810        let lifted_evals = self.lifted_evals;
811
812        // Extract witness-only lifted evals (public columns come first in trace).
813        let pub_cols = sig.public_cols();
814        let num_pub_bin = pub_cols.num_binary_poly_cols();
815        let num_pub_arb = pub_cols.num_arbitrary_poly_cols();
816        let num_pub_int = pub_cols.num_int_cols();
817        let total = sig.total_cols();
818        let num_total_bin = total.num_binary_poly_cols();
819        let num_total_arb = total.num_arbitrary_poly_cols();
820        let witness = sig.witness_cols();
821        let witness_arb_offset = add!(num_total_bin, num_pub_arb);
822        let witness_arb_end = add!(witness_arb_offset, witness.num_arbitrary_poly_cols());
823        let witness_int_offset = add!(add!(num_total_bin, num_total_arb), num_pub_int);
824        let witness_lifted_evals: Vec<_> = lifted_evals[num_pub_bin..num_total_bin]
825            .iter()
826            .chain(&lifted_evals[witness_arb_offset..witness_arb_end])
827            .chain(&lifted_evals[witness_int_offset..])
828            .cloned()
829            .collect();
830
831        Ok(Proof {
832            commitments,
833            ideal_check: self.ic_proof,
834            resolver: self.cpr_proof,
835            combined_sumcheck: self.combined_sumcheck,
836            multipoint_eval: self.mp_proof,
837            zip: zip_proof,
838            witness_lifted_evals,
839            lookup_proof: self.lookup_proof,
840            booleanity_proof: self.booleanity_proof,
841        })
842    }
843});
844
845//
846// prove() wrapper
847//
848
849impl<Zt, U, F, const D: usize, const FD: usize> ZincPlusPiop<Zt, U, F, D, FD>
850where
851    Zt: ZincTypes<D, FD>,
852    Zt::Int: ProjectableToField<F>,
853    <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<F>,
854    F: InnerTransparentField
855        + FromPrimitiveWithConfig
856        + for<'a> FromWithConfig<&'a Zt::Int>
857        + for<'a> FromWithConfig<&'a Zt::CombR>
858        + for<'a> FromWithConfig<&'a Zt::Chal>
859        + for<'a> FromWithConfig<&'a Zt::Pt>
860        + for<'a> MulByScalar<&'a F>
861        + FromRef<F>
862        + Send
863        + Sync
864        + 'static,
865    F::Inner:
866        ConstIntSemiring + ConstTranscribable + FromRef<Zt::Fmod> + Send + Sync + Zero + Default,
867    F::Modulus: ConstTranscribable + FromRef<Zt::Fmod>,
868    U: Uair + 'static,
869{
870    /// Zinc+ full PIOP prover.
871    ///
872    /// Runs all protocol steps in sequence and returns the assembled proof.
873    /// For per-step control, start with [`Self::step1_commit`] and chain the
874    /// individual `stepN_*` methods.
875    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
876    pub fn prove<const MLE_FIRST: bool, const CHECK_FOR_OVERFLOW: bool>(
877        pp: &(
878            ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
879            ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
880            ZipPlusParams<Zt::IntZt, Zt::IntLc>,
881        ),
882        trace: &UairTrace<'static, Zt::Int, Zt::Int, D, D>,
883        num_vars: usize,
884        project_scalar: impl Fn(&U::Scalar, &F::Config) -> DynamicPolynomialF<F>,
885    ) -> Result<Proof<F>, ProtocolError<F, U::Ideal>> {
886        let committed = Self::step0_fold(trace)?.step1_commit(pp, num_vars)?;
887
888        let ideal_checked = if MLE_FIRST {
889            committed
890                .step2_mle_first(project_scalar)?
891                .step3_ideal_check()?
892        } else {
893            committed
894                .step2_combined(project_scalar)?
895                .step3_ideal_check()?
896        };
897
898        ideal_checked
899            .step4_eval_projection()?
900            .step5_sumcheck()?
901            .step6_multipoint_eval()?
902            .step7_lift_and_project()?
903            .step8_pcs_open::<CHECK_FOR_OVERFLOW>()?
904            .finish()
905    }
906}
907
908#[allow(clippy::type_complexity)]
909fn commit_optionally<Zt: ZipTypes, Lc: LinearCode<Zt>>(
910    pp: &ZipPlusParams<Zt, Lc>,
911    trace: &[DenseMultilinearExtension<Zt::Eval>],
912) -> Result<(Option<ZipPlusHint<Zt::Cw>>, ZipPlusCommitment), ZipError> {
913    if trace.is_empty() {
914        Ok((
915            None,
916            ZipPlusCommitment {
917                root: Default::default(),
918                batch_size: 0,
919            },
920        ))
921    } else {
922        let (hint, commitment) = ZipPlus::commit(pp, trace)?;
923        Ok((Some(hint), commitment))
924    }
925}