Skip to main content

p3_sumcheck/layout/
opening.rs

1//! Opened claims on stacked tables.
2
3use alloc::vec::Vec;
4
5use p3_field::{ExtensionField, Field};
6use p3_multilinear_util::point::Point;
7use p3_multilinear_util::poly::Poly;
8
9use crate::Claim;
10use crate::svo::{SvoAccumulators, SvoPoint};
11
12/// Multi-opening claim over an SVO point.
13pub type ProverMultiClaim<F, EF> = MultiClaim<EF, SvoPoint<F, EF>, Vec<Poly<EF>>>;
14/// Virtual claim carrying precomputed SVO accumulators.
15pub type ProverVirtualClaim<EF> = Claim<EF, Point<EF>, SvoAccumulators<EF>>;
16
17/// Opening on the verifier side: column index plus claimed evaluation.
18pub type VerifierOpening<EF> = Opening<EF, ()>;
19/// Multi-opening claim over a plain point on the verifier side.
20pub type VerifierMultiClaim<EF> = MultiClaim<EF, Point<EF>, ()>;
21/// Virtual evaluation claim on the stacked polynomial (verifier side).
22pub type VerifierVirtualClaim<EF> = Claim<EF, Point<EF>, ()>;
23
24/// Single opening of one polynomial at a shared evaluation point.
25///
26/// # Virtual openings
27///
28/// A polynomial index of `None` represents a virtual opening detached from any source column.
29///
30/// Strategies create these internally as transient claims during accumulator batching.
31#[derive(Debug, Clone)]
32pub struct Opening<EF: Field, Data> {
33    /// Source column index, or `None` for a virtual opening.
34    pub(crate) poly_idx: Option<usize>,
35    /// Value of the polynomial at the shared claim point.
36    pub(crate) eval: EF,
37    /// Strategy-specific payload attached to this opening.
38    pub(crate) data: Data,
39}
40
41impl<EF: Field, Data> Opening<EF, Data> {
42    /// Returns the evaluation.
43    pub const fn eval(&self) -> EF {
44        self.eval
45    }
46
47    /// Returns the source column index, or `None` for a virtual opening.
48    pub const fn poly_idx(&self) -> Option<usize> {
49        self.poly_idx
50    }
51
52    /// Returns the strategy-specific payload.
53    pub const fn data(&self) -> &Data {
54        &self.data
55    }
56}
57
58impl<EF: Field> Opening<EF, ()> {
59    /// Builds an opening for a concrete table column.
60    ///
61    /// # Arguments
62    ///
63    /// - `poly_idx` — source column index inside the owning table.
64    /// - `eval`     — value of that column at the shared claim point.
65    pub const fn new(poly_idx: usize, eval: EF) -> Self {
66        Self {
67            poly_idx: Some(poly_idx),
68            eval,
69            data: (),
70        }
71    }
72}
73
74/// A batch of openings that share one evaluation point.
75///
76/// ```text
77///     point     ── shared by every opening
78///     openings  [opening_0, opening_1, ...]
79/// ```
80///
81/// # Alpha-ordering contract
82///
83/// - Each recorded opening consumes one power of the batching challenge.
84/// - The canonical ordering is insertion order, walked as:
85///     - placements, in witness-layout order,
86///     - claims inside each placement, in recording order,
87///     - openings inside each claim, in the order they entered `eval`.
88/// - Prover and verifier walk the same three-loop nest, so the alpha-to-claim
89///   mapping is forced to agree when the transcripts mirror each other.
90#[derive(Debug, Clone)]
91pub struct MultiClaim<F: ExtensionField<F>, Point, Data> {
92    /// Shared evaluation point of every opening in the batch.
93    pub(super) point: Point,
94    /// Openings attached to the shared point, in insertion order.
95    pub(super) openings: Vec<Opening<F, Data>>,
96}
97
98impl<EF: Field, Point, Data> MultiClaim<EF, Point, Data> {
99    /// Builds a batch sharing `point`, holding the given openings.
100    pub const fn new(point: Point, openings: Vec<Opening<EF, Data>>) -> Self {
101        Self { point, openings }
102    }
103
104    /// Returns the shared evaluation point.
105    pub const fn point(&self) -> &Point {
106        &self.point
107    }
108
109    /// Returns the number of openings.
110    pub const fn len(&self) -> usize {
111        self.openings.len()
112    }
113
114    /// Returns whether the batch holds no openings.
115    pub const fn is_empty(&self) -> bool {
116        self.openings.is_empty()
117    }
118
119    /// Returns the openings as a slice in insertion order.
120    pub fn openings(&self) -> &[Opening<EF, Data>] {
121        &self.openings
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use alloc::vec;
128    use alloc::vec::Vec;
129
130    use p3_baby_bear::BabyBear;
131    use p3_field::PrimeCharacteristicRing;
132
133    use super::*;
134
135    type F = BabyBear;
136
137    #[test]
138    fn opening_new_sets_poly_idx_eval_and_unit_data() {
139        // Fixture: concrete opening at column 3 with value 42.
140        let opening: Opening<F, ()> = Opening::new(3, F::from_u64(42));
141
142        // Constructor must wrap the index and attach unit payload.
143        //
144        //     poly_idx  = Some(3)
145        //     eval      = 42
146        //     data      = ()
147        assert_eq!(opening.poly_idx(), Some(3));
148        assert_eq!(opening.eval(), F::from_u64(42));
149        assert_eq!(opening.data(), &());
150    }
151
152    #[test]
153    fn opening_struct_literal_supports_virtual_form() {
154        // Virtual openings are built via struct literal because they require
155        // poly_idx = None, which the public constructor does not expose.
156        let opening: Opening<F, ()> = Opening {
157            poly_idx: None,
158            eval: F::from_u64(7),
159            data: (),
160        };
161
162        // Accessor must surface the None sentinel.
163        assert_eq!(opening.poly_idx(), None);
164        assert_eq!(opening.eval(), F::from_u64(7));
165    }
166
167    #[test]
168    fn opening_accessors_reflect_non_unit_data() {
169        // Fixture: Data = Vec<F> — used by strategies carrying per-round partials.
170        let data = vec![F::from_u64(1), F::from_u64(2), F::from_u64(3)];
171        let opening: Opening<F, Vec<F>> = Opening {
172            poly_idx: Some(0),
173            eval: F::from_u64(99),
174            data: data.clone(),
175        };
176
177        // Accessor returns the payload untouched (same length, same values).
178        assert_eq!(opening.data(), &data);
179    }
180
181    #[test]
182    fn opening_clone_copies_every_field() {
183        // Regression: derived Clone must not drop or swap any field.
184        let original: Opening<F, Vec<F>> = Opening {
185            poly_idx: Some(5),
186            eval: F::from_u64(11),
187            data: vec![F::from_u64(10)],
188        };
189        let cloned = original.clone();
190
191        assert_eq!(cloned.poly_idx(), original.poly_idx());
192        assert_eq!(cloned.eval(), original.eval());
193        assert_eq!(cloned.data(), original.data());
194    }
195
196    #[test]
197    fn multi_claim_new_preserves_shared_point() {
198        // Fixture: two openings at an arbitrary point value (u32 chosen for simplicity).
199        let openings = vec![
200            Opening::<F, ()>::new(0, F::from_u64(1)),
201            Opening::<F, ()>::new(1, F::from_u64(2)),
202        ];
203        let claim = MultiClaim::<F, u32, ()>::new(100, openings);
204
205        // Constructor must forward the point and the openings vector verbatim.
206        assert_eq!(*claim.point(), 100);
207        assert_eq!(claim.openings().len(), 2);
208    }
209
210    #[test]
211    fn multi_claim_len_matches_openings_count() {
212        // Cover empty, singleton, and multi-opening batches with one loop.
213        for n in [0usize, 1, 4] {
214            let openings: Vec<Opening<F, ()>> = (0..n)
215                .map(|i| Opening::new(i, F::from_u64(i as u64)))
216                .collect();
217            let claim = MultiClaim::<F, u32, ()>::new(0, openings);
218
219            // Invariant: reported length equals constructed size.
220            assert_eq!(claim.len(), n);
221        }
222    }
223
224    #[test]
225    fn multi_claim_is_empty_is_true_iff_no_openings() {
226        // Empty claim: is_empty must be true.
227        let empty: MultiClaim<F, u32, ()> = MultiClaim::new(0, vec![]);
228        assert!(empty.is_empty());
229
230        // Non-empty claim: is_empty must be false.
231        let filled = MultiClaim::<F, u32, ()>::new(0, vec![Opening::new(0, F::from_u64(1))]);
232        assert!(!filled.is_empty());
233    }
234
235    #[test]
236    fn multi_claim_openings_returns_insertion_order() {
237        // Build openings in a non-trivial poly_idx order.
238        //
239        //     insertion: [col 2, col 0, col 1]
240        //     openings(): must be the same slice, same order.
241        let expected = vec![
242            Opening::<F, ()>::new(2, F::from_u64(20)),
243            Opening::<F, ()>::new(0, F::from_u64(0)),
244            Opening::<F, ()>::new(1, F::from_u64(10)),
245        ];
246        let claim = MultiClaim::<F, u32, ()>::new(0, expected.clone());
247
248        for (i, got) in claim.openings().iter().enumerate() {
249            assert_eq!(got.poly_idx(), expected[i].poly_idx());
250            assert_eq!(got.eval(), expected[i].eval());
251        }
252    }
253
254    #[test]
255    fn multi_claim_clone_preserves_point_and_openings() {
256        // Regression: derived Clone must copy both the point and the Vec contents.
257        let claim = MultiClaim::<F, u32, ()>::new(
258            77,
259            vec![
260                Opening::new(1, F::from_u64(5)),
261                Opening::new(2, F::from_u64(6)),
262            ],
263        );
264        let cloned = claim.clone();
265
266        assert_eq!(*cloned.point(), *claim.point());
267        assert_eq!(cloned.len(), claim.len());
268        for (a, b) in cloned.openings().iter().zip(claim.openings()) {
269            assert_eq!(a.poly_idx(), b.poly_idx());
270            assert_eq!(a.eval(), b.eval());
271        }
272    }
273}