Skip to main content

p3_sumcheck/
table.rs

1use alloc::vec::Vec;
2
3use p3_matrix::Dimensions;
4use p3_util::log2_ceil_usize;
5
6/// Shape-only description of one verifier table.
7///
8/// # Contents
9///
10/// - Row count, always a power of two.
11/// - Column count, strictly positive.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct TableShape(Dimensions);
14
15impl TableShape {
16    /// Builds a table shape of `2^k` rows and `width` columns.
17    ///
18    /// # Panics
19    ///
20    /// - Column count must be at least one.
21    /// - Log row count must fit in the target's pointer width.
22    pub const fn new(num_variables: usize, width: usize) -> Self {
23        // Positive column count.
24        assert!(width > 0);
25        // Bound on the shift to rule out `1 << k` overflow on the current target.
26        assert!(num_variables < usize::BITS as usize);
27        // Expand the log row count to the concrete row count.
28        Self(Dimensions {
29            width,
30            height: 1 << num_variables,
31        })
32    }
33
34    /// Returns the number of variables per column.
35    pub const fn num_variables(&self) -> usize {
36        log2_ceil_usize(self.0.height)
37    }
38
39    /// Returns the number of columns.
40    pub const fn width(&self) -> usize {
41        self.0.width
42    }
43}
44
45/// Point-local opening schedule for one table.
46///
47/// Each outer entry corresponds to one sampled point for the table. The inner
48/// vector lists the column indices opened at that point.
49pub type PointSchedule = Vec<Vec<usize>>;
50
51/// Description of a table used to build randomized stacked-sumcheck witnesses.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct TableSpec {
54    /// Logical table shape used to generate the witness.
55    ///
56    /// The committed shape may be padded later if the table has fewer variables
57    /// than the first folding round consumes.
58    shape: TableShape,
59    /// Opening schedule local to this table.
60    point_schedule: PointSchedule,
61}
62
63impl TableSpec {
64    /// Builds a table spec from a shape and point-local opening schedule.
65    ///
66    /// # Panics
67    ///
68    /// - Every scheduled polynomial index must be less than the table width.
69    pub fn new(shape: TableShape, point_schedule: PointSchedule) -> Self {
70        assert!(
71            point_schedule
72                .iter()
73                .flatten()
74                .all(|&poly_idx| poly_idx < shape.width())
75        );
76        Self {
77            shape,
78            point_schedule,
79        }
80    }
81
82    /// Returns the logical table shape.
83    pub const fn shape(&self) -> &TableShape {
84        &self.shape
85    }
86
87    /// Returns the point-local opening schedule.
88    pub const fn point_schedule(&self) -> &PointSchedule {
89        &self.point_schedule
90    }
91
92    /// Pads this table shape to at least `min_num_variables`.
93    pub const fn pad_to_min_num_variables(&mut self, min_num_variables: usize) {
94        if self.shape.num_variables() < min_num_variables {
95            self.shape = TableShape::new(min_num_variables, self.shape.width());
96        }
97    }
98}
99
100/// Public protocol describing committed tables and their opening schedule.
101///
102/// This is the shape agreement between prover and verifier. The prover must
103/// commit to a witness whose table shapes match this protocol, and later opens
104/// exactly the point-local column batches listed here.
105#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct OpeningProtocol(Vec<TableSpec>);
107
108impl OpeningProtocol {
109    /// Builds an opening protocol from table specs.
110    pub const fn new(tables: Vec<TableSpec>) -> Self {
111        Self(tables)
112    }
113
114    /// Returns the table shapes in protocol order.
115    pub fn table_shapes(&self) -> Vec<TableShape> {
116        self.0.iter().map(|table| *table.shape()).collect()
117    }
118
119    /// Pads every table shape to at least `min_num_variables`.
120    ///
121    /// Opening schedules are unchanged because padding only adds zero rows to
122    /// the committed table.
123    pub fn pad_to_min_num_variables(mut self, min_num_variables: usize) -> Self {
124        self.0
125            .iter_mut()
126            .for_each(|table| table.pad_to_min_num_variables(min_num_variables));
127        self
128    }
129
130    /// Returns the total number of point-local opening batches.
131    pub fn num_openings(&self) -> usize {
132        self.0
133            .iter()
134            .map(|table| table.point_schedule().len())
135            .sum()
136    }
137
138    /// Iterates over all opening batches in transcript order.
139    pub fn iter_openings(&self) -> impl Iterator<Item = (usize, &[usize])> {
140        self.0.iter().enumerate().flat_map(|(table_idx, table)| {
141            table
142                .point_schedule()
143                .iter()
144                .map(move |polys| (table_idx, polys.as_slice()))
145        })
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use alloc::vec;
152
153    use super::*;
154
155    // Single-table protocol: arity 3, two columns, two opening points.
156    //
157    //     point 0: open columns {0, 1}
158    //     point 1: open column  {0}
159    //
160    // Yields num_openings() == 2 and iter_openings() emits the two
161    // batches in transcript order against table index 0.
162    fn single_table_protocol() -> OpeningProtocol {
163        OpeningProtocol::new(vec![TableSpec::new(
164            TableShape::new(3, 2),
165            vec![vec![0, 1], vec![0]],
166        )])
167    }
168
169    // Two-table protocol with distinct shapes and schedules.
170    //
171    //     table 0: arity 3, two cols. Schedule: open {0, 1} once.
172    //     table 1: arity 4, three cols. Schedule: open {0, 2}; open {1}.
173    //
174    // Yields num_openings() == 3 and iter_openings() emits the three
175    // batches as (0, [0, 1]), (1, [0, 2]), (1, [1]).
176    fn two_table_protocol() -> OpeningProtocol {
177        OpeningProtocol::new(vec![
178            TableSpec::new(TableShape::new(3, 2), vec![vec![0, 1]]),
179            TableSpec::new(TableShape::new(4, 3), vec![vec![0, 2], vec![1]]),
180        ])
181    }
182
183    #[test]
184    fn opening_protocol_table_shapes_returns_shapes_in_protocol_order() {
185        // Invariant:
186        //     table_shapes() returns one shape per table spec, in the order
187        //     they were handed to the constructor.
188        //
189        // Fixture state:
190        //     two-table protocol with shapes (3, 2) and (4, 3).
191        let protocol = two_table_protocol();
192
193        let shapes = protocol.table_shapes();
194
195        assert_eq!(shapes, vec![TableShape::new(3, 2), TableShape::new(4, 3)],);
196    }
197
198    #[test]
199    fn opening_protocol_num_openings_sums_per_table_schedules() {
200        // Invariant:
201        //     num_openings() equals the sum of point-schedule lengths across
202        //     every table.
203        //
204        // Fixture state:
205        //     table 0: 1 schedule entry.
206        //     table 1: 2 schedule entries.
207        //     Expected total: 3.
208        let protocol = two_table_protocol();
209        assert_eq!(protocol.num_openings(), 3);
210
211        // Edge case: empty protocol → no openings.
212        let empty = OpeningProtocol::new(vec![]);
213        assert_eq!(empty.num_openings(), 0);
214    }
215
216    #[test]
217    fn opening_protocol_iter_openings_yields_batches_in_transcript_order() {
218        // Invariant:
219        //     iter_openings() walks tables in protocol order, then walks
220        //     each table's point schedule in insertion order, emitting
221        //     (table_idx, &polys[..]) per batch.
222        //
223        // Fixture state:
224        //     two-table protocol with mixed schedules.
225        //     Expected stream: (0, [0, 1]), (1, [0, 2]), (1, [1]).
226        let protocol = two_table_protocol();
227
228        let collected: Vec<(usize, Vec<usize>)> = protocol
229            .iter_openings()
230            .map(|(table_idx, polys)| (table_idx, polys.to_vec()))
231            .collect();
232
233        assert_eq!(
234            collected,
235            vec![(0, vec![0, 1]), (1, vec![0, 2]), (1, vec![1]),],
236        );
237    }
238
239    #[test]
240    fn opening_protocol_pad_to_min_num_variables_grows_small_tables() {
241        // Invariant:
242        //     pad_to_min_num_variables raises every table arity below the
243        //     given floor while leaving widths and the opening schedule
244        //     untouched.
245        //
246        // Fixture state:
247        //     table 0: arity 3, two cols.
248        //     table 1: arity 4, three cols.
249        //     Floor: 5 → both tables widen to arity 5.
250        let padded = two_table_protocol().pad_to_min_num_variables(5);
251
252        let shapes = padded.table_shapes();
253        assert_eq!(shapes[0], TableShape::new(5, 2));
254        assert_eq!(shapes[1], TableShape::new(5, 3));
255        // Schedules survive the pad untouched.
256        assert_eq!(padded.num_openings(), 3);
257    }
258
259    #[test]
260    fn opening_protocol_pad_to_min_num_variables_leaves_large_tables_alone() {
261        // Invariant:
262        //     pad_to_min_num_variables is a one-way clamp: tables already at
263        //     or above the floor are not modified.
264        //
265        // Fixture state:
266        //     single table of arity 3; floor 2 → no change.
267        let original = single_table_protocol();
268        let original_shapes = original.table_shapes();
269
270        let padded = original.pad_to_min_num_variables(2);
271
272        assert_eq!(padded.table_shapes(), original_shapes);
273    }
274
275    #[test]
276    fn table_spec_accessors_forward_constructor_arguments() {
277        // Invariant:
278        //     A spec stores the shape and the schedule verbatim; the
279        //     accessors return references to the stored data.
280        //
281        // Fixture state:
282        //     shape (3, 2), schedule [[0, 1], [0]].
283        let shape = TableShape::new(3, 2);
284        let schedule: PointSchedule = vec![vec![0, 1], vec![0]];
285        let spec = TableSpec::new(shape, schedule.clone());
286
287        assert_eq!(*spec.shape(), shape);
288        assert_eq!(spec.point_schedule(), &schedule);
289    }
290
291    #[test]
292    #[should_panic]
293    fn table_spec_new_panics_on_out_of_range_poly_idx() {
294        // Invariant:
295        //     A schedule that addresses a column past the table width is
296        //     rejected at construction time, not deferred to use.
297        //
298        // Fixture state:
299        //     width = 2, schedule references column 2 (only 0 and 1 are valid).
300        let _ = TableSpec::new(TableShape::new(3, 2), vec![vec![2]]);
301    }
302
303    #[test]
304    fn table_spec_pad_to_min_num_variables_grows_small_arity() {
305        // Invariant:
306        //     The in-place padder raises the table's arity but never lowers it,
307        //     and never touches the column count.
308        //
309        // Fixture state:
310        //     start arity 2, width 3; pad to floor 4 → arity becomes 4.
311        let mut spec = TableSpec::new(TableShape::new(2, 3), vec![vec![0]]);
312
313        spec.pad_to_min_num_variables(4);
314
315        assert_eq!(spec.shape().num_variables(), 4);
316        assert_eq!(spec.shape().width(), 3);
317    }
318
319    #[test]
320    fn table_spec_pad_to_min_num_variables_leaves_larger_arity_alone() {
321        // Invariant:
322        //     A spec already wider than the floor is left unchanged.
323        //
324        // Fixture state:
325        //     start arity 5, width 1; pad to floor 3 → no change.
326        let mut spec = TableSpec::new(TableShape::new(5, 1), vec![vec![0]]);
327
328        spec.pad_to_min_num_variables(3);
329
330        assert_eq!(spec.shape().num_variables(), 5);
331        assert_eq!(spec.shape().width(), 1);
332    }
333}