Skip to main content

p3_binary_pcs/
boolean.rs

1//! A commitment to a function from the hypercube to `{0, 1}`, opened at any point.
2//!
3//! ```text
4//!     commit   a bit witness, bit-sliced
5//!     open     the multilinear extension at a point of the challenge field
6//! ```
7//!
8//! # Three pieces, one interface
9//!
10//! ```text
11//!     packing    bits  ->  the elements a commitment holds, by reinterpretation
12//!     switch     a claim about the bits  ->  a claim about the packing
13//!     opening    that claim  ->  discharged against the commitment
14//! ```
15//!
16//! # Booleanity is free
17//!
18//! The packing is a bijection between bit strings and elements of the level.
19//! Every bit pattern is an element, and every element is some bit pattern.
20//!
21//! A commitment to a packed multilinear is therefore a commitment to a bit witness.
22//! No commitment exists to a function the hypercube sends outside `{0, 1}`.
23//!
24//! There is no range check here because there is nothing one could rule out.
25//!
26//! # One opening, not one per claim
27//!
28//! A claim names a point and the readings asked for there:
29//!
30//! ```text
31//!     current   the witness at the point
32//!     next      the witness one row further on, the last row repeating
33//! ```
34//!
35//! One reduction answers every reading of its claim.
36//! A successor view whose rows outrun one element sends two more elements, not a second sumcheck.
37//!
38//! The `k` claims then share one sumcheck, under powers of one challenge `lambda`:
39//!
40//! ```text
41//!     per claim   its elements, checked against its own readings
42//!     once        one sumcheck of l' rounds, one surviving claim, one opened point
43//! ```
44//!
45//! `lambda` is drawn after every claim's elements are bound.
46//! The batch closes by comparing its one surviving value to the one opened.
47//!
48//! # What binds what
49//!
50//! Committing binds the root, and the verifying side replays that binding itself.
51//! The two sides therefore start from one sponge state.
52//!
53//! A protocol holding several commitments binds each of them once, in its own order.
54//! So the binding is a call of its own rather than a step inside verification.
55//!
56//! The reduction then binds the opening point itself, before it sends anything.
57//! A caller therefore owes no binding of its own, unlike a bare prescribed-point opening.
58//!
59//! The point the commitment is opened at comes out of the reduction's own rounds.
60//!
61//! # Soundness
62//!
63//! Three errors compose by a union bound.
64//!
65//! ```text
66//!     commitment       the packed multilinear's own budget, at one opened point
67//!     reduction        (d_log + K - 1 + 2 l') / |EF|, once for the whole batch
68//!     claim batching   (k - 1) / |EF|, the degree of lambda
69//! ```
70//!
71//! `K` counts the elements a claim sends, batched under powers of one challenge.
72//! It is three when a successor view's rows outrun one element, and one otherwise.
73//!
74//! None is subtracted from another.
75//!
76//! Both come back labelled, so a report says which one is short.
77
78use alloc::vec;
79use alloc::vec::Vec;
80
81use p3_binary_dft::EncodableLevel;
82use p3_binary_field::{PackedGf2, TowerLevel, Underlier};
83use p3_challenger::fs::TranscriptField;
84use p3_challenger::{CanObserve, CanSampleUniformBits, FieldChallenger, GrindingChallenger};
85use p3_commit::{Mmcs, MultilinearPcs};
86use p3_field::Field;
87use p3_multilinear_util::point::Point;
88use p3_security::SecurityTerm;
89use p3_security::multilinear::{
90    bit_ring_switch_claim_batching_term, bit_ring_switch_tensors_term, bit_ring_switch_term,
91};
92use p3_sumcheck::layout::{Layout, SuffixProver};
93use p3_sumcheck::ring_switch::bits::{
94    BitPacking, BitPackingView, BitRingSwitch, BitRingSwitchClaims, BitRingSwitchClaimsProof,
95    BitRingSwitchProofError,
96};
97use p3_sumcheck::{
98    OpeningBatch, OpeningProtocol, PrescribedOpeningSecurity, PrescribedPointPcs, TableShape,
99    TableSpec,
100};
101use serde::de::DeserializeOwned;
102use serde::{Deserialize, Serialize};
103use thiserror::Error;
104
105use crate::error::BinaryPcsError;
106use crate::fold::{ChallengeField, FoldAlphabet};
107use crate::packing::{Coordinates, PackError, PackedStack};
108use crate::params::{BinaryPcsConfig, BinaryPcsConfigError};
109use crate::pcs::BinaryPcs;
110use crate::proof::BinaryPcsProof;
111use crate::prover::BinaryPcsProverData;
112
113/// What a bit commitment keeps and sends, named without reference to a transcript.
114pub trait BooleanBackend<EF> {
115    /// Succinct binding commitment sent to the verifier.
116    type Commitment: Clone + Serialize + DeserializeOwned;
117
118    /// Prover-side data retained between committing and opening.
119    type ProverData;
120
121    /// The proof one opening carries.
122    type Proof: Clone + Serialize + DeserializeOwned;
123
124    /// Why a commitment or an opening was refused.
125    type Error: core::fmt::Debug;
126
127    /// Variables the committed bit witness has, so `2^n` bits in all.
128    fn num_variables(&self) -> usize;
129}
130
131/// A commitment to a function from the hypercube to `{0, 1}`.
132///
133/// The committed object is a bit witness.
134/// An opening answers for its multilinear extension at a point of the challenge field.
135///
136/// # What a proving system pays on top
137///
138/// A batched prover lends its trace back as a borrowed table of base-field cells.
139///
140/// A bit witness has none to lend, so an integration holds the table and packs at commit time.
141///
142/// The base field then has to be one the challenge field extends.
143///
144/// The narrowest such level of the tower is `GF(2^8)`.
145///
146/// This scheme also draws its challenges from the field its own elements live in.
147///
148/// So one cell per bit at the full challenge width is what the borrow costs today.
149///
150/// Constraints would also run wider if the base field packed bit-sliced.
151///
152/// The packing trait forbids it, casting a packed value to an array of scalars unchanged.
153///
154/// Bit-slicing is a compression, not a reinterpretation.
155pub trait BooleanMultilinearPcs<EF, Challenger>: BooleanBackend<EF> {
156    /// Bind a commitment into the transcript, as committing to one does.
157    ///
158    /// A prover binds while producing its commitment, and a verifier never produces one.
159    ///
160    /// Routing both sides through this call is what keeps the two sponges in step.
161    fn observe_commitment(&self, commitment: &Self::Commitment, challenger: &mut Challenger);
162
163    /// Commit to a bit witness supplied bit-sliced, lane `j` of block `b` being bit `d*b + j`.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error unless the witness covers exactly the committed hypercube.
168    fn commit_bits<U: Underlier>(
169        &self,
170        bits: &[PackedGf2<U>],
171        challenger: &mut Challenger,
172    ) -> Result<(Self::Commitment, Self::ProverData), Self::Error>;
173
174    /// Open the bit witness with the readings every opening asks for, in one proof.
175    ///
176    /// A reading is the witness at the point, or the witness one row further on.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error on an opening this commitment refuses.
181    #[allow(clippy::type_complexity)]
182    fn open_readings(
183        &self,
184        prover_data: Self::ProverData,
185        openings: &[BitOpening<EF>],
186        challenger: &mut Challenger,
187    ) -> Result<(Vec<BitReadings<EF>>, Self::Proof), Self::Error>;
188
189    /// Check one proof against the readings it claims at every opening.
190    ///
191    /// # Errors
192    ///
193    /// Returns an error on a false reading, a failed reduction, or an unclosed claim.
194    fn verify_readings(
195        &self,
196        commitment: &Self::Commitment,
197        openings: &[BitOpening<EF>],
198        readings: &[BitReadings<EF>],
199        proof: &Self::Proof,
200        challenger: &mut Challenger,
201    ) -> Result<(), Self::Error>;
202
203    /// Every labelled algebraic error one opening of this many claims charges.
204    ///
205    /// # Returns
206    ///
207    /// Nothing when the commitment declines to price the schedule.
208    fn readings_security(
209        &self,
210        num_claims: usize,
211        successor_tensors: bool,
212    ) -> Option<PrescribedOpeningSecurity>;
213
214    /// Open the multilinear extension at every point, in one proof.
215    ///
216    /// No point needs prior transcript binding: each reduction binds its own.
217    ///
218    /// # Returns
219    ///
220    /// One value per point, in the order the points were supplied.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error unless every point names the committed function's variables.
225    fn open_at_points(
226        &self,
227        prover_data: Self::ProverData,
228        points: &[Point<EF>],
229        challenger: &mut Challenger,
230    ) -> Result<(Vec<EF>, Self::Proof), Self::Error>;
231
232    /// Check one proof against the values it claims at every point.
233    ///
234    /// The commitment's binding is the caller's, replayed before this is reached.
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if a claim, a reduction, the commitment or the fold fails.
239    fn verify_at_points(
240        &self,
241        commitment: &Self::Commitment,
242        points: &[Point<EF>],
243        values: &[EF],
244        proof: &Self::Proof,
245        challenger: &mut Challenger,
246    ) -> Result<(), Self::Error>;
247}
248
249/// One claim about the bit witness: a point, and the readings asked for there.
250#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct BitOpening<EF> {
252    /// Point over every variable of the bit witness, its row coordinates trailing.
253    pub point: Point<EF>,
254    /// Trailing coordinates addressing rows; the successor view steps within them.
255    pub row_variables: usize,
256    /// Whether the witness at the point is asked for.
257    pub current: bool,
258    /// Whether the witness one row further on, the last row repeating, is asked for.
259    pub next: bool,
260}
261
262/// The readings one [`BitOpening`] asked for, and nothing where it asked for nothing.
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
264pub struct BitReadings<EF> {
265    /// The witness at the point.
266    pub current: Option<EF>,
267    /// The witness one row further on, the last row repeating.
268    pub next: Option<EF>,
269}
270
271/// A Boolean commitment built from the packing, the bit ring switch and the binary PCS.
272///
273/// The committed elements hold `d` bits each.
274/// The codeword is therefore `d` times shorter than an element-per-bit one.
275pub struct BooleanPcs<EF: EncodableLevel, MT, MX> {
276    /// The commitment the packed multilinear is discharged against.
277    inner: BinaryPcs<EF, EF, MT, MX>,
278    /// Schedule the commitment was built from, which prices its own openings.
279    config: BinaryPcsConfig,
280    /// Variables the bit witness has, which is the packing's plus the absorbed ones.
281    num_variables: usize,
282}
283
284impl<EF, MT, MX> BooleanPcs<EF, MT, MX>
285where
286    EF: ChallengeField<EF>
287        + EncodableLevel
288        + TranscriptField
289        + TowerLevel
290        + FoldAlphabet<EF>
291        + Coordinates,
292    MT: Mmcs<EF>,
293    MX: Mmcs<EF, Error = MT::Error>,
294{
295    /// Build a Boolean commitment over a bit witness of `num_variables` variables.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error unless the schedule was derived for `(EF, EF)`.
300    /// Returns an error unless the schedule commits exactly the elements the packing holds.
301    pub fn new(
302        config: BinaryPcsConfig,
303        mmcs: MT,
304        round_mmcs: MX,
305        num_variables: usize,
306    ) -> Result<Self, BooleanPcsError<EF, MT::Error>> {
307        let packed = Self::packed_variables(num_variables)?;
308        if config.num_variables() != packed {
309            return Err(BooleanPcsError::ConfigArity {
310                expected: packed,
311                actual: config.num_variables(),
312            });
313        }
314        Ok(Self {
315            inner: BinaryPcs::new(config, mmcs, round_mmcs).map_err(BooleanPcsError::Config)?,
316            config,
317            num_variables,
318        })
319    }
320
321    /// Variables the packing keeps, the witness's less the ones one element absorbs.
322    fn packed_variables(num_variables: usize) -> Result<usize, BooleanPcsError<EF, MT::Error>> {
323        let absorbed = BitRingSwitch::<EF>::ABSORBED;
324        num_variables
325            .checked_sub(absorbed)
326            .ok_or(BooleanPcsError::WitnessTooNarrow {
327                needed: absorbed,
328                actual: num_variables,
329            })
330    }
331
332    /// Variables the committed bit witness has, so `2^n` bits in all.
333    ///
334    /// The same number the trait reports, reachable without naming a challenger.
335    #[must_use]
336    pub const fn num_variables(&self) -> usize {
337        self.num_variables
338    }
339
340    /// The opening schedule the one surviving claim of a batch is discharged through.
341    ///
342    /// One table of one column, opened directly at one point.
343    fn protocol(&self) -> OpeningProtocol {
344        OpeningProtocol::new(vec![TableSpec::new(
345            TableShape::new(self.inner.num_variables(), 1),
346            vec![OpeningBatch::new(vec![0], Vec::new())],
347        )])
348    }
349
350    /// Soundness cost of reducing this many bit claims to one claim about the packing.
351    ///
352    /// ```text
353    ///     reduction        one batched ring switch, its rounds run once
354    ///     claim batching   lambda, of degree num_claims - 1
355    /// ```
356    #[must_use]
357    pub fn reduction_security(&self, num_claims: usize) -> [SecurityTerm; 2] {
358        [
359            bit_ring_switch_term(
360                num_claims.min(1),
361                BitRingSwitch::<EF>::ABSORBED,
362                self.inner.num_variables(),
363                EF::bits(),
364            ),
365            bit_ring_switch_claim_batching_term(num_claims, EF::bits()),
366        ]
367    }
368
369    /// Every labelled algebraic error one opening of this many points charges.
370    ///
371    /// A point asks for the current reading alone, so no reduction sends successor elements.
372    #[must_use]
373    pub fn opening_security(&self, num_claims: usize) -> PrescribedOpeningSecurity {
374        self.readings_security(num_claims, false)
375    }
376
377    /// Every labelled algebraic error one opening of this many claims charges.
378    ///
379    /// ```text
380    ///     commitment       the packed multilinear's own budget, at the one surviving point
381    ///     reduction        one batched bit ring switch, batching K elements under alpha
382    ///     claim batching   the claims folded under powers of lambda
383    /// ```
384    ///
385    /// `K` is three when `successor_tensors` holds, one otherwise.
386    /// A claim sends carry and last when its successor view's rows outrun one element.
387    /// Charging three once any one claim sends them is an upper bound.
388    ///
389    /// The one surviving claim is then closed by an equality, which carries no error.
390    ///
391    /// The terms are independent draws, so a caller composes them by a union bound.
392    /// Nothing here covers hash or transcript collisions, which the caller supplies.
393    #[must_use]
394    pub fn readings_security(
395        &self,
396        num_claims: usize,
397        successor_tensors: bool,
398    ) -> PrescribedOpeningSecurity {
399        // The tensor alone, or the tensor with carry and last.
400        let num_tensors = if successor_tensors { 3 } else { 1 };
401        let mut terms = vec![
402            self.config
403                .security_regime()
404                .opening_term(num_claims.min(1)),
405            bit_ring_switch_tensors_term(
406                num_claims.min(1),
407                num_tensors,
408                BitRingSwitch::<EF>::ABSORBED,
409                self.inner.num_variables(),
410                EF::bits(),
411            ),
412        ];
413        // A single claim draws no lambda, so its report carries no batching term.
414        if num_claims > 1 {
415            terms.push(bit_ring_switch_claim_batching_term(num_claims, EF::bits()));
416        }
417        PrescribedOpeningSecurity {
418            terms,
419            // Unique decoding fixes one candidate polynomial at commitment time.
420            log2_max_candidates: 0.0,
421        }
422    }
423
424    /// The packed multilinear the commitment holds, read back out of the retained table.
425    ///
426    /// # Panics
427    ///
428    /// Never for a table this scheme committed, since the read-back refuses only these:
429    ///
430    /// ```text
431    ///     sub-byte level   excluded by the encodable bound, which starts at a byte
432    ///     no hypercube     excluded by the layout, whose tables are power-of-two
433    /// ```
434    fn packing(prover_data: &BinaryPcsProverData<EF, EF, MT>) -> BitPackingView<'_, EF> {
435        BitPacking::from_packed(prover_data.table(0).poly(0))
436            .expect("a committed table is a hypercube over a byte-aligned level")
437    }
438
439    /// Check that every opening names the witness's variables and asks for a reading it has.
440    fn check_openings(
441        &self,
442        openings: &[BitOpening<EF>],
443    ) -> Result<(), BooleanPcsError<EF, MT::Error>> {
444        if openings.is_empty() {
445            return Err(BooleanPcsError::NoPoints);
446        }
447        for (index, opening) in openings.iter().enumerate() {
448            if opening.point.num_variables() != self.num_variables {
449                return Err(BooleanPcsError::PointArity {
450                    expected: self.num_variables,
451                    actual: opening.point.num_variables(),
452                });
453            }
454            if !opening.current && !opening.next {
455                return Err(BooleanPcsError::NoReading { index });
456            }
457            if opening.row_variables > self.num_variables {
458                return Err(BooleanPcsError::RowVariables {
459                    index,
460                    row_variables: opening.row_variables,
461                    num_variables: self.num_variables,
462                });
463            }
464        }
465        Ok(())
466    }
467
468    /// Every opening's reduction, gathered into the one batch that reduces them all.
469    fn claims(
470        openings: &[BitOpening<EF>],
471    ) -> Result<BitRingSwitchClaims<EF>, BooleanPcsError<EF, MT::Error>> {
472        let reductions = openings
473            .iter()
474            .map(Self::reduction)
475            .collect::<Result<Vec<_>, _>>()?;
476        BitRingSwitchClaims::new(reductions).map_err(BooleanPcsError::Reduction)
477    }
478
479    /// The reduction answering every reading one opening asks for.
480    ///
481    /// Only the successor reading needs the successor view.
482    /// An opening asking for the current reading alone plays the plain reduction.
483    fn reduction(
484        opening: &BitOpening<EF>,
485    ) -> Result<BitRingSwitch<EF>, BooleanPcsError<EF, MT::Error>> {
486        if opening.next {
487            BitRingSwitch::with_successor(&opening.point, opening.row_variables)
488        } else {
489            BitRingSwitch::new(&opening.point)
490        }
491        .map_err(BooleanPcsError::Reduction)
492    }
493
494    /// Each point as an opening asking for the current reading alone.
495    fn current_openings(points: &[Point<EF>]) -> Vec<BitOpening<EF>> {
496        points
497            .iter()
498            .map(|point| BitOpening {
499                point: point.clone(),
500                row_variables: point.num_variables(),
501                current: true,
502                next: false,
503            })
504            .collect()
505    }
506
507    /// Open the bit witness with the readings every opening asks for, in one proof.
508    ///
509    /// No point needs prior transcript binding: each reduction binds its own.
510    ///
511    /// # Returns
512    ///
513    /// One set of readings per opening, in the order the openings were supplied.
514    /// Each holds a value exactly where its opening asks for one.
515    ///
516    /// # Errors
517    ///
518    /// Before the transcript moves, an opening that:
519    ///
520    /// - does not name the committed function's variables,
521    /// - asks for no reading,
522    /// - or steps within more rows than the witness has.
523    #[allow(clippy::type_complexity)]
524    pub fn open_readings<Challenger>(
525        &self,
526        prover_data: BinaryPcsProverData<EF, EF, MT>,
527        openings: &[BitOpening<EF>],
528        challenger: &mut Challenger,
529    ) -> Result<(Vec<BitReadings<EF>>, BooleanProof<EF, MT, MX>), BooleanPcsError<EF, MT::Error>>
530    where
531        Challenger: FieldChallenger<EF>
532            + GrindingChallenger<Witness = EF>
533            + CanSampleUniformBits<EF>
534            + CanObserve<MT::Commitment>
535            + CanObserve<MX::Commitment>,
536    {
537        self.check_openings(openings)?;
538        // Every reduction is set up before any runs, so a refused one leaves the transcript alone.
539        let claims = Self::claims(openings)?;
540        let packing =
541            tracing::info_span!("read committed packing").in_scope(|| Self::packing(&prover_data));
542
543        // One batch for every opening, leaving one claim about the packing.
544        let (reduction, surviving_point, surviving_value) = tracing::info_span!("bit ring switch")
545            .in_scope(|| {
546                claims.prove::<<EF as ChallengeField<EF>>::SumcheckRepr, _, _>(&packing, challenger)
547            });
548
549        // The elements each claim sends already hold its readings.
550        // Read by columns they are the claimed values, so neither costs a pass of its own.
551        let readings = openings
552            .iter()
553            .zip(claims.reductions())
554            .zip(&reduction.claims)
555            .map(|((opening, switch), elements)| {
556                let current = opening
557                    .current
558                    .then(|| switch.incoming_claim(&elements.tensor));
559                let next = opening
560                    .next
561                    .then(|| switch.successor_claim(&elements.tensor, elements.successor.as_ref()))
562                    .transpose()
563                    .map_err(BooleanPcsError::Reduction)?;
564                Ok(BitReadings { current, next })
565            })
566            .collect::<Result<Vec<_>, BooleanPcsError<EF, MT::Error>>>()?;
567
568        // The surviving value crosses the wire twice, and the closing check is that the two agree.
569        // The surviving point came out of the batch's rounds, so it is bound already.
570        // The rounds folded the packing down to this value, sparing the commitment a pass.
571        let opening = self
572            .inner
573            .try_open_at_known(
574                prover_data,
575                &self.protocol(),
576                &[surviving_point],
577                &[OpeningBatch::new(vec![surviving_value], Vec::new())],
578                challenger,
579            )
580            .map_err(BooleanPcsError::Commitment)?;
581
582        Ok((readings, BooleanProof { reduction, opening }))
583    }
584
585    /// Check one proof against the readings it claims at every opening.
586    ///
587    /// The commitment's binding is the caller's, replayed before this is reached.
588    ///
589    /// # Errors
590    ///
591    /// Before the transcript moves:
592    ///
593    /// - An opening the proving side refuses.
594    /// - Reading or reduction counts that differ from the opening count.
595    /// - A reading present where its opening asks for none, or missing where it asks for one.
596    ///
597    /// After it: a false reading, a failed reduction, the commitment, or an unclosed claim.
598    pub fn verify_readings<Challenger>(
599        &self,
600        commitment: &MT::Commitment,
601        openings: &[BitOpening<EF>],
602        readings: &[BitReadings<EF>],
603        proof: &BooleanProof<EF, MT, MX>,
604        challenger: &mut Challenger,
605    ) -> Result<(), BooleanPcsError<EF, MT::Error>>
606    where
607        Challenger: FieldChallenger<EF>
608            + GrindingChallenger<Witness = EF>
609            + CanSampleUniformBits<EF>
610            + CanObserve<MT::Commitment>
611            + CanObserve<MX::Commitment>,
612    {
613        self.check_openings(openings)?;
614        if readings.len() != openings.len() || proof.reduction.claims.len() != openings.len() {
615            return Err(BooleanPcsError::ClaimCount {
616                expected: openings.len(),
617                values: readings.len(),
618                reductions: proof.reduction.claims.len(),
619            });
620        }
621        // The openings fix which readings are checked, so a missing one is never skipped.
622        if let Some(index) = openings
623            .iter()
624            .zip(readings)
625            .position(|(opening, reading)| {
626                reading.current.is_some() != opening.current
627                    || reading.next.is_some() != opening.next
628            })
629        {
630            return Err(BooleanPcsError::ReadingShape { index });
631        }
632        let claims = Self::claims(openings)?;
633
634        // The batch turns every reading about the bits into one claim about the packing.
635        let readings = readings
636            .iter()
637            .map(|reading| (reading.current, reading.next))
638            .collect::<Vec<_>>();
639        let (surviving_point, surviving_value) = claims
640            .verify_readings(&proof.reduction, &readings, challenger)
641            .map_err(BooleanPcsError::ReductionProof)?;
642
643        // One commitment opening pins the one surviving point to the committed polynomial.
644        let evals = self
645            .inner
646            .verify_at(
647                commitment,
648                &proof.opening,
649                &self.protocol(),
650                &[surviving_point],
651                challenger,
652            )
653            .map_err(BooleanPcsError::Commitment)?;
654
655        // The batch closes against the value opened at its surviving point.
656        //
657        //     batch    ->  t'(r') = s', the claim it left behind
658        //     opening  ->  the value the committed polynomial takes at r'
659        match evals.as_slice() {
660            [batch] if batch.current().first() == Some(&surviving_value) => Ok(()),
661            _ => Err(BooleanPcsError::SurvivingClaim),
662        }
663    }
664}
665
666impl<EF, MT, MX, Challenger> BooleanMultilinearPcs<EF, Challenger> for BooleanPcs<EF, MT, MX>
667where
668    EF: ChallengeField<EF>
669        + EncodableLevel
670        + TranscriptField
671        + TowerLevel
672        + FoldAlphabet<EF>
673        + Coordinates,
674    MT: Mmcs<EF>,
675    MX: Mmcs<EF, Error = MT::Error>,
676    Challenger: FieldChallenger<EF>
677        + GrindingChallenger<Witness = EF>
678        + CanSampleUniformBits<EF>
679        + CanObserve<MT::Commitment>
680        + CanObserve<MX::Commitment>,
681{
682    fn observe_commitment(&self, commitment: &Self::Commitment, challenger: &mut Challenger) {
683        self.inner.observe_commitment(commitment, challenger);
684    }
685
686    fn open_readings(
687        &self,
688        prover_data: Self::ProverData,
689        openings: &[BitOpening<EF>],
690        challenger: &mut Challenger,
691    ) -> Result<(Vec<BitReadings<EF>>, Self::Proof), Self::Error> {
692        Self::open_readings(self, prover_data, openings, challenger)
693    }
694
695    fn verify_readings(
696        &self,
697        commitment: &Self::Commitment,
698        openings: &[BitOpening<EF>],
699        readings: &[BitReadings<EF>],
700        proof: &Self::Proof,
701        challenger: &mut Challenger,
702    ) -> Result<(), Self::Error> {
703        Self::verify_readings(self, commitment, openings, readings, proof, challenger)
704    }
705
706    fn readings_security(
707        &self,
708        num_claims: usize,
709        successor_tensors: bool,
710    ) -> Option<PrescribedOpeningSecurity> {
711        Some(Self::readings_security(self, num_claims, successor_tensors))
712    }
713
714    fn commit_bits<U: Underlier>(
715        &self,
716        bits: &[PackedGf2<U>],
717        challenger: &mut Challenger,
718    ) -> Result<(Self::Commitment, Self::ProverData), Self::Error> {
719        // The packing is one copy of the bits, so the witness is never swept for arithmetic.
720        let stack = PackedStack::<PackedGf2<U>, EF>::from_columns(&[bits])?;
721        if stack.column_num_variables() != self.inner.num_variables() {
722            return Err(BooleanPcsError::WitnessArity {
723                expected: self.inner.num_variables(),
724                actual: stack.column_num_variables(),
725            });
726        }
727
728        let witness = SuffixProver::<EF, EF>::new_witness(vec![stack.into_table()], 0);
729        self.inner
730            .commit(witness, challenger)
731            .map_err(BooleanPcsError::Commitment)
732    }
733
734    fn open_at_points(
735        &self,
736        prover_data: Self::ProverData,
737        points: &[Point<EF>],
738        challenger: &mut Challenger,
739    ) -> Result<(Vec<EF>, Self::Proof), Self::Error> {
740        let (readings, proof) =
741            self.open_readings(prover_data, &Self::current_openings(points), challenger)?;
742        let values = readings
743            .into_iter()
744            .map(|reading| {
745                reading
746                    .current
747                    .expect("an opening asking for the current reading carries it")
748            })
749            .collect();
750        Ok((values, proof))
751    }
752
753    fn verify_at_points(
754        &self,
755        commitment: &Self::Commitment,
756        points: &[Point<EF>],
757        values: &[EF],
758        proof: &Self::Proof,
759        challenger: &mut Challenger,
760    ) -> Result<(), Self::Error> {
761        let readings: Vec<BitReadings<EF>> = values
762            .iter()
763            .map(|&value| BitReadings {
764                current: Some(value),
765                next: None,
766            })
767            .collect();
768        self.verify_readings(
769            commitment,
770            &Self::current_openings(points),
771            &readings,
772            proof,
773            challenger,
774        )
775    }
776}
777
778/// One Boolean opening: the reductions, and the commitment opening that discharges them.
779#[derive(Clone, Serialize, Deserialize)]
780#[serde(bound(
781    serialize = "EF: TowerLevel, MT::Commitment: Serialize, MT::MultiProof: Serialize, MX::Commitment: Serialize, MX::MultiProof: Serialize",
782    deserialize = "EF: TowerLevel, MT::Commitment: Deserialize<'de>, MT::MultiProof: Deserialize<'de>, MX::Commitment: Deserialize<'de>, MX::MultiProof: Deserialize<'de>"
783))]
784pub struct BooleanProof<EF: Field, MT: Mmcs<EF>, MX: Mmcs<EF>> {
785    /// One batched bit-alphabet ring switch, with every claim's elements in the order they came in.
786    pub reduction: BitRingSwitchClaimsProof<EF>,
787    /// The single commitment opening that discharges the one surviving claim.
788    pub opening: BinaryPcsProof<EF, EF, MT, MX>,
789}
790
791/// Why a Boolean commitment or opening was refused.
792#[derive(Debug, Error)]
793#[non_exhaustive]
794pub enum BooleanPcsError<EF, MmcsError> {
795    /// The witness is narrower than the coordinates one element absorbs.
796    #[error("a bit witness of {actual} variables cannot absorb {needed} into one element")]
797    WitnessTooNarrow {
798        /// Coordinates one element absorbs.
799        needed: usize,
800        /// Variables the witness has.
801        actual: usize,
802    },
803
804    /// The schedule was not derived for the level this commitment runs over.
805    #[error(transparent)]
806    Config(BinaryPcsConfigError),
807
808    /// The commitment schedule does not commit the elements the packing holds.
809    #[error("the schedule commits {actual} variables, the packing holds {expected}")]
810    ConfigArity {
811        /// Variables the packing holds.
812        expected: usize,
813        /// Variables the schedule commits.
814        actual: usize,
815    },
816
817    /// The witness does not cover the committed hypercube.
818    #[error("the packed witness has {actual} variables, expected {expected}")]
819    WitnessArity {
820        /// Variables the commitment holds.
821        expected: usize,
822        /// Variables the witness packs to.
823        actual: usize,
824    },
825
826    /// No opening point was supplied, so there is nothing to prove.
827    #[error("an opening needs at least one point")]
828    NoPoints,
829
830    /// The claim counts on the two sides of an opening disagree.
831    #[error("{expected} points against {values} values and {reductions} reductions")]
832    ClaimCount {
833        /// Points supplied.
834        expected: usize,
835        /// Values supplied.
836        values: usize,
837        /// Reductions the proof carries.
838        reductions: usize,
839    },
840
841    /// The opening point does not name the committed function's variables.
842    #[error("the opening point names {actual} variables, expected {expected}")]
843    PointArity {
844        /// Variables the committed function has.
845        expected: usize,
846        /// Variables the point names.
847        actual: usize,
848    },
849
850    /// An opening asks for neither reading, so nothing at its point would be checked.
851    #[error("opening {index} asks for neither the current nor the successor reading")]
852    NoReading {
853        /// Position of the opening among those supplied.
854        index: usize,
855    },
856
857    /// The successor view steps within more coordinates than the witness has.
858    #[error("opening {index} names {row_variables} row variables, the witness {num_variables}")]
859    RowVariables {
860        /// Position of the opening among those supplied.
861        index: usize,
862        /// Row coordinates the opening names.
863        row_variables: usize,
864        /// Variables the committed function has.
865        num_variables: usize,
866    },
867
868    /// A reading is present where its opening asks for none, or missing where it asks for one.
869    #[error("the readings at opening {index} are not the ones it asks for")]
870    ReadingShape {
871        /// Position of the opening among those supplied.
872        index: usize,
873    },
874
875    /// The bits could not be packed into the elements a commitment holds.
876    #[error(transparent)]
877    Packing(#[from] PackError),
878
879    /// The reduction could not be set up over the point supplied.
880    #[error(transparent)]
881    Reduction(p3_sumcheck::ring_switch::bits::BitRingSwitchError),
882
883    /// The reduction's own proof was rejected.
884    #[error(transparent)]
885    ReductionProof(BitRingSwitchProofError),
886
887    /// The commitment refused the opening.
888    #[error(transparent)]
889    Commitment(BinaryPcsError<EF, MmcsError>),
890
891    /// The commitment opened a value the reduction did not leave behind.
892    ///
893    /// Without this the reduction would prove a claim about some other polynomial.
894    #[error("the opened value is not the claim the reduction left behind")]
895    SurvivingClaim,
896}
897
898impl<EF, MT, MX> BooleanBackend<EF> for BooleanPcs<EF, MT, MX>
899where
900    EF: EncodableLevel + TowerLevel,
901    MT: Mmcs<EF>,
902    MX: Mmcs<EF, Error = MT::Error>,
903{
904    type Commitment = MT::Commitment;
905    type ProverData = BinaryPcsProverData<EF, EF, MT>;
906    type Proof = BooleanProof<EF, MT, MX>;
907    type Error = BooleanPcsError<EF, MT::Error>;
908
909    fn num_variables(&self) -> usize {
910        self.num_variables
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    use p3_binary_field::{BinaryField64, BinaryField128, Gf2, Ghash128, PackedGf2x64};
917    use p3_field::PrimeCharacteristicRing;
918    use p3_multilinear_util::poly::Poly;
919    use proptest::prelude::*;
920    use rand::rngs::SmallRng;
921    use rand::{RngExt, SeedableRng};
922
923    use super::*;
924    use crate::params::BinaryPcsParams;
925    use crate::test_util::{MyChallenger, MyMmcs, challenger, mmcs};
926
927    /// A verifying transcript with the commitment's binding already replayed.
928    fn replayed(
929        pcs: &BooleanPcs<EF, MyMmcs, MyMmcs>,
930        commitment: &<MyMmcs as Mmcs<EF>>::Commitment,
931    ) -> MyChallenger {
932        // The surrounding protocol owns this binding, so a test plays its part.
933        let mut challenger = challenger();
934        pcs.observe_commitment(commitment, &mut challenger);
935        challenger
936    }
937
938    type EF = BinaryField128;
939
940    /// Blocks a fixture of the given log bit count holds.
941    const fn blocks(log_bits: usize) -> usize {
942        1 << (log_bits - 6)
943    }
944
945    /// A Boolean commitment over a witness of `log_bits` variables.
946    fn boolean_pcs(log_bits: usize) -> BooleanPcs<EF, MyMmcs, MyMmcs> {
947        let params = BinaryPcsParams {
948            log_inv_rate: 2,
949            pow_bits: 0,
950            security_level: 40,
951        };
952        // The packing absorbs seven variables, so the commitment holds the rest.
953        let config = BinaryPcsConfig::try_new::<EF, EF>(log_bits - 7, params).unwrap();
954        BooleanPcs::new(config, mmcs(), mmcs(), log_bits).unwrap()
955    }
956
957    /// A random bit-sliced witness of the given log bit count.
958    fn witness(seed: u64, log_bits: usize) -> Vec<PackedGf2x64> {
959        let mut rng = SmallRng::seed_from_u64(seed);
960        (0..blocks(log_bits))
961            .map(|_| PackedGf2x64::new(rng.random::<u64>()))
962            .collect()
963    }
964
965    /// The witness as a multilinear over every variable, one element per bit.
966    ///
967    /// The definition of what an opening answers for, with no packing in it.
968    fn embedded(bits: &[PackedGf2x64]) -> Poly<EF> {
969        Poly::new(
970            bits.iter()
971                .flat_map(|block| {
972                    (0..PackedGf2x64::WIDTH).map(move |lane| {
973                        if block.get(lane) == Gf2::ONE {
974                            EF::ONE
975                        } else {
976                            EF::ZERO
977                        }
978                    })
979                })
980                .collect::<Vec<EF>>(),
981        )
982    }
983
984    #[test]
985    fn a_boolean_opening_round_trips_at_the_value_the_bits_define() {
986        // Invariant: an opening answers for the bit witness, not for its packing.
987        //
988        // Fixture state: 2^13 bits, of which one element absorbs 2^7.
989        //
990        //     witness     128 blocks of 64 lanes
991        //     commitment   64 elements, so six variables
992        //
993        // The value is compared against the one-element-per-bit multilinear.
994        // That reference shares nothing with the packing or with the reduction.
995        const LOG_BITS: usize = 13;
996
997        let bits = witness(0xB001, LOG_BITS);
998        let pcs = boolean_pcs(LOG_BITS);
999        assert_eq!(pcs.num_variables(), LOG_BITS);
1000
1001        let mut prover_chal = challenger();
1002        let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
1003
1004        let point = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0xB002), LOG_BITS);
1005        let points = alloc::vec![point.clone()];
1006        let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
1007
1008        assert_eq!(values, alloc::vec![embedded(&bits).eval_base(&point)]);
1009
1010        pcs.verify_at_points(
1011            &commitment,
1012            &points,
1013            &values,
1014            &proof,
1015            &mut replayed(&pcs, &commitment),
1016        )
1017        .unwrap();
1018    }
1019
1020    #[test]
1021    fn several_points_cost_one_commitment_opening() {
1022        // Invariant: every claim survives as one claim, discharged by one opening.
1023        //
1024        //     - reduction    one batch, binding every point before any draw
1025        //     - opening      one, carrying the one opened value at the surviving point
1026        //     - closing      one equality, against that opened value
1027        //
1028        // Folding the four claims under powers of lambda is charged in its own term.
1029        const LOG_BITS: usize = 13;
1030        const NUM_POINTS: usize = 4;
1031
1032        let bits = witness(0xB00F, LOG_BITS);
1033        let pcs = boolean_pcs(LOG_BITS);
1034        let mut rng = SmallRng::seed_from_u64(0xB010);
1035        let points: Vec<Point<EF>> = (0..NUM_POINTS)
1036            .map(|_| Point::<EF>::rand(&mut rng, LOG_BITS))
1037            .collect();
1038
1039        let mut prover_chal = challenger();
1040        let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
1041        let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
1042
1043        // One element per point, and one opened value for all of them.
1044        assert_eq!(proof.reduction.claims.len(), NUM_POINTS);
1045        assert_eq!(proof.opening.evals.len(), 1);
1046
1047        // Each value is the bit witness at its own point.
1048        let reference = embedded(&bits);
1049        for (point, &value) in points.iter().zip(&values) {
1050            assert_eq!(value, reference.eval_base(point));
1051        }
1052
1053        pcs.verify_at_points(
1054            &commitment,
1055            &points,
1056            &values,
1057            &proof,
1058            &mut replayed(&pcs, &commitment),
1059        )
1060        .unwrap();
1061
1062        // One wrong value among four is rejected, wherever it sits.
1063        for index in 0..NUM_POINTS {
1064            let mut tampered = values.clone();
1065            tampered[index] += EF::ONE;
1066            let refused = pcs
1067                .verify_at_points(
1068                    &commitment,
1069                    &points,
1070                    &tampered,
1071                    &proof,
1072                    &mut replayed(&pcs, &commitment),
1073                )
1074                .unwrap_err();
1075            // Reading the claim off the element is the check that refuses a moved value.
1076            assert!(
1077                matches!(
1078                    refused,
1079                    BooleanPcsError::ReductionProof(BitRingSwitchProofError::ClaimMismatch)
1080                ),
1081                "value {index}: {refused:?}"
1082            );
1083        }
1084
1085        // Three labelled terms: the opening, the one batched reduction, and lambda.
1086        let evidence = pcs.opening_security(NUM_POINTS);
1087        let labels: Vec<&str> = evidence.terms.iter().map(|term| term.label).collect();
1088        assert_eq!(
1089            labels,
1090            alloc::vec![
1091                p3_security::binary::BINARY_PCS_OPENING_LABEL,
1092                p3_security::BIT_RING_SWITCH_LABEL,
1093                p3_security::BIT_RING_SWITCH_CLAIM_BATCHING_LABEL,
1094            ]
1095        );
1096
1097        // One reduction of seven draws and six rounds, for all four points.
1098        assert_eq!(
1099            evidence.terms[1].bits.bits(),
1100            p3_security::bit_ring_switch_error(1, 7, 6, 128).bits()
1101        );
1102        // Lambda folds four claims, so its degree is three.
1103        assert_eq!(
1104            evidence.terms[2].bits.bits(),
1105            p3_security::bit_ring_switch_claim_batching_error(NUM_POINTS, 128).bits()
1106        );
1107        // The opening prices one point, as for a single claim.
1108        assert_eq!(
1109            evidence.terms[0].bits.bits(),
1110            pcs.opening_security(1).terms[0].bits.bits()
1111        );
1112    }
1113
1114    #[test]
1115    fn a_false_value_is_rejected() {
1116        // The element's column reading is what ties the claim to the committed bits.
1117        const LOG_BITS: usize = 13;
1118
1119        let bits = witness(0xFA15, LOG_BITS);
1120        let pcs = boolean_pcs(LOG_BITS);
1121        let mut prover_chal = challenger();
1122        let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
1123
1124        let points = alloc::vec![Point::<EF>::rand(
1125            &mut SmallRng::seed_from_u64(0xFA16),
1126            LOG_BITS
1127        )];
1128        let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
1129
1130        let false_values = alloc::vec![values[0] + EF::ONE];
1131        let err = pcs
1132            .verify_at_points(
1133                &commitment,
1134                &points,
1135                &false_values,
1136                &proof,
1137                &mut replayed(&pcs, &commitment),
1138            )
1139            .unwrap_err();
1140        assert!(matches!(err, BooleanPcsError::ReductionProof(_)), "{err:?}");
1141    }
1142
1143    #[test]
1144    fn a_batch_over_another_witness_leaves_a_claim_the_commitment_does_not_open() {
1145        // Invariant: the closing comparison is what ties the batch to the commitment.
1146        //
1147        // Mutation: reduce the claims over witness A and open witness B.
1148        //
1149        //     - transcript  A's batch runs on the sponge that absorbed B's root
1150        //     - reduction   replays and accepts, being a true proof about A
1151        //     - commitment  accepts, being a true opening of B
1152        //     - closing     t'_A(r') != t'_B(r')  ->  the claim does not survive
1153        //
1154        // A fresh sponge would split the transcripts at the first draw and pin nothing.
1155        const LOG_BITS: usize = 13;
1156
1157        let pcs = boolean_pcs(LOG_BITS);
1158        let mut rng = SmallRng::seed_from_u64(0x0A11);
1159        let points = (0..3)
1160            .map(|_| Point::<EF>::rand(&mut rng, LOG_BITS))
1161            .collect::<Vec<_>>();
1162
1163        // Witness A supplies the packing the batch runs over, B the root and the opening.
1164        let (_, data_a) = pcs
1165            .commit_bits(&witness(0xAAAA, LOG_BITS), &mut challenger())
1166            .unwrap();
1167
1168        let mut chal = challenger();
1169        let (root_b, data_b) = pcs
1170            .commit_bits(&witness(0xBBBB, LOG_BITS), &mut chal)
1171            .unwrap();
1172
1173        // The batch is a true proof about A, played on B's sponge.
1174        let packing_a = BooleanPcs::<EF, MyMmcs, MyMmcs>::packing(&data_a);
1175        let claims = BitRingSwitchClaims::new(
1176            points
1177                .iter()
1178                .map(|point| BitRingSwitch::new(point).unwrap())
1179                .collect(),
1180        )
1181        .unwrap();
1182        let (reduction, surviving_point, surviving_value) =
1183            claims.prove::<Ghash128, _, _>(&packing_a, &mut chal);
1184        let values = claims
1185            .reductions()
1186            .iter()
1187            .zip(&reduction.claims)
1188            .map(|(switch, elements)| switch.incoming_claim(&elements.tensor))
1189            .collect::<Vec<_>>();
1190
1191        // The commitment then opens B at the surviving point, where A and B disagree.
1192        let opening = pcs
1193            .inner
1194            .try_open_at(data_b, &pcs.protocol(), &[surviving_point], &mut chal)
1195            .unwrap();
1196        assert_ne!(opening.evals[0].current()[0], surviving_value);
1197
1198        let proof = BooleanProof { reduction, opening };
1199        let err = pcs
1200            .verify_at_points(
1201                &root_b,
1202                &points,
1203                &values,
1204                &proof,
1205                &mut replayed(&pcs, &root_b),
1206            )
1207            .unwrap_err();
1208        assert!(matches!(err, BooleanPcsError::SurvivingClaim), "{err:?}");
1209
1210        // B's own reductions at the same points are accepted, so the rejection is the mismatch.
1211        let mut honest = challenger();
1212        let (root, data) = pcs
1213            .commit_bits(&witness(0xBBBB, LOG_BITS), &mut honest)
1214            .unwrap();
1215        let (honest_values, honest_proof) = pcs.open_at_points(data, &points, &mut honest).unwrap();
1216        pcs.verify_at_points(
1217            &root,
1218            &points,
1219            &honest_values,
1220            &honest_proof,
1221            &mut replayed(&pcs, &root),
1222        )
1223        .unwrap();
1224    }
1225
1226    #[test]
1227    fn a_shape_the_commitment_does_not_hold_is_refused() {
1228        // Every mismatch is reported rather than asserted, because the shapes are inputs.
1229        let params = BinaryPcsParams {
1230            log_inv_rate: 2,
1231            pow_bits: 0,
1232            security_level: 40,
1233        };
1234
1235        // A witness narrower than one element absorbs has no packing at all.
1236        let config = BinaryPcsConfig::try_new::<EF, EF>(6, params).unwrap();
1237        let narrow_witness = BooleanPcs::<EF, MyMmcs, MyMmcs>::new(config, mmcs(), mmcs(), 4).err();
1238        assert!(matches!(
1239            narrow_witness,
1240            Some(BooleanPcsError::WitnessTooNarrow {
1241                needed: 7,
1242                actual: 4
1243            })
1244        ));
1245
1246        // A schedule for another committed field reaches the nested configuration error.
1247        let config = BinaryPcsConfig::try_new::<BinaryField64, EF>(6, params).unwrap();
1248        let wrong_field = BooleanPcs::<EF, MyMmcs, MyMmcs>::new(config, mmcs(), mmcs(), 13).err();
1249        assert!(matches!(
1250            wrong_field,
1251            Some(BooleanPcsError::Config(
1252                BinaryPcsConfigError::CommittedFieldMismatch {
1253                    derived: 64,
1254                    actual: 128,
1255                }
1256            ))
1257        ));
1258
1259        // A schedule committing a different arity than the packing holds is refused.
1260        let config = BinaryPcsConfig::try_new::<EF, EF>(6, params).unwrap();
1261        let wrong_arity = BooleanPcs::<EF, MyMmcs, MyMmcs>::new(config, mmcs(), mmcs(), 14).err();
1262        assert!(matches!(
1263            wrong_arity,
1264            Some(BooleanPcsError::ConfigArity {
1265                expected: 7,
1266                actual: 6
1267            })
1268        ));
1269
1270        // A point of the wrong width reaches neither the reduction nor the commitment.
1271        let pcs = boolean_pcs(13);
1272        let mut chal = challenger();
1273        let (commitment, data) = pcs.commit_bits(&witness(0x5417, 13), &mut chal).unwrap();
1274        let narrow = alloc::vec![Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x5418), 12)];
1275        assert!(matches!(
1276            pcs.open_at_points(data, &narrow, &mut chal).err(),
1277            Some(BooleanPcsError::PointArity {
1278                expected: 13,
1279                actual: 12
1280            })
1281        ));
1282        assert!(matches!(
1283            pcs.verify_at_points(&commitment, &narrow, &[EF::ZERO], &proof_stub(), &mut chal),
1284            Err(BooleanPcsError::PointArity { .. })
1285        ));
1286
1287        // No point at all describes nothing to prove.
1288        let (_, data) = pcs.commit_bits(&witness(0x5419, 13), &mut chal).unwrap();
1289        assert!(matches!(
1290            pcs.open_at_points(data, &[], &mut chal).err(),
1291            Some(BooleanPcsError::NoPoints)
1292        ));
1293    }
1294
1295    /// A proof the width check never looks past.
1296    fn proof_stub() -> BooleanProof<EF, MyMmcs, MyMmcs> {
1297        let bits = witness(0x57AB, 13);
1298        let pcs = boolean_pcs(13);
1299        let mut chal = challenger();
1300        let (_, data) = pcs.commit_bits(&bits, &mut chal).unwrap();
1301        let points = alloc::vec![Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x57AC), 13)];
1302        pcs.open_at_points(data, &points, &mut chal).unwrap().1
1303    }
1304
1305    /// The witness one row further on, the last row repeating, batched over the selector.
1306    ///
1307    /// The definition of the successor reading, with no packing in it:
1308    ///
1309    /// ```text
1310    ///     sum_{c, z} eq(selector, c) * eq(rho, z) * t(c, min(z + 1, max))
1311    /// ```
1312    ///
1313    /// The selector leads the point and the rows trail it, `row_variables` of them.
1314    fn dense_successor_reading(cells: &Poly<EF>, point: &Point<EF>, row_variables: usize) -> EF {
1315        let (selector, rho) = point.split_at(point.num_variables() - row_variables);
1316        let eq_selector = Poly::<EF>::new_from_point(selector.as_slice(), EF::ONE);
1317        let eq_rho = Poly::<EF>::new_from_point(rho.as_slice(), EF::ONE);
1318        let rows = 1usize << row_variables;
1319        let mut reading = EF::ZERO;
1320        for (c, &gate) in eq_selector.as_slice().iter().enumerate() {
1321            for (z, &weight) in eq_rho.as_slice().iter().enumerate() {
1322                let x = (z + 1).min(rows - 1);
1323                reading += gate * weight * cells.as_slice()[c * rows + x];
1324            }
1325        }
1326        reading
1327    }
1328
1329    /// Three openings, one per view: current alone, next alone, and both.
1330    ///
1331    /// ```text
1332    ///     opening 0   current,         no successor view
1333    ///     opening 1   next,            10 row variables, so carry and last travel
1334    ///     opening 2   current + next,   3 row variables, so the view is a column reading
1335    /// ```
1336    fn mixed_openings(seed: u64, log_bits: usize) -> Vec<BitOpening<EF>> {
1337        let mut rng = SmallRng::seed_from_u64(seed);
1338        [(log_bits, true, false), (10, false, true), (3, true, true)]
1339            .into_iter()
1340            .map(|(row_variables, current, next)| BitOpening {
1341                point: Point::<EF>::rand(&mut rng, log_bits),
1342                row_variables,
1343                current,
1344                next,
1345            })
1346            .collect()
1347    }
1348
1349    #[test]
1350    fn a_successor_reading_round_trips_at_the_value_the_bits_define() {
1351        // Invariant: both readings answer for the bit witness, whatever the row count.
1352        //
1353        // Fixture state: 2^13 bits, of which one element absorbs 2^7.
1354        //
1355        //     rows  0        the successor view reads the point itself
1356        //     rows  3, 7     the rows fit in one element, so no successor elements travel
1357        //     rows 10, 13    the rows outrun one element, so carry and last travel
1358        //
1359        // Both values are compared against the one-element-per-bit multilinear.
1360        const LOG_BITS: usize = 13;
1361
1362        let bits = witness(0x5ACC, LOG_BITS);
1363        let reference = embedded(&bits);
1364        let pcs = boolean_pcs(LOG_BITS);
1365        let point = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x5ACD), LOG_BITS);
1366
1367        let mut committed = challenger();
1368        let (commitment, data) = pcs.commit_bits(&bits, &mut committed).unwrap();
1369
1370        for row_variables in [0, 3, 7, 10, 13] {
1371            let openings = alloc::vec![BitOpening {
1372                point: point.clone(),
1373                row_variables,
1374                current: true,
1375                next: true,
1376            }];
1377            let mut prover_chal = committed.clone();
1378            let (readings, proof) = pcs
1379                .open_readings(data.clone(), &openings, &mut prover_chal)
1380                .unwrap();
1381
1382            assert_eq!(
1383                readings,
1384                alloc::vec![BitReadings {
1385                    current: Some(reference.eval_base(&point)),
1386                    next: Some(dense_successor_reading(&reference, &point, row_variables)),
1387                }],
1388                "rows {row_variables}"
1389            );
1390            // The successor elements travel exactly when the rows outrun one element.
1391            assert_eq!(
1392                proof.reduction.claims[0].successor.is_some(),
1393                row_variables > BitRingSwitch::<EF>::ABSORBED,
1394                "rows {row_variables}"
1395            );
1396
1397            pcs.verify_readings(
1398                &commitment,
1399                &openings,
1400                &readings,
1401                &proof,
1402                &mut replayed(&pcs, &commitment),
1403            )
1404            .unwrap();
1405        }
1406    }
1407
1408    #[test]
1409    fn several_openings_with_mixed_views_cost_one_commitment_opening() {
1410        // Invariant: each opening leaves one surviving claim, whichever readings it asks for.
1411        //
1412        //     - reduction    one batch, each claim answering every reading it asks for
1413        //     - opening      one, carrying the one opened value the batch survives with
1414        //     - readings     present exactly where asked, at the values the bits define
1415        const LOG_BITS: usize = 13;
1416
1417        let bits = witness(0x3A1D, LOG_BITS);
1418        let reference = embedded(&bits);
1419        let pcs = boolean_pcs(LOG_BITS);
1420        let openings = mixed_openings(0x3A1E, LOG_BITS);
1421
1422        let mut prover_chal = challenger();
1423        let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
1424        let (readings, proof) = pcs
1425            .open_readings(data, &openings, &mut prover_chal)
1426            .unwrap();
1427
1428        // One element per opening, and one opened value for all of them.
1429        assert_eq!(proof.reduction.claims.len(), 3);
1430        assert_eq!(proof.opening.evals.len(), 1);
1431        for batch in &proof.opening.evals {
1432            assert_eq!(batch.current().len(), 1);
1433            assert!(batch.next().is_empty());
1434        }
1435
1436        for (index, (opening, reading)) in openings.iter().zip(&readings).enumerate() {
1437            let current = opening.current.then(|| reference.eval_base(&opening.point));
1438            let next = opening.next.then(|| {
1439                dense_successor_reading(&reference, &opening.point, opening.row_variables)
1440            });
1441            assert_eq!(*reading, BitReadings { current, next }, "opening {index}");
1442        }
1443
1444        pcs.verify_readings(
1445            &commitment,
1446            &openings,
1447            &readings,
1448            &proof,
1449            &mut replayed(&pcs, &commitment),
1450        )
1451        .unwrap();
1452    }
1453
1454    #[test]
1455    fn a_false_successor_reading_is_rejected() {
1456        // The successor reading is tied to the elements' columns, as the current one is.
1457        //
1458        //     rows  3    read off the tensor alone
1459        //     rows 10    read off the tensor, carry and last
1460        const LOG_BITS: usize = 13;
1461
1462        let bits = witness(0xFA17, LOG_BITS);
1463        let pcs = boolean_pcs(LOG_BITS);
1464        let point = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0xFA18), LOG_BITS);
1465
1466        for row_variables in [3, 10] {
1467            let mut prover_chal = challenger();
1468            let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
1469            let openings = alloc::vec![BitOpening {
1470                point: point.clone(),
1471                row_variables,
1472                current: true,
1473                next: true,
1474            }];
1475            let (readings, proof) = pcs
1476                .open_readings(data, &openings, &mut prover_chal)
1477                .unwrap();
1478
1479            let false_readings = alloc::vec![BitReadings {
1480                next: readings[0].next.map(|next| next + EF::ONE),
1481                ..readings[0]
1482            }];
1483            let err = pcs
1484                .verify_readings(
1485                    &commitment,
1486                    &openings,
1487                    &false_readings,
1488                    &proof,
1489                    &mut replayed(&pcs, &commitment),
1490                )
1491                .unwrap_err();
1492            assert!(
1493                matches!(
1494                    err,
1495                    BooleanPcsError::ReductionProof(
1496                        BitRingSwitchProofError::SuccessorClaimMismatch
1497                    )
1498                ),
1499                "rows {row_variables}: {err:?}"
1500            );
1501        }
1502    }
1503
1504    #[test]
1505    fn the_point_api_is_the_current_reading_api() {
1506        // Invariant: a point is an opening asking for the current reading alone.
1507        //
1508        // Fixture state: one committed witness, and two copies of the sponge that committed it.
1509        //
1510        // Equal proof bytes do not show the transcripts stayed in step on their own.
1511        // Two sponges that diverged could still have produced the same bytes.
1512        // So the next draw of each is compared as well, on both sides.
1513        const LOG_BITS: usize = 13;
1514        const NUM_POINTS: usize = 3;
1515
1516        let bits = witness(0x9017, LOG_BITS);
1517        let pcs = boolean_pcs(LOG_BITS);
1518        let mut rng = SmallRng::seed_from_u64(0x9018);
1519        let points: Vec<Point<EF>> = (0..NUM_POINTS)
1520            .map(|_| Point::<EF>::rand(&mut rng, LOG_BITS))
1521            .collect();
1522        let openings: Vec<BitOpening<EF>> = points
1523            .iter()
1524            .map(|point| BitOpening {
1525                point: point.clone(),
1526                row_variables: point.num_variables(),
1527                current: true,
1528                next: false,
1529            })
1530            .collect();
1531
1532        let mut committed = challenger();
1533        let (commitment, data) = pcs.commit_bits(&bits, &mut committed).unwrap();
1534
1535        let mut point_chal = committed.clone();
1536        let (values, point_proof) = pcs
1537            .open_at_points(data.clone(), &points, &mut point_chal)
1538            .unwrap();
1539        let mut reading_chal = committed;
1540        let (readings, reading_proof) = pcs
1541            .open_readings(data, &openings, &mut reading_chal)
1542            .unwrap();
1543
1544        assert_eq!(
1545            postcard::to_allocvec(&point_proof).unwrap(),
1546            postcard::to_allocvec(&reading_proof).unwrap()
1547        );
1548        let expected: Vec<BitReadings<EF>> = values
1549            .iter()
1550            .map(|&value| BitReadings {
1551                current: Some(value),
1552                next: None,
1553            })
1554            .collect();
1555        assert_eq!(readings, expected);
1556        assert_eq!(
1557            point_chal.sample_algebra_element::<EF>(),
1558            reading_chal.sample_algebra_element::<EF>()
1559        );
1560
1561        // The two verifying routes leave their transcripts in step as well.
1562        let mut point_verifier = replayed(&pcs, &commitment);
1563        pcs.verify_at_points(
1564            &commitment,
1565            &points,
1566            &values,
1567            &point_proof,
1568            &mut point_verifier,
1569        )
1570        .unwrap();
1571        let mut reading_verifier = replayed(&pcs, &commitment);
1572        pcs.verify_readings(
1573            &commitment,
1574            &openings,
1575            &readings,
1576            &reading_proof,
1577            &mut reading_verifier,
1578        )
1579        .unwrap();
1580        assert_eq!(
1581            point_verifier.sample_algebra_element::<EF>(),
1582            reading_verifier.sample_algebra_element::<EF>()
1583        );
1584    }
1585
1586    #[test]
1587    fn successor_tensors_are_priced() {
1588        // Invariant: a reduction sending carry and last pays for batching three elements.
1589        //
1590        //     one element      (d_log + 2 l') / |EF|
1591        //     three elements   (d_log + 2 + 2 l') / |EF|
1592        //
1593        // The point API asks for no successor view, so it pays the one-element price.
1594        let pcs = boolean_pcs(13);
1595        let with = pcs.readings_security(1, true);
1596        let without = pcs.readings_security(1, false);
1597
1598        // The commitment's own term does not depend on the elements a reduction sends.
1599        assert_eq!(with.terms[0], without.terms[0]);
1600        assert_eq!(with.terms[1].label, p3_security::BIT_RING_SWITCH_LABEL);
1601        assert!(with.terms[1].bits.bits() < without.terms[1].bits.bits());
1602
1603        // One reduction of seven draws, two powers of alpha and six rounds.
1604        assert_eq!(
1605            with.terms[1].bits.bits(),
1606            p3_security::bit_ring_switch_tensors_error(1, 3, 7, 6, 128).bits()
1607        );
1608
1609        let points = pcs.opening_security(1);
1610        assert_eq!(points.terms, without.terms);
1611        assert_eq!(points.log2_max_candidates, without.log2_max_candidates);
1612    }
1613
1614    #[test]
1615    fn a_reading_shape_the_opening_did_not_ask_for_is_refused() {
1616        // Invariant: a reading is present exactly where its opening asks for one.
1617        //
1618        // Mutation: add or drop one reading, at each opening in turn.
1619        //
1620        // The refusal comes before the transcript moves.
1621        // So the sponge still draws what one that only replayed the binding draws.
1622        const LOG_BITS: usize = 13;
1623
1624        let pcs = boolean_pcs(LOG_BITS);
1625        let openings = mixed_openings(0x54A9, LOG_BITS);
1626        let mut prover_chal = challenger();
1627        let (commitment, data) = pcs
1628            .commit_bits(&witness(0x5AA0, LOG_BITS), &mut prover_chal)
1629            .unwrap();
1630        let (readings, proof) = pcs
1631            .open_readings(data, &openings, &mut prover_chal)
1632            .unwrap();
1633
1634        let stray = EF::ONE;
1635        let mutations = [
1636            (
1637                0,
1638                BitReadings {
1639                    next: Some(stray),
1640                    ..readings[0]
1641                },
1642            ),
1643            (
1644                0,
1645                BitReadings {
1646                    current: None,
1647                    ..readings[0]
1648                },
1649            ),
1650            (
1651                1,
1652                BitReadings {
1653                    current: Some(stray),
1654                    ..readings[1]
1655                },
1656            ),
1657            (
1658                2,
1659                BitReadings {
1660                    next: None,
1661                    ..readings[2]
1662                },
1663            ),
1664        ];
1665        for (index, reading) in mutations {
1666            let mut tampered = readings.clone();
1667            tampered[index] = reading;
1668            let mut verifier = replayed(&pcs, &commitment);
1669            let err = pcs
1670                .verify_readings(&commitment, &openings, &tampered, &proof, &mut verifier)
1671                .unwrap_err();
1672            assert!(
1673                matches!(err, BooleanPcsError::ReadingShape { index: refused } if refused == index),
1674                "{err:?}"
1675            );
1676            assert_eq!(
1677                verifier.sample_algebra_element::<EF>(),
1678                replayed(&pcs, &commitment).sample_algebra_element::<EF>(),
1679                "opening {index}"
1680            );
1681        }
1682    }
1683
1684    #[test]
1685    fn an_opening_the_witness_cannot_answer_is_refused() {
1686        // Invariant: an opening asks for some reading, over rows the witness has.
1687        //
1688        //     no reading       nothing would be checked
1689        //     too many rows    the successor view would step past the witness
1690        //
1691        // Each is refused at its own index, on both sides, before the transcript moves.
1692        const LOG_BITS: usize = 13;
1693
1694        let pcs = boolean_pcs(LOG_BITS);
1695        let mut chal = challenger();
1696        let (commitment, data) = pcs
1697            .commit_bits(&witness(0xA5C0, LOG_BITS), &mut chal)
1698            .unwrap();
1699        let mut rng = SmallRng::seed_from_u64(0xA5C1);
1700        let answered = BitOpening {
1701            point: Point::<EF>::rand(&mut rng, LOG_BITS),
1702            row_variables: LOG_BITS,
1703            current: true,
1704            next: false,
1705        };
1706        let silent = BitOpening {
1707            point: Point::<EF>::rand(&mut rng, LOG_BITS),
1708            row_variables: LOG_BITS,
1709            current: false,
1710            next: false,
1711        };
1712        let past_the_witness = BitOpening {
1713            point: Point::<EF>::rand(&mut rng, LOG_BITS),
1714            row_variables: LOG_BITS + 1,
1715            current: false,
1716            next: true,
1717        };
1718        let readings = [
1719            BitReadings {
1720                current: Some(EF::ZERO),
1721                next: None,
1722            },
1723            BitReadings {
1724                current: None,
1725                next: Some(EF::ZERO),
1726            },
1727        ];
1728
1729        let silent_openings = [answered.clone(), silent];
1730        let wide_openings = [answered, past_the_witness];
1731        let mut before = chal.clone();
1732
1733        let err = pcs
1734            .open_readings(data.clone(), &silent_openings, &mut chal)
1735            .err();
1736        assert!(
1737            matches!(err, Some(BooleanPcsError::NoReading { index: 1 })),
1738            "{err:?}"
1739        );
1740        let err = pcs.open_readings(data, &wide_openings, &mut chal).err();
1741        assert!(
1742            matches!(
1743                err,
1744                Some(BooleanPcsError::RowVariables {
1745                    index: 1,
1746                    row_variables: 14,
1747                    num_variables: 13,
1748                })
1749            ),
1750            "{err:?}"
1751        );
1752        assert_eq!(
1753            chal.sample_algebra_element::<EF>(),
1754            before.sample_algebra_element::<EF>()
1755        );
1756
1757        let proof = proof_stub();
1758        let mut verifier = replayed(&pcs, &commitment);
1759        let err = pcs
1760            .verify_readings(
1761                &commitment,
1762                &silent_openings,
1763                &readings,
1764                &proof,
1765                &mut verifier,
1766            )
1767            .unwrap_err();
1768        assert!(
1769            matches!(err, BooleanPcsError::NoReading { index: 1 }),
1770            "{err:?}"
1771        );
1772        let err = pcs
1773            .verify_readings(
1774                &commitment,
1775                &wide_openings,
1776                &readings,
1777                &proof,
1778                &mut verifier,
1779            )
1780            .unwrap_err();
1781        assert!(
1782            matches!(err, BooleanPcsError::RowVariables { index: 1, .. }),
1783            "{err:?}"
1784        );
1785        assert_eq!(
1786            verifier.sample_algebra_element::<EF>(),
1787            replayed(&pcs, &commitment).sample_algebra_element::<EF>()
1788        );
1789    }
1790
1791    proptest! {
1792        // Each case runs a whole commitment and opening on both sides.
1793        #![proptest_config(ProptestConfig { cases: 8, ..ProptestConfig::default() })]
1794
1795        /// Every witness height the fixture admits, at random points each time.
1796        #[test]
1797        fn a_boolean_opening_round_trips_over_random_inputs(
1798            log_bits in 13usize..=15,
1799            num_points in 1usize..=3,
1800            witness_seed: u64,
1801            point_seed: u64,
1802        ) {
1803            let bits = witness(witness_seed, log_bits);
1804            let pcs = boolean_pcs(log_bits);
1805
1806            let mut prover_chal = challenger();
1807            let (commitment, data) = pcs.commit_bits(&bits, &mut prover_chal).unwrap();
1808            let mut rng = SmallRng::seed_from_u64(point_seed);
1809            let points: Vec<Point<EF>> = (0..num_points)
1810                .map(|_| Point::<EF>::rand(&mut rng, log_bits))
1811                .collect();
1812            let (values, proof) = pcs.open_at_points(data, &points, &mut prover_chal).unwrap();
1813
1814            let reference = embedded(&bits);
1815            for (point, &value) in points.iter().zip(&values) {
1816                prop_assert_eq!(value, reference.eval_base(point));
1817            }
1818            prop_assert!(
1819                pcs.verify_at_points(
1820                    &commitment,
1821                    &points,
1822                    &values,
1823                    &proof,
1824                    &mut replayed(&pcs, &commitment)
1825                )
1826                .is_ok()
1827            );
1828        }
1829    }
1830}