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