Skip to main content

p3_sumcheck/layout/
mod.rs

1//! Stacked sumcheck layout.
2//!
3//! # Why stack
4//!
5//! - A WHIR commitment carries a fixed per-commitment overhead (FFT + Merkle).
6//! - Committing each table separately multiplies that overhead by the table count.
7//! - Stacking concatenates every table into one multilinear polynomial and
8//!   commits once, so a single commitment covers all tables.
9//! - WHIR natively supports multiple opening claims on the committed polynomial,
10//!   so no extra batching sumcheck is needed on top.
11//!
12//! # Layout of the stacked polynomial
13//!
14//! - Sort source tables by arity, largest first.
15//! - Lay columns out back-to-back on the boolean hypercube.
16//! - Each column takes a contiguous slot of size `2^arity`.
17//! - Pad with zeros up to the next power of two.
18//! - The concatenation is itself a multilinear polynomial.
19//!
20//! Example: three tables with `(4, 3, 2)` variables and one column each.
21//!
22//! ```text
23//!     +---- 16 ----+-- 8 --+-- 4 --+-- pad --+
24//!     |    P_1     |  P_2  |  P_3  |  zeros  |
25//!     +------------+-------+-------+---------+
26//!     0           16      24      28        32
27//! ```
28//!
29//! # Selectors: addressing a slot by a boolean prefix
30//!
31//! - Each column is reached by prefixing its local variables with a boolean
32//!   selector that picks the slot.
33//! - Local variables follow the selector bits.
34//!
35//! For the example above, with `P` the stacked polynomial:
36//!
37//! ```text
38//!     P_1(x_1, x_2, x_3, x_4) = P(0,       x_1, x_2, x_3, x_4)
39//!     P_2(x_1, x_2, x_3)      = P(1, 0,    x_1, x_2, x_3)
40//!     P_3(x_1, x_2)           = P(1, 1, 0, x_1, x_2)
41//! ```
42//!
43//! # Why selector lifts stay cheap in WHIR
44//!
45//! - The WHIR cost of adding an equality constraint `P(z) = y` scales as
46//!   `O(2^k)`, where `k` counts the coordinates of `z` that are not in `{0, 1}`.
47//! - Selector coordinates are always boolean, so they do not inflate `k`.
48//! - Lifting a local claim into a stacked claim therefore adds no asymptotic
49//!   cost beyond the original local coordinates.
50//!
51//! # Residual sumcheck: two binding modes
52//!
53//! After lifting, a batching challenge `alpha` collapses every recorded
54//! opening into one residual claim. The residual sumcheck can bind variables
55//! in two different orders:
56//!
57//! - Prefix-first binding: round one runs in SIMD-packed arithmetic, and the
58//!   remaining rounds drive a product polynomial.
59//! - Suffix-first binding: SVO accumulators are precomputed at claim-recording
60//!   time and folded round by round with Lagrange weights.
61//!
62//! Both modes end at the same residual product polynomial; the binding order
63//! only decides which fast-path tricks apply on the first rounds.
64//!
65//! # References
66//!
67//! - Stacking construction: "Minimal zkVM for Lean Ethereum", section 3.7.
68//! - WHIR proximity argument: ePrint 2024/1586.
69//! - SVO accumulators: ePrint 2025/1117, Algorithm 5.
70//! - Jagged PCS: ePrint 2025/0917.
71
72mod opening;
73mod plan;
74mod prover;
75mod verifier;
76mod witness;
77
78pub use opening::{
79    MultiClaim, Opening, ProverMultiClaim, ProverVirtualClaim, VerifierMultiClaim, VerifierOpening,
80    VerifierVirtualClaim,
81};
82pub use prover::{Layout, PrefixProver, SuffixProver};
83pub use verifier::Verifier;
84pub use witness::{Selector, Table, TablePlacement, Witness};
85
86use crate::strategy::VariableOrder;
87pub use crate::table::TableShape;
88
89/// Verifier-side metadata required to replay a committed layout.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct LayoutStrategy {
92    /// Whether selector bits are reversed and appended after local bits.
93    pub reverse_selectors: bool,
94    /// Variable order used by the residual WHIR/sumcheck rounds.
95    pub variable_order: VariableOrder,
96}
97
98impl LayoutStrategy {
99    pub const fn new(reverse_selectors: bool, variable_order: VariableOrder) -> Self {
100        Self {
101            reverse_selectors,
102            variable_order,
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn layout_strategy_new_stores_constructor_arguments_verbatim() {
113        // Invariant:
114        //     LayoutStrategy::new copies its two arguments into the matching
115        //     fields without mutation. The two reachable shapes — prefix
116        //     binding without selector reversal, and suffix binding with
117        //     selector reversal — are exercised exactly the same way.
118        //
119        // Fixture state:
120        //     case A: (reverse_selectors = false, Prefix)
121        //     case B: (reverse_selectors = true,  Suffix)
122        let case_a = LayoutStrategy::new(false, VariableOrder::Prefix);
123        let case_b = LayoutStrategy::new(true, VariableOrder::Suffix);
124
125        assert!(!case_a.reverse_selectors);
126        assert_eq!(case_a.variable_order, VariableOrder::Prefix);
127
128        assert!(case_b.reverse_selectors);
129        assert_eq!(case_b.variable_order, VariableOrder::Suffix);
130    }
131}