Skip to content

Commit 80d5537

Browse files
committed
wip jagged_commit
1 parent 16ac2a6 commit 80d5537

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

crates/mpcs/src/jagged.rs

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
//! Jagged PCS Commit Adaptor
2+
//!
3+
//! This module implements the commit protocol for the Jagged PCS as described in
4+
//! the SP1 Jagged PCS paper (<https://eprint.iacr.org/2025/917.pdf>) and Ceno issues #1272 / #1288.
5+
//!
6+
//! ## Overview
7+
//!
8+
//! The "Jagged PCS" reduces proof size by packing all trace polynomials from multiple
9+
//! chips into a single "giga" multilinear polynomial `q'`:
10+
//!
11+
//! ```text
12+
//! q' = bitrev(p_0) || bitrev(p_1) || ... || bitrev(p_N)
13+
//! ```
14+
//!
15+
//! where each `p_i` is a column polynomial extracted from the input trace matrices, and
16+
//! `bitrev` is the suffix-to-prefix bit-reversal transformation.
17+
//!
18+
//! ## Suffix-to-Prefix Transformation
19+
//!
20+
//! The main sumcheck prover outputs evaluations `v_i = p_i(r[(n-s)..n])` — i.e., at the
21+
//! **suffix** of the random challenge point. To make these evaluations compatible with the
22+
//! jagged sumcheck (which operates on prefix-aligned polynomials), we apply a bit-reversal
23+
//! permutation to each polynomial's evaluations:
24+
//!
25+
//! ```text
26+
//! p_i'[j] = p_i[bitrev_s(j)] (for j in 0..2^s)
27+
//! ```
28+
//!
29+
//! After bit-reversal, `v_i = p_i(r[(n-s)..n]) = p_i'(reverse(r[(n-s)..n]))`.
30+
//!
31+
//! ## Cumulative Heights
32+
//!
33+
//! The cumulative height sequence `t` tracks the starting position of each polynomial in `q'`:
34+
//! - `t[0] = 0`
35+
//! - `t[i+1] = t[i] + h_i` where `h_i = 2^(num_vars of p_i)` is the number of evaluations
36+
//!
37+
//! Given a position `b` in `q'`, the verifier can locate the corresponding `(i, r)` pair via:
38+
//! - `t[i-1] <= b < t[i]`
39+
//! - `r = b - t[i-1]`
40+
//!
41+
//! The cumulative heights allow the verifier to succinctly evaluate the indicator function
42+
//! `g(z_r, z_b, t[i-1], t[i])` needed for the jagged sumcheck.
43+
//!
44+
//! ## Commit Protocol
45+
//!
46+
//! 1. For each input matrix `M_k` (with `h_k` rows and `w_k` columns):
47+
//! a. Extract each column as a polynomial with `h_k` evaluations.
48+
//! b. Apply bit-reversal to the evaluations.
49+
//! 2. Concatenate all bit-reversed polynomials: `cat = bitrev(p_0) || bitrev(p_1) || ...`
50+
//! 3. Compute cumulative heights `t[i]`.
51+
//! 4. Pad `cat` to the next power of two (required for MLE representation).
52+
//! 5. Commit to the padded `cat` as a single-column matrix using the inner PCS.
53+
54+
use std::iter::once;
55+
56+
use crate::{Error, PolynomialCommitmentScheme};
57+
use ff_ext::ExtensionField;
58+
use itertools::Itertools;
59+
use p3::{
60+
matrix::{Matrix, bitrev::BitReversableMatrix},
61+
maybe_rayon::prelude::{
62+
IndexedParallelIterator, IntoParallelIterator, ParallelIterator, ParallelSliceMut,
63+
},
64+
};
65+
use serde::{Deserialize, Serialize};
66+
use witness::{InstancePaddingStrategy, RowMajorMatrix};
67+
68+
/// Commitment to a jagged polynomial `q'`, together with all witness data needed
69+
/// for opening proofs.
70+
///
71+
/// Generic over the inner PCS `Pcs` so that any `PolynomialCommitmentScheme` can
72+
/// serve as the underlying commitment engine.
73+
pub struct JaggedCommitmentWithWitness<E: ExtensionField, Pcs: PolynomialCommitmentScheme<E>> {
74+
/// Commitment (with witness) to the "giga" polynomial `q'` via `Pcs`.
75+
pub inner: Pcs::CommitmentWithWitness,
76+
/// Cumulative height sequence `t`:
77+
/// - `t[0] = 0`
78+
/// - `t[i+1] = t[i] + poly_heights[i]`
79+
/// - Length: `num_polys + 1`
80+
pub cumulative_heights: Vec<usize>,
81+
/// Number of evaluations `h_i = 2^(num_vars_i)` for each polynomial `p_i`.
82+
/// Length: `num_polys`.
83+
pub poly_heights: Vec<usize>,
84+
}
85+
86+
/// The pure commitment (without witness data) for a jagged polynomial `q'`.
87+
/// This is what the verifier receives.
88+
#[derive(Clone, Serialize, Deserialize)]
89+
#[serde(bound(serialize = "", deserialize = ""))]
90+
pub struct JaggedCommitment<E: ExtensionField, Pcs: PolynomialCommitmentScheme<E>> {
91+
/// Pure commitment to the underlying giga polynomial `q'`.
92+
pub inner: Pcs::Commitment,
93+
/// Cumulative height sequence `t` (verifier needs this to evaluate `f(b)`).
94+
pub cumulative_heights: Vec<usize>,
95+
}
96+
97+
impl<E: ExtensionField, Pcs: PolynomialCommitmentScheme<E>> JaggedCommitmentWithWitness<E, Pcs> {
98+
/// Extract the pure commitment (without witness data).
99+
pub fn to_commitment(&self) -> JaggedCommitment<E, Pcs> {
100+
JaggedCommitment {
101+
inner: Pcs::get_pure_commitment(&self.inner),
102+
cumulative_heights: self.cumulative_heights.clone(),
103+
}
104+
}
105+
106+
/// Total number of polynomials packed into `q'`.
107+
pub fn num_polys(&self) -> usize {
108+
self.poly_heights.len()
109+
}
110+
111+
/// Total number of evaluations in the *unpadded* concatenated polynomial
112+
/// (= `t[num_polys]` = `cumulative_heights.last()`).
113+
pub fn total_evaluations(&self) -> usize {
114+
self.cumulative_heights.last().copied().unwrap_or(0)
115+
}
116+
}
117+
118+
/// Commit to a sequence of row-major matrices using the Jagged PCS scheme.
119+
///
120+
/// This function implements the commit phase described in Ceno issue #1288:
121+
/// 1. For each matrix, bit-reverse its rows (suffix-to-prefix transformation).
122+
/// 2. Transpose the bit-reversed matrix (row-major → column-major), so each
123+
/// column polynomial occupies a contiguous region in memory.
124+
/// 3. Concatenate all column polynomials: `q' = col_0 || col_1 || ...`
125+
/// 4. Compute the cumulative height sequence `t`.
126+
/// 5. Commit to `q'` as a single-column matrix using `Pcs::batch_commit`.
127+
///
128+
/// # Arguments
129+
/// * `pp` — Prover parameters for `Pcs`.
130+
/// * `rmms` — Non-empty sequence of row-major matrices. This function uses each matrix's height exactly as given.
131+
///
132+
/// # Errors
133+
/// Returns `Error::InvalidPcsParam` if `rmms` is empty or all matrices are empty.
134+
/// Any error from the inner `Pcs::batch_commit` is propagated as-is.
135+
pub fn jagged_commit<E: ExtensionField, Pcs: PolynomialCommitmentScheme<E>>(
136+
pp: &Pcs::ProverParam,
137+
rmms: Vec<RowMajorMatrix<E::BaseField>>,
138+
) -> Result<JaggedCommitmentWithWitness<E, Pcs>, Error> {
139+
if rmms.is_empty() {
140+
return Err(Error::InvalidPcsParam(
141+
"jagged_commit: cannot commit to empty sequence of matrices".to_string(),
142+
));
143+
}
144+
145+
// --- Step 1: Compute cumulative heights ---
146+
let mut poly_heights: Vec<usize> = Vec::new();
147+
for rmm in &rmms {
148+
let num_rows = rmm.height();
149+
let num_cols = rmm.width();
150+
151+
if num_rows == 0 {
152+
return Err(Error::InvalidPcsParam(
153+
"jagged_commit: matrix has zero rows".to_string(),
154+
));
155+
}
156+
if num_cols == 0 {
157+
return Err(Error::InvalidPcsParam(
158+
"jagged_commit: matrix has zero columns".to_string(),
159+
));
160+
}
161+
for _ in 0..num_cols {
162+
poly_heights.push(num_rows);
163+
}
164+
}
165+
if poly_heights.is_empty() {
166+
return Err(Error::InvalidPcsParam(
167+
"jagged_commit: no polynomials found in input matrices".to_string(),
168+
));
169+
}
170+
// t[0] = 0, t[i+1] = t[i] + poly_heights[i]
171+
let cumulative_heights = poly_heights
172+
.iter()
173+
.chain(once(&0))
174+
.scan(0usize, |acc, &h| {
175+
let current = *acc;
176+
*acc += h;
177+
Some(current)
178+
})
179+
.collect::<Vec<usize>>();
180+
181+
// --- Steps 2 & 3: Bit-reverse rows, transpose, and write to concatenated ---
182+
let total_size = cumulative_heights.last().copied().unwrap();
183+
let mut concatenated: Vec<E::BaseField> = Vec::with_capacity(total_size);
184+
// Safety: every element in `concatenated[0..total_size]` is fully written
185+
// by the transpose loop below before it is read.
186+
unsafe { concatenated.set_len(total_size) };
187+
188+
// `poly_idx` tracks which poly (column index in cumulative_heights) is the
189+
// first polynomial of the current matrix.
190+
let mut poly_idx = 0;
191+
for rmm in &rmms {
192+
// Step 2: Bit-reverse the rows (suffix-to-prefix transformation).
193+
// br.values[i * n_cols + j] = original[bitrev(i)][j]
194+
let br = rmm.as_view().bit_reverse_rows().to_row_major_matrix();
195+
196+
let n_cols = br.width();
197+
let n_rows = br.height();
198+
let n_cells = n_cols * n_rows;
199+
200+
// The start position in `concatenated` for this matrix's block of polynomials.
201+
let start = cumulative_heights[poly_idx];
202+
203+
// Step 3: Transpose — write each column j of `br` (= one polynomial)
204+
// into its corresponding contiguous slice in `concatenated`.
205+
(0..n_cols)
206+
.into_par_iter()
207+
.zip(concatenated[start..start + n_cells].par_chunks_mut(n_rows))
208+
.for_each(|(j, chunk)| {
209+
br.values
210+
.iter()
211+
.skip(j)
212+
.step_by(n_cols)
213+
.zip_eq(chunk.iter_mut())
214+
.for_each(|(v, out)| *out = *v);
215+
});
216+
217+
poly_idx += n_cols;
218+
}
219+
220+
// --- Step 4: Commit via the inner PCS ---
221+
// q' is committed as a single-column matrix with height = total_size.
222+
let giga_rmm = RowMajorMatrix::<E::BaseField>::new_by_values(
223+
concatenated,
224+
1, // width = 1 (single polynomial q')
225+
InstancePaddingStrategy::Default,
226+
);
227+
228+
let inner = Pcs::batch_commit(pp, vec![giga_rmm])?;
229+
230+
Ok(JaggedCommitmentWithWitness {
231+
inner,
232+
cumulative_heights,
233+
poly_heights,
234+
})
235+
}
236+
237+
#[cfg(test)]
238+
mod tests {
239+
use super::*;
240+
use crate::{
241+
basefold::{Basefold, BasefoldRSParams},
242+
test_util::setup_pcs,
243+
};
244+
use ff_ext::GoldilocksExt2;
245+
use p3::{field::FieldAlgebra, goldilocks::Goldilocks};
246+
247+
type F = Goldilocks;
248+
type E = GoldilocksExt2;
249+
type Pcs = Basefold<E, BasefoldRSParams>;
250+
251+
fn make_rmm(num_rows: usize, num_cols: usize) -> RowMajorMatrix<F> {
252+
let values: Vec<F> = (0..num_rows * num_cols)
253+
.map(|i| F::from_canonical_u64(i as u64 + 1))
254+
.collect();
255+
RowMajorMatrix::<F>::new_by_values(values, num_cols, InstancePaddingStrategy::Default)
256+
}
257+
258+
#[test]
259+
fn test_cumulative_heights_single_matrix() {
260+
// 4x2 matrix → 2 polynomials with 4 evaluations each → cumulative = [0, 4, 8]
261+
let rmm = make_rmm(4, 2);
262+
let num_rows = rmm.height();
263+
let num_cols = rmm.width();
264+
let mut poly_heights = Vec::new();
265+
for _ in 0..num_cols {
266+
poly_heights.push(num_rows);
267+
}
268+
let mut ch = vec![0usize];
269+
for &h in &poly_heights {
270+
ch.push(ch.last().unwrap() + h);
271+
}
272+
assert_eq!(poly_heights, vec![4, 4]);
273+
assert_eq!(ch, vec![0, 4, 8]);
274+
}
275+
276+
#[test]
277+
fn test_cumulative_heights_multiple_matrices() {
278+
// 4x1 + 8x2 → heights [4, 8, 8] → cumulative [0, 4, 12, 20]
279+
let m1 = make_rmm(4, 1);
280+
let m2 = make_rmm(8, 2);
281+
let mut poly_heights: Vec<usize> = Vec::new();
282+
for rmm in &[m1, m2] {
283+
for _ in 0..rmm.width() {
284+
poly_heights.push(rmm.height());
285+
}
286+
}
287+
let mut ch = vec![0usize];
288+
for &h in &poly_heights {
289+
ch.push(ch.last().unwrap() + h);
290+
}
291+
assert_eq!(poly_heights, vec![4, 8, 8]);
292+
assert_eq!(ch, vec![0, 4, 12, 20]);
293+
}
294+
295+
#[test]
296+
fn test_jagged_commit_smoke() {
297+
// Two matrices: 4x1 and 4x2 → 3 polynomials, total 12 evals, padded to 16
298+
let (pp, _vp) = setup_pcs::<E, Pcs>(4);
299+
let m1 = make_rmm(4, 1);
300+
let m2 = make_rmm(4, 2);
301+
302+
let comm = jagged_commit::<E, Pcs>(&pp, vec![m1, m2]).expect("commit should succeed");
303+
304+
assert_eq!(comm.num_polys(), 3);
305+
assert_eq!(comm.poly_heights, vec![4, 4, 4]);
306+
assert_eq!(comm.cumulative_heights, vec![0, 4, 8, 12]);
307+
assert_eq!(comm.total_evaluations(), 12);
308+
309+
let pure = comm.to_commitment();
310+
assert_eq!(pure.cumulative_heights, vec![0, 4, 8, 12]);
311+
}
312+
313+
#[test]
314+
fn test_jagged_commit_single_poly() {
315+
// 8x1 matrix → 1 polynomial, 8 evals, no padding needed
316+
let (pp, _vp) = setup_pcs::<E, Pcs>(3);
317+
let m = make_rmm(8, 1);
318+
319+
let comm = jagged_commit::<E, Pcs>(&pp, vec![m]).expect("commit should succeed");
320+
321+
assert_eq!(comm.num_polys(), 1);
322+
assert_eq!(comm.poly_heights, vec![8]);
323+
assert_eq!(comm.cumulative_heights, vec![0, 8]);
324+
assert_eq!(comm.total_evaluations(), 8);
325+
}
326+
327+
#[test]
328+
fn test_jagged_commit_empty_error() {
329+
let (pp, _vp) = setup_pcs::<E, Pcs>(4);
330+
let result = jagged_commit::<E, Pcs>(&pp, vec![]);
331+
assert!(matches!(result, Err(Error::InvalidPcsParam(_))));
332+
}
333+
}

crates/mpcs/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ pub use basefold::{
267267
Basefold, BasefoldCommitment, BasefoldCommitmentWithWitness, BasefoldDefault, BasefoldParams,
268268
BasefoldRSParams, BasefoldSpec, EncodingScheme, RSCode, RSCodeDefaultSpec,
269269
};
270+
pub mod jagged;
271+
pub use jagged::{JaggedCommitment, JaggedCommitmentWithWitness, jagged_commit};
270272
#[cfg(feature = "whir")]
271273
extern crate whir as whir_external;
272274
#[cfg(feature = "whir")]

0 commit comments

Comments
 (0)