Skip to main content

p3_sumcheck/ring_switch/bits/
reduction.rs

1//! One ring-switching reduction at a bit alphabet, end to end.
2//!
3//! The five values the reduction is built from, the messages it sends, and the two sides that run it.
4//!
5//! Both sides are methods, because here the reduction has a type to hang them on.
6//! The sibling module's are free functions because there the reduction has none.
7
8use alloc::vec::Vec;
9use core::borrow::Borrow;
10
11use p3_binary_field::TowerLevel;
12use p3_challenger::fs::TranscriptField;
13use p3_challenger::{FieldChallenger, GrindingChallenger};
14use p3_field::{Field, PackedValue};
15use p3_maybe_rayon::prelude::*;
16use p3_multilinear_util::point::Point;
17use p3_multilinear_util::poly::Poly;
18use serde::{Deserialize, Serialize};
19
20use super::basis::{Coefficients, CoordinateSums};
21use super::equality::{FactoredEquality, scaled_sums_into};
22use super::packing::BitPacking;
23use super::products::LeftFactors;
24use super::tensor::BitTensor;
25use super::transcript::{
26    BitRingSwitchProverTranscript, BitRingSwitchShape, BitRingSwitchVerifierTranscript,
27    TranscriptWidth,
28};
29use crate::data::SumcheckData;
30use crate::error::SumcheckError;
31use crate::strategy::{Basis, FromTable, IntoTranscriptField, ReprSumcheckProver, VariableOrder};
32use crate::transcript::{ProverTranscript, SumcheckShape};
33
34/// Log of the hypercube points one task accumulates before its partial combines.
35///
36/// Large enough that the `d`-element accumulator and the block's own scaling are amortised,
37/// and small enough both to keep every core fed at the heights this reduction runs at and to
38/// keep the equality factor a block reads within cache.
39#[cfg(not(test))]
40const LOG_CHUNK: usize = PRODUCTION_LOG_CHUNK;
41
42/// Small enough under test that a case above one variable splits in chunks.
43///
44/// The partial-combination fold is then on the path the tests take.
45#[cfg(test)]
46const LOG_CHUNK: usize = 1;
47
48/// The value `LOG_CHUNK` takes outside test builds.
49const PRODUCTION_LOG_CHUNK: usize = 14;
50
51/// The hypercube points one task accumulates before its partial combines.
52const CHUNK: usize = 1 << LOG_CHUNK;
53
54/// Deepest compact depth the production path requests.
55///
56/// A run too small for it steps down to the deepest depth its size floor admits.
57const COMPACT_PRODUCTION_ROUNDS: usize = 4;
58/// Shallowest compact depth the production path requests.
59///
60/// A run too small for it takes the dense path.
61const COMPACT_PRODUCTION_MIN_ROUNDS: usize = 2;
62/// The generic compact driver is intentionally bounded while its ranked shapes are measured.
63const COMPACT_MAX_ROUNDS: usize = 6;
64/// Keep at least this many equality blocks per bank in the production path.
65const COMPACT_MIN_BLOCK_BITS: usize = 6;
66/// The materializer keeps a fixed total EF scratch budget across all compact banks.
67#[cfg(not(test))]
68const COMPACT_TOTAL_SCRATCH: usize = 1 << 14;
69
70/// Small enough under test that the materializer splits a tail in several chunks.
71///
72/// The bank offsets of every chunk past the first are then on the path the tests take.
73#[cfg(test)]
74const COMPACT_TOTAL_SCRATCH: usize = 1 << 4;
75
76/// The fewest free variables an unforced compact run of `requested_k` rounds accepts.
77///
78/// Each of its `2^requested_k` banks then spans at least `2^COMPACT_MIN_BLOCK_BITS` equality
79/// chunks of `2^log_chunk` points.
80const fn compact_size_floor(requested_k: usize, log_chunk: usize) -> usize {
81    requested_k + log_chunk + COMPACT_MIN_BLOCK_BITS
82}
83
84/// Bind prefix variables in one task-owned buffer without invoking Rayon recursively.
85fn bind_scratch_prefix<R: Field>(values: &mut Vec<R>, challenges: &[R]) {
86    for &challenge in challenges {
87        let half = values.len() / 2;
88        assert!(half > 0, "a compact prefix must leave a variable to bind");
89        let (low, high) = values.split_at_mut(half);
90        let (low_packed, low_tail) = R::Packing::pack_slice_with_suffix_mut(low);
91        let (high_packed, high_tail) = R::Packing::pack_slice_with_suffix(high);
92        let challenge_packed = R::Packing::broadcast(challenge);
93        for (low, &high) in low_packed.iter_mut().zip(high_packed) {
94            let old = *low;
95            *low = old + (high - old) * challenge_packed;
96        }
97        for (low, &high) in low_tail.iter_mut().zip(high_tail) {
98            *low += (high - *low) * challenge;
99        }
100        values.truncate(half);
101    }
102}
103
104/// Materialize the witness after binding the compact head, using one fixed total scratch budget.
105fn materialize_compact_packing<EF, R, S>(
106    packing: &BitPacking<EF, S>,
107    offset: usize,
108    n: usize,
109    compact_rounds: usize,
110    challenges: &[EF],
111) -> Poly<R>
112where
113    EF: TowerLevel + Send + Sync,
114    R: IntoTranscriptField<EF> + Sync,
115    S: Borrow<[EF]>,
116{
117    let _span = tracing::debug_span!("compact_packing").entered();
118    let banks = 1usize << compact_rounds;
119    let tail_evals = 1usize << (n - compact_rounds);
120    let output_chunk = (COMPACT_TOTAL_SCRATCH >> compact_rounds).max(1);
121    let prefix_challenges = challenges.iter().copied().map(R::from).collect::<Vec<_>>();
122    let source = packing.poly().as_slice();
123    let mut output = R::zero_vec(tail_evals);
124    let output_chunk_len = output_chunk;
125    output
126        .par_chunks_mut(output_chunk)
127        .enumerate()
128        .for_each(|(chunk_index, output_chunk)| {
129            let start = chunk_index * output_chunk_len;
130            let mut scratch = Vec::with_capacity(banks * output_chunk.len());
131            for bank in 0..banks {
132                let source_start = offset + bank * tail_evals + start;
133                scratch.extend_from_slice(&source[source_start..source_start + output_chunk.len()]);
134            }
135            let mut converted = R::from_table(scratch);
136            bind_scratch_prefix(&mut converted, &prefix_challenges);
137            output_chunk.copy_from_slice(&converted[..output_chunk.len()]);
138        });
139    Poly::new(output)
140}
141
142/// The `len` packed elements from `offset` on, copied out of the packing and held in `R`.
143///
144/// Only the selected slot feeds a sumcheck.
145/// This avoids cloning and folding unrelated columns of a stacked trace.
146pub(super) fn slot_packing<EF, R, S>(
147    packing: &BitPacking<EF, S>,
148    offset: usize,
149    len: usize,
150) -> Poly<R>
151where
152    EF: TowerLevel,
153    R: FromTable<EF>,
154    S: Borrow<[EF]>,
155{
156    let values = &packing.poly().as_slice()[offset..offset + len];
157    // The copy below is the only pass over the buffer, for a level handing back a zeroed
158    // allocation rather than writing one element at a time. A level taking the trait's
159    // default fills the slot serially first, at the width of the slot.
160    let mut slot = EF::zero_vec(len);
161    slot.par_chunks_mut(CHUNK)
162        .zip(values.par_chunks(CHUNK))
163        .for_each(|(slot, values)| slot.copy_from_slice(values));
164    Poly::new(R::from_table(slot))
165}
166
167/// One ring-switching reduction at a bit alphabet, before the batching draw.
168///
169/// # Overview
170///
171/// The reduction turns a claim about a bit witness into one about its packing:
172///
173/// ```text
174///     t(r) = s        a claim about the bits
175///       ->  one degree-two sumcheck of at most l' rounds
176///     t'(r') = s'     a claim about the packing, which a commitment answers
177/// ```
178///
179/// # The order the protocol fixes
180///
181/// The batching challenge is drawn after the tensor element is bound.
182/// So it cannot be a constructor argument.
183///
184/// ```text
185///     new(r)                              tensor, incoming_claim
186///     with_successor(r, a)                also successor_tensors, successor_claim
187///     bind                                r, then every element, then draw r'' [and alpha]
188///     batch(r'')                          initial_sum, closing_weight, weights
189///     batch_with_successor(r'', alpha)    the same, when successor elements are sent
190/// ```
191///
192/// Drawing `r''` first is unsound.
193///
194/// A bit matrix solving two `F_2`-linear systems moves the claim, sum held.
195/// The rounds and the closing check then accept a true surviving claim.
196///
197/// # What the split settles, and what it leaves
198///
199/// It settles that `tensor` never needs the challenge.
200/// Nothing is pushed into drawing one early just to obtain an element.
201///
202/// It does not settle the order.
203///
204/// The second stage borrows this one alone, so a caller can reach it first.
205/// The forgery goes through with the element formed last.
206///
207/// Only a transcript binding `r` and the element before `r''` fixes that.
208/// The two sides below own that transcript, so the ordering test is theirs.
209///
210/// # What it costs a verifier
211///
212/// Nothing witness-sized.
213/// This stage holds the kept coordinates and one `d`-entry table.
214///
215/// Only `tensor`, `successor_tensors` and `weights` touch the `2^l'` equality table.
216/// All three are prover-side, so each builds it rather than every caller paying.
217///
218/// # Why a bit alphabet is different
219///
220/// A coordinate is one bit, so a product by a coordinate is a conditional add.
221///
222/// Both the tensor accumulation and the weight multilinear are subset sums.
223/// A general alphabet needs `d` multiplications per hypercube point instead.
224///
225/// # The successor view
226///
227/// The same reduction can also answer the witness one row on, the last row repeating.
228/// The rows are the trailing `a` coordinates of the point; the selector leads it.
229///
230/// The successor weight splits at the packing boundary into three kept-side weights.
231/// With `x(w)` the kept row bits of `w`:
232///
233/// ```text
234///     eq      eq(r_high, w)                        the +1 settles inside the element
235///     carry   eq(r_high, w - 1)  where x(w) != 0   the +1 ripples out of the element
236///     last    eq(r_high, w)      where x(w) = max  the last row repeats
237/// ```
238///
239/// The first is the tensor above, so the view adds two elements and no sumcheck.
240/// Rows that fit in one element keep no row bits, and the view is a column reading alone.
241///
242/// ```text
243///     s_next = sum_{v >= 1} eq_low[v - 1] * col_v(tensor)
244///            + eq_low[d - 1] * ( col_0(carry) + col_{d-1}(last) )
245/// ```
246#[derive(Clone, Debug)]
247pub struct BitRingSwitch<EF> {
248    /// The evaluation point of the claim, over every variable of the witness.
249    ///
250    /// Held whole because the transcript binds it whole.
251    point: Point<EF>,
252    /// The equality table of the coordinates one packed element absorbs.
253    eq_low: Poly<EF>,
254    /// The trailing coordinates the successor view steps within, when there is one.
255    successor: Option<usize>,
256}
257
258impl<EF: TowerLevel> BitRingSwitch<EF> {
259    /// Number of coordinates one packed element absorbs.
260    pub const ABSORBED: usize = Coefficients::<EF>::LOG_DIMENSION;
261
262    /// Set up the reduction of a claim at one point.
263    ///
264    /// The batching challenge is not an argument.
265    /// It belongs after the element is bound, which the second stage is for.
266    ///
267    /// # Arguments
268    ///
269    /// The evaluation point of the claim, over every variable of the witness.
270    ///
271    /// # Errors
272    ///
273    /// Returns an error when the point is narrower than one element absorbs.
274    pub fn new(r: &Point<EF>) -> Result<Self, BitRingSwitchError> {
275        if r.num_variables() < Self::ABSORBED {
276            return Err(BitRingSwitchError::PointTooNarrow {
277                needed: Self::ABSORBED,
278                actual: r.num_variables(),
279            });
280        }
281
282        // The packing keeps the leading coordinates and absorbs the rest.
283        let (_, low) = r.split_at(r.num_variables() - Self::ABSORBED);
284
285        Ok(Self {
286            eq_low: Poly::new_from_point(low.as_slice(), EF::ONE),
287            point: r.clone(),
288            successor: None,
289        })
290    }
291
292    /// Set up the reduction of a claim at one point, also answering its successor view.
293    ///
294    /// The successor view reads the witness one row on, the last row repeating.
295    /// It steps within the trailing `row_variables` coordinates of the point.
296    /// The coordinates before them select a column and never move.
297    ///
298    /// # Errors
299    ///
300    /// - The row count exceeds the coordinates the point names.
301    /// - The point is narrower than one element absorbs.
302    pub fn with_successor(r: &Point<EF>, row_variables: usize) -> Result<Self, BitRingSwitchError> {
303        if row_variables > r.num_variables() {
304            return Err(BitRingSwitchError::RowVariables {
305                row_variables,
306                num_variables: r.num_variables(),
307            });
308        }
309        Ok(Self {
310            successor: Some(row_variables),
311            ..Self::new(r)?
312        })
313    }
314
315    /// Variables the packed polynomial this reduction runs against must have.
316    ///
317    /// This is the maximum number of sumcheck rounds the reduction takes.
318    /// Each leading Boolean selector coordinate selects a slot and removes one round.
319    pub fn num_variables(&self) -> usize {
320        self.point.num_variables() - Self::ABSORBED
321    }
322
323    /// The evaluation point of the claim, over every variable of the witness.
324    pub(super) const fn point(&self) -> &Point<EF> {
325        &self.point
326    }
327
328    /// The coordinates of the evaluation point the packing keeps.
329    ///
330    /// They lead the point, so this is a borrow rather than a split.
331    pub(super) fn high(&self) -> &[EF] {
332        &self.point.as_slice()[..self.num_variables()]
333    }
334
335    /// Row coordinates of the successor view the packing keeps, when there are any.
336    ///
337    /// Rows that fit in one element keep none, and the view is a column reading.
338    fn kept_row_variables(&self) -> Option<usize> {
339        self.successor
340            .filter(|&rows| rows > Self::ABSORBED)
341            .map(|rows| rows - Self::ABSORBED)
342    }
343
344    /// Whether the reduction sends the carry and last elements beside the tensor.
345    ///
346    /// True exactly for a successor view whose rows outrun one element.
347    #[must_use]
348    pub fn sends_successor_tensors(&self) -> bool {
349        self.kept_row_variables().is_some()
350    }
351
352    /// The transcript description this reduction plays.
353    pub(super) fn shape(&self) -> BitRingSwitchShape {
354        let num_variables = self.point.num_variables();
355        match self.successor {
356            Some(rows) if self.sends_successor_tensors() => {
357                BitRingSwitchShape::with_successor_rows(num_variables, rows)
358            }
359            _ => BitRingSwitchShape::new(num_variables),
360        }
361    }
362
363    /// Move on to the stage the batching challenge opens.
364    ///
365    /// # Errors
366    ///
367    /// - The reduction sends successor elements, which [`Self::batch_with_successor`] batches.
368    /// - The challenge does not name the absorbed coordinates.
369    pub fn batch<'a>(
370        &'a self,
371        r_batch: &Point<EF>,
372    ) -> Result<BitRingSwitchBatch<'a, EF>, BitRingSwitchError> {
373        if self.sends_successor_tensors() {
374            return Err(BitRingSwitchError::SuccessorBatching { expected: true });
375        }
376        self.batch_stage(r_batch, None)
377    }
378
379    /// Move on to the stage the batching challenges open, successor elements included.
380    ///
381    /// `alpha` weighs the three elements' batched rows into one sum:
382    ///
383    /// ```text
384    ///     rows(tensor) + alpha * rows(carry) + alpha^2 * rows(last)
385    /// ```
386    ///
387    /// # Errors
388    ///
389    /// - The reduction sends no successor elements, which [`Self::batch`] batches.
390    /// - The challenge does not name the absorbed coordinates.
391    pub fn batch_with_successor<'a>(
392        &'a self,
393        r_batch: &Point<EF>,
394        alpha: EF,
395    ) -> Result<BitRingSwitchBatch<'a, EF>, BitRingSwitchError> {
396        if !self.sends_successor_tensors() {
397            return Err(BitRingSwitchError::SuccessorBatching { expected: false });
398        }
399        self.batch_stage(r_batch, Some(alpha))
400    }
401
402    /// The second stage over whatever the transcript drew, `alpha` present or not.
403    pub(super) fn batch_drawn(
404        &self,
405        r_batch: &Point<EF>,
406        alpha: Option<EF>,
407    ) -> Result<BitRingSwitchBatch<'_, EF>, BitRingSwitchError> {
408        alpha.map_or_else(
409            || self.batch(r_batch),
410            |alpha| self.batch_with_successor(r_batch, alpha),
411        )
412    }
413
414    /// The second stage over either kind of reduction, once its kind is checked.
415    fn batch_stage(
416        &self,
417        r_batch: &Point<EF>,
418        alpha: Option<EF>,
419    ) -> Result<BitRingSwitchBatch<'_, EF>, BitRingSwitchError> {
420        if r_batch.num_variables() != Self::ABSORBED {
421            return Err(BitRingSwitchError::BatchWidthMismatch {
422                expected: Self::ABSORBED,
423                actual: r_batch.num_variables(),
424            });
425        }
426        Ok(BitRingSwitchBatch {
427            reduction: self,
428            eq_batch: Poly::new_from_point(r_batch.as_slice(), EF::ONE),
429            alpha,
430        })
431    }
432
433    /// How many leading kept coordinates a Boolean prefix may fix.
434    ///
435    /// A successor view caps it at the selector.
436    /// The equality factor of a Boolean row coordinate is an indicator too.
437    /// Yet the carry reads the element one before, which that indicator would cut away.
438    fn prefix_limit(&self) -> usize {
439        let limit = self.num_variables();
440        self.successor
441            .map_or(limit, |rows| limit.min(self.point.num_variables() - rows))
442    }
443
444    /// The leading Boolean coordinates and the address they spell.
445    ///
446    /// Only the first [`Self::prefix_limit`] coordinates are scanned.
447    pub(super) fn fixed_prefix(&self) -> (usize, usize) {
448        // Read the address from most significant bit to least significant bit.
449        let mut prefix = 0;
450        let mut address = 0usize;
451        for &coordinate in &self.high()[..self.prefix_limit()] {
452            // Zero selects the lower half of the remaining evaluation table.
453            if coordinate == EF::ZERO {
454                address <<= 1;
455            // One selects its upper half.
456            } else if coordinate == EF::ONE {
457                address = (address << 1) | 1;
458            // A field challenge needs an ordinary sumcheck round.
459            } else {
460                break;
461            }
462            // Count only coordinates whose values select a half exactly.
463            prefix += 1;
464        }
465        // The pair identifies the selected slot without allocating its equality table.
466        (prefix, address)
467    }
468
469    /// Where the equality table over the kept coordinates is nonzero, and its values there.
470    ///
471    /// # Algorithm
472    ///
473    /// The equality table factors over the variables.
474    /// A coordinate that is already zero or one turns its factor into an indicator.
475    ///
476    /// ```text
477    ///     high = (b_0 .. b_{p-1}, z_p .. z_{l'-1})   every b in {0, 1}
478    ///     eq(high, w) = 0  unless the top p bits of w spell b
479    /// ```
480    ///
481    /// The table is therefore supported on one run of `2^(l' - p)` consecutive elements,
482    /// and on that run it is the equality table of the remaining coordinates alone.
483    ///
484    /// A claim about one column of a stacked trace arrives exactly this way, its slot
485    /// address leading the row point.
486    ///
487    /// A point drawn from a transcript has no Boolean coordinate, so the run is the whole
488    /// hypercube and this is the dense table.
489    ///
490    /// # Returns
491    ///
492    /// - The number of leading coordinates fixed to bits.
493    /// - The first element of the supported run.
494    /// - The equality table over that run, in the factored form its sweeps read.
495    pub(super) fn support(&self) -> (usize, usize, FactoredEquality<EF>) {
496        // The equality table uses the first coordinate as the most significant index bit.
497        // A Boolean prefix therefore selects one contiguous run.
498        let (prefix, address) = self.fixed_prefix();
499
500        // One element of the run per assignment of the coordinates that are left.
501        let table = FactoredEquality::new(&self.high()[prefix..], LOG_CHUNK);
502        (prefix, address * table.num_evals(), table)
503    }
504
505    /// The committed polynomial restricted to the Boolean prefix of the claim, held in `R`.
506    ///
507    /// A leading bit coordinate selects one half without a sumcheck round.
508    /// Repeating that selection leaves exactly the slot the claim addresses.
509    ///
510    /// The slot is copied out of the packing, and the crossing into `R` runs over that copy.
511    fn restricted_packing<R: FromTable<EF>, S: Borrow<[EF]>>(
512        &self,
513        packing: &BitPacking<EF, S>,
514    ) -> Poly<R> {
515        // The support identifies the same contiguous slot in both equality and witness order.
516        let (prefix, address) = self.fixed_prefix();
517        let len = 1usize << (self.num_variables() - prefix);
518        slot_packing(packing, address * len, len)
519    }
520
521    /// Restore the Boolean slot address in front of a point inside that slot.
522    fn restore_prefix(&self, point: &Point<EF>) -> Point<EF> {
523        // The prefix is public and fixed by the incoming evaluation point.
524        let (prefix, _) = self.fixed_prefix();
525        let mut coordinates = self.high()[..prefix].to_vec();
526
527        // The random coordinates name the selected slot's remaining variables.
528        coordinates.extend_from_slice(point.as_slice());
529        Point::new(coordinates)
530    }
531
532    /// `sum_w eq(r_high, w) ⊗ t'(w)`, the element the prover sends.
533    ///
534    /// # Overview
535    ///
536    /// Read by columns it gives the bit planes at the kept coordinates.
537    /// That is the reading which tests the incoming claim.
538    ///
539    /// Read by rows it gives the sumcheck its starting sum.
540    ///
541    /// The two readings are of the same coefficients.
542    /// That is what makes a dishonest element catchable at all.
543    ///
544    /// # Performance
545    ///
546    /// Accumulated in place, one partial element per task.
547    /// Forming each term separately would allocate `d` elements per point.
548    ///
549    /// The equality table is built here rather than held.
550    /// That is strictly below the accumulation it feeds.
551    ///
552    /// It also keeps a verifier from paying for a table it never reads.
553    ///
554    /// Only the run the table is nonzero on is swept.
555    /// A claim about one column of a stacked trace reaches `2^(l' - p)` of its `2^l'` elements.
556    ///
557    /// # Errors
558    ///
559    /// Returns an error unless the packing has the reduction's variables.
560    pub fn tensor<S: Borrow<[EF]>>(
561        &self,
562        packing: &BitPacking<EF, S>,
563    ) -> Result<BitTensor<EF>, BitRingSwitchError>
564    where
565        EF: Send + Sync,
566    {
567        self.check_width(packing.num_variables())?;
568        let (_, offset, equality) = self.support();
569        Ok(Self::tensor_over(packing, offset, &equality))
570    }
571
572    /// The element sent, accumulated against the equality table of the supported run.
573    ///
574    /// Elements outside the run weigh zero, so leaving them out changes no sum.
575    pub(super) fn tensor_over<S: Borrow<[EF]>>(
576        packing: &BitPacking<EF, S>,
577        offset: usize,
578        equality: &FactoredEquality<EF>,
579    ) -> BitTensor<EF>
580    where
581        EF: Send + Sync,
582    {
583        let mut banks = Self::bank_tensors_over(packing, offset, 1, equality);
584        banks.pop().expect("one bank has one element")
585    }
586
587    /// The element of each of `banks` consecutive runs, accumulated in one sweep over them all.
588    ///
589    /// Run `b` starts `b * equality.num_evals()` elements past `offset`.
590    /// Every run is weighed against the same table.
591    ///
592    /// # Algorithm
593    ///
594    /// The equality weight of a block is its own weight times the block's outer weight, and
595    /// the first tensor leg carries the weight:
596    ///
597    /// ```text
598    ///     sum_j (outer_i * inner_j) ⊗ t'(w)  =  (outer_i ⊗ 1) * sum_j inner_j ⊗ t'(w)
599    /// ```
600    ///
601    /// So a block accumulates against the inner weights alone, and the outer weight is one
602    /// scaling of the partial element rather than one multiplication per point.
603    ///
604    /// Every block of every run reads the same inner weights, so the sweep splits over all the
605    /// runs' blocks at once rather than over one run at a time.
606    fn bank_tensors_over<S: Borrow<[EF]>>(
607        packing: &BitPacking<EF, S>,
608        offset: usize,
609        banks: usize,
610        equality: &FactoredEquality<EF>,
611    ) -> Vec<BitTensor<EF>>
612    where
613        EF: Send + Sync,
614    {
615        let values = &packing.poly().as_slice()[offset..offset + banks * equality.num_evals()];
616        let blocks = equality.outer().len();
617        // Every block reads the same inner weights, so their layout is prepared once.
618        let inner = LeftFactors::new(equality.inner());
619
620        values
621            .par_chunks(equality.block_len())
622            .enumerate()
623            .par_fold_reduce(
624                // The scratch is what a block accumulates into, so only a fold arm holds one.
625                || (alloc::vec![BitTensor::zero(); banks], None),
626                |(mut totals, mut scratch), (index, values)| {
627                    let (bank, block) = (index / blocks, index % blocks);
628                    totals[bank].add_scaled_columns(
629                        &inner.sum(values, &mut scratch),
630                        equality.outer()[block],
631                    );
632                    (totals, scratch)
633                },
634                |(mut totals, scratch), (partials, _)| {
635                    // Addition is associative, so regrouping cannot change it.
636                    for (total, partial) in totals.iter_mut().zip(partials) {
637                        *total += partial;
638                    }
639                    (totals, scratch)
640                },
641            )
642            .0
643    }
644
645    /// What the claim being reduced must equal, given the element sent.
646    ///
647    /// # Algorithm
648    ///
649    /// Column `v` is the witness's bit plane `v` read at the kept coordinates.
650    /// The absorbed coordinates weigh those planes back together:
651    ///
652    /// ```text
653    ///     t(r) = sum_v eq(r_low, v) * column v
654    /// ```
655    ///
656    /// This is the reduction's only use of the absorbed coordinates.
657    #[must_use]
658    pub fn incoming_claim(&self, tensor: &BitTensor<EF>) -> EF {
659        tensor
660            .columns()
661            .iter()
662            .zip(self.eq_low.as_slice())
663            .map(|(&column, &weight)| column * weight)
664            .sum()
665    }
666
667    /// The carry and last elements the successor view adds, when its rows outrun one element.
668    ///
669    /// # Algorithm
670    ///
671    /// Over the run the equality table `E` is supported on, with `x(w)` the kept row bits:
672    ///
673    /// ```text
674    ///     carry  =  sum_{x(w) != 0}   E[w - 1] ⊗ t'(w)    the element before, same column
675    ///     last   =  sum_{x(w) = max}  E[w]     ⊗ t'(w)    the last element of each column
676    /// ```
677    ///
678    /// The prefix never fixes a row coordinate, so `w - 1` stays inside the run.
679    ///
680    /// # Returns
681    ///
682    /// `None` unless the reduction sends successor elements.
683    ///
684    /// # Errors
685    ///
686    /// Returns an error unless the packing has the reduction's variables.
687    pub fn successor_tensors<S: Borrow<[EF]>>(
688        &self,
689        packing: &BitPacking<EF, S>,
690    ) -> Result<Option<SuccessorTensors<EF>>, BitRingSwitchError>
691    where
692        EF: Send + Sync,
693    {
694        self.check_width(packing.num_variables())?;
695        if self.kept_row_variables().is_none() {
696            return Ok(None);
697        }
698        let (_, offset, equality) = self.support();
699        Ok(self.successor_tensors_over(packing, offset, &equality))
700    }
701
702    /// The successor elements, accumulated against the equality table of the supported run.
703    ///
704    /// `None` unless the reduction sends successor elements.
705    pub(super) fn successor_tensors_over<S: Borrow<[EF]>>(
706        &self,
707        packing: &BitPacking<EF, S>,
708        offset: usize,
709        equality: &FactoredEquality<EF>,
710    ) -> Option<SuccessorTensors<EF>>
711    where
712        EF: Send + Sync,
713    {
714        let mut banks = self.bank_successor_tensors_over(packing, offset, 1, equality)?;
715        Some(banks.pop().expect("one bank has one pair of elements"))
716    }
717
718    /// The successor elements of each of `banks` consecutive runs, in one sweep over them all.
719    ///
720    /// The runs are laid out as in [`Self::bank_tensors_over`], and each holds whole columns.
721    ///
722    /// `None` unless the reduction sends successor elements.
723    fn bank_successor_tensors_over<S: Borrow<[EF]>>(
724        &self,
725        packing: &BitPacking<EF, S>,
726        offset: usize,
727        banks: usize,
728        equality: &FactoredEquality<EF>,
729    ) -> Option<Vec<SuccessorTensors<EF>>>
730    where
731        EF: Send + Sync,
732    {
733        let kept = self.kept_row_variables()?;
734
735        // Elements outside the run weigh zero in all three elements.
736        let block = equality.block_len();
737        let blocks = equality.outer().len();
738        let values = &packing.poly().as_slice()[offset..offset + banks * equality.num_evals()];
739        // The run holds whole columns, so the kept row bits are the low bits of `w`.
740        let column = 1usize << kept;
741        let max = column - 1;
742        let inner = equality.inner();
743
744        // Past a block's first element, the element before lies in the block, and in the same
745        // column unless the element starts one. Blocks and columns are both powers of two, so
746        // that turns on the offset in the block alone: every block reads the same inner
747        // weights shifted by one, which are therefore prepared once.
748        let shifted = (0..block)
749            .map(|j| if j & max == 0 { EF::ZERO } else { inner[j - 1] })
750            .collect::<Vec<_>>();
751        let shifted = LeftFactors::new(&shifted);
752
753        let zero = SuccessorTensors {
754            carry: BitTensor::zero(),
755            last: BitTensor::zero(),
756        };
757        let (totals, _) = values.par_chunks(block).enumerate().par_fold_reduce(
758            // The scratch is what a block accumulates into, so only a fold arm holds one.
759            || (alloc::vec![zero.clone(); banks], None),
760            |(mut totals, mut scratch), (index, values)| {
761                // A run holds whole columns, so the row bits restart with every run.
762                let (bank, index) = (index / blocks, index % blocks);
763                let SuccessorTensors { carry, last } = &mut totals[bank];
764                let weight = equality.outer()[index];
765                let start = (index * block) & max;
766                // The +1 ripples out of the element before, inside the same column.
767                // Before a block's first element, that closes the block before, under its weight.
768                if start != 0 {
769                    carry.add_exterior_product(equality.before_block(index), values[0]);
770                }
771                carry.add_scaled_columns(&shifted.sum(values, &mut scratch), weight);
772                // The last element of a column reads itself again.
773                // One element in a column qualifies, so its weight is formed directly.
774                for j in (max - start..values.len()).step_by(column) {
775                    last.add_exterior_product(weight * inner[j], values[j]);
776                }
777                (totals, scratch)
778            },
779            |(mut totals, scratch), (partials, _)| {
780                // Addition is associative, so regrouping cannot change it.
781                for (total, partial) in totals.iter_mut().zip(partials) {
782                    total.carry += partial.carry;
783                    total.last += partial.last;
784                }
785                (totals, scratch)
786            },
787        );
788        Some(totals)
789    }
790
791    /// The weights the successor claim puts on the tensor's columns.
792    ///
793    /// # Algorithm
794    ///
795    /// With kept row coordinates, a column is read one bit on, and none reads bit 0:
796    ///
797    /// ```text
798    ///     n[v] = eq_low[v - 1]   for v >= 1,   n[0] = 0
799    /// ```
800    ///
801    /// The +1 out of bit `d - 1` goes to the carry element instead.
802    ///
803    /// With none, the `2^a` rows sit inside the element, `x = v mod 2^a`:
804    ///
805    /// ```text
806    ///     n[v] = (x != 0 ? eq_low[v - 1] : 0) + (x = 2^a - 1 ? eq_low[v] : 0)
807    /// ```
808    fn successor_low_weights(&self, row_variables: usize) -> Vec<EF> {
809        let eq_low = self.eq_low.as_slice();
810        if self.sends_successor_tensors() {
811            return (0..eq_low.len())
812                .map(|v| if v == 0 { EF::ZERO } else { eq_low[v - 1] })
813                .collect();
814        }
815
816        let last = (1usize << row_variables) - 1;
817        (0..eq_low.len())
818            .map(|v| {
819                let row = v & last;
820                let settled = if row == 0 { EF::ZERO } else { eq_low[v - 1] };
821                let repeated = if row == last { eq_low[v] } else { EF::ZERO };
822                settled + repeated
823            })
824            .collect()
825    }
826
827    /// What the successor claim must equal, given the elements sent.
828    ///
829    /// # Algorithm
830    ///
831    /// ```text
832    ///     s_next = sum_v n[v] * col_v(tensor)
833    ///            + eq_low[d - 1] * ( col_0(carry) + col_{d-1}(last) )   with successor elements
834    /// ```
835    ///
836    /// `eq_low[d - 1]` is the product of the absorbed coordinates, the weight of the last bit.
837    ///
838    /// # Errors
839    ///
840    /// Returns an error when the reduction was set up without a successor view.
841    ///
842    /// # Panics
843    ///
844    /// Panics unless the successor elements are supplied exactly when the reduction sends them.
845    pub fn successor_claim(
846        &self,
847        tensor: &BitTensor<EF>,
848        successor: Option<&SuccessorTensors<EF>>,
849    ) -> Result<EF, BitRingSwitchError> {
850        let rows = self.successor.ok_or(BitRingSwitchError::NoSuccessorView)?;
851        assert_eq!(
852            successor.is_some(),
853            self.sends_successor_tensors(),
854            "the successor elements are supplied exactly when the reduction sends them"
855        );
856
857        let settled: EF = tensor
858            .columns()
859            .iter()
860            .zip(self.successor_low_weights(rows))
861            .map(|(&column, weight)| column * weight)
862            .sum();
863        Ok(successor.map_or(settled, |elements| {
864            let d = BitTensor::<EF>::DIMENSION;
865            let ripple = elements.carry.column(0) + elements.last.column(d - 1);
866            settled + self.eq_low.as_slice()[d - 1] * ripple
867        }))
868    }
869
870    /// Check that something names the variables this reduction runs over.
871    fn check_width(&self, actual: usize) -> Result<(), BitRingSwitchError> {
872        if actual == self.num_variables() {
873            Ok(())
874        } else {
875            Err(BitRingSwitchError::WidthMismatch {
876                expected: self.num_variables(),
877                actual,
878            })
879        }
880    }
881
882    /// The equality element at the kept coordinates and one other point.
883    ///
884    /// # Algorithm
885    ///
886    /// The equality polynomial factors over the variables:
887    ///
888    /// ```text
889    ///     eq(X, Y) = prod_i ( X_i*Y_i + (1 - X_i)(1 - Y_i) )
890    /// ```
891    ///
892    /// Each factor lands on a different tensor leg.
893    /// From the identity, every variable adds an agree and a disagree term:
894    ///
895    /// ```text
896    ///     agree     scale a copy's columns by a, its rows by b
897    ///     disagree  scale the element's columns by 1 - a, its rows by 1 - b
898    /// ```
899    ///
900    /// The two scalings commute, so either order gives the term.
901    /// The alternative sums the element over the hypercube, exponentially.
902    fn equality_element(&self, r_prime: &Point<EF>) -> BitTensor<EF> {
903        // Boolean leading coordinates select a committed slot directly.
904        // The equality element therefore spans only the coordinates left inside that slot.
905        let (prefix, _) = self.fixed_prefix();
906        let mut element = BitTensor::one();
907        for (&a, &b) in self.high()[prefix..].iter().zip(r_prime.as_slice()) {
908            let mut agree = element.clone();
909            agree.scale_columns(a);
910            agree.scale_rows(b);
911
912            element.scale_columns(EF::ONE - a);
913            element.scale_rows(EF::ONE - b);
914            element += agree;
915        }
916        element
917    }
918
919    /// The carry and last weights lifted into the algebra at one other point.
920    ///
921    /// # Algorithm
922    ///
923    /// Split the coordinates past the prefix, and the point, into selector and kept rows:
924    ///
925    /// ```text
926    ///     high[p..] = (selector, rho_h)       r' = (r'_sel, r'_rows)       |rho_h| = kept
927    /// ```
928    ///
929    /// Both weights factor into a selector part, an equality factor per coordinate,
930    /// and a row part:
931    ///
932    /// ```text
933    ///     carry  =  successor_element(rho_h, r'_rows)   * prod_i (1 + sel_i ⊗ 1 + 1 ⊗ r'_sel,i)
934    ///     last   =  prod(rho_h) ⊗ prod(r'_rows)         * prod_i (1 + sel_i ⊗ 1 + 1 ⊗ r'_sel,i)
935    /// ```
936    ///
937    /// # Panics
938    ///
939    /// Panics unless the reduction sends successor elements.
940    fn successor_elements(&self, r_prime: &Point<EF>) -> (BitTensor<EF>, BitTensor<EF>) {
941        let kept = self
942            .kept_row_variables()
943            .expect("only a reduction sending successor elements closes on them");
944        let (prefix, _) = self.fixed_prefix();
945        let rest = &self.high()[prefix..];
946        let (selector, rho) = rest.split_at(rest.len() - kept);
947        let (r_selector, r_rows) = r_prime.as_slice().split_at(r_prime.num_variables() - kept);
948
949        let mut carry = BitTensor::successor_element(rho, r_rows);
950        let mut last = BitTensor::exterior_product(
951            rho.iter().copied().product(),
952            r_rows.iter().copied().product(),
953        );
954        for (&a, &b) in selector.iter().zip(r_selector) {
955            carry.mul_equality_factor(a, b);
956            last.mul_equality_factor(a, b);
957        }
958        (carry, last)
959    }
960
961    /// The compact depth the production path requests over `n` free variables.
962    ///
963    /// The deepest depth the unforced gate admits, from [`COMPACT_PRODUCTION_ROUNDS`] down to
964    /// [`COMPACT_PRODUCTION_MIN_ROUNDS`]. Where none is admitted it is the shallowest, which the
965    /// gate then sends down the dense path.
966    fn production_depth(&self, n: usize) -> usize {
967        (COMPACT_PRODUCTION_MIN_ROUNDS..=COMPACT_PRODUCTION_ROUNDS)
968            .rev()
969            .find(|&depth| self.compact_depth_is_eligible(n, depth, false))
970            .unwrap_or(COMPACT_PRODUCTION_MIN_ROUNDS)
971    }
972
973    /// Common compact-depth guards, evaluated before bank slices or shifts are formed.
974    fn compact_depth_is_eligible(&self, n: usize, requested_k: usize, force: bool) -> bool {
975        if requested_k == 0 || requested_k > COMPACT_MAX_ROUNDS || n < requested_k {
976            return false;
977        }
978        if let Some(kept_rows) = self.kept_row_variables()
979            && (n < kept_rows || requested_k > n - kept_rows)
980        {
981            return false;
982        }
983        // At the shallowest production depth this floor is 2 + 14 + 6 = 22 free variables, but
984        // 2 + 1 + 6 = 9 under test, where `LOG_CHUNK` is 1, so no unit test runs the compact head
985        // at the shape or the chunk size a production build first accepts.
986        force || n >= compact_size_floor(requested_k, LOG_CHUNK)
987    }
988}
989
990/// The same reduction, once the batching challenge has been drawn.
991///
992/// Holding one of these is not evidence that the element was bound first.
993/// Enforcing that is the transcript's job, as the stage above records.
994#[derive(Clone, Debug)]
995pub struct BitRingSwitchBatch<'a, EF> {
996    /// The stage the evaluation point alone fixes.
997    reduction: &'a BitRingSwitch<EF>,
998    /// The equality table of the batching challenge.
999    eq_batch: Poly<EF>,
1000    /// The challenge weighing the carry and last elements, when the reduction sends them.
1001    alpha: Option<EF>,
1002}
1003
1004impl<EF: TowerLevel> BitRingSwitchBatch<'_, EF> {
1005    /// The weight multilinear the sumcheck runs against the packing.
1006    ///
1007    /// # Algorithm
1008    ///
1009    /// The equality table decomposes over `F_2`.
1010    /// The challenge weighs those coordinates:
1011    ///
1012    /// ```text
1013    ///     A(w) = sum_u eq(u, r_batch) * coordinate u of eq(r_high, w)
1014    /// ```
1015    ///
1016    /// The coordinates are bits, so each entry is a subset sum.
1017    ///
1018    /// With successor elements the carry and last weights are the same table, moved:
1019    ///
1020    /// ```text
1021    ///     weights(w) = A(w)
1022    ///                + alpha   * (x(w) != 0  ? A(w - 1) : 0)
1023    ///                + alpha^2 * (x(w) = max ? A(w)     : 0)
1024    /// ```
1025    ///
1026    /// # Performance
1027    ///
1028    /// Prover-side, like the element passes, the other places the equality table is built.
1029    ///
1030    /// The Boolean prefix selects one slot before the sumcheck starts.
1031    /// The table therefore contains that slot alone, with no zero runs around it.
1032    pub fn weights(&self) -> Poly<EF>
1033    where
1034        EF: Send + Sync,
1035    {
1036        let (_, _, equality) = self.reduction.support();
1037        self.weights_over(&equality)
1038    }
1039
1040    /// The weight multilinear, read off the equality table of the supported run, held in `R`.
1041    ///
1042    /// Every entry is a subset sum of the batching table, so carrying that table into `R`
1043    /// first puts the whole multilinear there for the cost of `d` conversions.
1044    ///
1045    /// The entries are sums and products of those images, so `R` must carry the arithmetic
1046    /// of `EF` and not merely its elements.
1047    pub(super) fn weights_over<R>(&self, equality: &FactoredEquality<EF>) -> Poly<R>
1048    where
1049        EF: Send + Sync,
1050        R: IntoTranscriptField<EF> + Sync,
1051    {
1052        let mut table = Poly::zero(equality.num_variables());
1053        let batching: Vec<R> = self
1054            .eq_batch
1055            .as_slice()
1056            .iter()
1057            .copied()
1058            .map(R::from)
1059            .collect();
1060        let sums = CoordinateSums::<EF, R>::new(&batching);
1061
1062        // Every entry belongs to the selected slot.
1063        // No zero run for another slot is allocated or folded.
1064        //
1065        // A block's entries are its own weights under one outer weight, and the coordinate
1066        // sums absorb that scale once per block instead of once per entry.
1067        //
1068        // The block is that amortisation unit and the split follows it, so a run holding
1069        // fewer blocks than there are threads sweeps on fewer tasks. Splitting below the
1070        // block does not recover them: a task spanning less than a block has to scale the
1071        // sums for itself, and one scaling costs more than the entries such a task writes.
1072        table
1073            .as_mut_slice()
1074            .par_chunks_mut(equality.block_len())
1075            .zip(equality.outer().par_iter())
1076            .for_each_init(
1077                // Scratch of the right shape, overwritten before each block reads it.
1078                || sums.clone(),
1079                |scaled, (block, &weight)| {
1080                    scaled_sums_into(scaled, &sums, weight);
1081                    scaled.apply_into(equality.inner(), block);
1082                },
1083            );
1084
1085        let Some(alpha) = self.alpha else {
1086            return table;
1087        };
1088        let kept = self
1089            .reduction
1090            .kept_row_variables()
1091            .expect("a batch carries alpha only for a reduction sending successor elements");
1092        let column = 1usize << kept;
1093        let max = column - 1;
1094        let alpha = R::from(alpha);
1095        let alpha_squared = alpha.square();
1096
1097        // The shift reads the entry before, which a column taken from its last row down still
1098        // finds unmoved. So the combination lands in the table it reads, in chunks of whole
1099        // columns.
1100        //
1101        // A column fits inside the run: `prefix_limit` lets no Boolean prefix fix a row
1102        // coordinate, so the table keeps at least the kept rows. Every chunk below is
1103        // therefore exactly `stride` long and splits into whole columns.
1104        let stride = equality.block_len().max(column).min(table.num_evals());
1105        debug_assert!(column.is_power_of_two() && stride.is_multiple_of(column));
1106        debug_assert!(table.num_evals().is_multiple_of(stride));
1107        table
1108            .as_mut_slice()
1109            .par_chunks_mut(stride)
1110            .for_each(|part| {
1111                for entries in part.chunks_mut(column) {
1112                    for row in (0..column).rev() {
1113                        let settled = entries[row];
1114                        let mut value = settled;
1115                        if row != 0 {
1116                            value += alpha * entries[row - 1];
1117                        }
1118                        if row == max {
1119                            value += alpha_squared * settled;
1120                        }
1121                        entries[row] = value;
1122                    }
1123                }
1124            });
1125        table
1126    }
1127
1128    /// Build the tail weights from the compact prover's settled coordinate images.
1129    fn weights_from_generators<R>(
1130        &self,
1131        equality: &FactoredEquality<EF>,
1132        generators: &[R],
1133    ) -> Poly<R>
1134    where
1135        EF: Send + Sync,
1136        R: IntoTranscriptField<EF> + Sync,
1137    {
1138        let sums = CoordinateSums::<EF, R>::new(generators);
1139        let mut table = Poly::zero(equality.num_variables());
1140        table
1141            .as_mut_slice()
1142            .par_chunks_mut(equality.block_len())
1143            .zip(equality.outer().par_iter())
1144            .for_each_init(
1145                || sums.clone(),
1146                |scaled, (block, &weight)| {
1147                    scaled_sums_into(scaled, &sums, weight);
1148                    scaled.apply_into(equality.inner(), block);
1149                },
1150            );
1151
1152        let Some(alpha) = self.alpha else {
1153            return table;
1154        };
1155        let kept = self
1156            .reduction
1157            .kept_row_variables()
1158            .expect("a compact alpha batch has successor rows");
1159        let column = 1usize << kept;
1160        let alpha = R::from(alpha);
1161        let alpha_squared = alpha.square();
1162        let max = column - 1;
1163        // A column fits inside the tail: the kept-row clause of `compact_depth_is_eligible` keeps
1164        // the compact head off the kept row coordinates, so the tail keeps at least the kept rows.
1165        // Every chunk below is therefore exactly `stride` long and splits into whole columns.
1166        let stride = equality.block_len().max(column).min(table.num_evals());
1167        debug_assert!(
1168            column.is_power_of_two() && stride.is_multiple_of(column),
1169            "the compact tail is shorter than one column; see compact_depth_is_eligible"
1170        );
1171        debug_assert!(
1172            table.num_evals().is_multiple_of(stride),
1173            "the compact tail does not split into whole strides; see compact_depth_is_eligible"
1174        );
1175        table
1176            .as_mut_slice()
1177            .par_chunks_mut(stride)
1178            .for_each(|part| {
1179                for entries in part.chunks_mut(column) {
1180                    for row in (0..column).rev() {
1181                        let settled = entries[row];
1182                        let mut value = settled;
1183                        if row != 0 {
1184                            value += alpha * entries[row - 1];
1185                        }
1186                        if row == max {
1187                            value += alpha_squared * settled;
1188                        }
1189                        entries[row] = value;
1190                    }
1191                }
1192            });
1193        table
1194    }
1195
1196    /// The sum the reduction's sumcheck starts from.
1197    ///
1198    /// Derived from the elements' rows rather than taken from the prover.
1199    /// That ties the sumcheck to the coefficients the claims were checked on.
1200    ///
1201    /// ```text
1202    ///     rows(tensor) [+ alpha * rows(carry) + alpha^2 * rows(last)]
1203    /// ```
1204    ///
1205    /// # Panics
1206    ///
1207    /// Panics unless the successor elements are supplied exactly when the batch carries `alpha`.
1208    #[must_use]
1209    pub fn initial_sum(
1210        &self,
1211        tensor: &BitTensor<EF>,
1212        successor: Option<&SuccessorTensors<EF>>,
1213    ) -> EF {
1214        let sum = self.batch_rows(tensor);
1215        match (self.alpha, successor) {
1216            (None, None) => sum,
1217            (Some(alpha), Some(elements)) => {
1218                sum + self.successor_rows(alpha, &elements.carry, &elements.last)
1219            }
1220            _ => panic!("the successor elements are supplied exactly when the batch carries alpha"),
1221        }
1222    }
1223
1224    /// The weight the surviving claim is scaled by, where the rounds ended.
1225    ///
1226    /// # Overview
1227    ///
1228    /// This is the weight multilinear read at that point.
1229    /// Taken through the equality element, not by a pass over the weights.
1230    ///
1231    /// # Errors
1232    ///
1233    /// Returns an error unless the point names the reduction's variables.
1234    pub fn closing_weight(&self, r_prime: &Point<EF>) -> Result<EF, BitRingSwitchError> {
1235        // The rounds run only over coordinates not fixed by the Boolean slot address.
1236        let (prefix, _) = self.reduction.fixed_prefix();
1237        self.reduction
1238            .check_width(prefix + r_prime.num_variables())?;
1239        let equality = self.batch_rows(&self.reduction.equality_element(r_prime));
1240        Ok(self.alpha.map_or(equality, |alpha| {
1241            let (carry, last) = self.reduction.successor_elements(r_prime);
1242            equality + self.successor_rows(alpha, &carry, &last)
1243        }))
1244    }
1245
1246    /// The rows of an element, batched against the batching challenge.
1247    ///
1248    /// Both ends of the sumcheck are this operation on a different element.
1249    fn batch_rows(&self, tensor: &BitTensor<EF>) -> EF {
1250        tensor
1251            .rows()
1252            .iter()
1253            .zip(self.eq_batch.as_slice())
1254            .map(|(&row, &weight)| row * weight)
1255            .sum()
1256    }
1257
1258    /// `alpha * rows(carry) + alpha^2 * rows(last)`, the successor share of either end.
1259    fn successor_rows(&self, alpha: EF, carry: &BitTensor<EF>, last: &BitTensor<EF>) -> EF {
1260        alpha * (self.batch_rows(carry) + alpha * self.batch_rows(last))
1261    }
1262}
1263
1264/// Reasons a bit-alphabet reduction cannot be set up or run.
1265#[derive(Debug, thiserror::Error, PartialEq, Eq)]
1266pub enum BitRingSwitchError {
1267    /// The evaluation point does not cover the coordinates one element absorbs.
1268    #[error("the evaluation point names {actual} variables, at least {needed} are needed")]
1269    PointTooNarrow {
1270        /// Coordinates one packed element absorbs.
1271        needed: usize,
1272        /// Coordinates the point names.
1273        actual: usize,
1274    },
1275    /// The batching challenge does not name the absorbed coordinates exactly.
1276    ///
1277    /// A shorter one would zip-truncate, dropping identity rows.
1278    /// Both sides move the same way, so no later check would catch it.
1279    #[error("the batching challenge names {actual} variables, expected {expected}")]
1280    BatchWidthMismatch {
1281        /// Coordinates the reduction absorbs.
1282        expected: usize,
1283        /// Coordinates the challenge names.
1284        actual: usize,
1285    },
1286    /// A polynomial or point does not name the variables the reduction uses.
1287    #[error("the reduction runs over {expected} variables, got {actual}")]
1288    WidthMismatch {
1289        /// Variables the reduction runs over.
1290        expected: usize,
1291        /// Variables the argument names.
1292        actual: usize,
1293    },
1294    /// The successor view steps within more coordinates than the point names.
1295    #[error("the successor view names {row_variables} row variables, the point {num_variables}")]
1296    RowVariables {
1297        /// Row coordinates the successor view was set up with.
1298        row_variables: usize,
1299        /// Coordinates the evaluation point names.
1300        num_variables: usize,
1301    },
1302    /// A batching stage for the other kind of reduction was asked for.
1303    #[error(
1304        "the batching stage does not fit the reduction, successor challenge expected: {expected}"
1305    )]
1306    SuccessorBatching {
1307        /// Whether the reduction sends successor elements, so its stage takes `alpha`.
1308        expected: bool,
1309    },
1310    /// A successor claim was asked of a reduction set up without a successor view.
1311    #[error("the reduction was set up without a successor view")]
1312    NoSuccessorView,
1313    /// A batch of claims was asked for with no claim in it.
1314    #[error("a batch of ring-switch claims needs at least one claim")]
1315    NoClaims,
1316}
1317
1318/// The two elements a successor view adds when its rows outrun one packed element.
1319///
1320/// With `E` the equality table over the kept coordinates and `x(w)` the kept row bits:
1321///
1322/// ```text
1323///     carry  =  sum_{x(w) != 0}   E[w - 1] ⊗ t'(w)
1324///     last   =  sum_{x(w) = max}  E[w]     ⊗ t'(w)
1325/// ```
1326///
1327/// The claim reads one column of each, the rounds read all their rows.
1328#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1329#[serde(bound(serialize = "EF: TowerLevel", deserialize = "EF: TowerLevel"))]
1330pub struct SuccessorTensors<EF> {
1331    /// The +1 rippling out of an element into the next one of its column.
1332    pub carry: BitTensor<EF>,
1333    /// The last element of each column, whose last row repeats.
1334    pub last: BitTensor<EF>,
1335}
1336
1337/// The messages one bit-alphabet reduction puts on the wire.
1338///
1339/// The element travels by rows, one bit per matrix entry:
1340///
1341/// ```text
1342///     by rows          d elements
1343///     byte per entry   d^2 elements
1344/// ```
1345///
1346/// A successor view whose rows outrun one element sends two more, the same way.
1347#[derive(Clone, Debug, Serialize, Deserialize)]
1348#[serde(bound(serialize = "EF: TowerLevel", deserialize = "EF: TowerLevel"))]
1349pub struct BitRingSwitchProof<EF> {
1350    /// The tensor element both checks read, by rows and by columns.
1351    pub tensor: BitTensor<EF>,
1352    /// The carry and last elements, present exactly when the setup sends them.
1353    ///
1354    /// The verifier derives whether they belong from its own setup, never from this field.
1355    pub successor: Option<SuccessorTensors<EF>>,
1356    /// The batched degree-two rounds left after any Boolean slot prefix is fixed.
1357    pub sumcheck: SumcheckData<EF, EF>,
1358    /// The value of the surviving claim.
1359    pub final_eval: EF,
1360}
1361
1362/// Why a bit-alphabet reduction was rejected.
1363#[derive(Debug, thiserror::Error, PartialEq, Eq)]
1364#[non_exhaustive]
1365pub enum BitRingSwitchProofError {
1366    /// A list crossing the wire is not the width the description fixes.
1367    #[error(transparent)]
1368    Width(#[from] TranscriptWidth),
1369
1370    /// The reduction could not be run over the point it was set up at.
1371    #[error(transparent)]
1372    Reduction(#[from] BitRingSwitchError),
1373
1374    /// The claimed evaluation is not the column reading of the element.
1375    #[error("the claimed evaluation is not the column reading of the tensor element")]
1376    ClaimMismatch,
1377
1378    /// The claimed successor evaluation is not the column reading of the elements.
1379    #[error("the claimed successor evaluation is not the column reading of the elements")]
1380    SuccessorClaimMismatch,
1381
1382    /// Neither a current nor a successor evaluation was supplied, so nothing is checked.
1383    #[error("no claimed evaluation was supplied")]
1384    NoReading,
1385
1386    /// A successor evaluation was supplied to a reduction without a successor view.
1387    #[error("a successor evaluation was supplied to a reduction without a successor view")]
1388    NoSuccessorView,
1389
1390    /// A round of the batched sumcheck failed.
1391    #[error(transparent)]
1392    Sumcheck(#[from] SumcheckError),
1393
1394    /// The surviving claim does not close the sumcheck against the closing weight.
1395    #[error("the surviving claim does not close the sumcheck")]
1396    FinalCheck,
1397
1398    /// A batch carries elements or readings for a different number of claims than its setup.
1399    #[error("the batch carries {actual} claims, its setup {expected}")]
1400    ClaimCount {
1401        /// Claims the setup was built over.
1402        expected: usize,
1403        /// Claims the proof or the readings carry.
1404        actual: usize,
1405    },
1406
1407    /// The sumcheck carries grinding witnesses this reduction never searches for.
1408    #[error("the sumcheck carries {actual} proof-of-work witnesses, expected none")]
1409    NonEmptyPowWitnesses {
1410        /// Witnesses the proof supplied.
1411        actual: usize,
1412    },
1413}
1414
1415/// The two sides of the reduction, each over the transcript the other replays.
1416///
1417/// # A reduction, not a filter
1418///
1419/// A false input claim is not rejected outright.
1420/// It survives as a false surviving claim, except with the probability below.
1421///
1422/// Two cases the rounds cannot catch are left to whatever discharges the claim:
1423///
1424/// - a closing weight of zero, which constrains the surviving value not at all
1425/// - a tampered element, with the rest of the proof adapted to the sum it implies
1426///
1427/// # Booleanity is free
1428///
1429/// The packing is a bijection between bit strings and elements of the level.
1430/// Every bit pattern is an element, and every element is some bit pattern.
1431///
1432/// A commitment to a packed multilinear is therefore a commitment to a bit witness.
1433/// No range check, no auxiliary constraint, and nothing here to verify.
1434///
1435/// # Soundness
1436///
1437/// The reduction's own error is `(d_log + K - 1 + 2 l') / |EF|` (eprint 2024/504, Theorem 3.5).
1438/// Here `l'` is the number of rounds that actually run after a Boolean prefix is fixed.
1439/// Charging the full packed arity is a conservative upper bound.
1440///
1441/// `K` is the number of elements sent: three with successor elements, one otherwise.
1442///
1443/// The three sources are:
1444///
1445/// - `d_log / |EF|` from the batching point that collapses each element's row claims.
1446/// - `(K - 1) / |EF|` from `alpha`, which collapses the `K` batched row claims into one.
1447/// - `2 l' / |EF|` for the rounds of degree-two sumcheck.
1448///
1449/// A wrong element leaves a nonzero batched row claim.
1450/// It is multilinear in `r''` and of degree `K - 1` in `alpha`.
1451///
1452/// That bound needs `r''` and `alpha` drawn after every element is bound.
1453/// The transcript fixes that order.
1454/// It also needs `K` taken from the public setup, never read from the proof.
1455///
1456/// Every term is per-attempt, because the description holds no grinding step.
1457/// A protocol needing a total bound supplies the grinding outside this run.
1458impl<EF: TranscriptField + TowerLevel> BitRingSwitch<EF> {
1459    /// Reduce the claims this reduction was set up over to one about the packing.
1460    ///
1461    /// # The field the rounds run in
1462    ///
1463    /// Every round multiplies a packing entry by a weight, and both tables are witness-sized.
1464    /// `R` is the representation those two tables and the rounds over them are held in.
1465    ///
1466    /// ```text
1467    ///     once per reduction   the restricted packing, the batching table, alpha,
1468    ///                          the initial claim, and the surviving claim on its way back
1469    ///     once per round       the challenge, and the round message on its way back
1470    ///     never                the tensor elements, the equality table, the point
1471    /// ```
1472    ///
1473    /// The weight multilinear is accumulated in `R` from the batching table, so it is
1474    /// built there rather than carried across.
1475    ///
1476    /// The map is a field isomorphism, so each round message is the one the challenge
1477    /// field's own tables measure and the transcript is unchanged.
1478    ///
1479    /// # Returns
1480    ///
1481    /// The proof, the point the rounds ended at, and the surviving claim's value.
1482    /// Any Boolean slot prefix is restored in front of the random coordinates.
1483    ///
1484    /// # Panics
1485    ///
1486    /// Panics unless the packing has the variables the evaluation point leaves.
1487    pub fn prove<R, Challenger, S>(
1488        &self,
1489        packing: &BitPacking<EF, S>,
1490        challenger: &mut Challenger,
1491    ) -> (BitRingSwitchProof<EF>, Point<EF>, EF)
1492    where
1493        EF: Send + Sync,
1494        R: IntoTranscriptField<EF>,
1495        Challenger: FieldChallenger<EF> + GrindingChallenger<Witness = EF>,
1496        S: Borrow<[EF]>,
1497    {
1498        let (prefix, _) = self.fixed_prefix();
1499        let depth = self.production_depth(self.num_variables() - prefix);
1500        self.prove_with_compact_depth::<R, Challenger, S>(packing, challenger, depth, false)
1501    }
1502
1503    /// Run the compact driver at a requested depth. The `force` flag is test/benchmark-only and
1504    /// bypasses the conservative production size gate while retaining algebraic eligibility.
1505    pub(crate) fn prove_with_compact_depth<R, Challenger, S>(
1506        &self,
1507        packing: &BitPacking<EF, S>,
1508        challenger: &mut Challenger,
1509        requested_k: usize,
1510        force: bool,
1511    ) -> (BitRingSwitchProof<EF>, Point<EF>, EF)
1512    where
1513        EF: Send + Sync,
1514        R: IntoTranscriptField<EF>,
1515        Challenger: FieldChallenger<EF> + GrindingChallenger<Witness = EF>,
1516        S: Borrow<[EF]>,
1517    {
1518        let max_rounds = self.num_variables();
1519        assert_eq!(
1520            packing.num_variables(),
1521            max_rounds,
1522            "the packing must have the {max_rounds} variables the evaluation point leaves"
1523        );
1524
1525        // Derive the selected slot before constructing any equality table. Compact runs only
1526        // need the suffix factor; the dense fallback constructs the full supported-run factor.
1527        let (prefix, address) = self.fixed_prefix();
1528        let n = max_rounds - prefix;
1529        let compact = self.compact_depth_is_eligible(n, requested_k, force);
1530        let offset = address * (1usize << n);
1531        let equality = (!compact).then(|| FactoredEquality::new(&self.high()[prefix..], LOG_CHUNK));
1532        let compact_equality =
1533            compact.then(|| FactoredEquality::new(&self.high()[prefix + requested_k..], LOG_CHUNK));
1534        let (tensor, successor, bank_tensors, bank_successors) = if compact {
1535            let _span = tracing::debug_span!("compact_stats", depth = requested_k).entered();
1536            let banks = 1usize << requested_k;
1537            let head = &self.high()[prefix..prefix + requested_k];
1538            let tail_equality = compact_equality.as_ref().expect("compact equality exists");
1539            let head_equality = Poly::new_from_point(head, EF::ONE);
1540            let bank_tensors = Self::bank_tensors_over(packing, offset, banks, tail_equality);
1541            let bank_successors =
1542                self.bank_successor_tensors_over(packing, offset, banks, tail_equality);
1543            let mut tensor = BitTensor::zero();
1544            for (&weight, bank) in head_equality.as_slice().iter().zip(&bank_tensors) {
1545                tensor.add_scaled_columns(bank, weight);
1546            }
1547            let successor = bank_successors.as_ref().map(|banks| {
1548                let mut carry = BitTensor::zero();
1549                let mut last = BitTensor::zero();
1550                for (&weight, bank) in head_equality.as_slice().iter().zip(banks) {
1551                    carry.add_scaled_columns(&bank.carry, weight);
1552                    last.add_scaled_columns(&bank.last, weight);
1553                }
1554                SuccessorTensors { carry, last }
1555            });
1556            (tensor, successor, Some(bank_tensors), bank_successors)
1557        } else {
1558            (
1559                Self::tensor_over(
1560                    packing,
1561                    offset,
1562                    equality.as_ref().expect("dense equality exists"),
1563                ),
1564                self.successor_tensors_over(
1565                    packing,
1566                    offset,
1567                    equality.as_ref().expect("dense equality exists"),
1568                ),
1569                None,
1570                None,
1571            )
1572        };
1573
1574        // The elements are functions of the kept coordinates alone.
1575        // They are therefore ready before the transcript needs them.
1576        let mut transcript =
1577            BitRingSwitchProverTranscript::<Challenger, EF>::new(challenger, self.shape());
1578        let (r_batch, alpha) = transcript.statement(
1579            &self.point,
1580            tensor.rows(),
1581            successor
1582                .as_ref()
1583                .map(|elements| (elements.carry.rows(), elements.last.rows())),
1584        );
1585
1586        // The batching challenges arrived after the elements, which is the order soundness needs.
1587        let batch = self
1588            .batch_drawn(&r_batch, alpha)
1589            .expect("the transcript draws what the reduction's kind batches with");
1590        let mut sumcheck = SumcheckData::default();
1591
1592        // A Boolean prefix is a public slot address. The compact branch keeps the first
1593        // requested coordinates in banked tensors and emits them through one n-round transcript.
1594        let (r_prime, final_eval) = if compact {
1595            let factors_span =
1596                tracing::debug_span!("compact_factors", depth = requested_k).entered();
1597            let bank_tensors = bank_tensors.expect("compact banks are present");
1598            let bank_successors = bank_successors.as_ref();
1599            let banks = 1usize << requested_k;
1600            let head = &self.high()[prefix..prefix + requested_k];
1601            let tail_equality = compact_equality.as_ref().expect("compact equality exists");
1602            let head_equality = Poly::new_from_point(head, EF::ONE);
1603            let mut batching = Vec::with_capacity(Coefficients::<EF>::DIMENSION);
1604            batching.extend(batch.eq_batch.as_slice().iter().copied().map(R::from));
1605            let coordinate_sums = CoordinateSums::<EF, R>::new(&batching);
1606            let mut factors = Vec::with_capacity(banks * BitTensor::<EF>::DIMENSION);
1607            let alpha = batch.alpha.map(R::from);
1608            let alpha_squared = alpha.map(|value| value.square());
1609            for bank in 0..banks {
1610                let successor = bank_successors.map(|all| &all[bank]);
1611                for coordinate in 0..BitTensor::<EF>::DIMENSION {
1612                    let mut value = R::from(bank_tensors[bank].rows()[coordinate]);
1613                    if let (Some(elements), Some(alpha), Some(alpha_squared)) =
1614                        (successor, alpha, alpha_squared)
1615                    {
1616                        value += alpha * R::from(elements.carry.rows()[coordinate])
1617                            + alpha_squared * R::from(elements.last.rows()[coordinate]);
1618                    }
1619                    factors.push(value);
1620                }
1621            }
1622            let mut generators = Vec::with_capacity(banks * BitTensor::<EF>::DIMENSION);
1623            for &weight in head_equality.as_slice() {
1624                for coordinate in 0..BitTensor::<EF>::DIMENSION {
1625                    let mut basis = Coefficients::<EF>::zero();
1626                    basis.set(coordinate);
1627                    generators.push(coordinate_sums.sum(weight * basis.element()));
1628                }
1629            }
1630            drop(bank_tensors);
1631            drop(factors_span);
1632            transcript.batched_sumcheck(|challenger| {
1633                let mut rounds_transcript = ProverTranscript::<Challenger, EF, EF>::new(
1634                    challenger,
1635                    SumcheckShape::new(n, 0, Basis::Evaluation),
1636                );
1637                let head_span = tracing::debug_span!("compact_head", depth = requested_k).entered();
1638                let mut compact_prover = ReprSumcheckProver::<EF, EF, R>::from_repr_tables(
1639                    VariableOrder::Prefix,
1640                    Poly::new(factors),
1641                    Poly::new(generators),
1642                    batch.initial_sum(&tensor, successor.as_ref()),
1643                );
1644                let mut challenges = Vec::with_capacity(n);
1645                for _ in 0..requested_k {
1646                    challenges.push(compact_prover.round(&mut sumcheck, &mut rounds_transcript));
1647                }
1648                let handoff_sum = compact_prover.claimed_sum();
1649                let generators = compact_prover.weights().into_evals();
1650                drop(compact_prover);
1651                drop(head_span);
1652
1653                let bound_packing = materialize_compact_packing::<EF, R, _>(
1654                    packing,
1655                    offset,
1656                    n,
1657                    requested_k,
1658                    &challenges,
1659                );
1660                let bound_weights = {
1661                    let _span =
1662                        tracing::debug_span!("compact_weights", depth = requested_k).entered();
1663                    batch.weights_from_generators::<R>(tail_equality, &generators)
1664                };
1665                let _span = tracing::debug_span!("compact_tail", depth = requested_k).entered();
1666                let mut tail_prover = ReprSumcheckProver::<EF, EF, R>::from_repr_tables(
1667                    VariableOrder::Prefix,
1668                    bound_packing,
1669                    bound_weights,
1670                    handoff_sum,
1671                );
1672                for _ in requested_k..n {
1673                    challenges.push(tail_prover.round(&mut sumcheck, &mut rounds_transcript));
1674                }
1675                let final_eval = tail_prover.evals().as_slice()[0].into_transcript();
1676                rounds_transcript.finish();
1677                (Point::new(challenges), final_eval)
1678            })
1679        } else {
1680            let restricted = self.restricted_packing::<R, _>(packing);
1681            let rounds = restricted.num_variables();
1682            let weights =
1683                batch.weights_over::<R>(equality.as_ref().expect("dense equality exists"));
1684            let mut prover = ReprSumcheckProver::<EF, EF, R>::from_repr_tables(
1685                VariableOrder::Prefix,
1686                restricted,
1687                weights,
1688                batch.initial_sum(&tensor, successor.as_ref()),
1689            );
1690            let r_prime = transcript.batched_sumcheck(|challenger| {
1691                prover.compute_sumcheck_polynomials(&mut sumcheck, challenger, rounds, 0)
1692            });
1693            let final_eval = prover.evals().as_slice()[0].into_transcript();
1694            (r_prime, final_eval)
1695        };
1696
1697        // After the last round the evaluation side has folded to the packing at that point.
1698        // No second pass over the packing is needed to find it.
1699        transcript.surviving_claim(final_eval);
1700        transcript.finish();
1701
1702        (
1703            BitRingSwitchProof {
1704                tensor,
1705                successor,
1706                sumcheck,
1707                final_eval,
1708            },
1709            self.restore_prefix(&r_prime),
1710            final_eval,
1711        )
1712    }
1713
1714    /// Replay the reduction against the current claim and return the claim it leaves behind.
1715    ///
1716    /// The same as [`Self::verify_readings`] with the current reading alone.
1717    ///
1718    /// # Errors
1719    ///
1720    /// As [`Self::verify_readings`].
1721    pub fn verify<Challenger>(
1722        &self,
1723        proof: &BitRingSwitchProof<EF>,
1724        claimed_sum: EF,
1725        challenger: &mut Challenger,
1726    ) -> Result<(Point<EF>, EF), BitRingSwitchProofError>
1727    where
1728        Challenger: FieldChallenger<EF> + GrindingChallenger<Witness = EF>,
1729    {
1730        self.verify_readings(proof, Some(claimed_sum), None, challenger)
1731    }
1732
1733    /// Replay the reduction and return the claim it leaves behind, as a point and a value.
1734    ///
1735    /// Discharging that pair against a commitment to the packing is the caller's business.
1736    ///
1737    /// # Arguments
1738    ///
1739    /// - The proof.
1740    /// - The claimed evaluation of the witness at the point, when it is to be checked.
1741    /// - The claimed evaluation of its successor view there, when it is to be checked.
1742    /// - The challenger, advanced only once every structural check passes.
1743    ///
1744    /// # Errors
1745    ///
1746    /// Before the challenger is touched:
1747    ///
1748    /// - A malformed element.
1749    /// - Successor elements whose presence disagrees with the setup.
1750    /// - A non-empty grinding witness list, since this reduction never grinds.
1751    /// - No reading supplied, or a successor reading the setup does not answer.
1752    ///
1753    /// After it:
1754    ///
1755    /// - A claimed evaluation disagreeing with the elements' columns.
1756    /// - A failed sumcheck round, or a final claim that does not close it.
1757    pub fn verify_readings<Challenger>(
1758        &self,
1759        proof: &BitRingSwitchProof<EF>,
1760        current: Option<EF>,
1761        next: Option<EF>,
1762        challenger: &mut Challenger,
1763    ) -> Result<(Point<EF>, EF), BitRingSwitchProofError>
1764    where
1765        Challenger: FieldChallenger<EF> + GrindingChallenger<Witness = EF>,
1766    {
1767        // Every structural rejection runs before the challenger is touched.
1768        // A malformed proof therefore never leaves a half-advanced transcript.
1769        self.check_structure(proof, current, next)?;
1770
1771        // The verifier derives the slot restriction from the public incoming point.
1772        // Proof data cannot choose how many rounds are expected.
1773        let (prefix, _) = self.fixed_prefix();
1774        let rounds = self.num_variables() - prefix;
1775        let mut transcript =
1776            BitRingSwitchVerifierTranscript::<Challenger, EF>::new(challenger, self.shape());
1777        let (r_batch, alpha) = transcript.statement(
1778            &self.point,
1779            proof.tensor.rows(),
1780            proof
1781                .successor
1782                .as_ref()
1783                .map(|elements| (elements.carry.rows(), elements.last.rows())),
1784        )?;
1785
1786        if let Err(error) = self.check_readings(proof, current, next) {
1787            transcript.abort();
1788            return Err(error);
1789        }
1790
1791        let batch = self
1792            .batch_drawn(&r_batch, alpha)
1793            .expect("the transcript draws what the reduction's kind batches with");
1794
1795        // The initial sum is derived from the elements' rows, never taken from the prover.
1796        // That is what makes a dishonest element catchable at all.
1797        let mut sum = batch.initial_sum(&proof.tensor, proof.successor.as_ref());
1798        let replay = transcript.batched_sumcheck(|challenger| {
1799            proof
1800                .sumcheck
1801                .verify_rounds(challenger, &mut sum, rounds, 0, Basis::Evaluation)
1802        });
1803        let r_prime = match replay {
1804            Ok(point) => point,
1805            Err(error) => {
1806                transcript.abort();
1807                return Err(error.into());
1808            }
1809        };
1810
1811        transcript.surviving_claim(proof.final_eval);
1812        transcript.finish();
1813
1814        // The rounds close on the weight multilinear at their end point, times the value.
1815        // The weight comes through the lifted elements rather than another pass.
1816        let closing = batch.closing_weight(&r_prime)?;
1817        if sum != closing * proof.final_eval {
1818            return Err(BitRingSwitchProofError::FinalCheck);
1819        }
1820
1821        Ok((self.restore_prefix(&r_prime), proof.final_eval))
1822    }
1823
1824    /// Every rejection that needs neither the transcript nor a claim check.
1825    fn check_structure(
1826        &self,
1827        proof: &BitRingSwitchProof<EF>,
1828        current: Option<EF>,
1829        next: Option<EF>,
1830    ) -> Result<(), BitRingSwitchProofError> {
1831        self.check_elements(&proof.tensor, proof.successor.as_ref(), current, next)?;
1832        check_no_grinding(&proof.sumcheck)
1833    }
1834
1835    /// Every rejection of one claim's elements and readings that needs no transcript.
1836    ///
1837    /// The grinding check belongs to the sumcheck, which a batch of claims shares.
1838    pub(super) fn check_elements(
1839        &self,
1840        tensor: &BitTensor<EF>,
1841        successor: Option<&SuccessorTensors<EF>>,
1842        current: Option<EF>,
1843        next: Option<EF>,
1844    ) -> Result<(), BitRingSwitchProofError> {
1845        // Every element is a square bit matrix, whichever of them the proof carries.
1846        let successor_elements = successor
1847            .into_iter()
1848            .flat_map(|elements| [&elements.carry, &elements.last]);
1849        if let Some(malformed) = core::iter::once(tensor)
1850            .chain(successor_elements)
1851            .find(|element| !element.is_well_formed())
1852        {
1853            return Err(TranscriptWidth::TensorRows {
1854                expected: BitTensor::<EF>::DIMENSION,
1855                actual: malformed.rows().len(),
1856            }
1857            .into());
1858        }
1859        // The element count is the setup's, never the proof's.
1860        let expected = self.sends_successor_tensors();
1861        if successor.is_some() != expected {
1862            return Err(TranscriptWidth::successor_elements(expected, successor.is_some()).into());
1863        }
1864        if current.is_none() && next.is_none() {
1865            return Err(BitRingSwitchProofError::NoReading);
1866        }
1867        if next.is_some() && self.successor.is_none() {
1868            return Err(BitRingSwitchProofError::NoSuccessorView);
1869        }
1870        Ok(())
1871    }
1872
1873    /// Check each supplied reading against the elements' columns, current before next.
1874    fn check_readings(
1875        &self,
1876        proof: &BitRingSwitchProof<EF>,
1877        current: Option<EF>,
1878        next: Option<EF>,
1879    ) -> Result<(), BitRingSwitchProofError> {
1880        self.check_element_readings(&proof.tensor, proof.successor.as_ref(), current, next)
1881    }
1882
1883    /// Check each supplied reading against one claim's elements, current before next.
1884    pub(super) fn check_element_readings(
1885        &self,
1886        tensor: &BitTensor<EF>,
1887        successor: Option<&SuccessorTensors<EF>>,
1888        current: Option<EF>,
1889        next: Option<EF>,
1890    ) -> Result<(), BitRingSwitchProofError> {
1891        // The columns are the witness's bit planes at the kept coordinates.
1892        // The absorbed coordinates weigh them back together, their only use here.
1893        if current.is_some_and(|claim| self.incoming_claim(tensor) != claim) {
1894            return Err(BitRingSwitchProofError::ClaimMismatch);
1895        }
1896        // The successor reads the same planes one bit on, and the successor elements' edges.
1897        if let Some(claim) = next
1898            && self.successor_claim(tensor, successor)? != claim
1899        {
1900            return Err(BitRingSwitchProofError::SuccessorClaimMismatch);
1901        }
1902        Ok(())
1903    }
1904}
1905
1906/// Refuse a sumcheck carrying grinding witnesses, which no bit-alphabet reduction searches for.
1907pub(super) const fn check_no_grinding<EF: Field>(
1908    sumcheck: &SumcheckData<EF, EF>,
1909) -> Result<(), BitRingSwitchProofError> {
1910    if sumcheck.pow_witnesses.is_empty() {
1911        Ok(())
1912    } else {
1913        Err(BitRingSwitchProofError::NonEmptyPowWitnesses {
1914            actual: sumcheck.pow_witnesses.len(),
1915        })
1916    }
1917}
1918
1919#[cfg(test)]
1920mod tests {
1921    use alloc::vec;
1922    use alloc::vec::Vec;
1923
1924    use p3_binary_field::{BinaryChallenger, BinaryField16, BinaryField128, Ghash128};
1925    use p3_challenger::{CanSample, HashChallenger};
1926    use p3_field::{Field, PrimeCharacteristicRing};
1927    use p3_keccak::Keccak256Hash;
1928    use proptest::prelude::*;
1929    use rand::rngs::SmallRng;
1930    use rand::{RngExt, SeedableRng};
1931
1932    use super::*;
1933
1934    type EF = BinaryField16;
1935    type Chal = BinaryChallenger<EF, HashChallenger<u8, Keccak256Hash, 32>>;
1936    type WideChal = BinaryChallenger<BinaryField128, HashChallenger<u8, Keccak256Hash, 32>>;
1937
1938    /// A fresh sponge, so the two sides start from the same state.
1939    fn challenger() -> Chal {
1940        Chal::from_hasher(Vec::new(), Keccak256Hash)
1941    }
1942
1943    /// A fresh wide-field sponge for representation differential checks.
1944    fn wide_challenger() -> WideChal {
1945        WideChal::from_hasher(Vec::new(), Keccak256Hash)
1946    }
1947
1948    /// A random bit witness of the given byte length.
1949    fn bits(seed: u64, bytes: usize) -> Vec<u8> {
1950        let mut rng = SmallRng::seed_from_u64(seed);
1951        (0..bytes).map(|_| rng.random::<u8>()).collect()
1952    }
1953
1954    /// One reduction over a witness of the given byte length.
1955    ///
1956    /// The batching challenge comes back beside it rather than folded in.
1957    /// A test then reaches the second stage the way the protocol does.
1958    fn fixture(
1959        seed: u64,
1960        bytes: usize,
1961    ) -> (BitRingSwitch<EF>, BitPacking<EF>, Point<EF>, Point<EF>) {
1962        let packing = BitPacking::<EF>::new(&bits(seed, bytes)).unwrap();
1963        let mut rng = SmallRng::seed_from_u64(seed ^ 0xFFFF);
1964
1965        // The whole point, of which the packing keeps the leading coordinates.
1966        let r = Point::<EF>::rand(
1967            &mut rng,
1968            packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
1969        );
1970        let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
1971        let reduction = BitRingSwitch::new(&r).unwrap();
1972
1973        (reduction, packing, r, r_batch)
1974    }
1975
1976    fn assert_successor_proofs_match_dense(
1977        reduction: &BitRingSwitch<EF>,
1978        packing: &BitPacking<EF>,
1979        requested_k: usize,
1980    ) {
1981        let tensor = reduction.tensor(packing).unwrap();
1982        let successor = reduction.successor_tensors(packing).unwrap();
1983        let current = reduction.incoming_claim(&tensor);
1984        let next = reduction
1985            .successor_claim(&tensor, successor.as_ref())
1986            .unwrap();
1987        let mut dense_challenger = challenger();
1988        let dense =
1989            reduction.prove_with_compact_depth::<EF, _, _>(packing, &mut dense_challenger, 0, true);
1990        let dense_next: EF = dense_challenger.sample();
1991        let mut requested_challenger = challenger();
1992        let requested = reduction.prove_with_compact_depth::<EF, _, _>(
1993            packing,
1994            &mut requested_challenger,
1995            requested_k,
1996            true,
1997        );
1998        let requested_next: EF = requested_challenger.sample();
1999        assert_eq!(requested.0.tensor, dense.0.tensor);
2000        assert_eq!(requested.0.successor, dense.0.successor);
2001        assert_eq!(
2002            requested.0.sumcheck.polynomial_evaluations,
2003            dense.0.sumcheck.polynomial_evaluations
2004        );
2005        assert_eq!(
2006            requested.0.sumcheck.pow_witnesses,
2007            dense.0.sumcheck.pow_witnesses
2008        );
2009        assert_eq!(requested.0.final_eval, dense.0.final_eval);
2010        assert_eq!(requested.1, dense.1);
2011        assert_eq!(requested.2, dense.2);
2012        assert_eq!(requested_next, dense_next);
2013        assert!(
2014            reduction
2015                .verify_readings(&requested.0, Some(current), Some(next), &mut challenger())
2016                .is_ok()
2017        );
2018    }
2019
2020    /// The witness as a multilinear over every variable, one element per bit.
2021    fn embedded(witness: &[u8]) -> Poly<EF> {
2022        embedded_over(witness)
2023    }
2024
2025    /// The same embedding over any level the bits are read into.
2026    fn embedded_over<K: Field>(witness: &[u8]) -> Poly<K> {
2027        Poly::new(
2028            (0..witness.len() * 8)
2029                .map(|cell| {
2030                    if (witness[cell / 8] >> (cell % 8)) & 1 == 1 {
2031                        K::ONE
2032                    } else {
2033                        K::ZERO
2034                    }
2035                })
2036                .collect::<Vec<_>>(),
2037        )
2038    }
2039
2040    #[test]
2041    fn compact_prefix_fold_matches_the_existing_prefix_kernel() {
2042        let mut rng = SmallRng::seed_from_u64(0xC0A7);
2043        for (variables, rounds) in [(0, 0), (1, 1), (4, 2), (7, 3), (8, 4)] {
2044            let mut rng_challenges = SmallRng::seed_from_u64(0xF01D + variables as u64);
2045            let challenges = if rounds >= 2 {
2046                vec![BinaryField128::ZERO, BinaryField128::ONE]
2047                    .into_iter()
2048                    .chain((2..rounds).map(|_| rng_challenges.random::<BinaryField128>()))
2049                    .collect::<Vec<_>>()
2050            } else {
2051                (0..rounds)
2052                    .map(|_| rng_challenges.random::<BinaryField128>())
2053                    .collect::<Vec<_>>()
2054            };
2055            let mut values: Vec<BinaryField128> = (0..(1usize << variables))
2056                .map(|_| rng.random::<BinaryField128>())
2057                .collect();
2058            let mut reference = Poly::new(values.clone());
2059            for &challenge in &challenges {
2060                reference.fix_prefix_var_mut(challenge);
2061            }
2062            bind_scratch_prefix(&mut values, &challenges);
2063            assert_eq!(values, reference.into_evals());
2064        }
2065    }
2066
2067    #[test]
2068    fn compact_composed_weight_map_matches_dense_at_zero_and_one_heads() {
2069        let packing = BitPacking::<EF>::new(&bits(0xC01F, 16)).unwrap();
2070        let mut rng = SmallRng::seed_from_u64(0xD01F);
2071        let reduction = BitRingSwitch::new(&Point::<EF>::rand(
2072            &mut rng,
2073            packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2074        ))
2075        .unwrap();
2076        let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
2077        let batch = reduction.batch(&r_batch).unwrap();
2078        let dense_equality = FactoredEquality::new(reduction.high(), LOG_CHUNK);
2079        let dense_weights = batch.weights_over::<EF>(&dense_equality);
2080        let head = &reduction.high()[..2];
2081        let head_equality = Poly::new_from_point(head, EF::ONE);
2082        let tail_equality = FactoredEquality::new(&reduction.high()[2..], LOG_CHUNK);
2083        let batching = batch.eq_batch.as_slice().to_vec();
2084        let coordinate_sums = CoordinateSums::<EF, EF>::new(&batching);
2085
2086        for challenges in [
2087            [EF::ZERO, EF::ZERO],
2088            [EF::ZERO, EF::ONE],
2089            [EF::ONE, EF::ZERO],
2090            [EF::ONE, EF::ONE],
2091        ] {
2092            let lambda = Poly::new_from_point(&challenges, EF::ONE);
2093            let mut generators = Vec::with_capacity(BitTensor::<EF>::DIMENSION);
2094            for coordinate in 0..BitTensor::<EF>::DIMENSION {
2095                let mut basis = Coefficients::<EF>::zero();
2096                basis.set(coordinate);
2097                let mut generator = EF::ZERO;
2098                for bank in 0..4 {
2099                    generator += lambda.as_slice()[bank]
2100                        * coordinate_sums.sum(head_equality.as_slice()[bank] * basis.element());
2101                }
2102                generators.push(generator);
2103            }
2104            let compact_weights = batch.weights_from_generators(&tail_equality, &generators);
2105            let mut folded_dense = dense_weights.clone();
2106            for &challenge in &challenges {
2107                folded_dense.fix_prefix_var_mut(challenge);
2108            }
2109            assert_eq!(compact_weights, folded_dense);
2110        }
2111    }
2112
2113    #[test]
2114    fn forced_compact_depths_match_the_dense_transcript() {
2115        for k in [1, 2, 4, 6] {
2116            for remaining_n in [k, k + 1] {
2117                let bytes = 2usize << remaining_n;
2118                let packing =
2119                    BitPacking::<EF>::new(&bits(0xC011 + k as u64 + remaining_n as u64, bytes))
2120                        .unwrap();
2121                let mut rng = SmallRng::seed_from_u64(0xD00D + k as u64 + remaining_n as u64);
2122                let mut point = Point::<EF>::rand(
2123                    &mut rng,
2124                    packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2125                )
2126                .as_slice()
2127                .to_vec();
2128                point[0] = loop {
2129                    let value = rng.random::<EF>();
2130                    if value != EF::ZERO && value != EF::ONE {
2131                        break value;
2132                    }
2133                };
2134                let reduction = BitRingSwitch::new(&Point::new(point)).unwrap();
2135                assert_eq!(packing.num_variables(), remaining_n);
2136                assert_eq!(reduction.fixed_prefix().0, 0);
2137                assert!(reduction.compact_depth_is_eligible(remaining_n, k, true));
2138                let current = reduction.incoming_claim(&reduction.tensor(&packing).unwrap());
2139                let mut dense_challenger = challenger();
2140                let (dense, dense_point, dense_value) = reduction
2141                    .prove_with_compact_depth::<EF, _, _>(&packing, &mut dense_challenger, 0, true);
2142                let dense_next: EF = dense_challenger.sample();
2143                let mut compact_challenger = challenger();
2144                let (compact, compact_point, compact_value) = reduction
2145                    .prove_with_compact_depth::<EF, _, _>(
2146                        &packing,
2147                        &mut compact_challenger,
2148                        k,
2149                        true,
2150                    );
2151                let compact_next: EF = compact_challenger.sample();
2152
2153                assert_eq!(compact.tensor, dense.tensor, "k={k}, n={remaining_n}");
2154                assert_eq!(compact.successor, dense.successor, "k={k}, n={remaining_n}");
2155                assert_eq!(
2156                    compact.sumcheck.polynomial_evaluations, dense.sumcheck.polynomial_evaluations,
2157                    "k={k}, n={remaining_n}"
2158                );
2159                assert_eq!(
2160                    compact.sumcheck.pow_witnesses, dense.sumcheck.pow_witnesses,
2161                    "k={k}, n={remaining_n}"
2162                );
2163                assert_eq!(compact_point, dense_point, "k={k}, n={remaining_n}");
2164                assert_eq!(compact_value, dense_value, "k={k}, n={remaining_n}");
2165                assert_eq!(
2166                    compact.final_eval, dense.final_eval,
2167                    "k={k}, n={remaining_n}"
2168                );
2169                assert_eq!(compact_next, dense_next, "k={k}, n={remaining_n}");
2170                assert!(
2171                    reduction
2172                        .verify(&compact, current, &mut challenger())
2173                        .is_ok()
2174                );
2175            }
2176        }
2177
2178        // The common guard must fall back before attempting a two-bank slice when only one
2179        // free coordinate remains.
2180        let packing = BitPacking::<EF>::new(&bits(0xC012, 4)).unwrap();
2181        let mut rng = SmallRng::seed_from_u64(0xD012);
2182        let reduction = BitRingSwitch::new(&Point::<EF>::rand(
2183            &mut rng,
2184            packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2185        ))
2186        .unwrap();
2187        let mut dense_challenger = challenger();
2188        let dense = reduction.prove_with_compact_depth::<EF, _, _>(
2189            &packing,
2190            &mut dense_challenger,
2191            0,
2192            true,
2193        );
2194        let mut compact_challenger = challenger();
2195        let compact = reduction.prove_with_compact_depth::<EF, _, _>(
2196            &packing,
2197            &mut compact_challenger,
2198            2,
2199            true,
2200        );
2201        assert_eq!(
2202            dense.0.sumcheck.polynomial_evaluations,
2203            compact.0.sumcheck.polynomial_evaluations
2204        );
2205        assert_eq!(dense.1, compact.1);
2206        assert_eq!(dense.2, compact.2);
2207    }
2208
2209    #[test]
2210    fn compact_prefix_uses_a_nonzero_selected_slot() {
2211        let packing = BitPacking::<EF>::new(&bits(0xC016, 64)).unwrap();
2212        let mut rng = SmallRng::seed_from_u64(0xD016);
2213        let mut coordinates = Point::<EF>::rand(
2214            &mut rng,
2215            packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2216        )
2217        .as_slice()
2218        .to_vec();
2219        coordinates[0] = EF::ONE;
2220        let reduction = BitRingSwitch::new(&Point::new(coordinates)).unwrap();
2221        let current = reduction.incoming_claim(&reduction.tensor(&packing).unwrap());
2222        let mut dense_challenger = challenger();
2223        let dense = reduction.prove_with_compact_depth::<EF, _, _>(
2224            &packing,
2225            &mut dense_challenger,
2226            0,
2227            true,
2228        );
2229        let dense_next: EF = dense_challenger.sample();
2230        let mut compact_challenger = challenger();
2231        let compact = reduction.prove_with_compact_depth::<EF, _, _>(
2232            &packing,
2233            &mut compact_challenger,
2234            2,
2235            true,
2236        );
2237        let compact_next: EF = compact_challenger.sample();
2238        assert_eq!(
2239            reduction.fixed_prefix().1,
2240            1,
2241            "the test must select the upper slot"
2242        );
2243        assert_eq!(compact.0.tensor, dense.0.tensor);
2244        assert_eq!(compact.0.successor, dense.0.successor);
2245        assert_eq!(
2246            compact.0.sumcheck.polynomial_evaluations,
2247            dense.0.sumcheck.polynomial_evaluations
2248        );
2249        assert_eq!(
2250            compact.0.sumcheck.pow_witnesses,
2251            dense.0.sumcheck.pow_witnesses
2252        );
2253        assert_eq!(compact.0.final_eval, dense.0.final_eval);
2254        assert_eq!(compact.1, dense.1);
2255        assert_eq!(compact.2, dense.2);
2256        assert_eq!(compact_next, dense_next);
2257        assert!(
2258            reduction
2259                .verify(&compact.0, current, &mut challenger())
2260                .is_ok()
2261        );
2262    }
2263
2264    #[test]
2265    fn successor_compact_depth_refuses_fixed_selector_prefixes() {
2266        let n = 4;
2267        let kept_rows = 2;
2268        let row_variables = BitRingSwitch::<EF>::ABSORBED + kept_rows;
2269        let total = n + BitRingSwitch::<EF>::ABSORBED;
2270        let mut rng = SmallRng::seed_from_u64(0xD017);
2271        let mut non_boolean = || loop {
2272            let value = rng.random::<EF>();
2273            if value != EF::ZERO && value != EF::ONE {
2274                break value;
2275            }
2276        };
2277
2278        // One fixed selector leaves only one free selector, so requested k2 is unsafe.
2279        let mut partial = vec![EF::ZERO; total];
2280        partial[0] = EF::ONE;
2281        partial[1] = non_boolean();
2282        partial[2..n].fill_with(&mut non_boolean);
2283        let partial_reduction =
2284            BitRingSwitch::with_successor(&Point::new(partial), row_variables).unwrap();
2285        let partial_prefix = partial_reduction.fixed_prefix().0;
2286        assert_eq!(partial_prefix, 1);
2287        assert!(!partial_reduction.compact_depth_is_eligible(n - partial_prefix, 2, true,));
2288        let fallback_packing = BitPacking::<EF>::new(&bits(0xD01B, 32)).unwrap();
2289        assert_successor_proofs_match_dense(&partial_reduction, &fallback_packing, 2);
2290
2291        // Fixing every selector leaves only the kept rows, and therefore no compact head.
2292        let mut all = vec![EF::ZERO; total];
2293        all[..n - kept_rows].fill(EF::ONE);
2294        all[n - kept_rows..n].fill_with(&mut non_boolean);
2295        let all_reduction = BitRingSwitch::with_successor(&Point::new(all), row_variables).unwrap();
2296        let all_prefix = all_reduction.fixed_prefix().0;
2297        assert_eq!(all_prefix, n - kept_rows);
2298        assert!(!all_reduction.compact_depth_is_eligible(n - all_prefix, 1, true));
2299        assert_successor_proofs_match_dense(&all_reduction, &fallback_packing, 2);
2300
2301        // A successor contained inside one packed element still takes the plain path and must
2302        // refuse a requested head wider than the one remaining variable.
2303        let inside_packing = BitPacking::<EF>::new(&bits(0xC018, 4)).unwrap();
2304        let inside_point = Point::<EF>::rand(
2305            &mut rng,
2306            inside_packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2307        );
2308        let inside =
2309            BitRingSwitch::with_successor(&inside_point, BitRingSwitch::<EF>::ABSORBED - 1)
2310                .unwrap();
2311        assert!(!inside.compact_depth_is_eligible(inside.num_variables(), 2, true));
2312        assert_successor_proofs_match_dense(&inside, &inside_packing, 2);
2313
2314        // A kept-row width above the test equality block still has complete columns in each
2315        // bank and must take the compact successor path.
2316        let wide_packing = BitPacking::<EF>::new(&bits(0xC019, 32)).unwrap();
2317        let mut wide_point = Point::<EF>::rand(
2318            &mut rng,
2319            wide_packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2320        )
2321        .as_slice()
2322        .to_vec();
2323        wide_point[0] = loop {
2324            let value = rng.random::<EF>();
2325            if value != EF::ZERO && value != EF::ONE {
2326                break value;
2327            }
2328        };
2329        let wide = BitRingSwitch::with_successor(
2330            &Point::new(wide_point),
2331            BitRingSwitch::<EF>::ABSORBED + 2,
2332        )
2333        .unwrap();
2334        assert_eq!(wide.fixed_prefix().0, 0);
2335        assert!(wide.compact_depth_is_eligible(wide.num_variables(), 2, true));
2336        let wide_tensor = wide.tensor(&wide_packing).unwrap();
2337        let wide_successor = wide.successor_tensors(&wide_packing).unwrap();
2338        let wide_current = wide.incoming_claim(&wide_tensor);
2339        let wide_next = wide
2340            .successor_claim(&wide_tensor, wide_successor.as_ref())
2341            .unwrap();
2342        let mut wide_dense_challenger = challenger();
2343        let wide_dense = wide.prove_with_compact_depth::<EF, _, _>(
2344            &wide_packing,
2345            &mut wide_dense_challenger,
2346            0,
2347            true,
2348        );
2349        let mut wide_compact_challenger = challenger();
2350        let wide_compact = wide.prove_with_compact_depth::<EF, _, _>(
2351            &wide_packing,
2352            &mut wide_compact_challenger,
2353            2,
2354            true,
2355        );
2356        assert_eq!(wide_compact.0.successor, wide_dense.0.successor);
2357        assert_eq!(
2358            wide_compact.0.sumcheck.polynomial_evaluations,
2359            wide_dense.0.sumcheck.polynomial_evaluations
2360        );
2361        assert!(
2362            wide.verify_readings(
2363                &wide_compact.0,
2364                Some(wide_current),
2365                Some(wide_next),
2366                &mut challenger(),
2367            )
2368            .is_ok()
2369        );
2370    }
2371
2372    #[test]
2373    fn compact_plain_path_handles_zero_one_and_sparse_packing() {
2374        let mut sparse = vec![0u8; 16];
2375        sparse[0] = 1;
2376        sparse[9] = 0x80;
2377        for (seed, witness) in [
2378            (0xD018, vec![0u8; 16]),
2379            (0xD019, vec![0xFFu8; 16]),
2380            (0xD01A, sparse),
2381        ] {
2382            let packing = BitPacking::<EF>::new(&witness).unwrap();
2383            let mut rng = SmallRng::seed_from_u64(seed);
2384            let reduction = BitRingSwitch::new(&Point::<EF>::rand(
2385                &mut rng,
2386                packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2387            ))
2388            .unwrap();
2389            let mut dense_challenger = challenger();
2390            let dense = reduction.prove_with_compact_depth::<EF, _, _>(
2391                &packing,
2392                &mut dense_challenger,
2393                0,
2394                true,
2395            );
2396            let mut compact_challenger = challenger();
2397            let compact = reduction.prove_with_compact_depth::<EF, _, _>(
2398                &packing,
2399                &mut compact_challenger,
2400                2,
2401                true,
2402            );
2403            assert_eq!(compact.0.tensor, dense.0.tensor);
2404            assert_eq!(
2405                compact.0.sumcheck.polynomial_evaluations,
2406                dense.0.sumcheck.polynomial_evaluations
2407            );
2408            assert_eq!(
2409                compact.0.sumcheck.pow_witnesses,
2410                dense.0.sumcheck.pow_witnesses
2411            );
2412            assert_eq!(compact.1, dense.1);
2413            assert_eq!(compact.2, dense.2);
2414        }
2415    }
2416
2417    #[test]
2418    fn compact_successor_and_ghash_paths_match_dense() {
2419        let packing = BitPacking::<EF>::new(&bits(0xC013, 16)).unwrap();
2420        let mut rng = SmallRng::seed_from_u64(0xD013);
2421        let reduction = BitRingSwitch::with_successor(
2422            &Point::<EF>::rand(
2423                &mut rng,
2424                packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2425            ),
2426            BitRingSwitch::<EF>::ABSORBED + 1,
2427        )
2428        .unwrap();
2429        let tensor = reduction.tensor(&packing).unwrap();
2430        let successor = reduction.successor_tensors(&packing).unwrap();
2431        let current = reduction.incoming_claim(&tensor);
2432        let next = reduction
2433            .successor_claim(&tensor, successor.as_ref())
2434            .unwrap();
2435        let mut dense_challenger = challenger();
2436        let (dense, dense_point, dense_value) = reduction.prove_with_compact_depth::<EF, _, _>(
2437            &packing,
2438            &mut dense_challenger,
2439            0,
2440            true,
2441        );
2442        let mut compact_challenger = challenger();
2443        let (compact, compact_point, compact_value) = reduction
2444            .prove_with_compact_depth::<EF, _, _>(&packing, &mut compact_challenger, 2, true);
2445        assert_eq!(compact.successor, dense.successor);
2446        assert_eq!(
2447            compact.sumcheck.polynomial_evaluations,
2448            dense.sumcheck.polynomial_evaluations
2449        );
2450        assert_eq!(compact_point, dense_point);
2451        assert_eq!(compact_value, dense_value);
2452        assert!(
2453            reduction
2454                .verify_readings(&compact, Some(current), Some(next), &mut challenger())
2455                .is_ok()
2456        );
2457
2458        for (bytes, requested_k, has_tail) in [(64, 2, false), (64, 1, true), (128, 2, true)] {
2459            let wide_packing =
2460                BitPacking::<BinaryField128>::new(&bits(0xC014 + bytes as u64, bytes)).unwrap();
2461            let mut wide_rng = SmallRng::seed_from_u64(0xD014 + bytes as u64);
2462            let mut wide_point = Point::<BinaryField128>::rand(
2463                &mut wide_rng,
2464                wide_packing.num_variables() + BitRingSwitch::<BinaryField128>::ABSORBED,
2465            )
2466            .as_slice()
2467            .to_vec();
2468            wide_point[0] = loop {
2469                let value = wide_rng.random::<BinaryField128>();
2470                if value != BinaryField128::ZERO && value != BinaryField128::ONE {
2471                    break value;
2472                }
2473            };
2474            let wide_reduction = BitRingSwitch::new(&Point::new(wide_point)).unwrap();
2475            assert_eq!(wide_reduction.fixed_prefix().0, 0);
2476            assert!(wide_packing.num_variables() >= requested_k);
2477            assert_eq!(wide_packing.num_variables() > requested_k, has_tail);
2478            assert!(wide_reduction.compact_depth_is_eligible(
2479                wide_packing.num_variables(),
2480                requested_k,
2481                true,
2482            ));
2483            let mut wide_dense_challenger = wide_challenger();
2484            let (wide_dense, wide_dense_point, wide_dense_value) = wide_reduction
2485                .prove_with_compact_depth::<BinaryField128, _, _>(
2486                    &wide_packing,
2487                    &mut wide_dense_challenger,
2488                    0,
2489                    true,
2490                );
2491            let mut wide_compact_challenger = wide_challenger();
2492            let (wide_compact, wide_compact_point, wide_compact_value) = wide_reduction
2493                .prove_with_compact_depth::<Ghash128, _, _>(
2494                    &wide_packing,
2495                    &mut wide_compact_challenger,
2496                    requested_k,
2497                    true,
2498                );
2499            let wide_dense_next: BinaryField128 = wide_dense_challenger.sample();
2500            let wide_compact_next: BinaryField128 = wide_compact_challenger.sample();
2501            assert_eq!(wide_compact.tensor, wide_dense.tensor);
2502            assert_eq!(wide_compact.successor, wide_dense.successor);
2503            assert_eq!(
2504                wide_compact.sumcheck.polynomial_evaluations,
2505                wide_dense.sumcheck.polynomial_evaluations
2506            );
2507            assert_eq!(
2508                wide_compact.sumcheck.pow_witnesses,
2509                wide_dense.sumcheck.pow_witnesses
2510            );
2511            assert_eq!(wide_compact_point, wide_dense_point);
2512            assert_eq!(wide_compact_value, wide_dense_value);
2513            assert_eq!(wide_compact.final_eval, wide_dense.final_eval);
2514            assert_eq!(wide_compact_next, wide_dense_next);
2515            let wide_current = wide_reduction.incoming_claim(&wide_compact.tensor);
2516            assert!(
2517                wide_reduction
2518                    .verify(&wide_compact, wide_current, &mut wide_challenger())
2519                    .is_ok()
2520            );
2521        }
2522    }
2523
2524    #[test]
2525    fn production_gate_uses_compact_two_rounds_at_the_minimum_shape() {
2526        let packing = BitPacking::<EF>::new(&bits(0xC015, 1024)).unwrap();
2527        let mut rng = SmallRng::seed_from_u64(0xD015);
2528        let mut point = Point::<EF>::rand(
2529            &mut rng,
2530            packing.num_variables() + BitRingSwitch::<EF>::ABSORBED,
2531        )
2532        .as_slice()
2533        .to_vec();
2534        point[0] = loop {
2535            let value = rng.random::<EF>();
2536            if value != EF::ZERO && value != EF::ONE {
2537                break value;
2538            }
2539        };
2540        let reduction = BitRingSwitch::new(&Point::new(point)).unwrap();
2541        // A production build first accepts 22 free variables, and a test build 9.
2542        assert_eq!(
2543            compact_size_floor(COMPACT_PRODUCTION_MIN_ROUNDS, PRODUCTION_LOG_CHUNK),
2544            22
2545        );
2546        assert_eq!(
2547            compact_size_floor(COMPACT_PRODUCTION_MIN_ROUNDS, LOG_CHUNK),
2548            9
2549        );
2550        assert_eq!(reduction.num_variables(), 9);
2551        assert!(reduction.compact_depth_is_eligible(
2552            reduction.num_variables(),
2553            COMPACT_PRODUCTION_MIN_ROUNDS,
2554            false,
2555        ));
2556        assert!(!reduction.compact_depth_is_eligible(
2557            reduction.num_variables() - 1,
2558            COMPACT_PRODUCTION_MIN_ROUNDS,
2559            false,
2560        ));
2561        assert_eq!(
2562            reduction.production_depth(reduction.num_variables()),
2563            COMPACT_PRODUCTION_MIN_ROUNDS
2564        );
2565        let mut dense_challenger = challenger();
2566        let dense = reduction.prove_with_compact_depth::<EF, _, _>(
2567            &packing,
2568            &mut dense_challenger,
2569            0,
2570            true,
2571        );
2572        let mut production_challenger = challenger();
2573        let production = reduction.prove::<EF, _, _>(&packing, &mut production_challenger);
2574        assert_eq!(
2575            production.0.sumcheck.polynomial_evaluations,
2576            dense.0.sumcheck.polynomial_evaluations
2577        );
2578        assert_eq!(production.1, dense.1);
2579        assert_eq!(production.2, dense.2);
2580    }
2581
2582    #[test]
2583    fn the_production_depth_steps_down_to_the_deepest_admitted_head() {
2584        // Invariant: each depth's size floor is one free variable above the depth below it.
2585        //
2586        // So every depth from the shallowest to the deepest is the one some run size gets,
2587        // and a run below the shallowest floor asks for the shallowest, which runs dense.
2588        let (reduction, ..) = fixture(0xDE97, 16);
2589        let floor = |depth| compact_size_floor(depth, LOG_CHUNK);
2590        let below = floor(COMPACT_PRODUCTION_MIN_ROUNDS) - 1;
2591        assert!(!reduction.compact_depth_is_eligible(below, COMPACT_PRODUCTION_MIN_ROUNDS, false));
2592        assert_eq!(
2593            reduction.production_depth(below),
2594            COMPACT_PRODUCTION_MIN_ROUNDS
2595        );
2596        for depth in COMPACT_PRODUCTION_MIN_ROUNDS..=COMPACT_PRODUCTION_ROUNDS {
2597            assert_eq!(reduction.production_depth(floor(depth)), depth);
2598        }
2599        assert_eq!(
2600            reduction.production_depth(floor(COMPACT_PRODUCTION_ROUNDS) + 3),
2601            COMPACT_PRODUCTION_ROUNDS
2602        );
2603
2604        // Kept row coordinates stay out of the head, which caps it below the size floor.
2605        let mut rng = SmallRng::seed_from_u64(0xDE98);
2606        let absorbed = BitRingSwitch::<EF>::ABSORBED;
2607        let r = Point::<EF>::rand(&mut rng, floor(COMPACT_PRODUCTION_ROUNDS) + absorbed);
2608        let kept_rows = floor(COMPACT_PRODUCTION_ROUNDS) - (COMPACT_PRODUCTION_ROUNDS - 1);
2609        let successor = BitRingSwitch::with_successor(&r, kept_rows + absorbed).unwrap();
2610        assert_eq!(
2611            successor.production_depth(floor(COMPACT_PRODUCTION_ROUNDS)),
2612            COMPACT_PRODUCTION_ROUNDS - 1
2613        );
2614    }
2615
2616    #[test]
2617    fn the_reduction_runs_over_the_variables_the_packing_keeps() {
2618        // Fixture state: 128 cells is 7 variables, of which 16 bits absorb 4.
2619        let (reduction, packing, _, _) = fixture(0x5AE, 16);
2620
2621        assert_eq!(BitRingSwitch::<EF>::ABSORBED, 4);
2622        assert_eq!(reduction.num_variables(), 3);
2623        assert_eq!(packing.num_variables(), 3);
2624    }
2625
2626    /// The equality table over the kept coordinates, dense over the whole hypercube.
2627    ///
2628    /// The production path computes the same table without the run of zeros a Boolean
2629    /// coordinate puts in it, so this is what every case below checks it against.
2630    fn dense_eq_high(reduction: &BitRingSwitch<EF>) -> Poly<EF> {
2631        Poly::new_from_point(
2632            &reduction.point.as_slice()[..reduction.num_variables()],
2633            EF::ONE,
2634        )
2635    }
2636
2637    #[test]
2638    fn the_incoming_claim_is_the_witness_at_the_point() {
2639        // Invariant: what the reduction demands of the claim is the witness's.
2640        //
2641        //     sum_v eq(r_low, v) * column v  ==  t(r)
2642        //
2643        // That identity is what the reduction's first check rests on.
2644        let witness = bits(0xC1A1, 16);
2645        let packing = BitPacking::<EF>::new(&witness).unwrap();
2646        let mut rng = SmallRng::seed_from_u64(0xF001);
2647        let r = Point::<EF>::rand(&mut rng, 7);
2648        let reduction = BitRingSwitch::new(&r).unwrap();
2649
2650        let tensor = reduction.tensor(&packing).unwrap();
2651
2652        assert_eq!(
2653            reduction.incoming_claim(&tensor),
2654            embedded(&witness).eval_base(&r)
2655        );
2656    }
2657
2658    #[test]
2659    fn the_columns_are_the_bit_planes_at_the_kept_coordinates() {
2660        // Invariant: column `v` is cells `d*w + v` read at `r_high`.
2661        // Checked column by column, so a failure localises.
2662        let (reduction, packing, _, _) = fixture(0xB1A, 16);
2663        let eq = dense_eq_high(&reduction);
2664
2665        for (v, &column) in reduction
2666            .tensor(&packing)
2667            .unwrap()
2668            .columns()
2669            .iter()
2670            .enumerate()
2671        {
2672            let expected: EF = (0..packing.len())
2673                .filter(|&w| packing.coefficients(w).get(v))
2674                .map(|w| eq.as_slice()[w])
2675                .sum();
2676            assert_eq!(column, expected, "column {v}");
2677        }
2678    }
2679
2680    #[test]
2681    fn each_row_is_its_own_weighted_sum() {
2682        // Invariant: row `u` is the packing weighted by coordinate `u` of eq.
2683        //
2684        //     row u  =  sum_w A_{w,u} * t'(w)
2685        let (reduction, packing, _, _) = fixture(0x0A5, 16);
2686        let eq = dense_eq_high(&reduction);
2687
2688        for (u, &row) in reduction
2689            .tensor(&packing)
2690            .unwrap()
2691            .rows()
2692            .iter()
2693            .enumerate()
2694        {
2695            let expected: EF = (0..packing.len())
2696                .filter(|&w| Coefficients::of(eq.as_slice()[w]).get(u))
2697                .map(|w| packing.poly().as_slice()[w])
2698                .sum();
2699            assert_eq!(row, expected, "row {u}");
2700        }
2701    }
2702
2703    #[test]
2704    fn a_weight_is_the_subset_sum_the_coordinates_select() {
2705        // Invariant: the weight at `w` adds the table where bits are set.
2706        let (reduction, _, _, r_batch) = fixture(0x5E7, 16);
2707        let batch = reduction.batch(&r_batch).unwrap();
2708        let weights = batch.weights();
2709
2710        // Built from the challenge here, not read off the stage under test.
2711        let eq_batch = Poly::<EF>::new_from_point(r_batch.as_slice(), EF::ONE);
2712
2713        for (w, &value) in dense_eq_high(&reduction).as_slice().iter().enumerate() {
2714            let expected: EF = Coefficients::of(value)
2715                .iter()
2716                .zip(eq_batch.as_slice())
2717                .filter(|&(bit, _)| bit)
2718                .map(|(_, &weight)| weight)
2719                .sum();
2720            assert_eq!(weights.as_slice()[w], expected, "point {w}");
2721        }
2722        assert_eq!(weights.num_variables(), reduction.num_variables());
2723    }
2724
2725    #[test]
2726    fn the_initial_sum_is_the_sum_the_sumcheck_proves() {
2727        // Invariant: the weighted sum over the hypercube is the batched rows.
2728        //
2729        //     sum_w A(w) * t'(w)  ==  sum_u eq(u, r_batch) * row u
2730        //
2731        // Both sides are derived, never sent, which a dishonest element hits.
2732        let (reduction, packing, _, r_batch) = fixture(0xBA7, 32);
2733        let batch = reduction.batch(&r_batch).unwrap();
2734        let tensor = reduction.tensor(&packing).unwrap();
2735
2736        let dot: EF = batch
2737            .weights()
2738            .as_slice()
2739            .iter()
2740            .zip(packing.poly().as_slice())
2741            .map(|(&weight, &value)| weight * value)
2742            .sum();
2743
2744        assert_eq!(dot, batch.initial_sum(&tensor, None));
2745    }
2746
2747    #[test]
2748    fn the_batched_rows_weigh_by_the_challenge_itself() {
2749        // Invariant: row `u` carries the equality weight at point `u`.
2750        //
2751        //     initial_sum(s_hat) == sum_u eq(hypercube(u, k), r'') * row u
2752        //
2753        // Every other batch-stage test reads the table this stage holds.
2754        // A stage ignoring the challenge would pass all of them.
2755        //
2756        // That is the forgery with a challenge every prover knows.
2757        //
2758        // So the reference here is built from the challenge itself.
2759        let (reduction, packing, _, r_batch) = fixture(0xC4A, 16);
2760        let batch = reduction.batch(&r_batch).unwrap();
2761        let tensor = reduction.tensor(&packing).unwrap();
2762        let absorbed = BitRingSwitch::<EF>::ABSORBED;
2763
2764        let expected: EF = tensor
2765            .rows()
2766            .iter()
2767            .enumerate()
2768            .map(|(u, &row)| {
2769                let point = Point::<EF>::hypercube(u, absorbed);
2770                row * Point::eval_eq(point.as_slice(), r_batch.as_slice())
2771            })
2772            .sum();
2773
2774        assert_eq!(batch.initial_sum(&tensor, None), expected);
2775    }
2776
2777    #[test]
2778    fn the_closing_weight_is_the_weight_multilinear_at_the_point() {
2779        // Invariant: the element closes the sumcheck with no weights pass.
2780        //
2781        //     closing_weight(r')  ==  A(r')
2782        let (reduction, _, _, r_batch) = fixture(0xA7, 16);
2783        let batch = reduction.batch(&r_batch).unwrap();
2784        let r_prime = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0xA8), 3);
2785
2786        assert_eq!(
2787            batch.closing_weight(&r_prime).unwrap(),
2788            batch.weights().eval_base(&r_prime)
2789        );
2790    }
2791
2792    #[test]
2793    fn the_equality_element_matches_its_hypercube_definition() {
2794        // The recurrence is linear in the variables.
2795        // The definition below is exponential.
2796        //
2797        //     e = sum_w eq(r_high, w) ⊗ eq(w, r')
2798        let mut rng = SmallRng::seed_from_u64(0xE9);
2799        for num_variables in 0..5 {
2800            let r = Point::<EF>::rand(&mut rng, num_variables + BitRingSwitch::<EF>::ABSORBED);
2801            let reduction = BitRingSwitch::new(&r).unwrap();
2802            let r_prime = Point::<EF>::rand(&mut rng, num_variables);
2803
2804            let eq_prime = Poly::<EF>::new_from_point(r_prime.as_slice(), EF::ONE);
2805            let mut expected = BitTensor::zero();
2806            let eq_high = dense_eq_high(&reduction);
2807            for (&a, &b) in eq_high.as_slice().iter().zip(eq_prime.as_slice()) {
2808                expected.add_exterior_product(a, b);
2809            }
2810
2811            assert_eq!(
2812                reduction.equality_element(&r_prime),
2813                expected,
2814                "{num_variables} variables"
2815            );
2816        }
2817    }
2818
2819    #[test]
2820    fn a_point_narrower_than_one_element_is_refused() {
2821        // Fewer coordinates than one element absorbs leaves nothing to keep.
2822        let mut rng = SmallRng::seed_from_u64(0xBAD);
2823        let r = Point::<EF>::rand(&mut rng, 3);
2824
2825        assert_eq!(
2826            BitRingSwitch::new(&r).unwrap_err(),
2827            BitRingSwitchError::PointTooNarrow {
2828                needed: 4,
2829                actual: 3
2830            }
2831        );
2832    }
2833
2834    #[test]
2835    fn a_batching_challenge_of_the_wrong_width_is_refused() {
2836        // A short challenge would zip-truncate, dropping identity rows.
2837        // Both sides move the same way, so no later check would catch it.
2838        let mut rng = SmallRng::seed_from_u64(0xBAD2);
2839        let r = Point::<EF>::rand(&mut rng, 7);
2840        let reduction = BitRingSwitch::new(&r).unwrap();
2841
2842        for width in [3usize, 5] {
2843            let r_batch = Point::<EF>::rand(&mut rng, width);
2844            assert_eq!(
2845                reduction.batch(&r_batch).unwrap_err(),
2846                BitRingSwitchError::BatchWidthMismatch {
2847                    expected: 4,
2848                    actual: width,
2849                }
2850            );
2851        }
2852    }
2853
2854    #[test]
2855    fn a_packing_of_the_wrong_width_is_refused() {
2856        // A mismatched packing would zip-truncate the accumulation.
2857        let (reduction, _, _, r_batch) = fixture(0xBAD3, 16);
2858        let batch = reduction.batch(&r_batch).unwrap();
2859        let wider = BitPacking::<EF>::new(&bits(0xBAD4, 32)).unwrap();
2860
2861        assert_eq!(
2862            reduction.tensor(&wider).unwrap_err(),
2863            BitRingSwitchError::WidthMismatch {
2864                expected: 3,
2865                actual: 4
2866            }
2867        );
2868        assert_eq!(
2869            batch
2870                .closing_weight(&Point::<EF>::new(alloc::vec![EF::ONE; 4]))
2871                .unwrap_err(),
2872            BitRingSwitchError::WidthMismatch {
2873                expected: 3,
2874                actual: 4
2875            }
2876        );
2877    }
2878
2879    #[test]
2880    fn a_boolean_prefix_restricts_the_sumcheck_without_changing_the_claim() {
2881        // Invariant: skipping the zeros of the equality table changes no sum.
2882        //
2883        // A claim about one column of a stacked trace arrives with a Boolean slot address
2884        // leading its row point, and that prefix makes the table an indicator:
2885        //
2886        //     high = (1, 0, z)   ->  eq(high, w) = 0 unless w is 100 or 101
2887        //
2888        // Fixture state: 2^7 cells, 4 absorbed, so 3 kept variables and 8 elements.
2889        //
2890        //     prefix 0   ->  the run is all 8, which is the dense case
2891        //     prefix 1   ->  4 of 8
2892        //     prefix 2   ->  2 of 8
2893        //     prefix 3   ->  1 of 8, the whole table an indicator
2894        //
2895        // The dense reference forms every term, zeros included, and must agree with each.
2896        let witness = bits(0xB0015, 16);
2897        let packing = BitPacking::<EF>::new(&witness).unwrap();
2898        let mut rng = SmallRng::seed_from_u64(0xB0016);
2899
2900        for prefix in 0..=3usize {
2901            for address in 0..1usize << prefix {
2902                // Leading coordinates spell the address, highest bit first.
2903                let mut coordinates: Vec<EF> = (0..prefix)
2904                    .map(|bit| {
2905                        if (address >> (prefix - 1 - bit)) & 1 == 1 {
2906                            EF::ONE
2907                        } else {
2908                            EF::ZERO
2909                        }
2910                    })
2911                    .collect();
2912                // The rest of the point uses non-Boolean challenges.
2913                // This keeps the intended prefix length exact in every fixture.
2914                coordinates.extend((prefix..7).map(|_| {
2915                    loop {
2916                        let coordinate = rng.random::<EF>();
2917                        if coordinate != EF::ZERO && coordinate != EF::ONE {
2918                            break coordinate;
2919                        }
2920                    }
2921                }));
2922                let r = Point::new(coordinates);
2923
2924                let reduction = BitRingSwitch::new(&r).unwrap();
2925                let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
2926                let batch = reduction.batch(&r_batch).unwrap();
2927
2928                // The element, against the term-by-term sum over the whole hypercube.
2929                let mut expected = BitTensor::zero();
2930                for (&weight, &value) in dense_eq_high(&reduction)
2931                    .as_slice()
2932                    .iter()
2933                    .zip(packing.poly().as_slice())
2934                {
2935                    expected.add_exterior_product(weight, value);
2936                }
2937                assert_eq!(
2938                    reduction.tensor(&packing).unwrap(),
2939                    expected,
2940                    "prefix {prefix} address {address}"
2941                );
2942
2943                // The weight multilinear, against the same table read subset sum by subset sum.
2944                let eq_batch = Poly::<EF>::new_from_point(r_batch.as_slice(), EF::ONE);
2945                let weights = batch.weights();
2946                assert_eq!(weights.num_variables(), reduction.num_variables() - prefix);
2947
2948                // The compact table is the selected run of the dense reference.
2949                let len = 1usize << (reduction.num_variables() - prefix);
2950                let offset = address * len;
2951                let dense = dense_eq_high(&reduction);
2952                for (w, &value) in dense.as_slice()[offset..offset + len].iter().enumerate() {
2953                    let want: EF = Coefficients::of(value)
2954                        .iter()
2955                        .zip(eq_batch.as_slice())
2956                        .filter(|&(bit, _)| bit)
2957                        .map(|(_, &weight)| weight)
2958                        .sum();
2959                    assert_eq!(weights.as_slice()[w], want, "prefix {prefix} point {w}");
2960                }
2961
2962                // The full protocol runs only inside the selected slot.
2963                // It restores the address before handing the point to the commitment.
2964                let claim = embedded(&witness).eval_base(&r);
2965                let (proof, surviving_point, surviving_value) =
2966                    reduction.prove::<EF, _, _>(&packing, &mut challenger());
2967                assert_eq!(
2968                    proof.sumcheck.num_rounds(),
2969                    reduction.num_variables() - prefix
2970                );
2971                assert_eq!(
2972                    &surviving_point.as_slice()[..prefix],
2973                    &r.as_slice()[..prefix]
2974                );
2975                assert_eq!(surviving_value, packing.poly().eval_base(&surviving_point));
2976
2977                // The replay derives the same restriction from the incoming point.
2978                let (verified_point, verified_value) =
2979                    reduction.verify(&proof, claim, &mut challenger()).unwrap();
2980                assert_eq!(verified_point, surviving_point);
2981                assert_eq!(verified_value, surviving_value);
2982            }
2983        }
2984    }
2985
2986    proptest! {
2987        #[test]
2988        fn the_chunked_accumulation_matches_the_term_by_term_sum(seed: u64, log_n in 1usize..6) {
2989            // The parallel path accumulates in chunks, combining partials.
2990            // The reference forms every term as its own element and adds them.
2991            let (reduction, packing, _, _) = fixture(seed, (1 << log_n) * 2);
2992
2993            let mut expected = BitTensor::zero();
2994            for (&weight, &value) in dense_eq_high(&reduction)
2995                .as_slice()
2996                .iter()
2997                .zip(packing.poly().as_slice())
2998            {
2999                expected += BitTensor::exterior_product(weight, value);
3000            }
3001
3002            prop_assert_eq!(reduction.tensor(&packing).unwrap(), expected);
3003        }
3004    }
3005
3006    #[test]
3007    fn the_reduction_round_trips_and_leaves_a_true_claim() {
3008        // Invariant: the surviving claim is the truth about the packed polynomial.
3009        //
3010        //     in    t(r) = s   at a random point over all 8 variables
3011        //     out   t'(r')     over the 4 the packing keeps, 4 being absorbed
3012        let witness = bits(0x81A5, 32);
3013        let packing = BitPacking::<EF>::new(&witness).unwrap();
3014        let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x81A6), 8);
3015        let reduction = BitRingSwitch::new(&r).unwrap();
3016        let claim = embedded(&witness).eval_base(&r);
3017
3018        let mut prover_chal = challenger();
3019        let (proof, r_prime_p, s_prime_p) = reduction.prove::<EF, _, _>(&packing, &mut prover_chal);
3020
3021        let mut verifier_chal = challenger();
3022        let (r_prime_v, s_prime_v) = reduction.verify(&proof, claim, &mut verifier_chal).unwrap();
3023
3024        assert_eq!(r_prime_p, r_prime_v);
3025        assert_eq!(s_prime_p, s_prime_v);
3026        assert_eq!(s_prime_v, packing.poly().eval_base(&r_prime_v));
3027    }
3028
3029    #[test]
3030    fn a_claim_the_element_does_not_support_is_rejected() {
3031        // The column reading is what ties the element to the incoming claim.
3032        let witness = bits(0x0AD, 32);
3033        let packing = BitPacking::<EF>::new(&witness).unwrap();
3034        let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x0AE), 8);
3035        let reduction = BitRingSwitch::new(&r).unwrap();
3036        let claim = embedded(&witness).eval_base(&r);
3037
3038        let (proof, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3039
3040        let err = reduction
3041            .verify(&proof, claim + EF::ONE, &mut challenger())
3042            .unwrap_err();
3043        assert_eq!(err, BitRingSwitchProofError::ClaimMismatch);
3044    }
3045
3046    #[test]
3047    fn a_tampered_element_is_caught_by_whichever_reading_it_moves() {
3048        // Invariant: both readings are of the same coefficients.
3049        //
3050        //     rows    -> the sum the rounds start from
3051        //     columns -> the claim they are checked against
3052        //
3053        // A tamper has to survive the column reading before the rounds ever run.
3054        // The two cases below are therefore checked separately.
3055        let witness = bits(0x7A17, 32);
3056        let packing = BitPacking::<EF>::new(&witness).unwrap();
3057        let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x7A18), 8);
3058        let reduction = BitRingSwitch::new(&r).unwrap();
3059        let claim = embedded(&witness).eval_base(&r);
3060        let (honest, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3061
3062        // Case 1: add one to a row.
3063        //
3064        // One is the first basis coordinate, so this flips entry (3, 0) of the matrix.
3065        // That moves column 0 as well, so the column reading moves with it.
3066        //
3067        // The run stops at the claim check without reaching a single round.
3068        let mut flipped = honest.clone();
3069        let mut rows = flipped.tensor.rows().to_vec();
3070        rows[3] += EF::ONE;
3071        flipped.tensor = BitTensor::try_from(rows).unwrap();
3072
3073        assert_eq!(
3074            reduction
3075                .verify(&flipped, claim, &mut challenger())
3076                .unwrap_err(),
3077            BitRingSwitchProofError::ClaimMismatch,
3078        );
3079
3080        // Case 2: a delta the column reading cannot see.
3081        //
3082        // The claim check weighs the columns by the absorbed coordinates:
3083        //
3084        //     claim   = sum_v column_v * eq_low[v]
3085        //     hidden  when  d_0 * eq_low[0] + d_1 * eq_low[1] = 0
3086        //
3087        // Over characteristic two that is d_1 = d_0 * eq_low[0] / eq_low[1].
3088        // Nothing constrains d_0, so any nonzero value does.
3089        //
3090        // It comes from a byte pattern, since an integer would reduce modulo two.
3091        let eq_low = reduction.eq_low.as_slice();
3092        assert_ne!(
3093            eq_low[1],
3094            EF::ZERO,
3095            "the construction divides by this entry"
3096        );
3097
3098        let d_0 = EF::from_le_byte_iter([0x2C, 0x9B].into_iter());
3099        let d_1 = d_0 * eq_low[0] * eq_low[1].inverse();
3100        assert_ne!(d_0, EF::ZERO);
3101
3102        // A row list read as columns is the transpose, which is its own inverse.
3103        let mut delta_columns = alloc::vec![EF::ZERO; BitTensor::<EF>::DIMENSION];
3104        delta_columns[0] = d_0;
3105        delta_columns[1] = d_1;
3106        let delta = BitTensor::try_from(delta_columns).unwrap().columns();
3107
3108        let mut hidden = honest.clone();
3109        let rows = hidden
3110            .tensor
3111            .rows()
3112            .iter()
3113            .zip(&delta)
3114            .map(|(&row, &shift)| row + shift)
3115            .collect::<Vec<_>>();
3116        hidden.tensor = BitTensor::try_from(rows).unwrap();
3117
3118        // The element genuinely changed, yet its column reading did not.
3119        assert_ne!(hidden.tensor, honest.tensor);
3120        assert_eq!(
3121            reduction.incoming_claim(&hidden.tensor),
3122            reduction.incoming_claim(&honest.tensor),
3123        );
3124
3125        // The row reading is what the rounds start from, and that one moved.
3126        //
3127        // No round rejects on its own.
3128        //
3129        //     sent      one value of the round polynomial, and its leading coefficient
3130        //     derived   the other value, from the running claim, never checked
3131        //
3132        //     wrong starting sum  ->  survives every round
3133        //     closing check       ->  the first place the two sides disagree
3134        assert_eq!(
3135            reduction
3136                .verify(&hidden, claim, &mut challenger())
3137                .unwrap_err(),
3138            BitRingSwitchProofError::FinalCheck,
3139        );
3140
3141        // The honest element passes the same path, so the rejection is the delta.
3142        assert!(reduction.verify(&honest, claim, &mut challenger()).is_ok());
3143    }
3144
3145    #[test]
3146    fn a_stray_grinding_witness_is_refused_before_the_transcript() {
3147        // Invariant: this reduction never grinds, so a witness rides along unbound.
3148        //
3149        // The rejection is structural, so it may not advance the sponge.
3150        //
3151        // A short element cannot be built at all.
3152        // Every route into one checks the row count, deserialization included.
3153        //
3154        // The verifier's shape check is therefore defence in depth, not a reachable path.
3155        let witness = bits(0x5407, 32);
3156        let packing = BitPacking::<EF>::new(&witness).unwrap();
3157        let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x5408), 8);
3158        let reduction = BitRingSwitch::new(&r).unwrap();
3159        let claim = embedded(&witness).eval_base(&r);
3160
3161        let (mut proof, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3162        proof.sumcheck.pow_witnesses.push(EF::ONE);
3163
3164        assert_eq!(
3165            reduction
3166                .verify(&proof, claim, &mut challenger())
3167                .unwrap_err(),
3168            BitRingSwitchProofError::NonEmptyPowWitnesses { actual: 1 }
3169        );
3170
3171        // A row count the level does not admit has no constructor.
3172        assert!(BitTensor::<EF>::try_from(alloc::vec![EF::ONE; 4]).is_err());
3173    }
3174
3175    #[test]
3176    fn a_point_the_prover_did_not_run_over_is_rejected() {
3177        // Invariant: the two sides must run over the same point, not merely the same width.
3178        //
3179        // The other point keeps the absorbed coordinates and moves one kept coordinate.
3180        // The column reading is then unchanged, so the claim check passes and the rest must catch it.
3181        //
3182        // That is the discriminating case.
3183        // Moving an absorbed coordinate would stop at the claim check instead.
3184        let witness = bits(0x9107, 32);
3185        let packing = BitPacking::<EF>::new(&witness).unwrap();
3186        let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(0x9108), 8);
3187        let claim = embedded(&witness).eval_base(&r);
3188        let reduction = BitRingSwitch::new(&r).unwrap();
3189        let (proof, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3190
3191        let mut moved = r.as_slice().to_vec();
3192        moved[0] += EF::ONE;
3193        let elsewhere = BitRingSwitch::new(&Point::new(moved)).unwrap();
3194
3195        let err = elsewhere
3196            .verify(&proof, claim, &mut challenger())
3197            .unwrap_err();
3198        assert_ne!(err, BitRingSwitchProofError::ClaimMismatch, "{err:?}");
3199    }
3200
3201    proptest! {
3202        // Each case runs a full reduction on both sides, so a few dozen keep the suite fast.
3203        #![proptest_config(ProptestConfig { cases: 24, ..ProptestConfig::default() })]
3204
3205        /// Every witness length the level admits, at a random point each time.
3206        #[test]
3207        fn the_reduction_round_trips_over_random_inputs(
3208            log_bytes in 1usize..=6,
3209            witness_seed: u64,
3210            point_seed: u64,
3211        ) {
3212            let witness = bits(witness_seed, 1 << log_bytes);
3213            let packing = BitPacking::<EF>::new(&witness).unwrap();
3214            let variables = log_bytes + 3;
3215            let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(point_seed), variables);
3216            let reduction = BitRingSwitch::new(&r).unwrap();
3217            let claim = embedded(&witness).eval_base(&r);
3218
3219            let (proof, _, s_prime_p) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3220            let (r_prime, s_prime) =
3221                reduction.verify(&proof, claim, &mut challenger()).unwrap();
3222
3223            prop_assert_eq!(s_prime, s_prime_p);
3224            prop_assert_eq!(s_prime, packing.poly().eval_base(&r_prime));
3225        }
3226
3227        /// Every witness length and every row count it admits, both readings each time.
3228        #[test]
3229        fn a_successor_view_round_trips_over_random_inputs(
3230            (log_bytes, row_variables) in (1usize..=6)
3231                .prop_flat_map(|log_bytes| (Just(log_bytes), 0..=log_bytes + 3)),
3232            witness_seed: u64,
3233            point_seed: u64,
3234        ) {
3235            let witness = bits(witness_seed, 1 << log_bytes);
3236            let packing = BitPacking::<EF>::new(&witness).unwrap();
3237            let variables = log_bytes + 3;
3238            let r = Point::<EF>::rand(&mut SmallRng::seed_from_u64(point_seed), variables);
3239            let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3240            let current = embedded(&witness).eval_base(&r);
3241            let next = dense_successor_claim(&witness, &r, row_variables);
3242
3243            let (proof, r_prime_p, s_prime_p) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3244            let (r_prime, s_prime) = reduction
3245                .verify_readings(&proof, Some(current), Some(next), &mut challenger())
3246                .unwrap();
3247
3248            prop_assert_eq!(&r_prime, &r_prime_p);
3249            prop_assert_eq!(s_prime, s_prime_p);
3250            prop_assert_eq!(s_prime, packing.poly().eval_base(&r_prime));
3251        }
3252    }
3253
3254    /// The witness one row further on, repeat-last, batched over the selector: the definition.
3255    ///
3256    ///     sum_{c, x} eq(selector, c) * t(c, min(z + 1, max)) * eq(rho, z)   summed over z
3257    ///
3258    /// The selector leads the point and the rows trail it, `row_variables` of them.
3259    fn dense_successor_claim(witness: &[u8], point: &Point<EF>, row_variables: usize) -> EF {
3260        let cells = embedded(witness);
3261        let (selector, rho) = point.split_at(point.num_variables() - row_variables);
3262        let eq_selector = Poly::<EF>::new_from_point(selector.as_slice(), EF::ONE);
3263        let eq_rho = Poly::<EF>::new_from_point(rho.as_slice(), EF::ONE);
3264        let rows = 1usize << row_variables;
3265        let mut claim = EF::ZERO;
3266        for (c, &gate) in eq_selector.as_slice().iter().enumerate() {
3267            for (z, &weight) in eq_rho.as_slice().iter().enumerate() {
3268                let x = (z + 1).min(rows - 1);
3269                claim += gate * weight * cells.as_slice()[c * rows + x];
3270            }
3271        }
3272        claim
3273    }
3274
3275    /// A field challenge that is neither zero nor one.
3276    ///
3277    /// A Boolean coordinate would move the prefix a fixture means to fix.
3278    fn non_boolean(rng: &mut SmallRng) -> EF {
3279        loop {
3280            let coordinate = rng.random::<EF>();
3281            if coordinate != EF::ZERO && coordinate != EF::ONE {
3282                return coordinate;
3283            }
3284        }
3285    }
3286
3287    /// How the selector coordinates, which lead the point, are chosen.
3288    #[derive(Clone, Copy, Debug)]
3289    enum Selector {
3290        /// Field challenges throughout, as a transcript draws them.
3291        Random,
3292        /// Bits throughout, spelling a slot address.
3293        Boolean,
3294        /// Two bits and a field challenge, repeating.
3295        Mixed,
3296    }
3297
3298    impl Selector {
3299        /// Every kind, in the order the tests sweep them.
3300        const ALL: [Self; 3] = [Self::Random, Self::Boolean, Self::Mixed];
3301    }
3302
3303    /// A point whose leading `num_variables - row_variables` coordinates are the selector.
3304    ///
3305    /// The row coordinates are always field challenges.
3306    fn successor_point(
3307        seed: u64,
3308        num_variables: usize,
3309        row_variables: usize,
3310        selector: Selector,
3311    ) -> Point<EF> {
3312        let mut rng = SmallRng::seed_from_u64(seed);
3313        let selector_variables = num_variables - row_variables;
3314        Point::new(
3315            (0..num_variables)
3316                .map(|i| {
3317                    let bit = if i % 2 == 0 { EF::ONE } else { EF::ZERO };
3318                    match selector {
3319                        _ if i >= selector_variables => non_boolean(&mut rng),
3320                        Selector::Random => non_boolean(&mut rng),
3321                        Selector::Boolean => bit,
3322                        Selector::Mixed if i % 3 == 2 => non_boolean(&mut rng),
3323                        Selector::Mixed => bit,
3324                    }
3325                })
3326                .collect(),
3327        )
3328    }
3329
3330    /// Row counts covering every case the successor view splits into, over 9 variables.
3331    ///
3332    /// ```text
3333    ///     0, 2, 4   ->  the rows fit in one element, no successor element is sent
3334    ///     6         ->  a 3-coordinate selector and 2 kept row coordinates
3335    ///     9         ->  no selector, every kept coordinate a row coordinate
3336    /// ```
3337    const ROW_VARIABLES: [usize; 5] = [0, 2, 4, 6, 9];
3338
3339    #[test]
3340    fn the_successor_claim_is_the_witness_one_row_on() {
3341        // Invariant: the column reading of the elements is the repeat-last successor claim.
3342        //
3343        //     sum_z eq(rho, z) * t(c, min(z + 1, max))   batched over the selector c
3344        //
3345        // Fixture state: 512 cells is 9 variables, 16 bits absorb 4, so 5 are kept.
3346        //
3347        // Every row count runs under a random, a Boolean and a mixed selector.
3348        // A Boolean selector moves the run the elements are accumulated over.
3349        let witness = bits(0x5CC1, 64);
3350        let packing = BitPacking::<EF>::new(&witness).unwrap();
3351
3352        for selector in Selector::ALL {
3353            for (seed, &row_variables) in (0x5CC2..).zip(&ROW_VARIABLES) {
3354                let r = successor_point(seed, 9, row_variables, selector);
3355                let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3356                let tensor = reduction.tensor(&packing).unwrap();
3357                let successor = reduction.successor_tensors(&packing).unwrap();
3358
3359                assert_eq!(
3360                    successor.is_some(),
3361                    row_variables > BitRingSwitch::<EF>::ABSORBED,
3362                    "{selector:?} selector, {row_variables} row variables"
3363                );
3364                assert_eq!(
3365                    reduction
3366                        .successor_claim(&tensor, successor.as_ref())
3367                        .unwrap(),
3368                    dense_successor_claim(&witness, &r, row_variables),
3369                    "{selector:?} selector, {row_variables} row variables"
3370                );
3371            }
3372        }
3373    }
3374
3375    #[test]
3376    fn the_successor_elements_are_their_term_by_term_sums() {
3377        // Invariant: the two elements are the terms the dense equality table names, added
3378        // one at a time. The production path adds a block against the inner weights alone
3379        // and scales the partial once, which is the same product regrouped.
3380        //
3381        //     carry  =  sum_{x(w) != 0}   E[w - 1] ⊗ t'(w)
3382        //     last   =  sum_{x(w) = max}  E[w]     ⊗ t'(w)
3383        let witness = bits(0x7E24, 64);
3384        let packing = BitPacking::<EF>::new(&witness).unwrap();
3385
3386        for &row_variables in &ROW_VARIABLES {
3387            for selector in Selector::ALL {
3388                let r = successor_point(0x7E25, 9, row_variables, selector);
3389                let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3390                let Some(kept) = reduction.kept_row_variables() else {
3391                    continue;
3392                };
3393                let table = dense_eq_high(&reduction);
3394                let max = (1usize << kept) - 1;
3395
3396                let mut carry = BitTensor::zero();
3397                let mut last = BitTensor::zero();
3398                for (w, &value) in packing.poly().as_slice().iter().enumerate() {
3399                    let row = w & max;
3400                    if row != 0 {
3401                        carry += BitTensor::exterior_product(table.as_slice()[w - 1], value);
3402                    }
3403                    if row == max {
3404                        last += BitTensor::exterior_product(table.as_slice()[w], value);
3405                    }
3406                }
3407
3408                let elements = reduction.successor_tensors(&packing).unwrap().unwrap();
3409                assert_eq!(elements.carry, carry, "{selector:?}, {row_variables} rows");
3410                assert_eq!(elements.last, last, "{selector:?}, {row_variables} rows");
3411            }
3412        }
3413    }
3414
3415    #[test]
3416    fn the_successor_weights_are_the_dense_table_entry_by_entry() {
3417        // Invariant: the weight table is the one a pass over the dense equality table builds,
3418        // entry for entry, shift and all.
3419        //
3420        //     A(w)       = sum over the set coordinates of eq(r_high, w) of eq(r_batch, .)
3421        //     weights(w) = A(w) + alpha * A(w - 1) [x(w) != 0] + alpha^2 * A(w) [x(w) = max]
3422        //
3423        // The production path takes the coordinate sums under the block's own scale and
3424        // combines the shift in place. Both group the same factors, and the field's
3425        // multiplication is associative, so nothing may differ at all.
3426        let mut rng = SmallRng::seed_from_u64(0xDE96);
3427        let r = successor_point(0xDE97, 9, 6, Selector::Random);
3428        let reduction = BitRingSwitch::with_successor(&r, 6).unwrap();
3429        let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
3430        let alpha = non_boolean(&mut rng);
3431
3432        // Built from the challenge here, not read off the stage under test.
3433        let eq_batch = Poly::<EF>::new_from_point(r_batch.as_slice(), EF::ONE);
3434        let base = dense_eq_high(&reduction)
3435            .as_slice()
3436            .iter()
3437            .map(|&value| {
3438                Coefficients::of(value)
3439                    .iter_set()
3440                    .map(|u| eq_batch.as_slice()[u])
3441                    .sum::<EF>()
3442            })
3443            .collect::<Vec<_>>();
3444
3445        let max = (1usize << reduction.kept_row_variables().unwrap()) - 1;
3446        let expected = (0..base.len())
3447            .map(|w| {
3448                let row = w & max;
3449                let mut value = base[w];
3450                if row != 0 {
3451                    value += alpha * base[w - 1];
3452                }
3453                if row == max {
3454                    value += alpha.square() * base[w];
3455                }
3456                value
3457            })
3458            .collect::<Vec<_>>();
3459
3460        let batch = reduction.batch_with_successor(&r_batch, alpha).unwrap();
3461        assert_eq!(batch.weights().as_slice(), expected.as_slice());
3462    }
3463
3464    #[test]
3465    fn the_sweeps_read_the_same_table_at_every_block_size() {
3466        // Invariant: the block is an amortisation unit, not part of what a sweep computes.
3467        //
3468        //     table[i * block + j]  ==  outer[i] * inner[j]   for every split of the point
3469        //
3470        // The size the reduction ships is far above the one these tests run at, and it is
3471        // what decides how much of each sweep is boundary and how much is interior: the
3472        // carry's first entry, the weights' stride against a column, and the count of blocks
3473        // a fold combines. Sweeping the split covers all three shapes at one place.
3474        let mut rng = SmallRng::seed_from_u64(0xB10C5);
3475        let packing = BitPacking::<EF>::new(&bits(0xB10C6, 64)).unwrap();
3476        let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
3477        let alpha = non_boolean(&mut rng);
3478
3479        for &row_variables in &ROW_VARIABLES {
3480            for selector in Selector::ALL {
3481                let r = successor_point(0xB10C7, 9, row_variables, selector);
3482                let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3483                let (prefix, offset, _) = reduction.support();
3484                let run = &reduction.high()[prefix..];
3485                let batch = reduction
3486                    .batch_drawn(
3487                        &r_batch,
3488                        reduction.sends_successor_tensors().then_some(alpha),
3489                    )
3490                    .unwrap();
3491
3492                // One entry per block leaves the outer factor the dense table itself.
3493                let dense = FactoredEquality::new(run, 0);
3494                let tensor = BitRingSwitch::tensor_over(&packing, offset, &dense);
3495                let successor = reduction.successor_tensors_over(&packing, offset, &dense);
3496                let weights = batch.weights_over::<EF>(&dense);
3497
3498                for log_block in 0..=run.len() + 2 {
3499                    let equality = FactoredEquality::new(run, log_block);
3500                    let case =
3501                        alloc::format!("{selector:?}, {row_variables} rows, 2^{log_block} block");
3502
3503                    assert_eq!(
3504                        BitRingSwitch::tensor_over(&packing, offset, &equality),
3505                        tensor,
3506                        "{case}"
3507                    );
3508                    assert_eq!(
3509                        reduction.successor_tensors_over(&packing, offset, &equality),
3510                        successor,
3511                        "{case}"
3512                    );
3513                    assert_eq!(
3514                        batch.weights_over::<EF>(&equality).as_slice(),
3515                        weights.as_slice(),
3516                        "{case}"
3517                    );
3518                }
3519            }
3520        }
3521    }
3522
3523    #[test]
3524    fn the_wide_sweeps_read_the_same_elements_at_every_block_size() {
3525        // Invariant: at the level the bit-block kernel is shaped for, the split changes nothing.
3526        //
3527        // Blocks of eight elements and up run that kernel where the target has one. The carry
3528        // meets columns shorter than a block, as long as one, and longer.
3529        type Wide = BinaryField128;
3530        let mut rng = SmallRng::seed_from_u64(0x31DE);
3531        let packing = BitPacking::<Wide>::new(&bits(0x31DF, 1 << 13)).unwrap();
3532        let num_variables = packing.num_variables() + BitRingSwitch::<Wide>::ABSORBED;
3533
3534        for row_variables in [8, 10, 12, num_variables] {
3535            let r = Point::<Wide>::rand(&mut rng, num_variables);
3536            let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3537            let (prefix, offset, _) = reduction.support();
3538            let run = &reduction.high()[prefix..];
3539
3540            // One entry per block sums every term through the buckets.
3541            let dense = FactoredEquality::new(run, 0);
3542            let tensor = BitRingSwitch::tensor_over(&packing, offset, &dense);
3543            let successor = reduction.successor_tensors_over(&packing, offset, &dense);
3544
3545            for log_block in 0..=run.len() {
3546                let equality = FactoredEquality::new(run, log_block);
3547                let case = alloc::format!("{row_variables} rows, 2^{log_block} block");
3548                assert_eq!(
3549                    BitRingSwitch::tensor_over(&packing, offset, &equality),
3550                    tensor,
3551                    "{case}"
3552                );
3553                assert_eq!(
3554                    reduction.successor_tensors_over(&packing, offset, &equality),
3555                    successor,
3556                    "{case}"
3557                );
3558            }
3559        }
3560    }
3561
3562    #[test]
3563    fn one_sweep_over_the_banks_gives_each_bank_its_own_elements() {
3564        // Invariant: sweeping the compact banks together changes no bank's elements.
3565        //
3566        // Four banks at the wide level, in blocks of eight elements and up, run the bit-block
3567        // kernel where the target has one. Each bank swept alone in blocks of one element is
3568        // summed through the buckets. The carry meets columns shorter than a block, longer
3569        // than one, and as long as a bank.
3570        type Wide = BinaryField128;
3571        let absorbed = BitRingSwitch::<Wide>::ABSORBED;
3572        let (head, tail, banks) = (2, 8, 4);
3573        let bank_len = 1 << tail;
3574        let packing = BitPacking::<Wide>::new(&bits(0xBA4C, banks * bank_len * 16)).unwrap();
3575        let num_variables = head + tail + absorbed;
3576        assert_eq!(packing.num_variables() + absorbed, num_variables);
3577        let mut rng = SmallRng::seed_from_u64(0xBA4D);
3578
3579        for row_variables in [absorbed + 1, absorbed + 3, absorbed + 6, absorbed + tail] {
3580            let r = Point::<Wide>::rand(&mut rng, num_variables);
3581            let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3582            let run = &reduction.high()[head..];
3583
3584            let single = FactoredEquality::new(run, 0);
3585            let tensors = (0..banks)
3586                .map(|bank| BitRingSwitch::tensor_over(&packing, bank * bank_len, &single))
3587                .collect::<Vec<_>>();
3588            let successors = (0..banks)
3589                .map(|bank| {
3590                    reduction
3591                        .successor_tensors_over(&packing, bank * bank_len, &single)
3592                        .unwrap()
3593                })
3594                .collect::<Vec<_>>();
3595
3596            for log_block in 3..=6 {
3597                let equality = FactoredEquality::new(run, log_block);
3598                let case = alloc::format!("{row_variables} rows, 2^{log_block} block");
3599                assert_eq!(
3600                    BitRingSwitch::bank_tensors_over(&packing, 0, banks, &equality),
3601                    tensors,
3602                    "{case}"
3603                );
3604                assert_eq!(
3605                    reduction.bank_successor_tensors_over(&packing, 0, banks, &equality),
3606                    Some(successors.clone()),
3607                    "{case}"
3608                );
3609            }
3610        }
3611    }
3612
3613    #[test]
3614    fn the_deepest_production_head_proves_what_the_dense_path_proves() {
3615        // Invariant: the production depth changes the prover's work, never its messages.
3616        //
3617        // The run is the size floor of the deepest production depth, so `prove` takes that
3618        // depth and splits the packing in its most banks. Rows of the successor view stay out
3619        // of the head, which a view with a few kept rows leaves room for.
3620        type Wide = BinaryField128;
3621        let absorbed = BitRingSwitch::<Wide>::ABSORBED;
3622        let n = compact_size_floor(COMPACT_PRODUCTION_ROUNDS, LOG_CHUNK);
3623        let packing = BitPacking::<Wide>::new(&bits(0xDE4, (1 << n) * 16)).unwrap();
3624        let mut rng = SmallRng::seed_from_u64(0xDE5);
3625        let r = Point::<Wide>::rand(&mut rng, n + absorbed);
3626
3627        for reduction in [
3628            BitRingSwitch::new(&r).unwrap(),
3629            BitRingSwitch::with_successor(&r, absorbed + 3).unwrap(),
3630        ] {
3631            assert_eq!(reduction.fixed_prefix().0, 0);
3632            assert_eq!(reduction.production_depth(n), COMPACT_PRODUCTION_ROUNDS);
3633
3634            let mut dense_challenger = wide_challenger();
3635            let dense = reduction.prove_with_compact_depth::<Ghash128, _, _>(
3636                &packing,
3637                &mut dense_challenger,
3638                0,
3639                true,
3640            );
3641            let mut production_challenger = wide_challenger();
3642            let production =
3643                reduction.prove::<Ghash128, _, _>(&packing, &mut production_challenger);
3644
3645            assert_eq!(production.0.tensor, dense.0.tensor);
3646            assert_eq!(production.0.successor, dense.0.successor);
3647            assert_eq!(
3648                production.0.sumcheck.polynomial_evaluations,
3649                dense.0.sumcheck.polynomial_evaluations
3650            );
3651            assert_eq!(production.0.final_eval, dense.0.final_eval);
3652            assert_eq!(production.1, dense.1);
3653            assert_eq!(production.2, dense.2);
3654            assert_eq!(
3655                CanSample::<Wide>::sample(&mut production_challenger),
3656                CanSample::<Wide>::sample(&mut dense_challenger)
3657            );
3658
3659            let current = reduction.incoming_claim(&production.0.tensor);
3660            let next = reduction.sends_successor_tensors().then(|| {
3661                reduction
3662                    .successor_claim(&production.0.tensor, production.0.successor.as_ref())
3663                    .unwrap()
3664            });
3665            assert!(
3666                reduction
3667                    .verify_readings(&production.0, Some(current), next, &mut wide_challenger())
3668                    .is_ok()
3669            );
3670        }
3671    }
3672
3673    #[test]
3674    fn the_successor_weights_close_the_batched_sum() {
3675        // Invariant: the weights are what both ends of the sumcheck read.
3676        //
3677        //     sum_w weights(w) * t'(w)  ==  initial_sum          rows of the three elements
3678        //     weights(r')               ==  closing_weight(r')   through the lifted elements
3679        //
3680        // Fixture state: 9 variables, 6 of them rows, so 2 kept row coordinates.
3681        let witness = bits(0xC105, 64);
3682        let packing = BitPacking::<EF>::new(&witness).unwrap();
3683        let mut rng = SmallRng::seed_from_u64(0xC106);
3684        let r = successor_point(0xC107, 9, 6, Selector::Random);
3685        let reduction = BitRingSwitch::with_successor(&r, 6).unwrap();
3686        let tensor = reduction.tensor(&packing).unwrap();
3687        let successor = reduction.successor_tensors(&packing).unwrap().unwrap();
3688
3689        let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
3690        let alpha = non_boolean(&mut rng);
3691        let batch = reduction.batch_with_successor(&r_batch, alpha).unwrap();
3692        let weights = batch.weights();
3693
3694        let dot: EF = weights
3695            .as_slice()
3696            .iter()
3697            .zip(packing.poly().as_slice())
3698            .map(|(&weight, &value)| weight * value)
3699            .sum();
3700        assert_eq!(batch.initial_sum(&tensor, Some(&successor)), dot);
3701
3702        let r_prime = Point::<EF>::rand(&mut rng, reduction.num_variables());
3703        assert_eq!(
3704            batch.closing_weight(&r_prime).unwrap(),
3705            weights.eval_base(&r_prime)
3706        );
3707    }
3708
3709    #[test]
3710    fn a_successor_view_round_trips() {
3711        // Invariant: both readings verify, and the surviving claim is the truth about the packing.
3712        //
3713        //     in    t(r) = current,  t one row on at r = next
3714        //     out   t'(r') = e       one claim, whatever the row count
3715        let witness = bits(0x7219, 64);
3716        let packing = BitPacking::<EF>::new(&witness).unwrap();
3717        let cells = embedded(&witness);
3718
3719        for selector in Selector::ALL {
3720            for (seed, &row_variables) in (0x721A..).zip(&ROW_VARIABLES) {
3721                let r = successor_point(seed, 9, row_variables, selector);
3722                let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3723                let current = cells.eval_base(&r);
3724                let next = dense_successor_claim(&witness, &r, row_variables);
3725
3726                let (proof, r_prime, value) =
3727                    reduction.prove::<EF, _, _>(&packing, &mut challenger());
3728                let verified = reduction
3729                    .verify_readings(&proof, Some(current), Some(next), &mut challenger())
3730                    .unwrap();
3731
3732                let case = alloc::format!("{selector:?} selector, {row_variables} row variables");
3733                assert_eq!(verified, (r_prime.clone(), value), "{case}");
3734                assert_eq!(value, packing.poly().eval_base(&r_prime), "{case}");
3735            }
3736        }
3737    }
3738
3739    #[test]
3740    fn a_successor_view_inside_one_element_plays_the_plain_transcript() {
3741        // Invariant: rows that fit in one element send nothing extra.
3742        //
3743        // The successor is then a column reading alone, which the verifier makes offline.
3744        // The prover's side of the run is the one a plain reduction at the point plays.
3745        let witness = bits(0x1D3, 64);
3746        let packing = BitPacking::<EF>::new(&witness).unwrap();
3747
3748        for row_variables in 0..=BitRingSwitch::<EF>::ABSORBED {
3749            let r = successor_point(0x1D4, 9, row_variables, Selector::Random);
3750            let plain = BitRingSwitch::new(&r).unwrap();
3751            let successor = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3752            assert!(!successor.sends_successor_tensors());
3753
3754            let mut plain_challenger = challenger();
3755            let (plain_proof, plain_point, plain_value) =
3756                plain.prove::<EF, _, _>(&packing, &mut plain_challenger);
3757            let mut successor_challenger = challenger();
3758            let (successor_proof, successor_point, successor_value) =
3759                successor.prove::<EF, _, _>(&packing, &mut successor_challenger);
3760
3761            assert_eq!(successor_proof.tensor, plain_proof.tensor);
3762            assert!(successor_proof.successor.is_none());
3763            assert_eq!(
3764                successor_point, plain_point,
3765                "{row_variables} row variables"
3766            );
3767            assert_eq!(
3768                successor_value, plain_value,
3769                "{row_variables} row variables"
3770            );
3771            assert_eq!(
3772                CanSample::<EF>::sample(&mut successor_challenger),
3773                CanSample::<EF>::sample(&mut plain_challenger),
3774                "{row_variables} row variables"
3775            );
3776        }
3777    }
3778
3779    #[test]
3780    fn the_prefix_stops_before_the_row_coordinates() {
3781        // Invariant: the Boolean prefix fixes selector coordinates only.
3782        //
3783        // A row coordinate fixed to a bit is no slot address.
3784        // The carry reads the element before, which may sit outside a run it would select.
3785        //
3786        // Fixture state: 9 variables, 6 of them rows.
3787        //
3788        //     point     (1, 0, 1 | 0, z | z, z, z, z)
3789        //                selector  kept   absorbed
3790        //     prefix    3, though the first row coordinate is a bit as well
3791        //     rounds    5 - 3 = 2, where a plain reduction at the point runs 1
3792        let witness = bits(0x9EF1, 64);
3793        let packing = BitPacking::<EF>::new(&witness).unwrap();
3794        let mut rng = SmallRng::seed_from_u64(0x9EF2);
3795        let mut coordinates = alloc::vec![EF::ONE, EF::ZERO, EF::ONE, EF::ZERO];
3796        coordinates.extend((4..9).map(|_| non_boolean(&mut rng)));
3797        let r = Point::new(coordinates);
3798
3799        let reduction = BitRingSwitch::with_successor(&r, 6).unwrap();
3800        let current = embedded(&witness).eval_base(&r);
3801        let next = dense_successor_claim(&witness, &r, 6);
3802
3803        let tensor = reduction.tensor(&packing).unwrap();
3804        let successor = reduction.successor_tensors(&packing).unwrap();
3805        assert_eq!(
3806            reduction
3807                .successor_claim(&tensor, successor.as_ref())
3808                .unwrap(),
3809            next
3810        );
3811
3812        let (proof, r_prime, value) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3813        assert_eq!(proof.sumcheck.num_rounds(), 2);
3814        assert_eq!(
3815            reduction
3816                .verify_readings(&proof, Some(current), Some(next), &mut challenger())
3817                .unwrap(),
3818            (r_prime, value)
3819        );
3820
3821        // The plain reduction eats the Boolean row coordinate too, which the cap is for.
3822        let (plain, _, _) = BitRingSwitch::new(&r)
3823            .unwrap()
3824            .prove::<EF, _, _>(&packing, &mut challenger());
3825        assert_eq!(plain.sumcheck.num_rounds(), 1);
3826    }
3827
3828    #[test]
3829    fn a_false_successor_claim_is_rejected() {
3830        // The column reading is what ties the elements to the successor claim.
3831        //
3832        // Checked with successor elements sent, and with the rows inside one element.
3833        let witness = bits(0xFA15, 64);
3834        let packing = BitPacking::<EF>::new(&witness).unwrap();
3835
3836        for row_variables in [2, 6] {
3837            let r = successor_point(0xFA16, 9, row_variables, Selector::Random);
3838            let reduction = BitRingSwitch::with_successor(&r, row_variables).unwrap();
3839            let current = embedded(&witness).eval_base(&r);
3840            let next = dense_successor_claim(&witness, &r, row_variables);
3841            let (proof, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3842
3843            assert_eq!(
3844                reduction
3845                    .verify_readings(
3846                        &proof,
3847                        Some(current),
3848                        Some(next + EF::ONE),
3849                        &mut challenger()
3850                    )
3851                    .unwrap_err(),
3852                BitRingSwitchProofError::SuccessorClaimMismatch,
3853                "{row_variables} row variables"
3854            );
3855        }
3856    }
3857
3858    #[test]
3859    fn a_tampered_carry_element_is_caught_by_the_closing_check() {
3860        // Invariant: the successor elements enter the rows the sumcheck starts from.
3861        //
3862        // The claim reads one column of each:
3863        //
3864        //     carry  ->  column 0
3865        //     last   ->  column d - 1
3866        //
3867        // A tamper in any other column passes the claim check.
3868        // Only the closing check is then left to catch it.
3869        //
3870        // Mutation: entry (3, 5) of carry, then entry (5, 0) of last.
3871        let witness = bits(0x7A3, 64);
3872        let packing = BitPacking::<EF>::new(&witness).unwrap();
3873        let r = successor_point(0x7A4, 9, 6, Selector::Random);
3874        let reduction = BitRingSwitch::with_successor(&r, 6).unwrap();
3875        let current = embedded(&witness).eval_base(&r);
3876        let next = dense_successor_claim(&witness, &r, 6);
3877        let (honest, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3878        let elements = honest.successor.clone().unwrap();
3879
3880        let flipped = |element: &BitTensor<EF>, row: usize, bit: EF| {
3881            let mut rows = element.rows().to_vec();
3882            rows[row] += bit;
3883            BitTensor::try_from(rows).unwrap()
3884        };
3885        let mut carry = honest.clone();
3886        carry.successor = Some(SuccessorTensors {
3887            carry: flipped(&elements.carry, 3, EF::from_repr(1 << 5)),
3888            last: elements.last.clone(),
3889        });
3890        let mut last = honest.clone();
3891        last.successor = Some(SuccessorTensors {
3892            carry: elements.carry.clone(),
3893            last: flipped(&elements.last, 5, EF::ONE),
3894        });
3895
3896        for (element, tampered) in [("carry", carry), ("last", last)] {
3897            // The element genuinely changed, yet the successor claim did not.
3898            assert_ne!(tampered.successor, honest.successor, "{element}");
3899            assert_eq!(
3900                reduction
3901                    .successor_claim(&tampered.tensor, tampered.successor.as_ref())
3902                    .unwrap(),
3903                next,
3904                "{element}"
3905            );
3906            assert_eq!(
3907                reduction
3908                    .verify_readings(&tampered, Some(current), Some(next), &mut challenger())
3909                    .unwrap_err(),
3910                BitRingSwitchProofError::FinalCheck,
3911                "{element}"
3912            );
3913        }
3914
3915        // The honest elements pass the same path, so the rejection is the tamper.
3916        assert!(
3917            reduction
3918                .verify_readings(&honest, Some(current), Some(next), &mut challenger())
3919                .is_ok()
3920        );
3921    }
3922
3923    #[test]
3924    fn a_successor_element_count_disagreeing_with_the_setup_is_refused() {
3925        // Invariant: the element count comes from the public setup, never from the proof.
3926        //
3927        //     6 row variables  ->  two elements expected, a proof without them
3928        //     2 row variables  ->  none expected, a proof carrying two
3929        //
3930        // The rejection is structural, so it may not advance the sponge.
3931        let witness = bits(0xC0DE, 64);
3932        let packing = BitPacking::<EF>::new(&witness).unwrap();
3933
3934        let r_sending = successor_point(0xC0DF, 9, 6, Selector::Random);
3935        let sending = BitRingSwitch::with_successor(&r_sending, 6).unwrap();
3936        let (mut missing, _, _) = sending.prove::<EF, _, _>(&packing, &mut challenger());
3937        let spare = missing.successor.take();
3938
3939        let r_fitting = successor_point(0xC0E0, 9, 2, Selector::Random);
3940        let fitting = BitRingSwitch::with_successor(&r_fitting, 2).unwrap();
3941        let (mut spurious, _, _) = fitting.prove::<EF, _, _>(&packing, &mut challenger());
3942        spurious.successor = spare;
3943
3944        for (reduction, proof, r, expected, actual) in [
3945            (&sending, &missing, &r_sending, 2, 0),
3946            (&fitting, &spurious, &r_fitting, 0, 2),
3947        ] {
3948            let current = embedded(&witness).eval_base(r);
3949            let mut used = challenger();
3950            let err = reduction
3951                .verify_readings(proof, Some(current), None, &mut used)
3952                .unwrap_err();
3953
3954            assert_eq!(
3955                err,
3956                BitRingSwitchProofError::Width(TranscriptWidth::SuccessorElements {
3957                    expected,
3958                    actual
3959                })
3960            );
3961            assert_eq!(
3962                CanSample::<EF>::sample(&mut used),
3963                CanSample::<EF>::sample(&mut challenger()),
3964                "the rejection advanced the sponge"
3965            );
3966        }
3967    }
3968
3969    #[test]
3970    fn a_reading_the_setup_does_not_answer_is_refused() {
3971        // A plain reduction has no successor view to check a next claim against.
3972        // A call supplying neither reading would check nothing at all.
3973        let (reduction, packing, _, _) = fixture(0x2EAD, 32);
3974        let (proof, _, _) = reduction.prove::<EF, _, _>(&packing, &mut challenger());
3975
3976        assert_eq!(
3977            reduction
3978                .verify_readings(&proof, None, Some(EF::ONE), &mut challenger())
3979                .unwrap_err(),
3980            BitRingSwitchProofError::NoSuccessorView
3981        );
3982        assert_eq!(
3983            reduction
3984                .verify_readings(&proof, None, None, &mut challenger())
3985                .unwrap_err(),
3986            BitRingSwitchProofError::NoReading
3987        );
3988    }
3989
3990    #[test]
3991    fn a_successor_setup_of_the_wrong_kind_is_refused() {
3992        // A row count wider than the point names no rows at all.
3993        // Each batching stage belongs to one kind of reduction.
3994        // A plain reduction has no successor claim to read.
3995        let mut rng = SmallRng::seed_from_u64(0x5E7C);
3996        let r = Point::<EF>::rand(&mut rng, 9);
3997        let r_batch = Point::<EF>::rand(&mut rng, BitRingSwitch::<EF>::ABSORBED);
3998
3999        assert_eq!(
4000            BitRingSwitch::with_successor(&r, 10).unwrap_err(),
4001            BitRingSwitchError::RowVariables {
4002                row_variables: 10,
4003                num_variables: 9
4004            }
4005        );
4006        assert_eq!(
4007            BitRingSwitch::with_successor(&r, 6)
4008                .unwrap()
4009                .batch(&r_batch)
4010                .unwrap_err(),
4011            BitRingSwitchError::SuccessorBatching { expected: true }
4012        );
4013        assert_eq!(
4014            BitRingSwitch::with_successor(&r, 4)
4015                .unwrap()
4016                .batch_with_successor(&r_batch, EF::ONE)
4017                .unwrap_err(),
4018            BitRingSwitchError::SuccessorBatching { expected: false }
4019        );
4020        assert_eq!(
4021            BitRingSwitch::new(&r)
4022                .unwrap()
4023                .successor_claim(&BitTensor::zero(), None)
4024                .unwrap_err(),
4025            BitRingSwitchError::NoSuccessorView
4026        );
4027    }
4028
4029    /// A 128-bit reduction, played once per representation its rounds can run in.
4030    ///
4031    /// The test level of this module is 16 bits wide and has no second representation,
4032    /// so the comparison widens to the level that does.
4033    ///
4034    /// # Returns
4035    ///
4036    /// The proof, the point the rounds ended at, the surviving claim, and the sponge left
4037    /// behind, once for the tower basis and once for the polynomial basis.
4038    #[allow(clippy::type_complexity)]
4039    fn both_representations(
4040        reduction: &BitRingSwitch<BinaryField128>,
4041        packing: &BitPacking<BinaryField128>,
4042    ) -> [(
4043        BitRingSwitchProof<BinaryField128>,
4044        Point<BinaryField128>,
4045        BinaryField128,
4046        WideChal,
4047    ); 2] {
4048        let mut tower_sponge = WideChal::from_hasher(Vec::new(), Keccak256Hash);
4049        let tower = reduction.prove::<BinaryField128, _, _>(packing, &mut tower_sponge);
4050        let mut poly_sponge = WideChal::from_hasher(Vec::new(), Keccak256Hash);
4051        let poly = reduction.prove::<Ghash128, _, _>(packing, &mut poly_sponge);
4052
4053        [
4054            (tower.0, tower.1, tower.2, tower_sponge),
4055            (poly.0, poly.1, poly.2, poly_sponge),
4056        ]
4057    }
4058
4059    #[test]
4060    fn a_borrowed_packing_plays_the_owned_packing_transcript() {
4061        // Invariant: the store the elements are read from reaches no wire.
4062        //
4063        // A commitment hands the reduction a view over the table it already holds,
4064        // and that is the only route production takes.
4065        let (reduction, packing, _, _) = fixture(0x5AE, 16);
4066        let borrowed = BitPacking::from_packed(packing.poly().as_view()).unwrap();
4067        assert_eq!(borrowed.num_variables(), packing.num_variables());
4068
4069        let (owned_proof, owned_point, owned_value) =
4070            reduction.prove::<EF, _, _>(&packing, &mut challenger());
4071        let (borrowed_proof, borrowed_point, borrowed_value) =
4072            reduction.prove::<EF, _, _>(&borrowed, &mut challenger());
4073
4074        assert_eq!(borrowed_proof.tensor, owned_proof.tensor);
4075        assert_eq!(borrowed_proof.successor, owned_proof.successor);
4076        assert_eq!(
4077            borrowed_proof.sumcheck.polynomial_evaluations,
4078            owned_proof.sumcheck.polynomial_evaluations
4079        );
4080        assert_eq!(borrowed_proof.final_eval, owned_proof.final_eval);
4081        assert_eq!(borrowed_point, owned_point);
4082        assert_eq!(borrowed_value, owned_value);
4083    }
4084
4085    #[test]
4086    fn the_polynomial_basis_rounds_play_the_tower_basis_transcript() {
4087        // Invariant: the representation the rounds run in reaches no wire.
4088        //
4089        // The two maps are inverse field isomorphisms, so every round polynomial is the
4090        // one the tower-basis tables measure, message by message.
4091        let witness = bits(0x9F17, 512);
4092        let packing = BitPacking::<BinaryField128>::new(&witness).unwrap();
4093        let absorbed = BitRingSwitch::<BinaryField128>::ABSORBED;
4094        let mut rng = SmallRng::seed_from_u64(0x2C0D);
4095        let num_variables = packing.num_variables() + absorbed;
4096
4097        // A random point, one whose Boolean prefix drops rounds, and one sending successor
4098        // elements, so every stage that crosses into the representation is on the path.
4099        let random = Point::<BinaryField128>::rand(&mut rng, num_variables);
4100        let mut addressed = random.as_slice().to_vec();
4101        addressed[0] = BinaryField128::ONE;
4102        addressed[1] = BinaryField128::ZERO;
4103        let addressed = Point::new(addressed);
4104        let reductions = [
4105            (BitRingSwitch::new(&random).unwrap(), &random),
4106            (BitRingSwitch::new(&addressed).unwrap(), &addressed),
4107            (
4108                BitRingSwitch::with_successor(&random, absorbed + 1).unwrap(),
4109                &random,
4110            ),
4111        ];
4112        let embedding = embedded_over::<BinaryField128>(&witness);
4113
4114        for (case, (reduction, point)) in reductions.iter().enumerate() {
4115            let [
4116                (tower, tower_point, tower_value, mut tower_sponge),
4117                (poly, poly_point, poly_value, mut poly_sponge),
4118            ] = both_representations(reduction, &packing);
4119
4120            // A Boolean prefix drops rounds, so the kept coordinates do not count them.
4121            assert!(poly.sumcheck.num_rounds() > 0, "case {case} runs no round");
4122            assert_eq!(poly.tensor, tower.tensor, "case {case}");
4123            assert_eq!(poly.successor, tower.successor, "case {case}");
4124            assert_eq!(
4125                poly.sumcheck.polynomial_evaluations, tower.sumcheck.polynomial_evaluations,
4126                "case {case}"
4127            );
4128            assert_eq!(
4129                poly.sumcheck.pow_witnesses, tower.sumcheck.pow_witnesses,
4130                "case {case}"
4131            );
4132            assert_eq!(poly.final_eval, tower.final_eval, "case {case}");
4133            assert_eq!(poly_point, tower_point, "case {case}");
4134            assert_eq!(poly_value, tower_value, "case {case}");
4135            assert_eq!(
4136                CanSample::<BinaryField128>::sample(&mut poly_sponge),
4137                CanSample::<BinaryField128>::sample(&mut tower_sponge),
4138                "case {case}"
4139            );
4140
4141            // Agreeing legs would still agree if both were wrong in the same way.
4142            // The claim is read off the witness, so the replay has to close on it.
4143            let claim = embedding.eval_base(*point);
4144            let mut verifier_sponge = WideChal::from_hasher(Vec::new(), Keccak256Hash);
4145            assert!(
4146                reduction.verify(&poly, claim, &mut verifier_sponge).is_ok(),
4147                "case {case}"
4148            );
4149        }
4150    }
4151}