Skip to main content

zip_plus/pcs/
phase_prove.rs

1use crate::{
2    ZipError,
3    code::LinearCode,
4    pcs::{
5        structs::{ZipPlus, ZipPlusHint, ZipPlusParams, ZipTypes},
6        utils::{point_to_tensor, validate_input},
7    },
8    pcs_transcript::PcsProverTranscript,
9};
10use crypto_primitives::{BaseFieldConfig, ProjectElementWithConfig};
11use itertools::Itertools;
12use num_traits::{ConstOne, ConstZero, Zero};
13#[cfg(feature = "parallel")]
14use rayon::prelude::*;
15use zinc_poly::{Polynomial, mle::DenseMultilinearExtension};
16use zinc_transcript::traits::{ConstTranscribable, Transcript};
17use zinc_utils::{
18    UNCHECKED, cfg_chunks, cfg_iter, cfg_iter_mut,
19    inner_product::{FieldInnerProduct, InnerProduct},
20    mul_by_scalar::MulByScalar,
21};
22
23impl<Zt: ZipTypes, Lc: LinearCode<Zt>> ZipPlus<Zt, Lc> {
24    /// Generates an opening proof for one or more committed multilinear
25    /// polynomials at an evaluation point, using the Zip+ protocol.
26    ///
27    /// This replaces the old two-phase (test + evaluate) approach with a single
28    /// merged phase. The key idea: alpha-projection (Eval → CombR) is used for
29    /// *both* the proximity argument and the evaluation claim, eliminating the
30    /// separate field-domain projection via `projecting_element` γ.
31    ///
32    /// # Algorithm
33    /// 1. Computes points: `(q_0, q_1) = point_to_tensor(point)` where `q_0`
34    ///    (length `num_rows`) combines rows and `q_1` (length `row_len`)
35    ///    combines columns.
36    /// 2. Per polynomial, samples random challenges `alphas` (`[α_0, …, α_d]`).
37    ///    For each decoded row `w_j` takes the inner product `<entry, alphas>`
38    ///    of every entry in the row, producing `w'_j` — a row of `CombR`
39    ///    integers.
40    /// 3. Computes `b` (length `num_rows`), accumulated across all polys: `b_j
41    ///    += <w'_j, q_1>` for each row `j`.
42    /// 4. Writes `b` to the transcript and computes `eval = <q_0, b>`.
43    /// 5. Samples combination coefficients `betas` (or hardcodes `[1]` when
44    ///    `num_rows == 1`) and computes `combined_row` (CombR, length
45    ///    `row_len`) = `sum_i(sum_j(s_j * w'_ij))`, accumulated across all
46    ///    polynomials
47    /// 6. Writes `combined_row` to the transcript.
48    /// 7. Opens `NUM_COLUMN_OPENINGS` Merkle columns: for each, squeezes a
49    ///    column index, writes per-polynomial column values (Cw entries), and
50    ///    appends the Merkle proof.
51    ///
52    /// # Transcript layout
53    /// ```text
54    /// [field_cfg sampled]
55    /// [per-poly alphas sampled]
56    /// [b written as F elements]
57    /// [coeffs s sampled (or hardcoded [1])]
58    /// [combined_row written as CombR]
59    /// [column openings: idx, per-poly column values, merkle proof] × NUM_COLUMN_OPENINGS
60    /// ```
61    ///
62    /// # Parameters
63    /// - `pp`: Public parameters containing `num_vars`, `num_rows`, and the
64    ///   linear code configuration.
65    /// - `polys`: Slice of multilinear polynomials (batch). All must have
66    ///   `num_vars` variables matching `pp`.
67    /// - `point`: The evaluation point (in `Zt::Pt` coordinates, length
68    ///   `num_vars`).
69    /// - `commit_hint`: The `ZipPlusHint` returned by `commit`, containing
70    ///   per-polynomial codeword matrices and the shared Merkle tree.
71    ///
72    /// # Returns
73    /// A `Result` containing:
74    /// - `F`: The combined evaluation `<q_0, b>`, which equals
75    ///   `sum_i(alpha_projected_eval_i(point))` across all batched polys.
76    /// - `ZipPlusProof`: The serialized transcript (b, combined_row, column
77    ///   openings + Merkle proofs) for the verifier.
78    ///
79    /// # Errors
80    /// - Returns `ZipError::InvalidPcsParam` if any polynomial has more
81    ///   variables than `pp` supports.
82    /// - Returns `ZipError::OverflowError` (when `CHECK_FOR_OVERFLOW` is true)
83    ///   if intermediate CombR sums exceed the integer precision.
84    pub fn prove<C, const CHECK_FOR_OVERFLOW: bool>(
85        transcript: &mut PcsProverTranscript,
86        pp: &ZipPlusParams<Zt, Lc>,
87        polys: &[DenseMultilinearExtension<Zt::Eval>],
88        point: &[Zt::Pt],
89        commit_hint: &ZipPlusHint<Zt::Cw>,
90        field_cfg: &C,
91    ) -> Result<C::Element, ZipError>
92    where
93        C: BaseFieldConfig
94            + ProjectElementWithConfig<Zt::Pt>
95            + ProjectElementWithConfig<Zt::CombR>
96            + Sync,
97        C::Integer: ConstTranscribable,
98        Zt::CombR: MulByScalar<Zt::Chal>,
99    {
100        let point = point
101            .iter()
102            .map(|v| field_cfg.project(v))
103            .collect::<Vec<C::Element>>();
104        Self::prove_f::<C, CHECK_FOR_OVERFLOW>(
105            transcript,
106            pp,
107            polys,
108            &point,
109            commit_hint,
110            field_cfg,
111        )
112    }
113
114    /// See [`Self::prove`] for details.
115    /// This version takes the evaluation point already mapped to the field
116    #[allow(clippy::arithmetic_side_effects)]
117    pub fn prove_f<C, const CHECK_FOR_OVERFLOW: bool>(
118        transcript: &mut PcsProverTranscript,
119        pp: &ZipPlusParams<Zt, Lc>,
120        polys: &[DenseMultilinearExtension<Zt::Eval>],
121        point: &[C::Element],
122        commit_hint: &ZipPlusHint<Zt::Cw>,
123        field_cfg: &C,
124    ) -> Result<C::Element, ZipError>
125    where
126        C: BaseFieldConfig + ProjectElementWithConfig<Zt::CombR> + Sync,
127        C::Integer: ConstTranscribable,
128        Zt::CombR: MulByScalar<Zt::Chal>,
129    {
130        let batch_size = polys.len();
131        validate_input::<Zt, Lc, _>(
132            "prove",
133            pp.num_vars,
134            pp.linear_code.row_len(),
135            batch_size,
136            polys,
137            &[point],
138        )?;
139
140        let num_rows = pp.num_rows;
141        let row_len = pp.linear_code.row_len();
142
143        // TODO Lift q0, q1 back to int and take following dot products on ints instead
144        // of MBSInnerProduct in field (see comboned row) We prove evaluations
145        // over the field, so integers need to be mapped to field elements first
146        let (q_0, q_1) = point_to_tensor(field_cfg, point, num_rows)?;
147
148        let degree_bound = Zt::Comb::DEGREE_BOUND;
149        let polys_as_comb_r: Vec<Vec<Zt::CombR>> = polys
150            .iter()
151            .map(|poly| {
152                let alphas = if degree_bound.is_zero() {
153                    vec![Zt::Chal::ONE]
154                } else {
155                    transcript.fs_transcript.get_challenges(degree_bound + 1)
156                };
157
158                cfg_iter!(poly.evaluations)
159                    .map(|eval| {
160                        Zt::EvalDotChal::inner_product::<CHECK_FOR_OVERFLOW>(
161                            &(),
162                            eval,
163                            &alphas,
164                            Zt::CombR::ZERO,
165                        )
166                        .map_err(ZipError::from)
167                    })
168                    .collect()
169            })
170            .try_collect()?;
171
172        let zero_f = field_cfg.zero();
173
174        // Compute per-polynomial row dot products, then sum across polynomials.
175        let b = {
176            let per_poly_b: Vec<Vec<C::Element>> = cfg_iter!(polys_as_comb_r)
177                .map(|poly_comb_r| {
178                    cfg_chunks!(poly_comb_r, row_len)
179                        .map(|row| {
180                            FieldInnerProduct::inner_product::<UNCHECKED>(
181                                field_cfg,
182                                &q_1,
183                                row,
184                                zero_f.clone(),
185                            )
186                        })
187                        .collect::<Result<Vec<C::Element>, _>>()
188                })
189                .collect::<Result<_, _>>()?;
190
191            let mut b = vec![zero_f.clone(); num_rows];
192            for poly_b in &per_poly_b {
193                b.iter_mut()
194                    .zip(poly_b)
195                    .for_each(|(a, d)| field_cfg.add_assign(a, d));
196            }
197            b
198        };
199
200        transcript.write_field_elements(field_cfg, &b)?;
201        // Compute eval = <q_0, b> (inner product in field), <q_2, b> in paper
202        let eval = q_0.iter().zip(&b).fold(zero_f, |mut acc, (l, r)| {
203            field_cfg.add_assign(&mut acc, &field_cfg.mul(l, r));
204            acc
205        });
206
207        // Matrix-vector product over the flat poly_comb_r layout:
208        // Each poly is a row-major (num_rows x row_len) matrix, and coeffs is the
209        // vector.
210        // combined_row[col] = sum_i sum_j (coeffs[j] * poly_i[j * row_len + col])
211
212        let coeffs = if pp.num_rows == 1 {
213            vec![Zt::Chal::ONE]
214        } else {
215            transcript
216                .fs_transcript
217                .get_challenges::<Zt::Chal>(num_rows)
218        };
219
220        let combined_row: Vec<Zt::CombR> = {
221            let mut combined = vec![Zt::CombR::ZERO; row_len];
222            cfg_iter_mut!(combined).enumerate().try_for_each(
223                |(col, acc)| -> Result<(), ZipError> {
224                    for poly_comb_r in &polys_as_comb_r {
225                        // Strided access: skip to column `col`, then step by `row_len`
226                        // to pick the col-th entry of each logical row.
227                        for (eval, coeff) in poly_comb_r
228                            .iter()
229                            .skip(col)
230                            .step_by(row_len)
231                            .zip(coeffs.iter())
232                        {
233                            let scaled: Zt::CombR = eval
234                                .clone()
235                                .mul_by_scalar::<CHECK_FOR_OVERFLOW>(coeff)
236                                .expect("Cannot multiply evaluation by coefficient");
237                            if CHECK_FOR_OVERFLOW {
238                                *acc = zinc_utils::add!(
239                                    *acc,
240                                    &scaled,
241                                    "Addition overflow while combining rows across polys"
242                                );
243                            } else {
244                                *acc += scaled;
245                            }
246                        }
247                    }
248                    Ok(())
249                },
250            )?;
251            combined
252        };
253
254        transcript.write_const_many(&combined_row)?;
255        for _ in 0..Zt::NUM_COLUMN_OPENINGS {
256            let column_idx = transcript.squeeze_challenge_idx(pp.linear_code.codeword_len());
257            Self::open_merkle_trees_for_column(transcript, commit_hint, column_idx)?;
258        }
259
260        Ok(eval)
261    }
262
263    /// See [`Self::prove`] for details.
264    #[inline(always)]
265    pub fn prove_single<C, const CHECK_FOR_OVERFLOW: bool>(
266        transcript: &mut PcsProverTranscript,
267        pp: &ZipPlusParams<Zt, Lc>,
268        poly: &DenseMultilinearExtension<Zt::Eval>,
269        point: &[Zt::Pt],
270        commit_hint: &ZipPlusHint<Zt::Cw>,
271        field_cfg: &C,
272    ) -> Result<C::Element, ZipError>
273    where
274        C: BaseFieldConfig
275            + ProjectElementWithConfig<Zt::Pt>
276            + ProjectElementWithConfig<Zt::CombR>
277            + Sync,
278        C::Integer: ConstTranscribable,
279        Zt::CombR: MulByScalar<Zt::Chal>,
280    {
281        Self::prove::<C, CHECK_FOR_OVERFLOW>(
282            transcript,
283            pp,
284            std::slice::from_ref(poly),
285            point,
286            commit_hint,
287            field_cfg,
288        )
289    }
290
291    pub(super) fn open_merkle_trees_for_column(
292        transcript: &mut PcsProverTranscript,
293        commit_hint: &ZipPlusHint<Zt::Cw>,
294        column_idx: usize,
295    ) -> Result<(), ZipError> {
296        for cw_matrix in &commit_hint.cw_matrices {
297            let column_values = cw_matrix.as_rows().map(|row| &row[column_idx]);
298            transcript.write_const_many_iter::<Zt::Cw, _>(column_values, cw_matrix.num_rows)?;
299        }
300
301        let merkle_proof = commit_hint
302            .merkle_tree
303            .prove(column_idx)
304            .map_err(|_| ZipError::InvalidPcsOpen("Failed to open merkle tree".into()))?;
305        transcript
306            .write_merkle_proof(&merkle_proof)
307            .map_err(|_| ZipError::InvalidPcsOpen("Failed to write a merkle tree proof".into()))?;
308
309        Ok(())
310    }
311}
312
313#[cfg(test)]
314#[allow(
315    clippy::arithmetic_side_effects,
316    clippy::cast_possible_truncation,
317    clippy::cast_possible_wrap
318)]
319mod tests {
320    use crate::{
321        code::iprs::IprsCode,
322        merkle::MerkleTree,
323        pcs::{
324            structs::{ZipPlus, ZipPlusHint},
325            test_utils::*,
326        },
327        pcs_transcript::PcsProverTranscript,
328    };
329    use crypto_primitives::{
330        FixedConfig, ProjectElementWithConfig, crypto_bigint_int::Int,
331        crypto_bigint_monty::MontyField, crypto_bigint_uint::U64,
332    };
333    use num_traits::ConstOne;
334    use zinc_poly::mle::DenseMultilinearExtension;
335    use zinc_utils::{CHECKED, from_ref::FromRef};
336
337    const INT_LIMBS: usize = U64::LIMBS;
338
339    const N: usize = INT_LIMBS;
340    const K: usize = INT_LIMBS * 4;
341    const M: usize = INT_LIMBS * 8;
342    const DEGREE_PLUS_ONE: usize = 3;
343
344    type Cfg = MontyField<K>;
345
346    type Zt = TestZipTypes<N, K, M>;
347    type C = IprsCode<Zt, TestIprsConfig, REP_FACTOR, CHECKED>;
348
349    type PolyZt = TestBinPolyZipTypes<K, M, DEGREE_PLUS_ONE>;
350    type PolyC = IprsCode<PolyZt, TestIprsConfig, REP_FACTOR, CHECKED>;
351
352    type TestZip = ZipPlus<Zt, C>;
353    type TestPolyZip = ZipPlus<PolyZt, PolyC>;
354
355    fn test_point(num_vars: usize) -> Vec<Int<INT_LIMBS>> {
356        (0..num_vars).map(|i| Int::from(i as i32 + 2)).collect()
357    }
358
359    #[test]
360    fn prove_succeeds_for_single_poly() {
361        let num_vars = 10;
362        let (pp, poly) = setup_test_params(num_vars);
363        let (hint, comm) = TestZip::commit_single(&pp, &poly).unwrap();
364        let point = test_point(num_vars);
365
366        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
367        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
368
369        let result = TestZip::prove_single::<Cfg, CHECKED>(
370            &mut transcript,
371            &pp,
372            &poly,
373            &point,
374            &hint,
375            &field_cfg,
376        );
377        assert!(result.is_ok());
378    }
379
380    #[test]
381    fn prove_succeeds_for_poly_type() {
382        let num_vars = 10;
383        let (pp, poly) = setup_poly_test_params(num_vars);
384        let (hint, comm) = TestPolyZip::commit_single(&pp, &poly).unwrap();
385        let point: Vec<i128> = (0..num_vars).map(|i| i as i128 + 2).collect();
386
387        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
388        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
389
390        let result = TestPolyZip::prove_single::<Cfg, CHECKED>(
391            &mut transcript,
392            &pp,
393            &poly,
394            &point,
395            &hint,
396            &field_cfg,
397        );
398        assert!(result.is_ok());
399    }
400
401    #[test]
402    fn prove_succeeds_with_corrupted_codeword() {
403        let num_vars = 10;
404        let (pp, poly) = setup_test_params(num_vars);
405        let (mut hint, comm) = TestZip::commit_single(&pp, &poly).unwrap();
406
407        {
408            let mut rows = hint.cw_matrices[0].to_rows_slices_mut();
409            assert!(!rows.is_empty());
410            rows[0][0] += Int::ONE;
411        }
412
413        let corrupted_tree = {
414            let all_rows: Vec<&[_]> = hint.cw_matrices.iter().flat_map(|m| m.as_rows()).collect();
415            MerkleTree::new(&all_rows)
416        };
417        let corrupted_hint = ZipPlusHint::new(hint.cw_matrices, corrupted_tree);
418
419        let point = test_point(num_vars);
420
421        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
422        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
423
424        let result = TestZip::prove_single::<Cfg, CHECKED>(
425            &mut transcript,
426            &pp,
427            &poly,
428            &point,
429            &corrupted_hint,
430            &field_cfg,
431        );
432        assert!(result.is_ok());
433    }
434
435    #[test]
436    fn prove_rejects_oversized_polynomial() {
437        let num_vars = 10;
438        let (pp, _) = setup_test_params(num_vars);
439        let oversized_poly: DenseMultilinearExtension<_> =
440            (0..1 << (num_vars + 1)).map(Int::from).collect();
441
442        let (hint, comm) =
443            TestZip::commit_single(&pp, &setup_test_params::<N, K, M>(num_vars).1).unwrap();
444
445        let point = test_point(num_vars);
446
447        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
448        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
449
450        let result = TestZip::prove_single::<Cfg, CHECKED>(
451            &mut transcript,
452            &pp,
453            &oversized_poly,
454            &point,
455            &hint,
456            &field_cfg,
457        );
458        assert!(result.is_err());
459    }
460
461    /// For TestZipTypes (degree_bound = 0), alphas = [1] so prove eval
462    /// equals poly(point) lifted to F.
463    #[test]
464    fn prove_returns_correct_evaluation() {
465        let num_vars = 10;
466        let (pp, poly) = setup_test_params(num_vars);
467        let (hint, comm) = TestZip::commit_single(&pp, &poly).unwrap();
468        let point = test_point(num_vars);
469
470        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
471        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
472
473        let eval_f = TestZip::prove_single::<Cfg, CHECKED>(
474            &mut transcript,
475            &pp,
476            &poly,
477            &point,
478            &hint,
479            &field_cfg,
480        )
481        .unwrap();
482
483        let poly_wide: DenseMultilinearExtension<Int<M>> =
484            poly.evaluations.iter().map(Int::from_ref).collect();
485        let point_wide: Vec<Int<M>> = point.iter().map(Int::from_ref).collect();
486        let expected_int = poly_wide
487            .evaluate(&FixedConfig::default(), &point_wide)
488            .unwrap();
489        let expected_f = field_cfg.project(&expected_int);
490
491        assert_eq!(eval_f, expected_f);
492    }
493
494    fn make_batch_polys(
495        num_vars: usize,
496        batch_size: usize,
497    ) -> Vec<DenseMultilinearExtension<Int<INT_LIMBS>>> {
498        let poly_size = 1 << num_vars;
499        (0..batch_size)
500            .map(|b| {
501                let base = (b * poly_size) as i32;
502                (base + 1..=base + poly_size as i32)
503                    .map(Int::from)
504                    .collect()
505            })
506            .collect()
507    }
508
509    #[test]
510    fn prove_succeeds_for_batch() {
511        let num_vars = 10;
512        let (pp, _) = setup_test_params(num_vars);
513        let polys = make_batch_polys(num_vars, 2);
514
515        let (hint, comm) = TestZip::commit(&pp, &polys).unwrap();
516        let point = test_point(num_vars);
517
518        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
519        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
520
521        let result =
522            TestZip::prove::<Cfg, CHECKED>(&mut transcript, &pp, &polys, &point, &hint, &field_cfg);
523        assert!(result.is_ok())
524    }
525
526    #[test]
527    fn prove_succeeds_for_batch_5() {
528        let num_vars = 10;
529        let (pp, _) = setup_test_params(num_vars);
530        let polys = make_batch_polys(num_vars, 5);
531
532        let (hint, comm) = TestZip::commit(&pp, &polys).unwrap();
533        let point = test_point(num_vars);
534
535        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
536        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
537
538        let result =
539            TestZip::prove::<Cfg, CHECKED>(&mut transcript, &pp, &polys, &point, &hint, &field_cfg);
540        assert!(result.is_ok())
541    }
542
543    #[test]
544    fn prove_with_corrupted_codeword_for_batch() {
545        let num_vars = 10;
546        let (pp, _) = setup_test_params(num_vars);
547        let polys = make_batch_polys(num_vars, 2);
548
549        let (mut hint, comm) = TestZip::commit(&pp, &polys).unwrap();
550
551        hint.cw_matrices[0].to_rows_slices_mut()[0][0] += Int::ONE;
552
553        let corrupted_tree = {
554            let all_rows: Vec<&[_]> = hint.cw_matrices.iter().flat_map(|m| m.as_rows()).collect();
555            MerkleTree::new(&all_rows)
556        };
557        let corrupted_hint = ZipPlusHint::new(hint.cw_matrices, corrupted_tree);
558
559        let point = test_point(num_vars);
560
561        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
562        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
563
564        let result = TestZip::prove::<Cfg, CHECKED>(
565            &mut transcript,
566            &pp,
567            &polys,
568            &point,
569            &corrupted_hint,
570            &field_cfg,
571        );
572        assert!(result.is_ok());
573    }
574
575    #[test]
576    fn prove_rejects_oversized_polynomial_in_batch() {
577        let num_vars = 10;
578        let (pp, _) = setup_test_params(num_vars);
579        let oversized: DenseMultilinearExtension<_> = (0..1 << 5).map(Int::from).collect();
580        let normal: DenseMultilinearExtension<_> = (1..=16).map(Int::from).collect();
581        let polys = vec![normal, oversized];
582
583        let (hint, comm) = TestZip::commit(&pp, &make_batch_polys(num_vars, 2)).unwrap();
584
585        let point = test_point(num_vars);
586
587        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
588        let field_cfg = get_field_cfg::<Zt, Cfg>(&mut transcript.fs_transcript);
589
590        let result =
591            TestZip::prove::<Cfg, CHECKED>(&mut transcript, &pp, &polys, &point, &hint, &field_cfg);
592        assert!(result.is_err());
593    }
594}