Skip to main content

p3_binary_pcs/
transcript.rs

1//! Fiat-Shamir transcript of one binary-tower PCS run.
2
3use alloc::vec::Vec;
4use core::marker::PhantomData;
5
6use p3_challenger::fs::{
7    DomainSeparator, FieldToFieldCodec, FieldUnit, Hierarchy, Interaction, InteractionPattern,
8    Kind, Length, ProverState, TranscriptField, VerifierState,
9};
10use p3_challenger::{CanObserve, CanSample, CanSampleUniformBits, GrindingChallenger};
11use p3_field::{AlgebraIdentity, Field};
12use p3_util::log2_strict_usize;
13
14use crate::params::BinaryPcsConfig;
15use crate::verifier::num_distinct_queries;
16
17/// Version byte bound into the transcript seed.
18///
19/// Bumping it separates two revisions of this protocol.
20///
21/// It separates them even when their step sequences agree.
22const VERSION: u8 = 1;
23
24/// Protocol name bound into the transcript seed.
25const NAME: &[u8] = b"p3-binary-pcs";
26
27/// Step label of one folded oracle's Merkle root.
28const ORACLE_COMMITMENT: &str = "oracle_commitment";
29
30/// Step label of the final codeword, sent in the clear.
31const FINAL_CODEWORD: &str = "final_codeword";
32
33/// Step label of the grinding step guarding the query positions.
34const QUERY_POW: &str = "query_pow";
35
36/// Step label of the query positions.
37const QUERY_INDICES: &str = "query_indices";
38
39/// Sponge alphabet of a challenger that speaks the committed tower level natively.
40type Alphabet<F> = FieldUnit<F>;
41
42/// Numbers that fix the transcript of one binary-tower PCS run.
43///
44/// Both sides derive this from their own configuration, never from a proof.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct BinaryPcsShape {
47    /// Arity of the committed polynomial.
48    pub num_variables: usize,
49    /// Variables each fold batch consumes.
50    pub log_folding_factor: usize,
51    /// Folded oracles committed between the fold batches.
52    ///
53    /// The last batch sends its codeword in the clear, so it commits nothing.
54    pub num_oracles: usize,
55    /// Symbols the final codeword carries.
56    pub final_codeword_len: usize,
57    /// Grinding difficulty guarding the query positions.
58    pub pow_bits: usize,
59    /// Bit width of one sampled fold pair.
60    pub pair_bits: usize,
61    /// Distinct pairs the run opens.
62    pub num_pairs: usize,
63}
64
65impl BinaryPcsShape {
66    /// Derive the shape of one run from its configuration.
67    ///
68    /// # Arguments
69    ///
70    /// - `config`: the parameter set both sides agreed on.
71    ///
72    /// # Panics
73    ///
74    /// When the base domain does not hold a whole number of fold pairs.
75    #[must_use]
76    pub const fn new(config: &BinaryPcsConfig) -> Self {
77        // Queries index pairs, so the sampled domain is one bit narrower.
78        let shift = config.log_folding_factor() - 1;
79        let pair_domain = config.domain_size() >> shift;
80
81        Self {
82            num_variables: config.num_variables(),
83            log_folding_factor: config.log_folding_factor(),
84            // Every batch but the last commits its folded oracle.
85            num_oracles: config.num_fold_batches() - 1,
86            final_codeword_len: 1 << config.log_final_len(),
87            pow_bits: config.pow_bits(),
88            pair_bits: log2_strict_usize(pair_domain / 2),
89            // A run asking for more pairs than the domain holds opens every pair.
90            num_pairs: num_distinct_queries(pair_domain, config.num_queries()),
91        }
92    }
93
94    /// Describe the transcript this shape fixes.
95    ///
96    /// The fold rounds are absent: each seeds its own sumcheck sub-transcript.
97    ///
98    /// ```text
99    ///     per oracle  ->  one Merkle root
100    ///     closing     ->  final codeword, grind, query positions
101    /// ```
102    ///
103    /// # Panics
104    ///
105    /// Never in practice.
106    ///
107    /// A flat sequence of leaf steps always passes structural validation.
108    #[must_use]
109    pub fn pattern<F, EF>(&self) -> InteractionPattern
110    where
111        F: TranscriptField,
112        EF: Field + AlgebraIdentity<F>,
113    {
114        let mut steps = Vec::with_capacity(self.num_oracles + 3);
115
116        // One root per folded oracle, so the oracle count is the step count.
117        for _ in 0..self.num_oracles {
118            steps.push(Interaction::opaque(
119                Hierarchy::Atomic,
120                Kind::Message,
121                ORACLE_COMMITMENT,
122                Length::Scalar,
123            ));
124        }
125
126        // The last word is uncommitted, so every symbol of it is bound.
127        steps.push(Interaction::algebra::<F, EF>(
128            Hierarchy::Atomic,
129            Kind::Message,
130            FINAL_CODEWORD,
131            Length::Fixed(self.final_codeword_len),
132        ));
133
134        // A zero difficulty describes no work, so it contributes no step.
135        if self.pow_bits > 0 {
136            steps.push(Interaction::algebra::<F, F>(
137                Hierarchy::Atomic,
138                Kind::Pow,
139                QUERY_POW,
140                Length::Fixed(self.pow_bits),
141            ));
142        }
143
144        // The positions are drawn distinct, so the count is what is kept.
145        steps.push(Interaction::uniform_bits(
146            Hierarchy::Atomic,
147            Kind::Challenge,
148            QUERY_INDICES,
149            self.pair_bits,
150            Length::Fixed(self.num_pairs),
151        ));
152
153        InteractionPattern::new(steps).expect("a flat sequence of leaf steps is always well formed")
154    }
155
156    /// Bind the protocol identity and this shape into a seed.
157    ///
158    /// # Soundness
159    ///
160    /// The step counts and widths ride the fingerprint on their own.
161    ///
162    /// The arity and the folding factor move no step, so they are bound as chunks.
163    ///
164    /// Without them, a run folding every variable at once shares a description.
165    ///
166    /// Every other arity at the same rate lands on it.
167    ///
168    /// ```text
169    ///     nv = 8, k = 8   ->  one batch, no oracle, rate-wide final word
170    ///     nv = 9, k = 9   ->  the same three numbers
171    /// ```
172    ///
173    /// The surrounding layout binds the arity too.
174    ///
175    /// This makes the seed self-contained rather than closing a reachable gap.
176    #[must_use]
177    pub fn domain_separator<F, EF>(&self) -> DomainSeparator<Alphabet<F>>
178    where
179        F: TranscriptField,
180        EF: Field + AlgebraIdentity<F>,
181    {
182        let mut separator = DomainSeparator::new(VERSION, NAME, self.pattern::<F, EF>());
183
184        separator
185            .instance(&(self.num_variables as u64).to_be_bytes())
186            .instance(&(self.log_folding_factor as u64).to_be_bytes());
187
188        separator
189    }
190}
191
192/// Prover-side transcript of one binary-tower PCS run.
193///
194/// The challenger is borrowed, not consumed.
195///
196/// The run sits inside a larger protocol, whose transcript continues where this one stops.
197pub struct BinaryPcsProverTranscript<'a, F: TranscriptField, EF, C> {
198    /// Driver walking the description and holding the borrowed sponge.
199    state: ProverState<&'a mut C, Alphabet<F>>,
200    /// The numbers this run was described with.
201    shape: BinaryPcsShape,
202    /// Marker for the field the folded codewords live in.
203    _challenge: PhantomData<EF>,
204}
205
206impl<'a, F, EF, C> BinaryPcsProverTranscript<'a, F, EF, C>
207where
208    F: TranscriptField,
209    EF: Field + AlgebraIdentity<F>,
210    C: CanObserve<F> + CanSample<F> + CanSampleUniformBits<F> + GrindingChallenger<Witness = F>,
211{
212    /// Seed the transcript from the shape.
213    ///
214    /// # Arguments
215    ///
216    /// - `challenger`: sponge of the surrounding protocol, borrowed for this run.
217    /// - `shape`: the numbers that fix this run's transcript.
218    pub fn new(challenger: &'a mut C, shape: BinaryPcsShape) -> Self {
219        // Seeding folds the shape fingerprint into the sponge before any step.
220        let separator = shape.domain_separator::<F, EF>();
221
222        Self {
223            state: ProverState::new(challenger, &separator),
224            shape,
225            _challenge: PhantomData,
226        }
227    }
228
229    /// Lend the sponge to the delegated sumcheck rounds of one fold batch.
230    ///
231    /// A sumcheck round seeds a sub-transcript of its own, so no step of it belongs here.
232    pub fn fold_batch<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
233        run(self.state.challenger_mut())
234    }
235
236    /// Bind one folded oracle's Merkle root.
237    pub fn oracle_commitment<Com>(&mut self, commitment: Com)
238    where
239        Com: Clone,
240        C: CanObserve<Com>,
241    {
242        self.state.observe_opaque(ORACLE_COMMITMENT, commitment);
243    }
244
245    /// Bind the final codeword, which travels in the clear.
246    ///
247    /// # Panics
248    ///
249    /// When the codeword is not the length the run was described with.
250    pub fn final_codeword(&mut self, codeword: &[EF]) {
251        assert_eq!(
252            codeword.len(),
253            self.shape.final_codeword_len,
254            "the final codeword must carry the described number of symbols",
255        );
256        let _bound = self
257            .state
258            .observe_extensions::<F, EF, FieldToFieldCodec<F>>(FINAL_CODEWORD, codeword);
259    }
260
261    /// Grind the site guarding the query positions.
262    ///
263    /// # Returns
264    ///
265    /// The witness the search found, or zero when the site asks for no work.
266    pub fn query_pow(&mut self) -> F {
267        if self.shape.pow_bits == 0 {
268            return F::ZERO;
269        }
270        self.state.observe_pow(QUERY_POW, self.shape.pow_bits)
271    }
272
273    /// Draw the distinct fold pairs this run opens.
274    ///
275    /// # Returns
276    ///
277    /// Each pair's low-indexed position, in ascending order.
278    pub fn query_pairs(&mut self) -> Vec<usize> {
279        let kept = self.state.challenge_uniform_bits_rejecting::<F>(
280            QUERY_INDICES,
281            self.shape.pair_bits,
282            self.shape.num_pairs,
283            |candidate, kept| !kept.contains(&candidate),
284        );
285        sorted_pairs(kept)
286    }
287
288    /// Release the completeness check without playing the rest of the description.
289    ///
290    /// A caller that deliberately runs only part of a run closes it this way.
291    ///
292    /// Dropping an unfinished driver otherwise panics.
293    pub fn abort(&mut self) {
294        self.state.abort();
295    }
296
297    /// Close the transcript once every described step has been played.
298    ///
299    /// # Panics
300    ///
301    /// When the run played fewer steps than it was described with.
302    pub fn finish(self) {
303        assert!(
304            self.state.finalize().is_empty(),
305            "a binary PCS run carries its own proof values",
306        );
307    }
308}
309
310/// Verifier-side transcript of one binary-tower PCS run.
311///
312/// Mirrors the prover driver step for step, so the two walk one description.
313pub struct BinaryPcsVerifierTranscript<'a, F: TranscriptField, EF, C> {
314    /// Driver walking the description and holding the borrowed sponge.
315    state: VerifierState<'static, &'a mut C, Alphabet<F>>,
316    /// The numbers this run was described with.
317    shape: BinaryPcsShape,
318    /// Marker for the field the folded codewords live in.
319    _challenge: PhantomData<EF>,
320}
321
322impl<'a, F, EF, C> BinaryPcsVerifierTranscript<'a, F, EF, C>
323where
324    F: TranscriptField,
325    EF: Field + AlgebraIdentity<F>,
326    C: CanObserve<F> + CanSample<F> + CanSampleUniformBits<F> + GrindingChallenger<Witness = F>,
327{
328    /// Seed the transcript from the shape.
329    ///
330    /// # Arguments
331    ///
332    /// - `challenger`: sponge of the surrounding protocol, borrowed for this run.
333    /// - `shape`: the numbers that fix this run's transcript.
334    pub fn new(challenger: &'a mut C, shape: BinaryPcsShape) -> Self {
335        let separator = shape.domain_separator::<F, EF>();
336
337        Self {
338            state: VerifierState::new(challenger, &separator, &[]),
339            shape,
340            _challenge: PhantomData,
341        }
342    }
343
344    /// Lend the sponge to the delegated sumcheck rounds of one fold batch.
345    pub fn fold_batch<R>(&mut self, run: impl FnOnce(&mut C) -> R) -> R {
346        run(self.state.challenger_mut())
347    }
348
349    /// Replay one folded oracle's Merkle root.
350    pub fn oracle_commitment<Com>(&mut self, commitment: Com)
351    where
352        Com: Clone,
353        C: CanObserve<Com>,
354    {
355        self.state.observe_opaque(ORACLE_COMMITMENT, commitment);
356    }
357
358    /// Replay the final codeword the proof carries.
359    ///
360    /// # Errors
361    ///
362    /// When the codeword is not the length the run was described with.
363    pub fn final_codeword(&mut self, codeword: &[EF]) -> Result<(), TranscriptFailure<F>> {
364        // The length comes from the proof, so a mismatch is a rejection.
365        //
366        // Releasing the completeness check keeps this the only failure.
367        if codeword.len() != self.shape.final_codeword_len {
368            self.state.abort();
369            return Err(TranscriptFailure::FinalCodewordLength {
370                expected: self.shape.final_codeword_len,
371                got: codeword.len(),
372            });
373        }
374
375        let _bound = self
376            .state
377            .observe_extensions::<F, EF, FieldToFieldCodec<F>>(FINAL_CODEWORD, codeword);
378
379        Ok(())
380    }
381
382    /// Replay the grind guarding the query positions.
383    ///
384    /// # Errors
385    ///
386    /// - The witness is not zero where the site asks for no work.
387    /// - The witness misses the difficulty the site requires.
388    pub fn query_pow(&mut self, witness: F) -> Result<(), TranscriptFailure<F>> {
389        if self.shape.pow_bits == 0 {
390            // A zero-difficulty site plays no step, so nothing else reads this field.
391            //
392            // Pinning it here is what stops any value from riding along unbound.
393            if witness != F::ZERO {
394                // Releasing the completeness check keeps this rejection the only failure.
395                self.state.abort();
396                return Err(TranscriptFailure::NonCanonicalPowWitness { actual: witness });
397            }
398            return Ok(());
399        }
400
401        self.state
402            .observe_pow(QUERY_POW, self.shape.pow_bits, witness)
403            .map_err(|_| TranscriptFailure::PowWitness {
404                bits: self.shape.pow_bits,
405            })
406    }
407
408    /// Redraw the distinct fold pairs this run opens.
409    pub fn query_pairs(&mut self) -> Vec<usize> {
410        let kept = self.state.challenge_uniform_bits_rejecting::<F>(
411            QUERY_INDICES,
412            self.shape.pair_bits,
413            self.shape.num_pairs,
414            |candidate, kept| !kept.contains(&candidate),
415        );
416        sorted_pairs(kept)
417    }
418
419    /// Release the completeness check because the proof is being rejected.
420    pub fn abort(&mut self) {
421        self.state.abort();
422    }
423
424    /// Close the transcript once every described step has been replayed.
425    ///
426    /// # Panics
427    ///
428    /// When the run replayed fewer steps than it was described with.
429    pub fn finish(self) {
430        self.state
431            .finalize()
432            .expect("a binary PCS run reads an empty wire");
433    }
434}
435
436/// A transcript step the proof failed to satisfy.
437#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
438#[non_exhaustive]
439pub enum TranscriptFailure<F> {
440    /// The final codeword carries a symbol count the run never described.
441    #[error("final codeword length mismatch: expected {expected}, got {got}")]
442    FinalCodewordLength {
443        /// Symbol count the run was described with.
444        expected: usize,
445        /// Symbol count the proof carries.
446        got: usize,
447    },
448    /// A grinding witness did not meet the difficulty its step requires.
449    #[error("query grinding witness clears fewer than {bits} bits")]
450    PowWitness {
451        /// Difficulty the site requires, in bits.
452        bits: usize,
453    },
454    /// A grinding witness is not the value a zero difficulty admits.
455    ///
456    /// At zero bits the site reads no witness, so nothing else binds the field.
457    ///
458    /// Zero is the only value an honest prover emits, so zero is the only one accepted.
459    #[error("query grinding witness is {actual} at zero difficulty, expected zero")]
460    NonCanonicalPowWitness {
461        /// The witness the proof carries.
462        actual: F,
463    },
464}
465
466/// Turn the drawn pair indices into ascending low-indexed positions.
467///
468/// The draw keeps them distinct but in draw order.
469///
470/// Both sides sort, so the opened positions are one canonical list.
471fn sorted_pairs(kept: Vec<p3_challenger::fs::TranscriptBound<usize>>) -> Vec<usize> {
472    let mut pairs: Vec<usize> = kept
473        .into_iter()
474        .map(p3_challenger::fs::TranscriptBound::into_inner)
475        .collect();
476    pairs.sort_unstable();
477    // A pair indexes two adjacent symbols, so its position is the low one.
478    pairs.into_iter().map(|pair| pair << 1).collect()
479}
480
481#[cfg(test)]
482mod tests {
483    use alloc::string::String;
484    use alloc::vec;
485
486    use p3_binary_field::BinaryField128;
487    use p3_challenger::CanSample;
488    use p3_challenger::fs::PROTOCOL_ID_LEN;
489    use p3_challenger::testing::{assert_seeds_pairwise_distinct, pow_difficulties, seed_digest};
490    use p3_field::PrimeCharacteristicRing;
491    use p3_security::grinding::{
492        UNPRICED_GRINDING_SITES, grinding_step, is_unpriced_grinding_site,
493    };
494
495    use super::*;
496    use crate::params::BinaryPcsParams;
497
498    /// The protocol name as the security vocabulary spells it.
499    const NAME_STR: &str = "p3-binary-pcs";
500    use crate::test_util::challenger;
501
502    /// The configuration every shape below is derived from.
503    fn base_config() -> BinaryPcsConfig {
504        BinaryPcsConfig::try_new::<BinaryField128, BinaryField128>(
505            8,
506            BinaryPcsParams {
507                log_inv_rate: 2,
508                pow_bits: 4,
509                security_level: 100,
510            },
511        )
512        .unwrap()
513        .try_with_folding(1)
514        .unwrap()
515    }
516
517    #[test]
518    fn every_knob_of_a_run_reaches_the_seed() {
519        // Invariant: one field changed moves the seed, and no two changes collide.
520        //
521        // Fixture state: the shape derived from the reference configuration.
522        //
523        // Exhaustiveness check: every field named, none elided by a rest pattern.
524        //
525        // A field added to the shape stops this from compiling.
526        let BinaryPcsShape {
527            num_variables: _,
528            log_folding_factor: _,
529            num_oracles: _,
530            final_codeword_len: _,
531            pow_bits: _,
532            pair_bits: _,
533            num_pairs: _,
534        } = BinaryPcsShape::new(&base_config());
535
536        let digest = |shape: BinaryPcsShape| {
537            seed_digest(&shape.domain_separator::<BinaryField128, BinaryField128>())
538        };
539        let base = BinaryPcsShape::new(&base_config());
540
541        let mut seeds = vec![(String::from("baseline"), digest(base))];
542
543        // The arity moves no step, so it is bound as a chunk instead.
544        let mut wider = base;
545        wider.num_variables += 1;
546        seeds.push((String::from("num variables"), digest(wider)));
547
548        // So does the folding factor.
549        let mut coarser = base;
550        coarser.log_folding_factor += 1;
551        seeds.push((String::from("folding factor"), digest(coarser)));
552
553        // One more folded oracle is one more described step.
554        let mut more_oracles = base;
555        more_oracles.num_oracles += 1;
556        seeds.push((String::from("oracle count"), digest(more_oracles)));
557
558        // The final word travels in the clear, so its width is a step width.
559        let mut longer_final = base;
560        longer_final.final_codeword_len += 1;
561        seeds.push((String::from("final codeword"), digest(longer_final)));
562
563        // The grinding difficulty rides inside its own step.
564        let mut harder = base;
565        harder.pow_bits += 1;
566        seeds.push((String::from("pow bits"), digest(harder)));
567
568        // A narrower index draws from a smaller domain, which is a different claim.
569        let mut narrower = base;
570        narrower.pair_bits -= 1;
571        seeds.push((String::from("pair bits"), digest(narrower)));
572
573        // One more opened pair is one more position the proof must answer.
574        let mut more_pairs = base;
575        more_pairs.num_pairs += 1;
576        seeds.push((String::from("pair count"), digest(more_pairs)));
577
578        assert_seeds_pairwise_distinct(&seeds);
579    }
580
581    #[test]
582    fn a_zero_difficulty_run_describes_no_grinding_step() {
583        // Invariant: a zero-difficulty site contributes no step.
584        //
585        // The two cases therefore cannot share a description.
586        //
587        //     bits = 0  ->  no grinding step at all
588        //     bits = 4  ->  one step carrying the difficulty
589        let mut unground = BinaryPcsShape::new(&base_config());
590        unground.pow_bits = 0;
591        assert!(pow_difficulties(&unground.pattern::<BinaryField128, BinaryField128>()).is_empty());
592
593        let ground = BinaryPcsShape::new(&base_config());
594        assert_eq!(
595            pow_difficulties(&ground.pattern::<BinaryField128, BinaryField128>()),
596            vec![(QUERY_POW, 4)]
597        );
598    }
599
600    #[test]
601    fn the_protocol_name_fits_the_identifier_and_extends_nothing() {
602        // Invariant: the name is what separates this protocol from every other.
603        //
604        // It must fit the identifier, which carries its length in the last byte.
605        assert!(NAME.len() < PROTOCOL_ID_LEN - 1);
606
607        // No workspace protocol name starts with this one.
608        //
609        // No prefix relation therefore needs a length byte to part it from a longer name.
610        assert!(!NAME.starts_with(b"p3-binary-field"));
611    }
612
613    #[test]
614    fn the_grinding_sites_and_the_unpriced_table_name_each_other() {
615        // Invariant: a proof-of-work step is a soundness parameter in two places.
616        //
617        //     transcript  ->  the difficulty the pattern describes
618        //     model       ->  the difficulty the security report credits
619        //
620        // A site in neither vocabulary is a difficulty nobody compares.
621        //
622        // The workspace suite runs both directions for every protocol it can reach.
623        //
624        // This one seeds over a binary tower field.
625        //
626        // Its separator has a different sponge alphabet, so it cannot join that sweep.
627        let shape = BinaryPcsShape::new(&base_config());
628
629        for (label, _bits) in pow_difficulties(&shape.pattern::<BinaryField128, BinaryField128>()) {
630            let budgeted = grinding_step(NAME_STR, label).is_some();
631            let priced_elsewhere = is_unpriced_grinding_site(NAME_STR, label);
632
633            assert!(
634                budgeted || priced_elsewhere,
635                "p3-binary-pcs/{label} grinds, but no vocabulary classifies it",
636            );
637            assert!(
638                !(budgeted && priced_elsewhere),
639                "p3-binary-pcs/{label} is both compared against the model and priced elsewhere",
640            );
641        }
642
643        // The other direction: a listed row must name a step this run describes.
644        //
645        // Without it, dropping the grind would leave the row silently stale.
646        let described: Vec<&str> =
647            pow_difficulties(&shape.pattern::<BinaryField128, BinaryField128>())
648                .into_iter()
649                .map(|(label, _)| label)
650                .collect();
651
652        for &(protocol, label) in &UNPRICED_GRINDING_SITES {
653            if protocol != NAME_STR {
654                continue;
655            }
656            assert!(
657                described.contains(&label),
658                "p3-binary-pcs/{label} is listed as priced elsewhere, but no step describes it",
659            );
660        }
661    }
662
663    #[test]
664    fn a_perturbed_final_codeword_moves_the_state_the_caller_continues_from() {
665        // Invariant: every symbol of the uncommitted final word is bound.
666        //
667        // Changing one moves the grind and every position drawn after it.
668        //
669        // Fixture state: a four-symbol codeword.
670        //
671        // Mutation: bump the last symbol.
672        let shape = BinaryPcsShape::new(&base_config());
673
674        let run = |codeword: &[BinaryField128]| {
675            let mut ch = challenger();
676            let mut transcript = BinaryPcsProverTranscript::new(&mut ch, shape);
677            for _ in 0..shape.num_oracles {
678                transcript.oracle_commitment(
679                    p3_symmetric::MerkleCap::<BinaryField128, [u8; 32]>::new(vec![[0u8; 32]]),
680                );
681            }
682            transcript.final_codeword(codeword);
683            let _witness = transcript.query_pow();
684            let positions = transcript.query_pairs();
685            transcript.finish();
686            (positions, CanSample::<BinaryField128>::sample(&mut ch))
687        };
688
689        let honest = vec![BinaryField128::ZERO; shape.final_codeword_len];
690        let mut tampered = honest.clone();
691        *tampered.last_mut().unwrap() = BinaryField128::ONE;
692
693        assert_ne!(run(&honest), run(&tampered));
694    }
695}