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}