p3_sumcheck/table.rs
1use alloc::vec::Vec;
2
3use p3_field::Field;
4use p3_matrix::Dimensions;
5use p3_multilinear_util::point::Point;
6use p3_util::log2_ceil_usize;
7use serde::{Deserialize, Serialize};
8
9/// Shape-only description of one verifier table.
10///
11/// # Contents
12///
13/// - Row count, always a power of two.
14/// - Column count, strictly positive.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct TableShape(Dimensions);
17
18impl TableShape {
19 /// Builds a table shape of `2^k` rows and `width` columns.
20 ///
21 /// # Panics
22 ///
23 /// - Column count must be at least one.
24 /// - Log row count must fit in the target's pointer width.
25 pub const fn new(num_variables: usize, width: usize) -> Self {
26 // Positive column count.
27 assert!(width > 0);
28 // Bound on the shift to rule out `1 << k` overflow on the current target.
29 assert!(num_variables < usize::BITS as usize);
30 // Expand the log row count to the concrete row count.
31 Self(Dimensions {
32 width,
33 height: 1 << num_variables,
34 })
35 }
36
37 /// Returns the number of variables per column.
38 pub const fn num_variables(&self) -> usize {
39 log2_ceil_usize(self.0.height)
40 }
41
42 /// Returns the number of columns.
43 pub const fn width(&self) -> usize {
44 self.0.width
45 }
46}
47
48/// Opening batch sampled at one transcript point for a table.
49///
50/// # Overview
51///
52/// - One batch records what is opened at a single sampled point.
53/// - It holds two column-index lists evaluated at that same point.
54/// - Transcript and evaluation order is always the first list, then the second.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct OpeningBatch<T> {
57 /// Columns evaluated directly at the sampled point.
58 current: Vec<T>,
59 /// Columns evaluated through the repeat-last successor view at the same point.
60 next: Vec<T>,
61}
62
63impl<T> OpeningBatch<T> {
64 /// Builds an opening batch from the direct list and the successor-view list.
65 ///
66 /// # Arguments
67 ///
68 /// - `current`: columns evaluated directly at the sampled point.
69 /// - `next`: columns evaluated through the repeat-last successor view.
70 ///
71 /// # Panics
72 ///
73 /// Panics if both lists are empty.
74 pub fn new(current: Vec<T>, next: Vec<T>) -> Self {
75 // Invariant: a batch must name at least one column on either side.
76 // An empty batch would consume a transcript point yet open nothing.
77 assert!(
78 !current.is_empty() || !next.is_empty(),
79 "opening batch must name at least one column"
80 );
81 Self { current, next }
82 }
83
84 /// Returns the columns evaluated directly at the sampled point.
85 pub fn current(&self) -> &[T] {
86 &self.current
87 }
88
89 /// Returns the columns evaluated through the repeat-last successor view.
90 pub fn next(&self) -> &[T] {
91 &self.next
92 }
93
94 /// Returns the total number of entries across both lists.
95 pub const fn len(&self) -> usize {
96 // Both sides share the same sampled point, so the batch size is their sum.
97 self.current.len() + self.next.len()
98 }
99
100 /// Returns true when neither list holds an entry.
101 pub const fn is_empty(&self) -> bool {
102 self.current.is_empty() && self.next.is_empty()
103 }
104
105 /// Returns true when both per-side list lengths match the other batch.
106 ///
107 /// # Arguments
108 ///
109 /// - `other`: the batch whose per-side lengths are compared against this one.
110 pub const fn has_same_shape<U>(&self, other: &OpeningBatch<U>) -> bool {
111 // Shape is the pair of side lengths; entry values are irrelevant here.
112 self.current.len() == other.current.len() && self.next.len() == other.next.len()
113 }
114}
115
116impl<T: Clone> OpeningBatch<T> {
117 /// Flattens both lists into one vector in transcript order.
118 ///
119 /// # Returns
120 ///
121 /// - The direct entries first.
122 /// - The successor-view entries second.
123 pub fn to_vec(&self) -> Vec<T> {
124 // Walk in transcript order (direct entries, then successor-view entries)
125 // and copy each entry into a flat vector.
126 self.current.iter().chain(&self.next).cloned().collect()
127 }
128}
129
130/// Column indices requested at one sampled point for a table.
131///
132/// # Overview
133///
134/// - One request names the columns opened at a single sampled point.
135/// - Each side may name direct columns, successor-view columns, or both.
136pub type OpeningRequest = OpeningBatch<usize>;
137
138/// Claimed column evaluations matching one request at a single sampled point.
139pub type OpeningEvals<EF> = OpeningBatch<EF>;
140
141/// Sequence of per-point opening requests for one table.
142///
143/// One entry per sampled point, in transcript order.
144pub type PointSchedule = Vec<OpeningRequest>;
145
146/// Description of a table used to build randomized stacked-sumcheck witnesses.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct TableSpec {
149 /// Logical table shape used to generate the witness.
150 ///
151 /// The committed shape may be padded later if the table has fewer variables
152 /// than the first folding round consumes.
153 shape: TableShape,
154 /// Opening schedule local to this table.
155 point_schedule: PointSchedule,
156}
157
158impl TableSpec {
159 /// Builds a table spec from a shape and point-local opening schedule.
160 ///
161 /// # Panics
162 ///
163 /// - Every scheduled polynomial index must be less than the table width.
164 /// - A column may not repeat within one side of a batch.
165 pub fn new(shape: TableShape, point_schedule: PointSchedule) -> Self {
166 // Invariant: every requested column must address a real column of the table.
167 // Both sides of each batch index into the same set of table columns,
168 // so direct and successor-view indices are validated against one width.
169 assert!(point_schedule.iter().all(|batch| {
170 batch
171 .current()
172 .iter()
173 .chain(batch.next())
174 .all(|&poly_idx| poly_idx < shape.width())
175 }));
176 // Invariant: a column appears at most once per side of a batch.
177 // Opening one column on both sides is intended.
178 // Repeating it within one side only burns a challenge power on a duplicate claim.
179 let side_has_no_repeat = |cols: &[usize]| {
180 cols.iter()
181 .enumerate()
182 .all(|(i, col)| !cols[..i].contains(col))
183 };
184 assert!(
185 point_schedule.iter().all(
186 |batch| side_has_no_repeat(batch.current()) && side_has_no_repeat(batch.next())
187 ),
188 "an opening batch must not repeat a column within its current or next side"
189 );
190 Self {
191 shape,
192 point_schedule,
193 }
194 }
195
196 /// Returns the logical table shape.
197 pub const fn shape(&self) -> &TableShape {
198 &self.shape
199 }
200
201 /// Returns the point-local opening schedule.
202 pub const fn point_schedule(&self) -> &PointSchedule {
203 &self.point_schedule
204 }
205
206 /// Pads this table shape to at least `min_num_variables`.
207 pub const fn pad_to_min_num_variables(&mut self, min_num_variables: usize) {
208 if self.shape.num_variables() < min_num_variables {
209 self.shape = TableShape::new(min_num_variables, self.shape.width());
210 }
211 }
212}
213
214/// Public protocol describing committed tables and their opening schedule.
215///
216/// This is the shape agreement between prover and verifier. The prover must
217/// commit to a witness whose table shapes match this protocol, and later opens
218/// exactly the point-local column batches listed here.
219#[derive(Debug, Clone, Default, PartialEq, Eq)]
220pub struct OpeningProtocol(Vec<TableSpec>);
221
222impl OpeningProtocol {
223 /// Builds an opening protocol from table specs.
224 pub const fn new(tables: Vec<TableSpec>) -> Self {
225 Self(tables)
226 }
227
228 /// Returns the table shapes in protocol order.
229 pub fn table_shapes(&self) -> Vec<TableShape> {
230 self.0.iter().map(|table| *table.shape()).collect()
231 }
232
233 /// Pads every table shape to at least `min_num_variables`.
234 ///
235 /// The column index lists in each schedule are unchanged.
236 /// Padding only appends zero rows to the committed table.
237 ///
238 /// The successor view is always taken over the padded space.
239 /// Padding therefore moves the repeated boundary row into the zero pad.
240 /// The last real row's successor then reads a pad row instead of repeating itself.
241 /// A successor opening after padding is a different polynomial identity than over the unpadded table.
242 /// Prover and verifier still agree, since both work over the padded space.
243 pub fn pad_to_min_num_variables(mut self, min_num_variables: usize) -> Self {
244 self.0
245 .iter_mut()
246 .for_each(|table| table.pad_to_min_num_variables(min_num_variables));
247 self
248 }
249
250 /// Returns the total number of point-local opening batches.
251 pub fn num_openings(&self) -> usize {
252 self.0
253 .iter()
254 .map(|table| table.point_schedule().len())
255 .sum()
256 }
257
258 /// Iterates over all opening batches in transcript order.
259 pub fn iter_openings(&self) -> impl Iterator<Item = (usize, &OpeningRequest)> {
260 // Walk tables in protocol order, then each table's points in schedule order.
261 // The flat stream pairs every request with the index of its owning table.
262 //
263 // table 0: [req, req] table 1: [req]
264 // -> (0, req), (0, req), (1, req)
265 self.0.iter().enumerate().flat_map(|(table_idx, table)| {
266 table
267 .point_schedule()
268 .iter()
269 .map(move |batch| (table_idx, batch))
270 })
271 }
272
273 /// Entries across every batch, or `None` when the count overflows `usize`.
274 pub fn checked_num_claims(&self) -> Option<usize> {
275 self.iter_openings()
276 .try_fold(0usize, |count, (_, batch)| count.checked_add(batch.len()))
277 }
278
279 /// Cells across every table, `2^k` rows times width each, or `None` on overflow.
280 pub fn checked_num_cells(&self) -> Option<usize> {
281 self.0.iter().try_fold(0usize, |total, table| {
282 let shape = table.shape();
283 let rows = 1usize.checked_shl(shape.num_variables().try_into().ok()?)?;
284 total.checked_add(rows.checked_mul(shape.width())?)
285 })
286 }
287
288 /// One point per batch, each with its table's arity.
289 ///
290 /// The count is checked first, then batches in transcript order.
291 ///
292 /// # Errors
293 ///
294 /// The count mismatch, or else the first batch whose point has the wrong arity.
295 pub fn check_points<EF: Field>(
296 &self,
297 points: &[Point<EF>],
298 ) -> Result<(), OpeningPointMismatch> {
299 let expected = self.num_openings();
300 if points.len() != expected {
301 return Err(OpeningPointMismatch::Count {
302 expected,
303 actual: points.len(),
304 });
305 }
306 for ((table, _), point) in self.iter_openings().zip(points) {
307 let expected = self.0[table].shape().num_variables();
308 if point.num_variables() != expected {
309 return Err(OpeningPointMismatch::Arity {
310 table,
311 expected,
312 actual: point.num_variables(),
313 });
314 }
315 }
316 Ok(())
317 }
318}
319
320/// Why a list of opening points does not fit an opening protocol.
321#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
322pub enum OpeningPointMismatch {
323 /// One point per opening batch is required.
324 #[error("{actual} points against {expected} opening batches")]
325 Count {
326 /// Opening batches the protocol names.
327 expected: usize,
328 /// Points supplied.
329 actual: usize,
330 },
331 /// A point does not name the variables of the table its batch opens.
332 #[error("the point for table {table} names {actual} variables, expected {expected}")]
333 Arity {
334 /// Table the batch opens.
335 table: usize,
336 /// Variables of that table.
337 expected: usize,
338 /// Variables the point names.
339 actual: usize,
340 },
341}
342
343#[cfg(test)]
344mod tests {
345 use alloc::vec;
346
347 use p3_baby_bear::BabyBear;
348 use p3_field::PrimeCharacteristicRing;
349
350 use super::*;
351
352 // Single-table protocol: arity 3, two columns, two opening points.
353 //
354 // point 0: direct columns {0, 1}
355 // point 1: direct column {0}
356 //
357 // Total opening count is 2; the two batches stream in transcript order
358 // against table index 0.
359 fn single_table_protocol() -> OpeningProtocol {
360 OpeningProtocol::new(vec![TableSpec::new(
361 TableShape::new(3, 2),
362 vec![
363 OpeningBatch::new(vec![0, 1], Vec::new()),
364 OpeningBatch::new(vec![0], Vec::new()),
365 ],
366 )])
367 }
368
369 // Two-table protocol with distinct shapes and schedules.
370 //
371 // table 0: arity 3, two cols. one point, direct {0, 1}
372 // table 1: arity 4, three cols. point a direct {0, 2}; point b direct {1}
373 //
374 // Total opening count is 3; the stream is
375 // (0, direct [0, 1]), (1, direct [0, 2]), (1, direct [1]).
376 fn two_table_protocol() -> OpeningProtocol {
377 OpeningProtocol::new(vec![
378 TableSpec::new(
379 TableShape::new(3, 2),
380 vec![OpeningBatch::new(vec![0, 1], Vec::new())],
381 ),
382 TableSpec::new(
383 TableShape::new(4, 3),
384 vec![
385 OpeningBatch::new(vec![0, 2], Vec::new()),
386 OpeningBatch::new(vec![1], Vec::new()),
387 ],
388 ),
389 ])
390 }
391
392 #[test]
393 fn opening_protocol_table_shapes_returns_shapes_in_protocol_order() {
394 // Invariant:
395 // table_shapes() returns one shape per table spec, in the order
396 // they were handed to the constructor.
397 //
398 // Fixture state:
399 // two-table protocol with shapes (3, 2) and (4, 3).
400 let protocol = two_table_protocol();
401
402 let shapes = protocol.table_shapes();
403
404 assert_eq!(shapes, vec![TableShape::new(3, 2), TableShape::new(4, 3)],);
405 }
406
407 #[test]
408 fn opening_protocol_num_openings_sums_per_table_schedules() {
409 // Invariant:
410 // num_openings() equals the sum of point-schedule lengths across
411 // every table.
412 //
413 // Fixture state:
414 // table 0: 1 schedule entry.
415 // table 1: 2 schedule entries.
416 // Expected total: 3.
417 let protocol = two_table_protocol();
418 assert_eq!(protocol.num_openings(), 3);
419
420 // Edge case: empty protocol → no openings.
421 let empty = OpeningProtocol::new(vec![]);
422 assert_eq!(empty.num_openings(), 0);
423 }
424
425 #[test]
426 fn opening_protocol_iter_openings_yields_batches_in_transcript_order() {
427 // Invariant:
428 // The opening stream walks tables in protocol order, then each
429 // table's points in schedule order, pairing every batch with the
430 // index of its owning table.
431 //
432 // Fixture state:
433 // two-table protocol; every batch has direct columns and an empty
434 // successor-view side.
435 //
436 // Each tuple is (table index, direct columns, successor-view columns).
437 let protocol = two_table_protocol();
438
439 let collected: Vec<(usize, Vec<usize>, Vec<usize>)> = protocol
440 .iter_openings()
441 .map(|(table_idx, batch)| (table_idx, batch.current().to_vec(), batch.next().to_vec()))
442 .collect();
443
444 assert_eq!(
445 collected,
446 vec![
447 (0, vec![0, 1], vec![]),
448 (1, vec![0, 2], vec![]),
449 (1, vec![1], vec![]),
450 ],
451 );
452 }
453
454 #[test]
455 fn opening_protocol_pad_to_min_num_variables_grows_small_tables() {
456 // Invariant:
457 // pad_to_min_num_variables raises every table arity below the
458 // given floor while leaving widths and the opening schedule
459 // untouched.
460 //
461 // Fixture state:
462 // table 0: arity 3, two cols.
463 // table 1: arity 4, three cols.
464 // Floor: 5 → both tables widen to arity 5.
465 let padded = two_table_protocol().pad_to_min_num_variables(5);
466
467 let shapes = padded.table_shapes();
468 assert_eq!(shapes[0], TableShape::new(5, 2));
469 assert_eq!(shapes[1], TableShape::new(5, 3));
470 // Schedules survive the pad untouched.
471 assert_eq!(padded.num_openings(), 3);
472 }
473
474 #[test]
475 fn opening_protocol_pad_to_min_num_variables_leaves_large_tables_alone() {
476 // Invariant:
477 // pad_to_min_num_variables is a one-way clamp: tables already at
478 // or above the floor are not modified.
479 //
480 // Fixture state:
481 // single table of arity 3; floor 2 → no change.
482 let original = single_table_protocol();
483 let original_shapes = original.table_shapes();
484
485 let padded = original.pad_to_min_num_variables(2);
486
487 assert_eq!(padded.table_shapes(), original_shapes);
488 }
489
490 #[test]
491 fn table_spec_accessors_forward_constructor_arguments() {
492 // Invariant:
493 // A spec stores the shape and the schedule verbatim; the
494 // accessors return references to the stored data.
495 //
496 // Fixture state:
497 // shape (3, 2), schedule [[0, 1], [0]].
498 let shape = TableShape::new(3, 2);
499 let schedule = vec![
500 OpeningBatch::new(vec![0, 1], Vec::new()),
501 OpeningBatch::new(vec![0], Vec::new()),
502 ];
503 let spec = TableSpec::new(shape, schedule.clone());
504
505 assert_eq!(*spec.shape(), shape);
506 assert_eq!(spec.point_schedule(), &schedule);
507 }
508
509 #[test]
510 fn table_spec_accepts_mixed_current_next_batches() {
511 // Invariant:
512 // One sampled point may carry both direct openings and
513 // successor-view openings.
514 // The batch length is the sum of the two side lengths.
515 //
516 // Fixture state:
517 // width 3, two points.
518 //
519 // point 0: direct {0, 2}, successor-view {1} -> len 3
520 // point 1: direct {}, successor-view {2} -> len 1
521 let spec = TableSpec::new(
522 TableShape::new(3, 3),
523 vec![
524 OpeningBatch::new(vec![0, 2], vec![1]),
525 OpeningBatch::new(Vec::new(), vec![2]),
526 ],
527 );
528
529 let batches = spec.point_schedule();
530 assert_eq!(batches.len(), 2);
531 assert_eq!(batches[0].current(), &[0, 2]);
532 assert_eq!(batches[0].next(), &[1]);
533 assert_eq!(batches[0].len(), 3);
534 assert_eq!(batches[1].current(), &[] as &[usize]);
535 assert_eq!(batches[1].next(), &[2]);
536 assert_eq!(batches[1].len(), 1);
537 }
538
539 #[test]
540 #[should_panic]
541 fn table_spec_new_panics_on_out_of_range_poly_idx() {
542 // Invariant:
543 // A schedule that addresses a column past the table width is
544 // rejected at construction time, not deferred to use.
545 //
546 // Fixture state:
547 // width = 2, schedule references column 2 (only 0 and 1 are valid).
548 let _ = TableSpec::new(
549 TableShape::new(3, 2),
550 vec![OpeningBatch::new(vec![2], Vec::new())],
551 );
552 }
553
554 #[test]
555 #[should_panic]
556 fn table_spec_new_panics_on_out_of_range_next_poly_idx() {
557 // Invariant:
558 // The successor-view side is validated against the table width too,
559 // not just the direct side.
560 //
561 // Fixture state:
562 // width = 2; successor-view names column 2 (only 0 and 1 are valid).
563 let _ = TableSpec::new(
564 TableShape::new(3, 2),
565 vec![OpeningBatch::new(vec![0], vec![2])],
566 );
567 }
568
569 #[test]
570 #[should_panic]
571 fn table_spec_new_panics_on_duplicate_column_in_one_side() {
572 // Invariant:
573 // A column may not repeat within one side of a batch.
574 //
575 // Fixture state:
576 // width = 2; the direct side names column 0 twice.
577 let _ = TableSpec::new(
578 TableShape::new(3, 2),
579 vec![OpeningBatch::new(vec![0, 0], Vec::new())],
580 );
581 }
582
583 #[test]
584 fn table_spec_new_accepts_same_column_across_both_sides() {
585 // Invariant:
586 // Opening one column on both sides (direct and successor-view) is
587 // intended and must be accepted; only within-side repeats are rejected.
588 let spec = TableSpec::new(
589 TableShape::new(3, 2),
590 vec![OpeningBatch::new(vec![0], vec![0])],
591 );
592 assert_eq!(spec.point_schedule().len(), 1);
593 }
594
595 #[test]
596 #[should_panic]
597 fn opening_batch_new_panics_on_empty_batch() {
598 // Invariant:
599 // A batch with no columns on either side is rejected at construction.
600 // Such a batch would consume a transcript point yet open nothing.
601 let _: OpeningBatch<usize> = OpeningBatch::new(vec![], vec![]);
602 }
603
604 #[test]
605 fn table_spec_pad_to_min_num_variables_grows_small_arity() {
606 // Invariant:
607 // The in-place padder raises the table's arity but never lowers it,
608 // and never touches the column count.
609 //
610 // Fixture state:
611 // start arity 2, width 3; pad to floor 4 → arity becomes 4.
612 let mut spec = TableSpec::new(
613 TableShape::new(2, 3),
614 vec![OpeningBatch::new(vec![0], Vec::new())],
615 );
616
617 spec.pad_to_min_num_variables(4);
618
619 assert_eq!(spec.shape().num_variables(), 4);
620 assert_eq!(spec.shape().width(), 3);
621 }
622
623 #[test]
624 fn table_spec_pad_to_min_num_variables_leaves_larger_arity_alone() {
625 // Invariant:
626 // A spec already wider than the floor is left unchanged.
627 //
628 // Fixture state:
629 // start arity 5, width 1; pad to floor 3 → no change.
630 let mut spec = TableSpec::new(
631 TableShape::new(5, 1),
632 vec![OpeningBatch::new(vec![0], Vec::new())],
633 );
634
635 spec.pad_to_min_num_variables(3);
636
637 assert_eq!(spec.shape().num_variables(), 5);
638 assert_eq!(spec.shape().width(), 1);
639 }
640
641 // One zero point per requested arity, in the order given.
642 fn points(arities: &[usize]) -> Vec<Point<BabyBear>> {
643 arities
644 .iter()
645 .map(|&num_variables| Point::new(vec![BabyBear::ZERO; num_variables]))
646 .collect()
647 }
648
649 #[test]
650 fn opening_protocol_counts_claims_and_cells() {
651 // Invariant:
652 // Claims are the batch entries summed over every batch.
653 // Cells are rows times width summed over every table.
654 //
655 // Fixture state:
656 // table 0: 2^3 rows, 2 cols. one batch of 2 entries.
657 // table 1: 2^4 rows, 3 cols. batches of 2 and 1 entries.
658 // Expected: claims 2 + 2 + 1 = 5, cells 8·2 + 16·3 = 64.
659 let protocol = two_table_protocol();
660 assert_eq!(protocol.checked_num_claims(), Some(5));
661 assert_eq!(protocol.checked_num_cells(), Some(64));
662
663 // Edge case: empty protocol → nothing to count.
664 let empty = OpeningProtocol::new(vec![]);
665 assert_eq!(empty.checked_num_claims(), Some(0));
666 assert_eq!(empty.checked_num_cells(), Some(0));
667 }
668
669 #[test]
670 fn checked_num_cells_reports_an_overflow() {
671 // Invariant:
672 // A cell count past usize is reported, never wrapped.
673 //
674 // Fixture state:
675 // one table of the largest representable arity and 3 columns.
676 // Its row count fits in usize, its cell count does not.
677 let protocol = OpeningProtocol::new(vec![TableSpec::new(
678 TableShape::new(usize::BITS as usize - 1, 3),
679 vec![OpeningBatch::new(vec![0], Vec::new())],
680 )]);
681
682 assert_eq!(protocol.checked_num_cells(), None);
683 }
684
685 #[test]
686 fn check_points_reports_the_count_before_any_arity() {
687 // Invariant:
688 // A wrong point count is reported even when every point also has the wrong arity.
689 //
690 // Fixture state:
691 // two-table protocol, three batches.
692 let protocol = two_table_protocol();
693
694 assert_eq!(
695 protocol.check_points(&points(&[1, 1])),
696 Err(OpeningPointMismatch::Count {
697 expected: 3,
698 actual: 2,
699 }),
700 );
701 assert_eq!(
702 protocol.check_points(&points(&[1, 1, 1, 1])),
703 Err(OpeningPointMismatch::Count {
704 expected: 3,
705 actual: 4,
706 }),
707 );
708 }
709
710 #[test]
711 fn check_points_reports_the_first_batch_with_the_wrong_arity() {
712 // Invariant:
713 // Batches are checked in transcript order and the first mismatch wins.
714 //
715 // Fixture state:
716 // two-table protocol; batches open tables 0, 1, 1 (arities 3, 4, 4).
717 // Points of arity 3, 3, 5: batches 2 and 3 are both wrong.
718 let protocol = two_table_protocol();
719
720 assert_eq!(
721 protocol.check_points(&points(&[3, 3, 5])),
722 Err(OpeningPointMismatch::Arity {
723 table: 1,
724 expected: 4,
725 actual: 3,
726 }),
727 );
728 }
729
730 #[test]
731 fn check_points_accepts_matching_points() {
732 // Invariant:
733 // One point per batch, each of its table's arity, is accepted.
734 //
735 // Fixture state:
736 // two-table protocol; points of arity 3, 4, 4.
737 let protocol = two_table_protocol();
738
739 assert_eq!(protocol.check_points(&points(&[3, 4, 4])), Ok(()));
740
741 // Edge case: empty protocol takes no points.
742 let empty = OpeningProtocol::new(vec![]);
743 assert_eq!(empty.check_points::<BabyBear>(&[]), Ok(()));
744 }
745}