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}