Skip to main content

zinc_protocol/
lib.rs

1//! Zinc+ PIOP for UCS - end-to-end protocol.
2//!
3//! Implements the Zinc+ compiler pipeline (cf. paper, Section "Zinc+
4//! Compiler"):
5//!
6//! ```text
7//! Z[X]  --\phi_q-->  F_q[X]  --MLE eval-->  F_q[X]  --\psi_a-->  F_q
8//!         Step 1               Step 2                  Step 3
9//! ```
10//!
11//! After the three compiler steps, the protocol continues with:
12//!
13//! - Step 4: combined CPR + Lookup multi-degree sumcheck (CPR group at degree
14//!   `max_deg+2`, one lookup group per table type; shared eval point `r*`)
15//! - Step 5: multi-point evaluation sumcheck (combines up/down evals at r* into
16//!   a single evaluation point r_0)
17//! - Step 6: lift-and-project (unprojected MLE evaluations at r_0)
18//! - Step 7: Zip+ PCS open/verify at r_0
19
20pub mod prover;
21pub mod verifier;
22
23#[cfg(feature = "parallel")]
24use rayon::prelude::*;
25
26use crypto_primitives::{ConstIntRing, ConstIntSemiring, FromWithConfig, PrimeField, Semiring};
27use std::{fmt::Debug, marker::PhantomData};
28use thiserror::Error;
29use zinc_piop::{
30    combined_poly_resolver::{CombinedPolyResolverError, Proof as CombinedPolyResolverProof},
31    ideal_check::{IdealCheckError, Proof as IdealCheckProof},
32    lookup::{BatchedLookupProof, LookupError},
33    multipoint_eval::{MultipointEvalError, Proof as MultipointEvalProof},
34    projections::ProjectedTrace,
35    sumcheck::multi_degree::MultiDegreeSumcheckProof,
36};
37use zinc_poly::{
38    ConstCoeffBitWidth, EvaluationError as PolyEvaluationError,
39    mle::DenseMultilinearExtension,
40    univariate::{
41        binary::BinaryPoly,
42        dense::DensePolynomial,
43        dynamic::over_field::{DynamicPolyVecF, DynamicPolynomialF},
44    },
45};
46use zinc_primality::PrimalityTest;
47use zinc_transcript::traits::{ConstTranscribable, GenTranscribable, Transcribable, Transcript};
48use zinc_uair::{Uair, ideal::Ideal};
49use zinc_utils::{cfg_extend, cfg_into_iter, cfg_iter, named::Named};
50use zip_plus::{
51    ZipError,
52    code::LinearCode,
53    pcs::structs::{ZipPlusCommitment, ZipTypes},
54};
55
56//
57// Data structures
58//
59
60/// Full proof produced by the Zinc+ PIOP for UCS.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct Proof<F: PrimeField> {
63    /// Zip+ commitments to the witness columns.
64    pub commitments: (ZipPlusCommitment, ZipPlusCommitment, ZipPlusCommitment),
65    /// Serialized PCS proof data (Zip+ proving transcripts).
66    pub zip: Vec<u8>,
67    /// Randomized ideal check proof.
68    pub ideal_check: IdealCheckProof<F>,
69    /// Combined polynomial resolver proof (up_evals + down_evals).
70    pub resolver: CombinedPolyResolverProof<F>,
71    /// Multi-degree sumcheck proof (CPR group + future lookup groups).
72    pub combined_sumcheck: MultiDegreeSumcheckProof<F>,
73    /// Multi-point evaluation sumcheck proof (combines up_evals and
74    /// down_evals at r' into a single evaluation point r_0).
75    pub multipoint_eval: MultipointEvalProof<F>,
76    /// Witness-only polynomial MLE evaluations at r_0 in F_q[X]
77    /// (after \phi_q, before \psi_a), ordered as
78    /// `[wit_bin..., wit_arb..., wit_int...]`.
79    /// The verifier recomputes public lifted_evals from public data,
80    /// interleaves them with these, and derives scalar open_evals via
81    /// \psi_a for the sumcheck consistency check and Zip+ PCS verify.
82    pub witness_lifted_evals: Vec<DynamicPolynomialF<F>>,
83    /// Lookup argument proof. `None` when the UAIR has no lookup specs.
84    pub lookup_proof: Option<BatchedLookupProof<F>>,
85}
86
87impl<F> GenTranscribable for Proof<F>
88where
89    F: PrimeField,
90    F::Inner: ConstTranscribable,
91    F::Modulus: ConstTranscribable,
92{
93    fn read_transcription_bytes_exact(bytes: &[u8]) -> Self {
94        let (commit0, bytes) = ZipPlusCommitment::read_transcription_bytes_subset(bytes);
95        let (commit1, bytes) = ZipPlusCommitment::read_transcription_bytes_subset(bytes);
96        let (commit2, bytes) = ZipPlusCommitment::read_transcription_bytes_subset(bytes);
97
98        let (zip_len, bytes) = u32::read_transcription_bytes_subset(bytes);
99        let zip_len = usize::try_from(zip_len).expect("zip length must fit into usize");
100        let (zip_bytes, bytes) = bytes.split_at(zip_len);
101        let zip = zip_bytes.to_vec();
102
103        let (ideal_check, bytes) = IdealCheckProof::<F>::read_transcription_bytes_subset(bytes);
104        let (resolver, bytes) =
105            CombinedPolyResolverProof::<F>::read_transcription_bytes_subset(bytes);
106        let (combined_sumcheck, bytes) =
107            MultiDegreeSumcheckProof::<F>::read_transcription_bytes_subset(bytes);
108        let (multipoint_eval, bytes) =
109            MultipointEvalProof::<F>::read_transcription_bytes_subset(bytes);
110
111        let (witness_vec, bytes) = DynamicPolyVecF::<F>::read_transcription_bytes_subset(bytes);
112        let witness_lifted_evals = witness_vec.0;
113
114        // TODO: deserialize lookup_proof once BatchedLookupProof gets
115        // Transcribable impls (lookup is not yet implemented).
116        assert!(bytes.is_empty(), "All bytes should be consumed");
117
118        Self {
119            commitments: (commit0, commit1, commit2),
120            zip,
121            ideal_check,
122            resolver,
123            combined_sumcheck,
124            multipoint_eval,
125            witness_lifted_evals,
126            lookup_proof: None,
127        }
128    }
129
130    fn write_transcription_bytes_exact(&self, mut buf: &mut [u8]) {
131        // 3 commitments (ConstTranscribable - no length prefix)
132        buf = self.commitments.0.write_transcription_bytes_subset(buf);
133        buf = self.commitments.1.write_transcription_bytes_subset(buf);
134        buf = self.commitments.2.write_transcription_bytes_subset(buf);
135
136        // zip: u32 length + raw bytes
137        let zip_len = u32::try_from(self.zip.len()).expect("zip length must fit into u32");
138        zip_len.write_transcription_bytes_exact(&mut buf[..u32::NUM_BYTES]);
139        buf = &mut buf[u32::NUM_BYTES..];
140        buf[..self.zip.len()].copy_from_slice(&self.zip);
141        buf = &mut buf[self.zip.len()..];
142
143        // ideal_check: u32 length prefix + data
144        buf = self.ideal_check.write_transcription_bytes_subset(buf);
145
146        // resolver: u32 length prefix + data
147        buf = self.resolver.write_transcription_bytes_subset(buf);
148
149        // combined_sumcheck: u32 length prefix + data
150        buf = self.combined_sumcheck.write_transcription_bytes_subset(buf);
151
152        // multipoint_eval: u32 length prefix + data
153        buf = self.multipoint_eval.write_transcription_bytes_subset(buf);
154
155        // witness_lifted_evals: u32 length prefix + DynamicPolyVecF encoding
156        // TODO: serialize lookup_proof once BatchedLookupProof gets
157        // Transcribable impls (lookup is not yet implemented).
158        DynamicPolyVecF::reinterpret(&self.witness_lifted_evals)
159            .write_transcription_bytes_subset(buf);
160    }
161}
162
163impl<F> Transcribable for Proof<F>
164where
165    F: PrimeField,
166    F::Inner: ConstTranscribable,
167    F::Modulus: ConstTranscribable,
168{
169    #[allow(clippy::arithmetic_side_effects)]
170    fn get_num_bytes(&self) -> usize {
171        let witness_vec = DynamicPolyVecF::reinterpret(&self.witness_lifted_evals);
172        3 * ZipPlusCommitment::NUM_BYTES
173            + u32::NUM_BYTES
174            + self.zip.len()
175            + IdealCheckProof::<F>::LENGTH_NUM_BYTES
176            + self.ideal_check.get_num_bytes()
177            + CombinedPolyResolverProof::<F>::LENGTH_NUM_BYTES
178            + self.resolver.get_num_bytes()
179            + MultiDegreeSumcheckProof::<F>::LENGTH_NUM_BYTES
180            + self.combined_sumcheck.get_num_bytes()
181            + MultipointEvalProof::<F>::LENGTH_NUM_BYTES
182            + self.multipoint_eval.get_num_bytes()
183            // TODO: add lookup_proof size once BatchedLookupProof gets
184            // Transcribable impls (lookup is not yet implemented).
185            + DynamicPolyVecF::<F>::LENGTH_NUM_BYTES
186            + witness_vec.get_num_bytes()
187    }
188}
189
190/// Trait bundling the various type parameters for the public inputs (NYI),
191/// witness and Zinc+ PIOP.
192pub trait ZincTypes<const DEGREE_PLUS_ONE: usize>: Clone + Debug {
193    /// Main integer type for the protocol, used as a coefficient type for the
194    /// arbitrary polynomial trace columns and for the integer trace columns.
195    type Int: Semiring
196        + ConstTranscribable
197        + ConstCoeffBitWidth
198        + Named
199        + Default
200        + Clone
201        + Send
202        + Sync
203        + 'static;
204
205    /// Projecting element to project Zip+ evaluations and UAIR scalars to the
206    /// field.
207    type Chal: ConstIntRing + ConstTranscribable + Named;
208
209    /// Evaluation point type, used for all column types in Zip+ to evaluate
210    /// multilinear polynomials.
211    type Pt: ConstIntRing;
212
213    type CombR;
214
215    /// Randomly sampled field modulus type, used throughout the protocol for
216    /// finite field operations.
217    type Fmod: ConstIntSemiring + ConstTranscribable + Named;
218
219    /// Primality test for the field modulus.
220    type PrimeTest: PrimalityTest<Self::Fmod>;
221
222    /// Zip+ types for the binary polynomial trace columns.
223    type BinaryZt: ZipTypes<
224            Eval = BinaryPoly<DEGREE_PLUS_ONE>,
225            Chal = Self::Chal,
226            Pt = Self::Pt,
227            CombR = Self::CombR,
228            Fmod = Self::Fmod,
229            PrimeTest = Self::PrimeTest,
230        >;
231
232    /// Zip+ types for the arbitrary polynomial trace columns.
233    type ArbitraryZt: ZipTypes<
234            Eval = DensePolynomial<Self::Int, DEGREE_PLUS_ONE>,
235            Chal = Self::Chal,
236            Pt = Self::Pt,
237            CombR = Self::CombR,
238            Fmod = Self::Fmod,
239            PrimeTest = Self::PrimeTest,
240        >;
241
242    /// Zip+ types for the integer trace columns.
243    type IntZt: ZipTypes<
244            Eval = Self::Int,
245            Chal = Self::Chal,
246            Pt = Self::Pt,
247            CombR = Self::CombR,
248            Fmod = Self::Fmod,
249            PrimeTest = Self::PrimeTest,
250        >;
251
252    /// Linear code used in Zip+ for the binary polynomial trace columns.
253    type BinaryLc: LinearCode<Self::BinaryZt>;
254
255    /// Linear code used in Zip+ for the arbitrary polynomial trace columns.
256    type ArbitraryLc: LinearCode<Self::ArbitraryZt>;
257
258    /// Linear code used in Zip+ for the integer trace columns.
259    type IntLc: LinearCode<Self::IntZt>;
260}
261
262/// Main struct for the Zinc+ PIOP. The protocol is implemented as associated
263/// functions on it.
264///
265/// (Note that type parameters are further constrained in the impl blocks for
266/// the prover and verifier)
267#[derive(Copy, Clone, Default, Debug)]
268pub struct ZincPlusPiop<Zt, U, F, const DEGREE_PLUS_ONE: usize>(PhantomData<(Zt, U, F)>)
269where
270    Zt: ZincTypes<DEGREE_PLUS_ONE>,
271    U: Uair,
272    F: PrimeField;
273
274/// Error type for error happening during the protocol execution (prover and
275/// verifier).
276#[derive(Debug, Error)]
277pub enum ProtocolError<F: PrimeField, I: Ideal> {
278    #[error("ideal check failed: {0}")]
279    IdealCheck(#[from] IdealCheckError<F, I>),
280    #[error("combined poly resolver failed: {0}")]
281    Resolver(#[from] CombinedPolyResolverError<F>),
282    #[error("scalar projection failed: {0}")]
283    ScalarProjection(PolyEvaluationError),
284    #[error("multi-point evaluation failed: {0}")]
285    MultipointEval(#[from] MultipointEvalError<F>),
286    #[error("lifted eval psi_a projection failed: {0}")]
287    LiftedEvalProjection(PolyEvaluationError),
288    #[error("lookup argument failed: {0}")]
289    Lookup(#[from] LookupError),
290    #[error("PCS error: {0}")]
291    Pcs(#[from] ZipError),
292    #[error("PCS verification failed at column {0}: {1}")]
293    PcsVerification(usize, ZipError),
294}
295
296//
297// Helper functions
298//
299
300/// Absorb public column entries into the Fiat-Shamir transcript.
301///
302/// Each entry is serialized via `ConstTranscribable::write_transcription_bytes`
303/// and absorbed. This must be called in the same order by both prover and
304/// verifier, after commitments and before the random prime draw.
305fn absorb_public_columns<T: ConstTranscribable>(
306    transcript: &mut impl Transcript,
307    cols: &[DenseMultilinearExtension<T>],
308) {
309    let mut buf = vec![0u8; T::NUM_BYTES];
310    for col in cols {
311        for entry in col.iter() {
312            entry.write_transcription_bytes_exact(&mut buf);
313            transcript.absorb_slice(&buf);
314        }
315    }
316}
317
318/// Compute per-column lifted MLE evaluations at `point`.
319///
320/// For each column j, returns `\sum_b eq(b, point) * v_j(b)` as a polynomial
321/// in `F_q[X]` (coefficient-wise MLE evaluation). Dispatches on the trace
322/// layout internally.
323///
324/// Binary columns exploit the 0/1 structure for conditional additions only.
325/// The `eq(point, *)` table is built once and reused across all columns.
326#[allow(clippy::arithmetic_side_effects)]
327fn compute_lifted_evals<F: PrimeField, const D: usize>(
328    point: &[F],
329    trace_bin_poly: &[DenseMultilinearExtension<BinaryPoly<D>>],
330    projected_trace: &ProjectedTrace<F>,
331    field_cfg: &F::Config,
332) -> Vec<DynamicPolynomialF<F>> {
333    let eq_table = zinc_poly::utils::build_eq_x_r_vec(point, field_cfg)
334        .expect("compute_lifted_evals: eq table build failed");
335
336    let n_bin = trace_bin_poly.len();
337    let zero = F::zero_with_cfg(field_cfg);
338
339    // Binary columns: exploit 0/1 structure for conditional additions.
340    let mut result: Vec<DynamicPolynomialF<F>> = cfg_iter!(trace_bin_poly)
341        .map(|col| {
342            let mut coeffs = vec![zero.clone(); D];
343            for (b, entry) in col.iter().enumerate() {
344                for (l, coeff) in entry.iter().enumerate() {
345                    if coeff.into_inner() {
346                        coeffs[l] += &eq_table[b];
347                    }
348                }
349            }
350            DynamicPolynomialF::new_trimmed(coeffs)
351        })
352        .collect();
353
354    // Non-binary columns: coefficient-wise eq-weighted sum.
355    fn weighted_eq_sum<'a, F2: PrimeField + 'a>(
356        col: impl Iterator<Item = &'a DynamicPolynomialF<F2>> + Clone,
357        eq_table: &[F2],
358        zero: &F2,
359    ) -> DynamicPolynomialF<F2> {
360        let num_coeffs = col.clone().map(|e| e.coeffs.len()).max().unwrap_or(0);
361        let mut coeffs = vec![zero.clone(); num_coeffs];
362        for (b, entry) in col.enumerate() {
363            for (l, coeff) in entry.coeffs.iter().enumerate() {
364                let mut term = eq_table[b].clone();
365                term *= coeff;
366                coeffs[l] += &term;
367            }
368        }
369        DynamicPolynomialF::new_trimmed(coeffs)
370    }
371
372    match projected_trace {
373        ProjectedTrace::RowMajor(t) => {
374            let num_cols = t.first().map(|r| r.len()).unwrap_or(0);
375            cfg_extend!(
376                result,
377                cfg_into_iter!(n_bin..num_cols).map(|col_idx| weighted_eq_sum(
378                    t.iter().map(|row| &row[col_idx]),
379                    &eq_table,
380                    &zero,
381                ))
382            );
383        }
384        ProjectedTrace::ColumnMajor(t) => {
385            cfg_extend!(
386                result,
387                cfg_iter!(t[n_bin..]).map(|col_mle| weighted_eq_sum(
388                    col_mle.iter(),
389                    &eq_table,
390                    &zero,
391                ))
392            );
393        }
394    }
395
396    result
397}
398
399/// Project a DensePolynomial scalar to DynamicPolynomialF by projecting each
400/// coefficient via \phi_q.
401pub fn project_scalar_fn<R, F, const D: usize>(
402    scalar: &DensePolynomial<R, D>,
403    field_cfg: &F::Config,
404) -> DynamicPolynomialF<F>
405where
406    F: PrimeField + for<'a> FromWithConfig<&'a R>,
407{
408    scalar
409        .iter()
410        .map(|coeff| F::from_with_cfg(coeff, field_cfg))
411        .collect()
412}
413
414//
415// Tests
416//
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use crypto_bigint::U64;
422    use crypto_primitives::{
423        Field, crypto_bigint_int::Int, crypto_bigint_monty::MontyField, crypto_bigint_uint::Uint,
424    };
425    use rand::rng;
426    use zinc_piop::{
427        combined_poly_resolver::CombinedPolyResolverError, multipoint_eval::MultipointEvalError,
428    };
429    use zinc_poly::univariate::{binary::BinaryPolyInnerProduct, dense::DensePolyInnerProduct};
430    use zinc_primality::MillerRabin;
431    use zinc_test_uair::{
432        BigLinearUair, BigLinearUairWithPublicInput, BinaryDecompositionUair, GenerateRandomTrace,
433        TestUairMixedShifts, TestUairNoMultiplication, TestUairSimpleMultiplication,
434    };
435    use zinc_uair::{
436        degree_counter::count_max_degree, ideal::DegreeOneIdeal, ideal_collector::IdealOrZero,
437    };
438    use zinc_utils::{
439        CHECKED,
440        from_ref::FromRef,
441        inner_product::{MBSInnerProduct, ScalarProduct},
442        projectable_to_field::ProjectableToField,
443    };
444    use zip_plus::{
445        code::{
446            iprs::{IprsCode, PnttConfigF65537},
447            raa::{RaaCode, RaaConfig},
448        },
449        pcs::structs::{ZipPlus, ZipPlusParams},
450        pcs_transcript::PcsProverTranscript,
451    };
452
453    const INT_LIMBS: usize = U64::LIMBS;
454    const FIELD_LIMBS: usize = U64::LIMBS * 3;
455    const DEGREE_PLUS_ONE: usize = 32;
456
457    // Zip+ type parameters.
458
459    const K: usize = INT_LIMBS * 4;
460    const M: usize = INT_LIMBS * 8;
461
462    const REP: usize = 4;
463
464    type F = MontyField<FIELD_LIMBS>;
465
466    #[derive(Debug, Clone)]
467    pub struct BinPolyZipTypes {}
468    impl ZipTypes for BinPolyZipTypes {
469        const NUM_COLUMN_OPENINGS: usize = 147;
470        type Eval = BinaryPoly<DEGREE_PLUS_ONE>;
471        type Cw = DensePolynomial<i64, DEGREE_PLUS_ONE>;
472        type Fmod = Uint<FIELD_LIMBS>;
473        type PrimeTest = MillerRabin;
474        type Chal = i128;
475        type Pt = i128;
476        type CombR = Int<M>;
477        type Comb = DensePolynomial<Self::CombR, DEGREE_PLUS_ONE>;
478        type EvalDotChal = BinaryPolyInnerProduct<Self::Chal, DEGREE_PLUS_ONE>;
479        type CombDotChal = DensePolyInnerProduct<
480            Self::CombR,
481            Self::Chal,
482            Self::CombR,
483            MBSInnerProduct,
484            DEGREE_PLUS_ONE,
485        >;
486        type ArrCombRDotChal = MBSInnerProduct;
487    }
488
489    #[derive(Debug, Clone)]
490    pub struct ArbitraryPolyZipTypesIprs {}
491    impl ZipTypes for ArbitraryPolyZipTypesIprs {
492        const NUM_COLUMN_OPENINGS: usize = 147;
493        type Eval = DensePolynomial<i64, DEGREE_PLUS_ONE>;
494        type Cw = DensePolynomial<i64, DEGREE_PLUS_ONE>;
495        type Fmod = Uint<FIELD_LIMBS>;
496        type PrimeTest = MillerRabin;
497        type Chal = i128;
498        type Pt = i128;
499        type CombR = Int<M>;
500        type Comb = DensePolynomial<Self::CombR, DEGREE_PLUS_ONE>;
501        type EvalDotChal =
502            DensePolyInnerProduct<i64, Self::Chal, Self::CombR, MBSInnerProduct, DEGREE_PLUS_ONE>;
503        type CombDotChal = DensePolyInnerProduct<
504            Self::CombR,
505            Self::Chal,
506            Self::CombR,
507            MBSInnerProduct,
508            DEGREE_PLUS_ONE,
509        >;
510        type ArrCombRDotChal = MBSInnerProduct;
511    }
512
513    /// Arbitrary poly ZipTypes with wider codewords for RAA encoding.
514    /// RAA accumulation grows the bit-width, so Cw needs more bits than Eval.
515    #[derive(Debug, Clone)]
516    pub struct ArbitraryPolyZipTypesRaa {}
517    impl ZipTypes for ArbitraryPolyZipTypesRaa {
518        const NUM_COLUMN_OPENINGS: usize = 147;
519        type Eval = DensePolynomial<i64, DEGREE_PLUS_ONE>;
520        type Cw = DensePolynomial<Int<K>, DEGREE_PLUS_ONE>;
521        type Fmod = Uint<FIELD_LIMBS>;
522        type PrimeTest = MillerRabin;
523        type Chal = i128;
524        type Pt = i128;
525        type CombR = Int<M>;
526        type Comb = DensePolynomial<Self::CombR, DEGREE_PLUS_ONE>;
527        type EvalDotChal =
528            DensePolyInnerProduct<i64, Self::Chal, Self::CombR, MBSInnerProduct, DEGREE_PLUS_ONE>;
529        type CombDotChal = DensePolyInnerProduct<
530            Self::CombR,
531            Self::Chal,
532            Self::CombR,
533            MBSInnerProduct,
534            DEGREE_PLUS_ONE,
535        >;
536        type ArrCombRDotChal = MBSInnerProduct;
537    }
538
539    type ZtInt = i64;
540
541    #[derive(Debug, Clone)]
542    pub struct IntZipTypes {}
543    impl ZipTypes for IntZipTypes {
544        const NUM_COLUMN_OPENINGS: usize = 147;
545        type Eval = ZtInt;
546        type Cw = i128;
547        type Fmod = Uint<FIELD_LIMBS>;
548        type PrimeTest = MillerRabin;
549        type Chal = i128;
550        type Pt = i128;
551        type CombR = Int<M>;
552        type Comb = Self::CombR;
553        type EvalDotChal = ScalarProduct;
554        type CombDotChal = ScalarProduct;
555        type ArrCombRDotChal = MBSInnerProduct;
556    }
557
558    #[derive(Clone, Debug)]
559    struct TestZincTypesIprs;
560
561    impl ZincTypes<DEGREE_PLUS_ONE> for TestZincTypesIprs {
562        type Int = ZtInt;
563        type Chal = i128;
564        type Pt = i128;
565        type CombR = Int<M>;
566        type Fmod = Uint<FIELD_LIMBS>;
567        type PrimeTest = MillerRabin;
568
569        type BinaryZt = BinPolyZipTypes;
570        type ArbitraryZt = ArbitraryPolyZipTypesIprs;
571        type IntZt = IntZipTypes;
572
573        type BinaryLc = IprsCode<Self::BinaryZt, PnttConfigF65537, REP, CHECKED>;
574        type ArbitraryLc = IprsCode<Self::ArbitraryZt, PnttConfigF65537, REP, CHECKED>;
575        type IntLc = IprsCode<Self::IntZt, PnttConfigF65537, REP, CHECKED>;
576    }
577
578    #[derive(Copy, Clone)]
579    struct TestRaaConfig;
580    impl RaaConfig for TestRaaConfig {
581        const PERMUTE_IN_PLACE: bool = false;
582        const CHECK_FOR_OVERFLOWS: bool = true;
583    }
584
585    #[derive(Clone, Debug)]
586    struct TestZincTypesRaa;
587
588    impl ZincTypes<DEGREE_PLUS_ONE> for TestZincTypesRaa {
589        type Int = i64;
590        type Chal = i128;
591        type Pt = i128;
592        type CombR = Int<M>;
593        type Fmod = Uint<FIELD_LIMBS>;
594        type PrimeTest = MillerRabin;
595
596        type BinaryZt = BinPolyZipTypes;
597        type ArbitraryZt = ArbitraryPolyZipTypesRaa;
598        type IntZt = IntZipTypes;
599
600        type BinaryLc = RaaCode<Self::BinaryZt, TestRaaConfig, REP>;
601        type ArbitraryLc = RaaCode<Self::ArbitraryZt, TestRaaConfig, REP>;
602        type IntLc = RaaCode<Self::IntZt, TestRaaConfig, REP>;
603    }
604
605    /// Use row size equal to poly size, resulting in flat single-row matrices
606    fn make_iprs<Zt: ZipTypes>(num_vars: usize) -> IprsCode<Zt, PnttConfigF65537, REP, CHECKED> {
607        let poly_size = 1 << num_vars;
608        IprsCode::new_with_optimal_depth(poly_size).unwrap()
609    }
610
611    /// Set up Zip+ PCS parameters for a given number of MLE variables.
612    #[allow(clippy::type_complexity)]
613    fn setup_pp<Zt>(
614        num_vars: usize,
615        linear_codes: (Zt::BinaryLc, Zt::ArbitraryLc, Zt::IntLc),
616    ) -> (
617        ZipPlusParams<Zt::BinaryZt, Zt::BinaryLc>,
618        ZipPlusParams<Zt::ArbitraryZt, Zt::ArbitraryLc>,
619        ZipPlusParams<Zt::IntZt, Zt::IntLc>,
620    )
621    where
622        Zt: ZincTypes<DEGREE_PLUS_ONE>,
623    {
624        let poly_size = 1 << num_vars;
625        (
626            ZipPlus::<Zt::BinaryZt, Zt::BinaryLc>::setup(poly_size, linear_codes.0),
627            ZipPlus::<Zt::ArbitraryZt, Zt::ArbitraryLc>::setup(poly_size, linear_codes.1),
628            ZipPlus::<Zt::IntZt, Zt::IntLc>::setup(poly_size, linear_codes.2),
629        )
630    }
631
632    macro_rules! default_project_ideal {
633        () => {
634            |ideal, field_cfg| ideal.map(|i| DegreeOneIdeal::from_with_cfg(i, field_cfg))
635        };
636    }
637
638    #[allow(clippy::result_large_err)]
639    fn do_test<Zt, U>(
640        num_vars: usize,
641        linear_codes: (Zt::BinaryLc, Zt::ArbitraryLc, Zt::IntLc),
642        project_ideal: impl Fn(
643            &IdealOrZero<U::Ideal>,
644            &<F as PrimeField>::Config,
645        ) -> IdealOrZero<DegreeOneIdeal<F>>
646        + Copy,
647        tamper: impl Fn(&mut Proof<F>),
648        check_verification: impl Fn(Result<(), ProtocolError<F, IdealOrZero<DegreeOneIdeal<F>>>>),
649    ) where
650        Zt: ZincTypes<DEGREE_PLUS_ONE>,
651        <Zt::BinaryZt as ZipTypes>::Cw: ProjectableToField<F>,
652        <Zt::ArbitraryZt as ZipTypes>::Eval: ProjectableToField<F>,
653        <Zt::ArbitraryZt as ZipTypes>::Cw: ProjectableToField<F>,
654        <Zt::IntZt as ZipTypes>::Cw: ProjectableToField<F>,
655        U: Uair<Scalar = DensePolynomial<Zt::Int, DEGREE_PLUS_ONE>>
656            + GenerateRandomTrace<DEGREE_PLUS_ONE, PolyCoeff = Zt::Int, Int = Zt::Int>
657            + 'static,
658        F: for<'a> FromWithConfig<&'a Zt::Int>
659            + for<'a> FromWithConfig<&'a Zt::CombR>
660            + for<'a> FromWithConfig<&'a Zt::Chal>
661            + for<'a> FromWithConfig<&'a Zt::Pt>,
662        <F as Field>::Inner: FromRef<Zt::Fmod>,
663        <F as Field>::Modulus: FromRef<Zt::Fmod>,
664    {
665        let mut rng = rng();
666        let pp = setup_pp::<Zt>(num_vars, linear_codes);
667
668        let trace = U::generate_random_trace(num_vars, &mut rng);
669
670        let sig = U::signature();
671        let public_trace = trace.public(&sig);
672
673        macro_rules! run_protocol {
674            ($mle_first:ident) => {
675                let mut proof = ZincPlusPiop::<Zt, U, F, DEGREE_PLUS_ONE>::prove::<
676                    { $mle_first },
677                    CHECKED,
678                >(&pp, &trace, num_vars, project_scalar_fn)
679                .expect("Prover failed");
680
681                // Checking that the proof can be properly serialized and deserialized
682                let mut transcript = PcsProverTranscript::new_from_commitments(std::iter::empty());
683                transcript.write(&proof).expect("Failed to serialize proof");
684                let mut transcript = transcript.into_verification_transcript();
685                let proof_2 = transcript
686                    .read()
687                    .expect("Failed to deserialize proof after serialization");
688                assert_eq!(proof, proof_2);
689
690                tamper(&mut proof);
691
692                let verification_result =
693                    ZincPlusPiop::<Zt, U, F, DEGREE_PLUS_ONE>::verify::<_, CHECKED>(
694                        &pp,
695                        proof,
696                        &public_trace,
697                        num_vars,
698                        project_scalar_fn,
699                        project_ideal,
700                    );
701                check_verification(verification_result);
702            };
703        }
704
705        run_protocol!(false);
706
707        if count_max_degree::<U>() <= 1 {
708            // For linear constraints, also test the MLE-first ideal check approach.
709            run_protocol!(true);
710        }
711    }
712
713    /// End-to-end test: TestUairNoMultiplication.
714    ///
715    /// UAIR constraint: a + b - c \in (X - 2)
716    /// (one constraint, no polynomial multiplication, ideal = <X - 2>).
717    #[test]
718    fn test_e2e_no_multiplication() {
719        let num_vars = 8;
720        do_test::<TestZincTypesIprs, TestUairNoMultiplication<ZtInt>>(
721            num_vars,
722            (
723                make_iprs(num_vars),
724                make_iprs(num_vars),
725                make_iprs(num_vars),
726            ),
727            default_project_ideal!(),
728            |_| {},
729            |res| res.unwrap(),
730        );
731    }
732
733    /// End-to-end test: TestUairSimpleMultiplication.
734    ///
735    /// UAIR constraints (3 total, no ideals):
736    ///   up[0] * up[1] = down[0]
737    ///   up[1] * up[2] = down[1]
738    ///   up[0] * up[2] = down[2]
739    ///
740    /// Uses RAA code with small num_vars (2) because chained polynomial
741    /// multiplication causes exponential growth in both degree and coefficient
742    /// magnitude. With num_vars=2 (4 rows), max degree=6 and max coefficient
743    /// ~= 127^8 ~= 2^56, which fits in i64.
744    #[test]
745    fn test_e2e_simple_multiplication() {
746        let num_vars = 2;
747        do_test::<TestZincTypesRaa, TestUairSimpleMultiplication<ZtInt>>(
748            num_vars,
749            (
750                RaaCode::new(num_vars),
751                RaaCode::new(num_vars),
752                RaaCode::new(num_vars),
753            ),
754            |_ideal, _field_cfg| IdealOrZero::<DegreeOneIdeal<F>>::zero(),
755            |_| {},
756            |res| res.unwrap(),
757        );
758    }
759
760    /// End-to-end test: TestUairMixedShifts.
761    ///
762    /// Uses mixed shift amounts (col a: shift 1, col b: shift 2).
763    /// Constraints: a[i+1] = a[i] + b[i], c[i] = b[i+2].
764    #[test]
765    fn test_e2e_mixed_shifts() {
766        let num_vars = 8;
767        do_test::<TestZincTypesIprs, TestUairMixedShifts<ZtInt>>(
768            num_vars,
769            (
770                make_iprs(num_vars),
771                make_iprs(num_vars),
772                make_iprs(num_vars),
773            ),
774            |_ideal, _field_cfg| IdealOrZero::<DegreeOneIdeal<F>>::zero(),
775            |_| {},
776            |res| res.unwrap(),
777        );
778    }
779
780    /// End-to-end test: BinaryDecompositionUair.
781    ///
782    /// Uses binary_poly (1 col) and int (1 col) trace types.
783    /// UAIR constraint: binary_poly[0] - int[0] \in <X - 2>
784    #[test]
785    fn test_e2e_binary_decomposition() {
786        let num_vars = 8;
787        do_test::<TestZincTypesIprs, BinaryDecompositionUair<ZtInt>>(
788            num_vars,
789            (
790                make_iprs(num_vars),
791                make_iprs(num_vars),
792                make_iprs(num_vars),
793            ),
794            default_project_ideal!(),
795            |_| {},
796            |res| res.unwrap(),
797        );
798    }
799
800    /// End-to-end test: BigLinearUair.
801    ///
802    /// Uses 16 binary_poly cols and 1 int col.
803    /// UAIR constraints:
804    ///   sum(up.binary_poly[0..16]) - up.int[0] \in <X - 1>
805    ///   down.binary_poly[0] - up.int[0] \in <X - 2>
806    ///   up.binary_poly[i] - down.binary_poly[i] = 0, for i=1..15
807    #[test]
808    fn test_e2e_big_linear() {
809        let num_vars = 8;
810        do_test::<TestZincTypesIprs, BigLinearUair<ZtInt>>(
811            num_vars,
812            (
813                make_iprs(num_vars),
814                make_iprs(num_vars),
815                make_iprs(num_vars),
816            ),
817            default_project_ideal!(),
818            |_| {},
819            |res| res.unwrap(),
820        );
821    }
822
823    /// End-to-end test: BigLinearUairWithPublicInput.
824    ///
825    /// Same as [`BigLinearUair`], but with the first few binary_poly columns as
826    /// public inputs.
827    #[test]
828    fn test_e2e_big_linear_with_public_input() {
829        let num_vars = 8;
830        do_test::<TestZincTypesIprs, BigLinearUairWithPublicInput<ZtInt>>(
831            num_vars,
832            (
833                make_iprs(num_vars),
834                make_iprs(num_vars),
835                make_iprs(num_vars),
836            ),
837            default_project_ideal!(),
838            |_| {},
839            |res| res.unwrap(),
840        );
841    }
842
843    //
844    // Negative tests for BigLinearUairWithPublicInput: verify that proof
845    // tampering is detected.
846    //
847
848    #[test]
849    fn test_big_linear_tamper_lifted_evals() {
850        let num_vars = 8;
851        do_test::<TestZincTypesIprs, BigLinearUairWithPublicInput<ZtInt>>(
852            num_vars,
853            (
854                make_iprs(num_vars),
855                make_iprs(num_vars),
856                make_iprs(num_vars),
857            ),
858            default_project_ideal!(),
859            |proof| proof.witness_lifted_evals.swap(0, 1),
860            |res| {
861                assert!(matches!(
862                    res.unwrap_err(),
863                    ProtocolError::MultipointEval(MultipointEvalError::ClaimMismatch { .. })
864                ));
865            },
866        );
867    }
868
869    #[test]
870    fn test_big_linear_tamper_up_evals() {
871        let num_vars = 8;
872        do_test::<TestZincTypesIprs, BigLinearUairWithPublicInput<ZtInt>>(
873            num_vars,
874            (
875                make_iprs(num_vars),
876                make_iprs(num_vars),
877                make_iprs(num_vars),
878            ),
879            default_project_ideal!(),
880            |proof| proof.resolver.up_evals.swap(0, 1),
881            |res| {
882                assert!(matches!(
883                    res.unwrap_err(),
884                    ProtocolError::Resolver(
885                        CombinedPolyResolverError::ClaimValueDoesNotMatch { .. }
886                    )
887                ));
888            },
889        );
890    }
891
892    #[test]
893    fn test_big_linear_tamper_down_evals() {
894        let num_vars = 8;
895        do_test::<TestZincTypesIprs, BigLinearUairWithPublicInput<ZtInt>>(
896            num_vars,
897            (
898                make_iprs(num_vars),
899                make_iprs(num_vars),
900                make_iprs(num_vars),
901            ),
902            default_project_ideal!(),
903            |proof| proof.resolver.down_evals.swap(0, 1),
904            |res| {
905                assert!(matches!(
906                    res.unwrap_err(),
907                    ProtocolError::Resolver(
908                        CombinedPolyResolverError::ClaimValueDoesNotMatch { .. }
909                    )
910                ));
911            },
912        );
913    }
914
915    // Tampering the commitment root causes the verifier to sample different
916    // challenges. The ideal check fails first because the prover's
917    // combined_mle_values were computed under the original transcript.
918    #[test]
919    fn test_big_linear_tamper_commitment() {
920        let num_vars = 8;
921        do_test::<TestZincTypesIprs, BigLinearUairWithPublicInput<ZtInt>>(
922            num_vars,
923            (
924                make_iprs(num_vars),
925                make_iprs(num_vars),
926                make_iprs(num_vars),
927            ),
928            default_project_ideal!(),
929            |proof| proof.commitments.0.root = Default::default(),
930            |res| {
931                assert!(matches!(res.unwrap_err(), ProtocolError::IdealCheck(..)));
932            },
933        );
934    }
935
936    #[test]
937    fn test_big_linear_tamper_ideal_check() {
938        let num_vars = 8;
939        do_test::<TestZincTypesIprs, BigLinearUairWithPublicInput<ZtInt>>(
940            num_vars,
941            (
942                make_iprs(num_vars),
943                make_iprs(num_vars),
944                make_iprs(num_vars),
945            ),
946            default_project_ideal!(),
947            |proof| proof.ideal_check.combined_mle_values.swap(0, 1),
948            |res| {
949                assert!(matches!(res.unwrap_err(), ProtocolError::IdealCheck(..)));
950            },
951        );
952    }
953}