p3_sumcheck/layout/verifier.rs
1//! Verifier-side reconstruction of the stacked layout and its claims.
2
3use alloc::vec;
4use alloc::vec::Vec;
5use core::marker::PhantomData;
6
7use p3_field::{ExtensionField, Field, dot_product};
8use p3_multilinear_util::point::Point;
9
10use crate::Claim;
11use crate::constraints::Constraint;
12use crate::constraints::statement::EqStatement;
13use crate::layout::LayoutStrategy;
14use crate::layout::opening::{VerifierMultiClaim, VerifierOpening, VerifierVirtualClaim};
15use crate::layout::plan::{LayoutShape, plan_layout};
16use crate::layout::witness::{Selector, TablePlacement};
17use crate::table::TableShape;
18
19/// Verifier-side layout and claim registry.
20#[derive(Debug, Clone)]
21pub struct Verifier<F: Field, EF: ExtensionField<F>> {
22 /// Per-table placement metadata inside the stacked polynomial.
23 placements: Vec<TablePlacement>,
24 /// Side-map: source-table index → position into `placements`.
25 ///
26 /// Lets lookups by source table run in O(1) instead of a linear scan over placements.
27 placement_by_table: Vec<usize>,
28 /// Number of variables of the stacked polynomial.
29 k: usize,
30 /// Concrete claims recorded per source table.
31 claim_map: Vec<Vec<VerifierMultiClaim<EF>>>,
32 /// Virtual claims sampled directly on the stacked polynomial.
33 virtual_claims: Vec<VerifierVirtualClaim<EF>>,
34 /// Whether selector bitstrings are reversed and laid out after local bits.
35 strategy: LayoutStrategy,
36 /// Marker to tie the challenger's field type
37 _marker: PhantomData<F>,
38}
39
40impl<F: Field, EF: ExtensionField<F>> Verifier<F, EF> {
41 /// Reconstructs the verifier-side layout from table shapes.
42 ///
43 /// # Algorithm
44 ///
45 /// - Sort table indices by arity ascending.
46 /// - Iterate reversed, so largest tables get placed first.
47 /// - Each column occupies one slot of size `2^arity`.
48 /// - Stacked arity equals `log2` of total stacked size, rounded up.
49 pub fn new(tables: &[TableShape], strategy: LayoutStrategy) -> Self {
50 // Delegate slot assignment to the shared planner (same routine as the prover).
51 let shapes: Vec<LayoutShape> = tables
52 .iter()
53 .map(|t| LayoutShape {
54 arity: t.num_variables(),
55 width: t.width(),
56 })
57 .collect();
58 let (k, mut placements) = plan_layout(&shapes);
59 if strategy.reverse_selectors {
60 placements
61 .iter_mut()
62 .for_each(TablePlacement::reverse_selectors);
63 }
64
65 // Build the side-map: source-table index → index into `placements`.
66 let mut placement_by_table = vec![0usize; tables.len()];
67 for (i, p) in placements.iter().enumerate() {
68 placement_by_table[p.idx()] = i;
69 }
70
71 Self {
72 placements,
73 placement_by_table,
74 k,
75 // One (empty) concrete-claim list per source table.
76 claim_map: (0..tables.len()).map(|_| Vec::new()).collect(),
77 // No virtual claims recorded yet.
78 virtual_claims: Vec::new(),
79 strategy,
80 _marker: PhantomData,
81 }
82 }
83
84 /// Return the layout strategy this verifier was constructed with.
85 ///
86 /// Downstream protocols read it to dispatch on the binding direction
87 /// without threading the strategy through their own state.
88 pub const fn strategy(&self) -> LayoutStrategy {
89 self.strategy
90 }
91
92 /// Returns the arity of the source table at the given index.
93 pub fn num_variables_table(&self, table_idx: usize) -> usize {
94 // Look up this table's placement; every column shares the same selector bit-width.
95 let placement = self.placement(table_idx);
96 let selector_vars = placement
97 .selectors()
98 .first()
99 .map(Selector::num_variables)
100 .unwrap_or(0);
101 // Table arity is the stacked arity minus the selector bits that address the slot.
102 self.k - selector_vars
103 }
104
105 /// Records concrete opening claims for one table.
106 ///
107 /// # Arguments
108 ///
109 /// - `table_idx` — source table index.
110 /// - `polys` — column indices that were opened; must be non-empty.
111 /// - `evals` — claimed evaluations, aligned with the column list.
112 /// - `challenger` — Fiat–Shamir transcript.
113 ///
114 /// # Fiat–Shamir
115 ///
116 /// - Samples the opening point internally from the challenger.
117 /// - Absorbs the evaluations into the transcript.
118 /// - Mirrors exactly the prover's `eval` absorption order.
119 ///
120 /// # Panics
121 ///
122 /// - Columns list must be non-empty.
123 /// - Column list and evaluation list must have equal length.
124 /// - Every column index must be in range for this table.
125 pub fn add_claim<Ch>(
126 &mut self,
127 table_idx: usize,
128 polys: &[usize],
129 evals: &[EF],
130 challenger: &mut Ch,
131 ) where
132 Ch: p3_challenger::FieldChallenger<F> + p3_challenger::GrindingChallenger<Witness = F>,
133 {
134 let placement = self.placement(table_idx);
135 // Preconditions.
136 assert!(
137 !polys.is_empty(),
138 "opening schedule must name at least one column"
139 );
140 assert_eq!(polys.len(), evals.len());
141 assert!(polys.iter().all(|&i| i < placement.num_polys()));
142
143 // Sample the local-frame opening point from the transcript.
144 let point = Point::expand_from_univariate(
145 challenger.sample_algebra_element(),
146 self.num_variables_table(table_idx),
147 );
148
149 // Absorb the evals into the transcript; mirrors the prover's eval.
150 challenger.observe_algebra_slice(evals);
151
152 // Pair each column index with its claimed evaluation into an opening.
153 let openings = polys
154 .iter()
155 .zip(evals.iter())
156 .map(|(&poly_idx, &eval)| VerifierOpening::new(poly_idx, eval))
157 .collect();
158
159 // Store the batch under this table's claim list.
160 self.claim_map[table_idx].push(VerifierMultiClaim::new(point, openings));
161 }
162
163 /// Records a virtual evaluation claim on the full stacked polynomial.
164 ///
165 /// # Fiat–Shamir
166 ///
167 /// - Samples the opening point from the challenger.
168 /// - Absorbs the evaluation into the transcript.
169 /// - Mirrors exactly the prover's `add_virtual_eval` absorption order.
170 pub fn add_virtual_eval<Ch>(&mut self, eval: EF, challenger: &mut Ch)
171 where
172 Ch: p3_challenger::FieldChallenger<F> + p3_challenger::GrindingChallenger<Witness = F>,
173 {
174 // Sample a challenge point covering every stacked variable.
175 let point = Point::expand_from_univariate(challenger.sample_algebra_element(), self.k);
176 // Absorb the evaluation into the transcript.
177 challenger.observe_algebra_element(eval);
178 // Record the claim with unit payload (verifier side carries no extras).
179 self.virtual_claims.push(Claim {
180 point,
181 eval,
182 data: (),
183 });
184 }
185
186 /// Computes the batched claimed sum across concrete and virtual openings.
187 ///
188 /// ```text
189 /// sum = sum_{i} alpha^i * eval_i
190 /// ```
191 ///
192 /// # Traversal order
193 ///
194 /// - Concrete openings first, walked in stacked-polynomial order.
195 /// - Virtual claims continue the alpha sequence after the concrete ones.
196 pub fn sum(&self, alpha: EF) -> EF {
197 // Concrete openings: three nested iterators, no filter. Alphas match insertion order.
198 let concrete = dot_product::<EF, _, _>(
199 self.placements
200 .iter()
201 .flat_map(|placement| &self.claim_map[placement.idx()])
202 .flat_map(VerifierMultiClaim::openings)
203 .map(VerifierOpening::eval),
204 alpha.powers(),
205 );
206
207 // Virtual claims: continue the alpha sequence right after the concrete ones.
208 let virtuals = dot_product::<EF, _, _>(
209 self.virtual_claims.iter().map(VerifierVirtualClaim::eval),
210 alpha.shifted_powers(alpha.exp_u64(self.num_claims() as u64)),
211 );
212
213 concrete + virtuals
214 }
215
216 /// Builds the verifier-side equality constraint batching every claim.
217 ///
218 /// # Contributions
219 ///
220 /// - Concrete opening: equality at the selector-lifted claim point.
221 /// - Virtual claim: equality at the full stacked point.
222 pub fn constraint(&self, alpha: EF) -> Constraint<F, EF> {
223 // Output statement spans the full stacked variable space.
224 let mut eq_statement = EqStatement::initialize(self.k);
225
226 // Concrete contributions: walk each opening in insertion order and lift
227 // its claim point through the selector for that opening's column.
228 for placement in &self.placements {
229 for claim in &self.claim_map[placement.idx()] {
230 for opening in claim.openings() {
231 // The opening's column tells us which selector to lift through.
232 // Claims recorded through `add_claim` always bind a column,
233 // so a virtual (column-free) opening cannot appear here.
234 let col = opening
235 .poly_idx()
236 .expect("concrete claims only hold column-bound openings");
237 let lifted = if self.strategy.reverse_selectors {
238 placement.selectors()[col].lift_suffix(claim.point())
239 } else {
240 placement.selectors()[col].lift_prefix(claim.point())
241 };
242 eq_statement.add_evaluated_constraint(lifted, opening.eval());
243 }
244 }
245 }
246
247 // Virtual contributions: claim points already span the full stacked space.
248 for claim in &self.virtual_claims {
249 eq_statement.add_evaluated_constraint(claim.point.clone(), claim.eval);
250 }
251
252 // Wrap the assembled statement into an alpha-batched constraint.
253 Constraint::new_eq_only(alpha, eq_statement)
254 }
255
256 /// Returns the placement metadata for the given source table.
257 ///
258 /// O(1) via the side-map built at construction.
259 fn placement(&self, table_idx: usize) -> &TablePlacement {
260 &self.placements[self.placement_by_table[table_idx]]
261 }
262
263 /// Returns the number of concrete openings recorded so far.
264 fn num_claims(&self) -> usize {
265 self.claim_map
266 .iter()
267 .flat_map(|claims| claims.iter().map(VerifierMultiClaim::len))
268 .sum()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use alloc::vec;
275
276 use p3_field::PrimeCharacteristicRing;
277
278 use super::*;
279 use crate::strategy::VariableOrder;
280 use crate::tests::{EF, F, challenger};
281
282 const fn prefix_strategy() -> LayoutStrategy {
283 LayoutStrategy::new(false, VariableOrder::Prefix)
284 }
285
286 #[test]
287 fn table_shape_new_stores_dimensions_and_derives_getters() {
288 // Invariant:
289 // A shape of log-rows = k and width = w exposes:
290 // k() = k
291 // width() = w
292 //
293 // Fixture state:
294 // k = 3, width = 2 → height = 8.
295 let shape = TableShape::new(3, 2);
296
297 // Check each derived quantity.
298 assert_eq!(shape.num_variables(), 3);
299 assert_eq!(shape.width(), 2);
300 }
301
302 #[test]
303 #[should_panic]
304 fn table_shape_new_rejects_zero_width() {
305 // Invariant:
306 // A shape with zero columns is disallowed.
307 let _ = TableShape::new(3, 0);
308 }
309
310 #[test]
311 fn verifier_new_places_largest_table_first() {
312 // Invariant:
313 // new() sorts tables by arity ascending and places them reversed,
314 // so the first placement corresponds to the largest table.
315 //
316 // Fixture state:
317 // shapes: [arity 9 (t0), arity 10 (t1)]
318 // expected placement order: [t1 (arity 10), t0 (arity 9)]
319 let shapes = vec![TableShape::new(9, 2), TableShape::new(10, 2)];
320 let verifier: Verifier<F, EF> = Verifier::new(&shapes, prefix_strategy());
321
322 // First placement must point back at the larger table.
323 assert_eq!(verifier.placements[0].idx(), 1);
324 // Second placement must point back at the smaller table.
325 assert_eq!(verifier.placements[1].idx(), 0);
326 }
327
328 #[test]
329 fn verifier_num_variables_table_derives_from_selector_bits() {
330 // Invariant:
331 // num_variables_table equals the stacked arity minus the selector bits.
332 //
333 // Fixture state:
334 // shapes: [arity 9 × 2 cols, arity 10 × 2 cols]
335 // stacked rows = 2^9 * 2 + 2^10 * 2 = 3 * 2^10 → k = ceil(log2) = 11
336 // table 0 (arity 9) → selector bits = 2 → num_variables_table = 9
337 // table 1 (arity 10) → selector bits = 1 → num_variables_table = 10
338 let shapes = vec![TableShape::new(9, 2), TableShape::new(10, 2)];
339 let verifier: Verifier<F, EF> = Verifier::new(&shapes, prefix_strategy());
340
341 // Check derivation per table.
342 assert_eq!(verifier.num_variables_table(0), 9);
343 assert_eq!(verifier.num_variables_table(1), 10);
344 }
345
346 #[test]
347 fn verifier_add_claim_increments_num_claims() {
348 // Invariant:
349 // num_claims sums the opening count across every recorded claim.
350 //
351 // Fixture state:
352 // fresh verifier: 0 openings
353 // after add_claim(table 0, [0, 1]): 2 openings
354 // after add_claim(table 1, [0]): 3 openings
355 let shapes = vec![TableShape::new(9, 2), TableShape::new(10, 2)];
356 let mut verifier: Verifier<F, EF> = Verifier::new(&shapes, prefix_strategy());
357 assert_eq!(verifier.num_claims(), 0);
358
359 // Fresh Fiat-Shamir transcript; add_claim samples the point and absorbs.
360 let mut ch = challenger();
361
362 // Add two openings on table 0; count jumps from 0 to 2.
363 verifier.add_claim(0, &[0, 1], &[EF::from_u64(7), EF::from_u64(11)], &mut ch);
364 assert_eq!(verifier.num_claims(), 2);
365
366 // Add one opening on table 1; count jumps from 2 to 3.
367 verifier.add_claim(1, &[0], &[EF::from_u64(13)], &mut ch);
368 assert_eq!(verifier.num_claims(), 3);
369 }
370
371 #[test]
372 fn verifier_sum_weights_concrete_then_virtual_by_alpha_powers() {
373 // Invariant:
374 // sum(alpha) = eval_0 * alpha^0 + eval_1 * alpha^1 + virtual * alpha^2
375 // for two concrete openings followed by one virtual claim.
376 //
377 // Fixture state:
378 // concrete openings: column 0 → 7, column 1 → 11 on table 0
379 // virtual claim: value 13
380 // alpha: 5
381 let shapes = vec![TableShape::new(9, 2)];
382 let mut verifier: Verifier<F, EF> = Verifier::new(&shapes, prefix_strategy());
383
384 // Fresh transcript; points are sampled inside add_claim / add_virtual_eval.
385 let mut ch = challenger();
386
387 // Concrete claim: two columns of table 0.
388 verifier.add_claim(0, &[0, 1], &[EF::from_u64(7), EF::from_u64(11)], &mut ch);
389
390 // Virtual claim: covers the full stacked space.
391 verifier.add_virtual_eval(EF::from_u64(13), &mut ch);
392
393 // Manual batched sum with alpha = 5.
394 let alpha = EF::from_u64(5);
395 let expected =
396 EF::from_u64(7) + alpha * EF::from_u64(11) + alpha.exp_u64(2) * EF::from_u64(13);
397
398 // Check: the helper must match the hand-rolled formula.
399 assert_eq!(verifier.sum(alpha), expected);
400 }
401
402 #[test]
403 fn verifier_sum_empty_returns_zero() {
404 // Invariant:
405 // With no claims recorded, sum(alpha) is zero for any alpha.
406 let shapes = vec![TableShape::new(9, 1)];
407 let verifier: Verifier<F, EF> = Verifier::new(&shapes, prefix_strategy());
408
409 // Pick a non-trivial alpha to rule out accidental zero cancellation.
410 let alpha = EF::from_u64(42);
411 assert_eq!(verifier.sum(alpha), EF::ZERO);
412 }
413
414 #[test]
415 fn verifier_sum_skips_untouched_tables() {
416 // Invariant:
417 // A table with no recorded claims contributes zero to the sum
418 // and does not consume alpha powers.
419 //
420 // Fixture state:
421 // two tables; only table 1 records an opening.
422 // alpha: 3, eval: 9 → expected sum = 9 (alpha^0 only).
423 let shapes = vec![TableShape::new(9, 2), TableShape::new(10, 2)];
424 let mut verifier: Verifier<F, EF> = Verifier::new(&shapes, prefix_strategy());
425
426 // Record a single opening on table 1; table 0 stays empty.
427 let mut ch = challenger();
428 verifier.add_claim(1, &[0], &[EF::from_u64(9)], &mut ch);
429
430 // Check: only one opening → sum equals its eval scaled by alpha^0.
431 assert_eq!(verifier.sum(EF::from_u64(3)), EF::from_u64(9));
432 }
433}