Skip to main content

zip_plus/pcs/
phase_commit.rs

1use std::slice::from_ref;
2
3use crate::{
4    ZipError,
5    code::LinearCode,
6    merkle::MerkleTree,
7    pcs::{
8        structs::{ZipPlus, ZipPlusCommitment, ZipPlusHint, ZipPlusParams, ZipTypes},
9        utils::validate_input,
10    },
11};
12use crypto_primitives::DenseRowMatrix;
13use uninit::out_ref::Out;
14use zinc_utils::{cfg_chunks, cfg_chunks_mut, cfg_iter};
15
16#[cfg(feature = "parallel")]
17use rayon::prelude::*;
18use zinc_poly::mle::DenseMultilinearExtension;
19
20impl<Zt: ZipTypes, Lc: LinearCode<Zt>> ZipPlus<Zt, Lc> {
21    /// Creates a commitment to one or more multilinear polynomials using the
22    /// ZIP PCS scheme.
23    ///
24    /// This function implements the commitment phase of the ZIP polynomial
25    /// commitment scheme. It encodes each polynomial's evaluations using a
26    /// linear error-correcting code and then creates a single Merkle tree
27    /// commitment over the interleaved columns.
28    ///
29    /// # Algorithm
30    /// 1. Validates that each polynomial's number of variables matches the
31    ///    parameters.
32    /// 2. Arranges each polynomial's evaluations into a matrix with
33    ///    `pp.num_rows` rows.
34    /// 3. Encodes each row using the specified linear code, expanding its
35    ///    length from `row_len` to `codeword_len`.
36    /// 4. Constructs a single Merkle tree where `leaf_j = H(poly_1_col_j ||
37    ///    poly_2_col_j || ...)`.
38    /// 5. Returns the full commitment data (for the prover) and a compact
39    ///    commitment (for the verifier).
40    ///
41    /// # Parameters
42    /// - `pp`: Public parameters (`ZipPlusParams`) containing the configuration
43    ///   for the commitment scheme.
44    /// - `polys`: Slice of multilinear polynomials to be committed to.
45    ///
46    /// # Returns
47    /// A `Result` containing a tuple of:
48    /// - `ZipPlusHint`: Per-polynomial encoded rows and the shared Merkle tree,
49    ///   kept by the prover for the opening phase.
50    /// - `ZipPlusCommitment`: The compact commitment (Merkle root and batch
51    ///   size), to be sent to the verifier.
52    ///
53    /// # Errors
54    /// - Returns `Error::InvalidPcsParam` if any polynomial has more variables
55    ///   than the parameters support.
56    ///
57    /// # Panics
58    /// - Panics if any polynomial's evaluation count does not match
59    ///   `pp.num_rows * pp.linear_code.row_len()`.
60    #[allow(clippy::arithmetic_side_effects)]
61    pub fn commit(
62        pp: &ZipPlusParams<Zt, Lc>,
63        polys: &[DenseMultilinearExtension<Zt::Eval>],
64    ) -> Result<(ZipPlusHint<Zt::Cw>, ZipPlusCommitment), ZipError> {
65        assert!(
66            !polys.is_empty(),
67            "Batch must contain at least one polynomial"
68        );
69        let batch_size = polys.len();
70        let row_len = pp.linear_code.row_len();
71        validate_input::<Zt, Lc, bool>(
72            "commit",
73            pp.num_vars,
74            pp.linear_code.row_len(),
75            batch_size,
76            polys,
77            &[],
78        )?;
79
80        let expected_num_evals = pp.num_rows * row_len;
81        let cw_matrices: Vec<DenseRowMatrix<Zt::Cw>> = cfg_iter!(polys).map(|poly| {
82            assert_eq!(
83                poly.len(),
84                expected_num_evals,
85                "Polynomial has an incorrect number of evaluations ({}) for the expected matrix size ({})",
86                poly.len(),
87                expected_num_evals
88            );
89
90            Self::encode_rows(pp, poly)
91        }).collect();
92
93        let all_rows: Vec<&[Zt::Cw]> = cw_matrices.iter().flat_map(|m| m.as_rows()).collect();
94        let merkle_tree = MerkleTree::new(&all_rows);
95        let root = merkle_tree.root();
96
97        Ok((
98            ZipPlusHint::new(cw_matrices, merkle_tree),
99            ZipPlusCommitment { root, batch_size },
100        ))
101    }
102
103    /// Creates a commitment without constructing Merkle trees.
104    ///
105    /// This function performs the encoding step of the commitment phase but
106    /// deliberately skips the computationally intensive step of building
107    /// Merkle trees. It is intended **for testing and benchmarking purposes
108    /// only**, where the full commitment structure is not required.
109    ///
110    /// # Parameters
111    /// - `pp`: Public parameters (`ZipPlusParams`).
112    /// - `poly`: The multilinear polynomial to commit to.
113    ///
114    /// # Returns
115    /// A `Result` containing `ZipPlusHint` with the encoded rows but
116    /// empty Merkle trees, and a `ZipPlusCommitment` with an empty
117    /// vector of roots.
118    #[allow(dead_code)]
119    pub fn commit_no_merkle(
120        pp: &ZipPlusParams<Zt, Lc>,
121        poly: &DenseMultilinearExtension<Zt::Eval>,
122    ) -> Result<DenseRowMatrix<Zt::Cw>, ZipError> {
123        validate_input::<Zt, Lc, bool>(
124            "commit",
125            pp.num_vars,
126            pp.linear_code.row_len(),
127            1,
128            from_ref(poly),
129            &[],
130        )?;
131
132        Ok(Self::encode_rows(pp, poly))
133    }
134
135    pub fn commit_single(
136        pp: &ZipPlusParams<Zt, Lc>,
137        poly: &DenseMultilinearExtension<Zt::Eval>,
138    ) -> Result<(ZipPlusHint<Zt::Cw>, ZipPlusCommitment), ZipError> {
139        Self::commit(pp, std::slice::from_ref(poly))
140    }
141
142    /// Encodes the evaluations of a polynomial by arranging them into rows and
143    /// applying a linear code.
144    ///
145    /// This function treats the polynomial's flat evaluation vector as a matrix
146    /// with `pp.num_rows` and encodes each row individually. The resulting
147    /// encoded rows are concatenated into a single flat vector. This
148    /// operation can be parallelized if the `parallel` feature is enabled.
149    ///
150    /// # Parameters
151    /// - `pp`: Public parameters containing matrix dimensions and the linear
152    ///   code.
153    /// - `codeword_len`: The length of an encoded row.
154    /// - `poly`: The polynomial whose evaluations are to be encoded.
155    ///
156    /// # Returns
157    /// A `Vec<Int<K>>` containing all the encoded rows concatenated together.
158    #[allow(clippy::arithmetic_side_effects)]
159    pub fn encode_rows(pp: &ZipPlusParams<Zt, Lc>, evals: &[Zt::Eval]) -> DenseRowMatrix<Zt::Cw> {
160        let row_len = pp.linear_code.row_len();
161        let codeword_len = pp.linear_code.codeword_len();
162
163        // Performance: Using DenseRowMatrix's linearized row in an uninit form
164        // is much more performant that using Vec<Vec<_>>.
165        let mut encoded_matrix = DenseRowMatrix::<Zt::Cw>::uninit(pp.num_rows, codeword_len);
166
167        cfg_chunks_mut!(encoded_matrix.data, codeword_len)
168            .zip(cfg_chunks!(evals, row_len))
169            .for_each(|(row, evals)| {
170                let encoded: Vec<Zt::Cw> = pp.linear_code.encode(evals);
171                Out::from(row).copy_from_slice(encoded.as_slice());
172            });
173
174        // Safe because we have just initialized all elements.
175        unsafe { encoded_matrix.init() }
176    }
177}
178
179//TODO. Review and add proper test
180#[cfg(test)]
181#[allow(
182    clippy::arithmetic_side_effects,
183    clippy::cast_possible_truncation,
184    clippy::cast_possible_wrap
185)]
186mod tests {
187    use crate::{
188        code::{LinearCode, iprs::IprsCode},
189        merkle::{MerkleTree, MtHash},
190        pcs::{
191            structs::{ZipPlus, ZipPlusParams, ZipTypes},
192            test_utils::*,
193        },
194        pcs_transcript::PcsProverTranscript,
195    };
196    use crypto_bigint::{Random, U64, U256, Word};
197    use crypto_primitives::{
198        Matrix, boolean::Boolean, crypto_bigint_boxed_monty::BoxedMontyField,
199        crypto_bigint_int::Int,
200    };
201    use itertools::Itertools;
202    use num_traits::Zero;
203    use rand::{Rng, rng};
204    use std::sync::LazyLock;
205    use zinc_poly::{mle::DenseMultilinearExtension, univariate::binary::BinaryPoly};
206    use zinc_utils::CHECKED;
207
208    const INT_LIMBS: usize = U64::LIMBS;
209
210    const N: usize = INT_LIMBS;
211    const K: usize = INT_LIMBS * 4;
212    const M: usize = INT_LIMBS * 8;
213    const DEGREE_PLUS_ONE: usize = 3;
214
215    type Zt = TestZipTypes<N, K, M>;
216    type C = IprsCode<Zt, TestIprsConfig, REP_FACTOR, CHECKED>;
217    static C: LazyLock<C> = LazyLock::new(|| C::new(IPRS_ROW_LEN, IPRS_DEPTH).unwrap());
218
219    type PolyZt = TestBinPolyZipTypes<K, M, DEGREE_PLUS_ONE>;
220    type PolyC = IprsCode<PolyZt, TestIprsConfig, REP_FACTOR, CHECKED>;
221    static POLY_C: LazyLock<PolyC> =
222        LazyLock::new(|| PolyC::new(IPRS_ROW_LEN, IPRS_DEPTH).unwrap());
223
224    type TestZip = ZipPlus<Zt, C>;
225    type TestPolyZip = ZipPlus<PolyZt, PolyC>;
226
227    #[test]
228    fn commit_rejects_too_many_variables() {
229        let num_vars = 10;
230        let (pp, _) = setup_test_params(num_vars);
231
232        // Create MLE with a larger number of variables
233        let poly: DenseMultilinearExtension<_> =
234            (1..=(1 << (num_vars + 1))).map(Int::from).collect();
235
236        let result = TestZip::commit_single(&pp, &poly);
237        assert!(result.is_err());
238    }
239
240    #[test]
241    fn commit_is_deterministic() {
242        let num_vars = 10;
243        let (pp, poly) = setup_test_params(num_vars);
244
245        let result1 = TestZip::commit_single(&pp, &poly).unwrap();
246        let result2 = TestZip::commit_single(&pp, &poly).unwrap();
247
248        assert_eq!(result1.1.root, result2.1.root);
249    }
250
251    #[test]
252    fn different_polynomials_produce_different_commitments() {
253        let num_vars = 10;
254        let (pp, _) = setup_test_params(num_vars);
255        let poly_size = 1 << num_vars;
256
257        let poly1 = DenseMultilinearExtension::from_evaluations_vec(
258            num_vars,
259            vec![Int::from(1); poly_size],
260            Zero::zero(),
261        );
262        let poly2 = DenseMultilinearExtension::from_evaluations_vec(
263            num_vars,
264            vec![Int::from(2); poly_size],
265            Zero::zero(),
266        );
267
268        let (_, commitment1) = TestZip::commit_single(&pp, &poly1).unwrap();
269        let (_, commitment2) = TestZip::commit_single(&pp, &poly2).unwrap();
270
271        assert_ne!(commitment1.root, commitment2.root);
272    }
273
274    #[test]
275    fn commit_succeeds_for_small_polynomial() {
276        let num_vars = 4;
277        let num_rows = (1_usize << num_vars).div_ceil(C.row_len());
278        let pp = ZipPlusParams::new(num_vars, num_rows, C.clone());
279
280        let evaluations = vec![Int::from(42); 1 << num_vars];
281        let mut poly =
282            DenseMultilinearExtension::from_evaluations_vec(num_vars, evaluations, Zero::zero());
283        // Zero-pad MLE to match the expected number of evaluations
284        poly.evaluations
285            .resize(pp.num_rows * pp.linear_code.row_len(), Zero::zero());
286
287        let result = TestZip::commit_single(&pp, &poly);
288        assert!(result.is_ok());
289    }
290
291    #[test]
292    fn commit_succeeds_for_two_variables() {
293        let num_vars = 2;
294        let num_rows = (1_usize << num_vars).div_ceil(C.row_len());
295        let pp = ZipPlusParams::new(num_vars, num_rows, C.clone());
296
297        let poly_size = 1 << num_vars;
298        let evaluations: Vec<_> = (1..=poly_size).map(Int::from).collect();
299        let mut poly =
300            DenseMultilinearExtension::from_evaluations_vec(num_vars, evaluations, Zero::zero());
301        // Zero-pad MLE to match the expected number of evaluations
302        poly.evaluations
303            .resize(pp.num_rows * pp.linear_code.row_len(), Zero::zero());
304
305        let result = TestZip::commit_single(&pp, &poly);
306        assert!(result.is_ok());
307    }
308
309    #[test]
310    fn batch_commit_produces_different_root_than_individual_commits() {
311        let num_vars = 10;
312        let (pp, _) = setup_test_params(num_vars);
313        let poly_size = 1 << num_vars;
314
315        let poly1: DenseMultilinearExtension<_> = (1..=poly_size).map(Int::from).collect();
316        let poly2: DenseMultilinearExtension<_> =
317            (poly_size + 1..=2 * poly_size).map(Int::from).collect();
318
319        let (_, batched_comm) = TestZip::commit(&pp, &[poly1.clone(), poly2.clone()]).unwrap();
320        let (_, comm1) = TestZip::commit_single(&pp, &poly1).unwrap();
321        let (_, comm2) = TestZip::commit_single(&pp, &poly2).unwrap();
322
323        assert_ne!(batched_comm.root, comm1.root);
324        assert_ne!(batched_comm.root, comm2.root);
325    }
326
327    #[test]
328    fn batch_commit_is_deterministic() {
329        let num_vars = 10;
330        let (pp, _) = setup_test_params(num_vars);
331        let poly_size = 1 << num_vars;
332
333        let poly1: DenseMultilinearExtension<_> = (1..=poly_size).map(Int::from).collect();
334        let poly2: DenseMultilinearExtension<_> =
335            (poly_size + 1..=2 * poly_size).map(Int::from).collect();
336
337        let (_, comm_a) = TestZip::commit(&pp, &[poly1.clone(), poly2.clone()]).unwrap();
338        let (_, comm_b) = TestZip::commit(&pp, &[poly1, poly2]).unwrap();
339
340        assert_eq!(comm_a.root, comm_b.root);
341    }
342
343    #[test]
344    fn batch_commit_batch_size_is_correct() {
345        let num_vars = 10;
346        let (pp, _) = setup_test_params(num_vars);
347        let poly_size = 1 << num_vars;
348
349        let polys: Vec<DenseMultilinearExtension<_>> = (0..5)
350            .map(|offset| {
351                let start = offset * poly_size + 1;
352                (start..start + poly_size).map(Int::from).collect()
353            })
354            .collect();
355
356        let (hint, comm) = TestZip::commit(&pp, &polys).unwrap();
357        assert_eq!(comm.batch_size, 5);
358        assert_eq!(hint.cw_matrices.len(), 5);
359    }
360
361    #[test]
362    fn encode_rows_produces_correct_size() {
363        let num_vars = 10;
364        let (pp, poly) = setup_test_params(num_vars);
365        let encoded = TestZip::encode_rows(&pp, &poly);
366
367        assert_eq!(encoded.num_rows, pp.num_rows);
368        assert_eq!(encoded.num_cols, pp.linear_code.codeword_len());
369    }
370
371    /// Verifies that the output of `encode_rows` is semantically correct by
372    /// comparing it to a direct, row-by-row encoding.
373    #[test]
374    fn encoded_rows_match_linear_code_definition() {
375        let num_vars = 10;
376        let (pp, poly) = setup_test_params(num_vars);
377        let encoded = TestZip::encode_rows(&pp, &poly);
378
379        for (i, row_chunk) in encoded.as_rows().enumerate() {
380            let start = i * pp.linear_code.row_len();
381            let end = start + pp.linear_code.row_len();
382            let row_evals = &poly[start..end];
383            let expected_encoding = pp.linear_code.encode(row_evals);
384            assert_eq!(
385                row_chunk,
386                expected_encoding.as_slice(),
387                "Row {i} encoding mismatch",
388            );
389        }
390    }
391
392    /// Verifies that corrupting the encoded data after commitment results in a
393    /// different Merkle root.
394    #[test]
395    fn corrupted_encoding_changes_merkle_root() {
396        let num_vars = 10;
397        let (pp, poly) = setup_test_params(num_vars);
398        let (data, commitment) = TestZip::commit_single(&pp, &poly).unwrap();
399
400        assert!(!data.cw_matrices[0].is_empty());
401        let mut cw_matrix = data.cw_matrices[0].to_rows();
402        cw_matrix[0][0] = Int::from(999999);
403        let corrupted_row = cw_matrix[0].clone();
404        let new_tree = MerkleTree::new(&[corrupted_row.as_slice()]);
405        assert_ne!(
406            new_tree.root(),
407            commitment.root,
408            "Corruption should change Merkle root"
409        );
410    }
411
412    #[test]
413    fn batch_commit_single_poly_matches_single_commit() {
414        let num_vars = 10;
415        let (pp, poly) = setup_test_params(num_vars);
416
417        let polys = std::slice::from_ref(&poly);
418        let (batched_hint, batched_comm) = TestZip::commit(&pp, polys).unwrap();
419        let (single_hint, single_comm) = TestZip::commit_single(&pp, &poly).unwrap();
420
421        assert_eq!(batched_comm.root, single_comm.root);
422        assert_eq!(batched_hint.cw_matrices.len(), 1);
423        assert_eq!(batched_hint.cw_matrices[0], single_hint.cw_matrices[0]);
424    }
425
426    #[test]
427    fn encoded_rows_are_nonzero_for_nonzero_input() {
428        let num_vars = 10;
429        let (pp, poly) = setup_test_params(num_vars);
430        let encoded = TestZip::encode_rows(&pp, &poly.evaluations);
431
432        assert_eq!(encoded.num_rows, pp.num_rows);
433        assert_eq!(encoded.num_cols, pp.linear_code.codeword_len());
434
435        let non_zero_count = encoded.as_rows().flatten().filter(|x| !x.is_zero()).count();
436        assert!(non_zero_count > 0);
437    }
438
439    #[test]
440    fn commit_produces_correct_merkle_tree_count() {
441        let num_vars = 10;
442        let (pp, poly) = setup_test_params(num_vars);
443        let (hint, _) = TestZip::commit_single(&pp, &poly).unwrap();
444
445        assert_eq!(hint.cw_matrices[0].num_rows, pp.num_rows);
446        assert_eq!(hint.cw_matrices[0].num_cols, pp.linear_code.codeword_len());
447    }
448
449    #[test]
450    #[cfg(feature = "parallel")]
451    fn encoding_is_consistent_across_threads() {
452        use rayon::prelude::*;
453
454        let num_vars = 10;
455        let poly_size = 1 << num_vars;
456        let evaluations = (1..=poly_size).map(|v| Int::from(v as i32)).collect();
457        let poly =
458            DenseMultilinearExtension::from_evaluations_vec(num_vars, evaluations, Zero::zero());
459
460        let results: Vec<Vec<Vec<Int<4>>>> = (0..10)
461            .into_par_iter()
462            .map(|_| {
463                let row_len = 1 << (num_vars / 2);
464                let pp = ZipPlusParams::new(num_vars, poly_size / row_len, C.clone());
465
466                let rows = TestZip::encode_rows(&pp, &poly.evaluations);
467                let rows: Vec<Vec<_>> = rows
468                    .data
469                    .chunks_exact(pp.linear_code.codeword_len())
470                    .map(|chunk| chunk.to_vec())
471                    .collect();
472                rows
473            })
474            .collect();
475
476        for w in results.windows(2) {
477            assert_eq!(
478                w[0], w[1],
479                "Parallel encoding runs produced inconsistent results: {:?} vs {:?}",
480                w[0], w[1]
481            );
482        }
483    }
484
485    #[test]
486    fn commit_succeeds_for_zero_polynomial() {
487        let num_vars = 10;
488        let (pp, _) = setup_test_params(num_vars);
489        let poly_size = 1 << num_vars;
490        let zero_poly = DenseMultilinearExtension::from_evaluations_vec(
491            num_vars,
492            vec![Zero::zero(); poly_size],
493            Zero::zero(),
494        );
495        let result = TestZip::commit_single(&pp, &zero_poly);
496        assert!(result.is_ok());
497    }
498
499    #[test]
500    fn commit_succeeds_for_alternating_values() {
501        let num_vars = 10;
502        let (pp, _) = setup_test_params(num_vars);
503        let poly_size = 1 << num_vars;
504        let alternating = (0..poly_size)
505            .map(|i| Int::from(if i % 2 == 0 { 1 } else { -1 }))
506            .collect();
507        let poly =
508            DenseMultilinearExtension::from_evaluations_vec(num_vars, alternating, Zero::zero());
509        let result = TestZip::commit_single(&pp, &poly);
510        assert!(result.is_ok());
511    }
512
513    #[test]
514    #[should_panic(expected = "Batch must contain at least one polynomial")]
515    fn batch_commit_on_empty_slice_panics() {
516        let num_vars = 10;
517        let (pp, _) = setup_test_params(num_vars);
518        let empty_polys: Vec<DenseMultilinearExtension<Int<INT_LIMBS>>> = vec![];
519        let _ = TestZip::commit(&pp, &empty_polys);
520    }
521
522    #[test]
523    fn encode_rows_succeeds_for_single_row() {
524        let num_vars = 10;
525        let poly_size = 1 << num_vars;
526        let pp = ZipPlusParams::new(num_vars, 1, C.clone());
527
528        // Create a polynomial with 2 variables and 4 evaluations
529        let evaluations = vec![Int::from(5); poly_size];
530        let poly =
531            DenseMultilinearExtension::from_evaluations_vec(num_vars, evaluations, Zero::zero());
532        let encoded = TestZip::encode_rows(&pp, &poly.evaluations);
533        assert_eq!(encoded.num_rows, 1);
534        assert_eq!(encoded.num_cols, pp.linear_code.codeword_len());
535    }
536
537    #[test]
538    fn encode_rows_succeeds_for_single_poly_row() {
539        let num_vars = 10;
540        let pp = ZipPlusParams::new(num_vars, 1, POLY_C.clone());
541
542        // Create a polynomial with 2 variables and 4 evaluations
543        let evaluations = vec![
544            BinaryPoly::new(vec![Boolean::FALSE, Boolean::FALSE]),
545            BinaryPoly::new(vec![Boolean::FALSE, Boolean::TRUE]),
546            BinaryPoly::new(vec![Boolean::TRUE, Boolean::FALSE]),
547        ];
548        let poly =
549            DenseMultilinearExtension::from_evaluations_vec(num_vars, evaluations, Zero::zero());
550        let encoded = TestPolyZip::encode_rows(&pp, &poly.evaluations);
551        assert_eq!(encoded.num_rows, 1);
552        assert_eq!(encoded.num_cols, pp.linear_code.codeword_len());
553    }
554
555    #[test]
556    fn matrix_dimensions_are_invariant() {
557        let test_cases = vec![(8, 1), (10, 4), (12, 16)];
558        for (num_vars, expected_rows) in test_cases {
559            let (pp, poly) = setup_test_params(num_vars);
560            assert_eq!(pp.num_rows, expected_rows);
561            let result = TestZip::commit_single(&pp, &poly);
562            assert!(result.is_ok());
563        }
564    }
565
566    #[test]
567    #[should_panic]
568    fn reject_incompatible_dimensions() {
569        let num_vars = 10;
570        let (pp, poly) = setup_test_params(num_vars);
571        let incompatible_pp = ZipPlusParams::new(8, 8, pp.linear_code);
572        TestZip::commit_single(&incompatible_pp, &poly).unwrap();
573    }
574
575    #[test]
576    fn linear_code_preserves_linearity() {
577        let num_vars = 10;
578        let (pp, poly) = setup_test_params(num_vars);
579        let encoded = TestZip::encode_rows(&pp, &poly.evaluations);
580        let row_len = pp.linear_code.row_len();
581        let codeword_len = pp.linear_code.codeword_len();
582        let row1_evals = &poly.evaluations[0..row_len];
583        let row2_evals = &poly.evaluations[row_len..2 * row_len];
584        let a = Int::from(3);
585        let b = Int::<4>::from(5);
586        let combined_evals: Vec<_> = (0..row_len)
587            .map(|i| a * row1_evals[i] + b.resize() * row2_evals[i])
588            .collect();
589        let combined_encoded = pp.linear_code.encode(&combined_evals);
590        let rows = encoded.as_rows().collect_vec();
591        let row1_encoded = rows[0];
592        let row2_encoded = rows[1];
593        let expected_combined: Vec<_> = (0..codeword_len)
594            .map(|i| a.resize() * row1_encoded[i] + b.resize() * row2_encoded[i])
595            .collect();
596        assert_eq!(combined_encoded, expected_combined);
597    }
598
599    #[test]
600    #[should_panic]
601    fn commit_panics_if_evaluations_not_multiple_of_row_len() {
602        let num_vars = 10;
603        let (pp, mut poly) = setup_test_params(num_vars);
604        poly.evaluations.truncate(15);
605        assert_eq!(poly.evaluations.len(), 15);
606        let _ = TestZip::commit_single(&pp, &poly);
607    }
608
609    #[test]
610    fn commit_with_many_variables() {
611        let num_vars = 16;
612        let (pp, poly) = setup_test_params(num_vars);
613        assert_eq!(pp.num_vars, num_vars);
614        let result = TestZip::commit_single(&pp, &poly);
615        assert!(result.is_ok());
616    }
617
618    #[test]
619    fn commit_with_smallest_matrix_arrangement() {
620        let num_vars = 8;
621        let (pp, poly) = setup_test_params(num_vars);
622        let poly_size = 1 << num_vars;
623        assert_eq!(pp.num_rows, 1);
624        assert_eq!(pp.linear_code.row_len(), poly_size);
625        let result = TestZip::commit_single(&pp, &poly);
626        assert!(result.is_ok());
627    }
628
629    #[test]
630    fn encode_rows_handles_large_integer_values() {
631        let num_vars = 10;
632        let (pp, _) = setup_test_params(num_vars);
633        let poly_size = 1 << num_vars;
634        let max_val = Int::<INT_LIMBS>::from(i64::MAX);
635        let poly = DenseMultilinearExtension::from_evaluations_vec(
636            num_vars,
637            vec![max_val; poly_size],
638            Zero::zero(),
639        );
640        let encoded_rows = TestZip::encode_rows(&pp, &poly.evaluations);
641        assert_eq!(encoded_rows.num_rows, pp.num_rows);
642        assert_eq!(encoded_rows.num_cols, pp.linear_code.codeword_len());
643    }
644
645    #[test]
646    #[should_panic(expected = "row_width.is_power_of_two()")]
647    fn merkle_tree_new_panics_on_non_power_of_two_leaves() {
648        let leaves_data: Vec<Int<INT_LIMBS>> = (0..7).map(Int::from).collect();
649        let _ = MerkleTree::new(&[leaves_data.as_slice()]);
650    }
651
652    fn make_poly_batch(
653        num_vars: usize,
654        batch_size: usize,
655    ) -> Vec<DenseMultilinearExtension<BinaryPoly<DEGREE_PLUS_ONE>>> {
656        let poly_size = 1 << num_vars;
657        let d = DEGREE_PLUS_ONE - 1;
658        (0..batch_size)
659            .map(|b| {
660                let coeffs: Vec<Boolean> = (0..poly_size * d)
661                    .map(|i| ((i + b * 7) % 3 != 0).into())
662                    .collect();
663                let evals = coeffs.chunks_exact(d).map(BinaryPoly::new).collect_vec();
664                DenseMultilinearExtension::from_evaluations_vec(num_vars, evals, Zero::zero())
665            })
666            .collect()
667    }
668
669    #[test]
670    fn batch_commit_poly_succeeds() {
671        let num_vars = 8;
672        let (pp, _) = setup_poly_test_params::<K, M, DEGREE_PLUS_ONE>(num_vars);
673        let polys = make_poly_batch(num_vars, 3);
674
675        let (hint, comm) = TestPolyZip::commit(&pp, &polys).unwrap();
676        assert_eq!(comm.batch_size, 3);
677        assert_eq!(hint.cw_matrices.len(), 3);
678    }
679
680    #[test]
681    fn batch_commit_poly_is_deterministic() {
682        let num_vars = 8;
683        let (pp, _) = setup_poly_test_params::<K, M, DEGREE_PLUS_ONE>(num_vars);
684        let polys = make_poly_batch(num_vars, 2);
685
686        let (_, comm_a) = TestPolyZip::commit(&pp, &polys).unwrap();
687        let (_, comm_b) = TestPolyZip::commit(&pp, &polys).unwrap();
688        assert_eq!(comm_a.root, comm_b.root);
689    }
690
691    #[test]
692    fn batch_commit_poly_single_matches_commit_single() {
693        let num_vars = 10;
694        let (pp, poly) = setup_poly_test_params::<K, M, DEGREE_PLUS_ONE>(num_vars);
695
696        let polys = std::slice::from_ref(&poly);
697        let (single_hint, single_comm) = TestPolyZip::commit_single(&pp, &poly).unwrap();
698        let (batched_hint, batched_comm) = TestPolyZip::commit(&pp, polys).unwrap();
699
700        assert_eq!(batched_comm.root, single_comm.root);
701        assert_eq!(batched_hint.cw_matrices.len(), 1);
702        assert_eq!(batched_hint.cw_matrices[0], single_hint.cw_matrices[0]);
703    }
704
705    #[test]
706    fn batch_commit_poly_different_polys_produce_different_roots() {
707        let num_vars = 10;
708        let (pp, _) = setup_poly_test_params::<K, M, DEGREE_PLUS_ONE>(num_vars);
709        let polys = make_poly_batch(num_vars, 2);
710
711        let (_, batched) = TestPolyZip::commit(&pp, &polys).unwrap();
712        let (_, single0) = TestPolyZip::commit_single(&pp, &polys[0]).unwrap();
713        let (_, single1) = TestPolyZip::commit_single(&pp, &polys[1]).unwrap();
714
715        assert_ne!(batched.root, single0.root);
716        assert_ne!(batched.root, single1.root);
717    }
718
719    #[test]
720    fn batch_commit_cw_matrices_are_distinct_per_poly() {
721        let num_vars = 10;
722        let (pp, _) = setup_test_params(num_vars);
723        let poly_size = 1 << num_vars;
724        let polys: Vec<DenseMultilinearExtension<_>> = vec![
725            (1..=poly_size).map(Int::from).collect(),
726            (poly_size + 1..=poly_size * 2).map(Int::from).collect(),
727        ];
728
729        let (hint, _) = TestZip::commit(&pp, &polys).unwrap();
730        assert_eq!(hint.cw_matrices.len(), 2);
731        assert_ne!(hint.cw_matrices[0], hint.cw_matrices[1]);
732    }
733
734    #[test]
735    fn proof_size_is_correct_for_parameters() {
736        use std::mem::size_of;
737
738        fn calculate_expected_proof_size_bytes(
739            pp: &ZipPlusParams<Zt, C>,
740            batch_size: usize,
741        ) -> usize {
742            // size of a single entry of cw_matrix
743            let size_of_zt_k = K * size_of::<Word>();
744            // size of CombR in combine row
745            let size_of_zt_m = M * size_of::<Word>();
746            // size_f = field_value || field_modulus
747            let size_of_f = 2 * U256::LIMBS * size_of::<Word>();
748            let size_of_usize_field = size_of::<u64>();
749            let size_of_path_elem = size_of::<MtHash>();
750
751            let codeword_len = pp.linear_code.codeword_len();
752            let merkle_depth = codeword_len.next_power_of_two().ilog2() as usize;
753
754            // b vectors: per poly, 1-byte length prefix + num_rows field elements
755            let b_phase_size = batch_size * (1 + pp.num_rows * size_of_f);
756            let combined_row_size = pp.linear_code.row_len() * size_of_zt_m;
757
758            // Column openings: per opening, column values from all cw_matrices + one Merkle
759            // proof
760            let column_values_size = batch_size * pp.num_rows * size_of_zt_k;
761            let single_merkle_proof_size =
762                size_of_usize_field * 3 + merkle_depth * size_of_path_elem;
763            let column_opening_phase_size =
764                Zt::NUM_COLUMN_OPENINGS * (column_values_size + single_merkle_proof_size);
765
766            b_phase_size + combined_row_size + column_opening_phase_size
767        }
768
769        type F = BoxedMontyField;
770
771        let mut rng = rng();
772        let num_vars = 10;
773        let poly_size: usize = 1 << num_vars;
774        let param = TestZip::setup(poly_size, C.clone());
775        let evaluations: Vec<_> = (0..poly_size)
776            .map(|_| <Zt as ZipTypes>::Eval::from(rng.random::<i8>()))
777            .collect();
778        let mle =
779            DenseMultilinearExtension::from_evaluations_slice(num_vars, &evaluations, Zero::zero());
780        let point: Vec<_> = (0..num_vars)
781            .map(|_| <Zt as ZipTypes>::Pt::random(&mut rng))
782            .collect();
783
784        let (hint, comm) = TestZip::commit_single(&param, &mle).unwrap();
785        let mut transcript = PcsProverTranscript::new_from_commitment(&comm);
786        let field_cfg = get_field_cfg::<Zt, F>(&mut transcript.fs_transcript);
787
788        let _eval_f = TestZip::prove_single::<F, CHECKED>(
789            &mut transcript,
790            &param,
791            &mle,
792            &point,
793            &hint,
794            &field_cfg,
795        )
796        .unwrap();
797        let actual_proof_size_bytes = transcript.stream.get_ref().len();
798        let expected_proof_size_bytes = calculate_expected_proof_size_bytes(&param, 1);
799        assert_eq!(actual_proof_size_bytes, expected_proof_size_bytes);
800    }
801}