Skip to main content

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}