Skip to main content

p3_binary_pcs/
boolean_trace.rs

1//! A batched trace of Boolean columns, committed as bits and opened as columns.
2//!
3//! ```text
4//!     in    one table per AIR instance, cells in {0, 1}
5//!     out   one bit witness, committed packed, opened column by column
6//! ```
7//!
8//! # From a column claim to a bit claim
9//!
10//! Every column occupies one contiguous run of the bit witness.
11//!
12//! A run starts at a multiple of its own length, so its address is a bit string.
13//!
14//! ```text
15//!     column of slot s, 2^a rows  ->  bits [s * 2^a, (s + 1) * 2^a)
16//!     its value at a point r      ->  the witness at (bits of s, r)
17//! ```
18//!
19//! The slot address therefore prefixes the row point, and nothing is re-indexed.
20//!
21//! The planner that assigns the slots is the one the commitment schemes use.
22//!
23//! # Work stays inside the column
24//!
25//! The slot prefix is fixed before the ring-switch sumcheck starts. For a complete opening
26//! of one table, every column at the current row and either no column or every column at
27//! the next row, all its columns are combined at one fresh random column point after their
28//! claimed values are bound, so each batch uses one ring switch.
29//! Other valid protocols retain one reduction per opened column.
30//!
31//! If one element holds `2^d_log` bits, a column folds `2^max(a - d_log, 0)` elements.
32//!
33//! Opening `W` equal-height columns shares the row equality weights across columns. The
34//! optimized complete-table route scans those weights once per batch and opens one padded
35//! stacked point; subset, reordered, mixed-height and mixed-view protocols use the
36//! per-column route.
37//!
38//! # Booleanity is still free
39//!
40//! The packing is a bijection between bit strings and the elements a commitment holds.
41//!
42//! No commitment exists to a function the hypercube sends outside `{0, 1}`.
43//!
44//! A cell outside `{0, 1}` is refused at commitment rather than range-checked.
45//!
46//! The opened values are the bits, so a proof whose own trace disagrees with them fails.
47//!
48//! # What a batch opens
49//!
50//! A batch names columns read at the current row, and columns read one row ahead.
51//!
52//! The successor view is repeat-last: row `x` reads row `x + 1`, and the last row itself.
53//!
54//! It weights the column by the equality table shifted up by one row, which the
55//! reduction underneath answers beside the reading at the point.
56//!
57//! The step stays inside the table's own row variables, so it never enters a slot address.
58//!
59//! ```text
60//!     one table, every batch reading all columns    ->  one reduction per batch
61//!     of both views, or of the current alone            over one shared column point
62//!     anything else                                 ->  one reduction per column read
63//! ```
64//!
65//! A column named by both views of a batch is one reduction answering both readings.
66//!
67//! The values of a batch are its current ones, in the order the batch names them,
68//! followed by its next ones in the order the batch names those.
69//!
70//! An AIR whose constraints read only the current row names no successor column.
71
72mod evaluate;
73mod gather;
74mod plan;
75
76use alloc::vec::Vec;
77use core::marker::PhantomData;
78
79use p3_binary_dft::EncodableLevel;
80use p3_binary_field::TowerLevel;
81use p3_challenger::fs::TranscriptField;
82use p3_challenger::{CanObserve, CanSampleUniformBits, FieldChallenger, GrindingChallenger};
83use p3_commit::{Mmcs, MultilinearPcs};
84use p3_field::Field;
85use p3_multilinear_util::point::Point;
86use p3_multilinear_util::poly::Poly;
87use p3_multilinear_util::split_eq::SplitEq;
88use p3_sumcheck::layout::{Table, plan_stacked_layout};
89use p3_sumcheck::ring_switch::bits::BitRingSwitch;
90use p3_sumcheck::{
91    OpeningEvals, OpeningProtocol, PrescribedOpeningSecurity, PrescribedPointPcs, TableShape,
92};
93use serde::{Deserialize, Serialize};
94use thiserror::Error;
95
96use self::plan::{OpeningRoute, opening_evals, sample_points, value_count};
97use crate::boolean::{
98    BitOpening, BitReadings, BooleanBackend, BooleanMultilinearPcs, BooleanPcs, BooleanPcsError,
99    BooleanProof,
100};
101use crate::boolean_trace_transcript::{
102    ColumnBatchProverTranscript, ColumnBatchShape, ColumnBatchVerifierTranscript,
103};
104use crate::fold::{ChallengeField, FoldAlphabet};
105use crate::packing::Coordinates;
106use crate::params::BinaryPcsConfig;
107use crate::prover::BinaryPcsProverData;
108
109/// Bits one word of the staging buffer holds.
110const WORD_BITS: usize = 64;
111
112/// A commitment to a batch of Boolean trace tables, packed into one bit witness.
113///
114/// The committed object is the bits, so the codeword is as short as the alphabet allows.
115///
116/// The tables themselves are retained, for a prover that evaluates constraints over them.
117pub struct BooleanTraceCommitment<EF, B> {
118    /// The bit commitment every column claim is discharged against.
119    inner: B,
120    /// Marker tying the commitment to its challenge field; carries no runtime state.
121    _marker: PhantomData<EF>,
122}
123
124/// The trace commitment discharged through the folding-only bit commitment.
125pub type BooleanTracePcs<EF, MT, MX> = BooleanTraceCommitment<EF, BooleanPcs<EF, MT, MX>>;
126
127impl<EF, MT, MX> BooleanTraceCommitment<EF, BooleanPcs<EF, MT, MX>>
128where
129    EF: ChallengeField<EF>
130        + EncodableLevel
131        + TranscriptField
132        + TowerLevel
133        + FoldAlphabet<EF>
134        + Coordinates,
135    MT: Mmcs<EF>,
136    MX: Mmcs<EF, Error = MT::Error>,
137{
138    /// Build a commitment over a batch of tables stacking to `num_variables` bits.
139    ///
140    /// The arity is the one the layout planner derives from the table shapes.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error unless the schedule was derived for the committed alphabet.
145    ///
146    /// Returns an error unless the schedule commits exactly the elements the packing holds.
147    pub fn new(
148        config: BinaryPcsConfig,
149        mmcs: MT,
150        round_mmcs: MX,
151        num_variables: usize,
152    ) -> Result<Self, BooleanTraceError<EF, MT::Error>> {
153        BooleanPcs::new(config, mmcs, round_mmcs, num_variables)
154            .map(Self::from_commitment)
155            .map_err(BooleanTraceCommitmentError::Boolean)
156    }
157}
158
159impl<EF, B> BooleanTraceCommitment<EF, B>
160where
161    EF: ChallengeField<EF>
162        + EncodableLevel
163        + TranscriptField
164        + TowerLevel
165        + FoldAlphabet<EF>
166        + Coordinates,
167    B: BooleanBackend<EF>,
168{
169    /// Stack trace tables into an already-built bit commitment.
170    pub const fn from_commitment(inner: B) -> Self {
171        Self {
172            inner,
173            _marker: PhantomData,
174        }
175    }
176
177    /// Variables the stacked bit witness has, so `2^n` bits in all.
178    #[must_use]
179    pub fn num_variables(&self) -> usize {
180        self.inner.num_variables()
181    }
182
183    /// Evaluate the zero-padded column-value vector at its sampled column point.
184    fn combine_columns(values: &[EF], column_point: &Point<EF>) -> EF {
185        let padded_len = 1usize << column_point.num_variables();
186        let mut padded = Vec::with_capacity(padded_len);
187        padded.extend_from_slice(values);
188        padded.resize(padded_len, EF::ZERO);
189        SplitEq::<EF, EF>::new_packed(column_point, EF::ONE).eval_ext(Poly::new(padded).as_view())
190    }
191}
192
193/// The committed trace tables, held until the commitment is opened.
194pub struct BooleanTraceCommitmentData<EF: Field, D> {
195    /// Prover data of the bit commitment underneath.
196    inner: D,
197    /// Source tables, lent back to whatever evaluates constraints over them.
198    tables: Vec<Table<EF>>,
199}
200
201/// The retained data of a trace behind the folding-only bit commitment.
202pub type BooleanTraceData<EF, MT> = BooleanTraceCommitmentData<EF, BinaryPcsProverData<EF, EF, MT>>;
203
204impl<EF: Field, D> Clone for BooleanTraceCommitmentData<EF, D>
205where
206    D: Clone,
207{
208    fn clone(&self) -> Self {
209        Self {
210            inner: self.inner.clone(),
211            tables: self.tables.clone(),
212        }
213    }
214}
215
216impl<EF: Field, D> BooleanTraceCommitmentData<EF, D> {
217    /// One committed table, in the order the tables were supplied.
218    ///
219    /// # Panics
220    ///
221    /// Panics on a table index this commitment does not hold.
222    #[must_use]
223    pub fn table(&self, index: usize) -> &Table<EF> {
224        &self.tables[index]
225    }
226}
227
228/// One opening of a Boolean trace: the column values, and the bit proof behind them.
229#[derive(Clone, Serialize, Deserialize)]
230#[serde(bound(
231    serialize = "EF: TowerLevel, P: Serialize",
232    deserialize = "EF: TowerLevel, P: Deserialize<'de>"
233))]
234pub struct BooleanTraceCommitmentProof<EF: Field, P> {
235    /// One value per column a batch reads, its current ones first, batches in transcript order.
236    pub values: Vec<EF>,
237    /// The bit commitment's own proof, answering for every value at once.
238    pub opening: P,
239}
240
241/// One opening of a trace behind the folding-only bit commitment.
242pub type BooleanTraceProof<EF, MT, MX> = BooleanTraceCommitmentProof<EF, BooleanProof<EF, MT, MX>>;
243
244/// Why a trace behind the folding-only bit commitment could not be committed or opened.
245pub type BooleanTraceError<EF, MmcsError> =
246    BooleanTraceCommitmentError<BooleanPcsError<EF, MmcsError>>;
247
248/// Why a Boolean trace could not be committed or opened.
249#[derive(Debug, Error)]
250#[non_exhaustive]
251pub enum BooleanTraceCommitmentError<E> {
252    /// The bit commitment underneath refused the witness or the opening.
253    #[error(transparent)]
254    Boolean(E),
255
256    /// The table shapes do not stack to the arity this commitment holds.
257    #[error("the shapes stack to {actual} variables, the commitment holds {expected}")]
258    StackedArity {
259        /// Variables the commitment holds.
260        expected: usize,
261        /// Variables the shapes stack to.
262        actual: usize,
263    },
264
265    /// The retained source tables do not have the exact shapes promised by the opening protocol.
266    #[error("table {table} has shape {actual:?}, protocol requires {expected:?}")]
267    TableShapeMismatch {
268        /// Table whose retained shape disagreed with the protocol.
269        table: usize,
270        /// Shape the protocol describes.
271        expected: TableShape,
272        /// Shape the committed prover data retains.
273        actual: TableShape,
274    },
275
276    /// The protocol and retained source table lists have different lengths.
277    #[error("the prover retains {actual} tables, while the opening protocol describes {expected}")]
278    TableCountMismatch {
279        /// Number of tables described by the protocol.
280        expected: usize,
281        /// Number of tables retained by the prover.
282        actual: usize,
283    },
284
285    /// A trace cell holds neither zero nor one, so it addresses no bit.
286    #[error("column {column} of table {table} holds a cell outside the two Boolean values")]
287    NonBooleanCell {
288        /// Table holding the cell.
289        table: usize,
290        /// Column holding the cell.
291        column: usize,
292    },
293
294    /// One point per opening batch is required.
295    #[error("{actual} points against {expected} opening batches")]
296    PointCount {
297        /// Batches the protocol schedules.
298        expected: usize,
299        /// Points supplied.
300        actual: usize,
301    },
302
303    /// A point does not name the rows of the table it opens.
304    #[error("the point for table {table} names {actual} variables, expected {expected}")]
305    PointArity {
306        /// Table the point opens.
307        table: usize,
308        /// Variables that table's rows need.
309        expected: usize,
310        /// Variables the point names.
311        actual: usize,
312    },
313
314    /// The proof carries a different number of values than the protocol opens.
315    ///
316    /// A column read at both rows carries one reading per row, so the count is readings,
317    /// one per value position.
318    #[error("the proof carries {actual} values against {expected} opened readings")]
319    ValueCount {
320        /// Readings the protocol opens, one per value position.
321        expected: usize,
322        /// Values the proof carries.
323        actual: usize,
324    },
325
326    /// A typed column-batching transcript could not replay its proof shape.
327    #[error(transparent)]
328    ColumnBatchTranscript(#[from] p3_challenger::fs::TranscriptError),
329
330    /// The inner opening disagreed with the prover's independently computed batch value.
331    #[error("column batch {batch} returned an aggregate value different from its claimed columns")]
332    ColumnBatchValueMismatch {
333        /// Batch whose aggregate value disagreed.
334        batch: usize,
335    },
336}
337
338impl<EF, B, Challenger> MultilinearPcs<EF, Challenger> for BooleanTraceCommitment<EF, B>
339where
340    EF: ChallengeField<EF>
341        + EncodableLevel
342        + TranscriptField
343        + TowerLevel
344        + FoldAlphabet<EF>
345        + Coordinates,
346    B: BooleanMultilinearPcs<EF, Challenger>,
347    Challenger: FieldChallenger<EF>
348        + GrindingChallenger<Witness = EF>
349        + CanSampleUniformBits<EF>
350        + CanObserve<B::Commitment>,
351{
352    type Val = EF;
353    type Commitment = B::Commitment;
354    type ProverData = BooleanTraceCommitmentData<EF, B::ProverData>;
355    type Proof = BooleanTraceCommitmentProof<EF, B::Proof>;
356    type Error = BooleanTraceCommitmentError<B::Error>;
357    type ProverError = BooleanTraceCommitmentError<B::Error>;
358    type Witness = Vec<Table<EF>>;
359    type OpeningProtocol = OpeningProtocol;
360
361    fn num_vars(&self) -> usize {
362        self.num_variables()
363    }
364
365    fn commit(
366        &self,
367        witness: Self::Witness,
368        challenger: &mut Challenger,
369    ) -> Result<(Self::Commitment, Self::ProverData), Self::ProverError> {
370        // Gathering runs before the transcript is touched, so a refusal leaves it alone.
371        let bits =
372            tracing::info_span!("gather boolean bits").in_scope(|| self.gather_bits(&witness))?;
373        let (commitment, inner) = self
374            .inner
375            .commit_bits(&bits, challenger)
376            .map_err(BooleanTraceCommitmentError::Boolean)?;
377        Ok((
378            commitment,
379            BooleanTraceCommitmentData {
380                inner,
381                tables: witness,
382            },
383        ))
384    }
385
386    fn observe_commitment(&self, commitment: &Self::Commitment, challenger: &mut Challenger) {
387        self.inner.observe_commitment(commitment, challenger);
388    }
389
390    fn open(
391        &self,
392        prover_data: Self::ProverData,
393        protocol: Self::OpeningProtocol,
394        challenger: &mut Challenger,
395    ) -> Result<Self::Proof, Self::ProverError> {
396        Self::validate_source_shapes(&prover_data.tables, &protocol)?;
397        // The sampled convention draws every batch's point from the transcript.
398        let points = sample_points(&protocol, challenger);
399        self.open_at(prover_data, &protocol, &points, challenger)
400    }
401
402    fn verify(
403        &self,
404        commitment: &Self::Commitment,
405        proof: &Self::Proof,
406        challenger: &mut Challenger,
407        protocol: Self::OpeningProtocol,
408    ) -> Result<(), Self::Error> {
409        // Committing bound the root on the prover's sponge, so this binds it here.
410        //
411        // The points are then sampled from that sponge, as the prover sampled them.
412        //
413        // The prescribed-point entry point is the one that leaves the binding to its caller.
414        self.observe_commitment(commitment, challenger);
415
416        // The same draws the prover made, in the same order, before anything is checked.
417        let points = sample_points(&protocol, challenger);
418        self.verify_at(commitment, proof, &protocol, &points, challenger)
419            .map(|_| ())
420    }
421}
422
423impl<EF, B, Challenger> PrescribedPointPcs<EF, Challenger> for BooleanTraceCommitment<EF, B>
424where
425    EF: ChallengeField<EF>
426        + EncodableLevel
427        + TranscriptField
428        + TowerLevel
429        + FoldAlphabet<EF>
430        + Coordinates,
431    B: BooleanMultilinearPcs<EF, Challenger>,
432    Challenger: FieldChallenger<EF>
433        + GrindingChallenger<Witness = EF>
434        + CanSampleUniformBits<EF>
435        + CanObserve<B::Commitment>,
436{
437    fn prescribed_security(&self, protocol: &OpeningProtocol) -> Option<PrescribedOpeningSecurity> {
438        // A protocol this scheme would refuse gets no assessment, so a caller fails closed.
439        let shapes = protocol.table_shapes();
440        if plan_stacked_layout(&shapes).0 != self.num_variables() {
441            return None;
442        }
443        // A reduction sends carry and last once its successor view outruns one element.
444        let absorbed = BitRingSwitch::<EF>::ABSORBED;
445        let route = OpeningRoute::new(protocol);
446        let mut security = self.inner.readings_security(
447            route.num_reductions(),
448            route.successor_tensors(&shapes, absorbed),
449        )?;
450        if let OpeningRoute::Batched(shape) = route {
451            // Both combined claims of a batch read one column point, so each is charged.
452            //
453            // The batching challenge is drawn before any candidate has been named.
454            //
455            // So it pays the same list the ring-switch reduction below it paid for.
456            //
457            // A union bound taken once does not shrink the set the next draw faces.
458            security.charge_reduction(p3_security::multilinear::column_batch_term(
459                shape.num_batches * shape.num_views(),
460                shape.column_variables(),
461                EF::bits(),
462            ));
463        }
464        Some(security)
465    }
466
467    fn open_at(
468        &self,
469        prover_data: Self::ProverData,
470        protocol: &OpeningProtocol,
471        points: &[Point<EF>],
472        challenger: &mut Challenger,
473    ) -> Result<Self::Proof, Self::ProverError> {
474        // Every shape and every point is checked before the transcript moves.
475        Self::validate_source_shapes(&prover_data.tables, protocol)?;
476        let placements = self.validate_opening(protocol, points)?;
477        let shape = match OpeningRoute::new(protocol) {
478            OpeningRoute::PerColumn(plan) => {
479                let openings = Self::bit_openings(protocol, plan.claims(), points, &placements);
480                let (readings, opening) = self
481                    .inner
482                    .open_readings(prover_data.inner, &openings, challenger)
483                    .map_err(BooleanTraceCommitmentError::Boolean)?;
484                let values = plan.values(&readings);
485                return Ok(BooleanTraceCommitmentProof { values, opening });
486            }
487            OpeningRoute::Batched(shape) => shape,
488        };
489
490        let BooleanTraceCommitmentData { inner, tables } = prover_data;
491        let ColumnBatchShape { width, next, .. } = shape;
492        let run = shape.values_per_batch();
493        let mut values = Vec::with_capacity(run * shape.num_batches);
494        tracing::info_span!("evaluate boolean columns", width, next).in_scope(|| {
495            for point in points {
496                let (current, successor) = Self::evaluate_views(&tables[0], point, next);
497                values.extend(current);
498                values.extend(successor);
499            }
500        });
501
502        let mut transcript = ColumnBatchProverTranscript::new(challenger, shape);
503        let mut openings = Vec::with_capacity(shape.num_batches);
504        let mut expected = Vec::with_capacity(shape.num_batches);
505        for (point, batch_values) in points.iter().zip(values.chunks_exact(run)) {
506            let (current, successor) = batch_values.split_at(width);
507            // Both value runs are bound before the point that combines either of them.
508            let column_point = transcript.batch(point, current, successor);
509            expected.push(BitReadings {
510                current: Some(Self::combine_columns(current, &column_point)),
511                next: next.then(|| Self::combine_columns(successor, &column_point)),
512            });
513            let mut lifted_point = column_point;
514            lifted_point.extend(point);
515            openings.push(BitOpening {
516                point: lifted_point,
517                row_variables: shape.table_variables,
518                current: true,
519                next,
520            });
521        }
522        transcript.finish();
523
524        let (readings, opening) = self
525            .inner
526            .open_readings(inner, &openings, challenger)
527            .map_err(BooleanTraceCommitmentError::Boolean)?;
528        if readings.len() != expected.len() {
529            return Err(BooleanTraceCommitmentError::ColumnBatchValueMismatch {
530                batch: expected.len(),
531            });
532        }
533        for (batch, (actual, expected)) in readings.iter().zip(&expected).enumerate() {
534            if actual != expected {
535                return Err(BooleanTraceCommitmentError::ColumnBatchValueMismatch { batch });
536            }
537        }
538        Ok(BooleanTraceCommitmentProof { values, opening })
539    }
540
541    fn verify_at(
542        &self,
543        commitment: &Self::Commitment,
544        proof: &Self::Proof,
545        protocol: &OpeningProtocol,
546        points: &[Point<EF>],
547        challenger: &mut Challenger,
548    ) -> Result<Vec<OpeningEvals<EF>>, Self::Error> {
549        // Every shape, point and value count is checked before the transcript moves.
550        let placements = self.validate_opening(protocol, points)?;
551        let expected_values = value_count(protocol);
552        if proof.values.len() != expected_values {
553            return Err(BooleanTraceCommitmentError::ValueCount {
554                expected: expected_values,
555                actual: proof.values.len(),
556            });
557        }
558
559        let shape = match OpeningRoute::new(protocol) {
560            OpeningRoute::PerColumn(plan) => {
561                let openings = Self::bit_openings(protocol, plan.claims(), points, &placements);
562
563                // One bit proof answers for every column of every batch at once.
564                self.inner
565                    .verify_readings(
566                        commitment,
567                        &openings,
568                        &plan.readings(&proof.values),
569                        &proof.opening,
570                        challenger,
571                    )
572                    .map_err(BooleanTraceCommitmentError::Boolean)?;
573                return Ok(opening_evals(protocol, &proof.values));
574            }
575            OpeningRoute::Batched(shape) => shape,
576        };
577
578        let ColumnBatchShape { width, next, .. } = shape;
579        let run = shape.values_per_batch();
580        let mut transcript = ColumnBatchVerifierTranscript::new(challenger, shape);
581        let mut openings = Vec::with_capacity(shape.num_batches);
582        let mut readings = Vec::with_capacity(shape.num_batches);
583        for (point, batch_values) in points.iter().zip(proof.values.chunks_exact(run)) {
584            let (current, successor) = batch_values.split_at(width);
585            // Both value runs are bound before the point that combines either of them.
586            let column_point = transcript.batch(point, current, successor)?;
587            readings.push(BitReadings {
588                current: Some(Self::combine_columns(current, &column_point)),
589                next: next.then(|| Self::combine_columns(successor, &column_point)),
590            });
591            let mut lifted_point = column_point;
592            lifted_point.extend(point);
593            openings.push(BitOpening {
594                point: lifted_point,
595                row_variables: shape.table_variables,
596                current: true,
597                next,
598            });
599        }
600        transcript.finish();
601
602        // One bit proof answers for every batched claim at once.
603        self.inner
604            .verify_readings(commitment, &openings, &readings, &proof.opening, challenger)
605            .map_err(BooleanTraceCommitmentError::Boolean)?;
606
607        Ok(opening_evals(protocol, &proof.values))
608    }
609}
610
611#[cfg(test)]
612mod tests;