p3_sumcheck/transcript.rs
1//! Fiat-Shamir transcript of the quadratic sumcheck.
2//!
3//! # Overview
4//!
5//! This module is the single statement of what a batch of quadratic sumcheck rounds absorbs.
6//! Both sides consume it through the same three calls.
7//!
8//! The description itself is a value, built from the numbers that shape a batch.
9//!
10//! # Shape
11//!
12//! ```text
13//! round 0: polynomial 2 extension elements
14//! grinding present only when the difficulty is positive
15//! challenge 1 extension element
16//! round 1: ...
17//! ```
18//!
19//! A quadratic round polynomial has three values and two of them cross the wire.
20//! The third is what the round identity of the basis reconstructs.
21//!
22//! # Sub-protocol
23//!
24//! A sumcheck is almost always a phase of something larger.
25//!
26//! It therefore seeds a transcript of its own from a borrowed sponge, and hands the sponge back.
27//! The surrounding protocol keeps its own description, and brackets this run inside it.
28//!
29//! That is what lets the two descriptions be written independently and still compose.
30//!
31//! # Empty runs
32//!
33//! A batch of zero rounds absorbs nothing and draws nothing.
34//!
35//! Such a batch is therefore not a transcript at all.
36//! No seed is folded in, and no step is played.
37//!
38//! A caller whose configuration reduces to zero rounds needs no special case of its own.
39//!
40//! # What the shape binds
41//!
42//! A fingerprint of the description enters the sponge before any step runs.
43//!
44//! The round count and the grinding difficulty both change the description.
45//! The fingerprint therefore covers them.
46//!
47//! The basis does not change it.
48//! It renames the two values a round sends without moving a single step.
49//! It is bound as an instance label instead.
50//!
51//! No shipped prover or verifier drives the projective reading yet.
52//! The label makes the two seeds differ before one exists.
53
54use alloc::vec::Vec;
55use core::marker::PhantomData;
56
57use p3_challenger::fs::{
58 DomainSeparator, FieldToFieldCodec, FieldUnit, Hierarchy, Interaction, InteractionPattern,
59 Kind, Length, ProverState, TranscriptField, VerifierState,
60};
61use p3_challenger::{CanObserve, CanSample, GrindingChallenger};
62use p3_field::ExtensionField;
63
64use crate::error::SumcheckError;
65use crate::strategy::Basis;
66
67/// Version byte bound into the transcript seed.
68///
69/// Bumping it separates two revisions of this protocol.
70/// It does so even when their step sequences agree.
71const VERSION: u8 = 1;
72
73/// Protocol name bound into the transcript seed.
74pub(crate) const NAME: &[u8] = b"p3-sumcheck-quadratic";
75
76/// Step label of the two round-polynomial values a round sends.
77const ROUND_POLY: &str = "round_poly";
78
79/// Step label of a per-round grinding step.
80const ROUND_POW: &str = "round_pow";
81
82/// Step label of a per-round challenge.
83const ROUND_CHALLENGE: &str = "round_challenge";
84
85/// Values of the round polynomial that cross the wire.
86const VALUES_PER_ROUND: usize = 2;
87
88/// Sponge alphabet of a challenger that speaks the base field natively.
89type Alphabet<F> = FieldUnit<F>;
90
91/// Numbers that fix the transcript of one batch of quadratic sumcheck rounds.
92///
93/// Both sides build this from their own configuration, never from a proof.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct SumcheckShape {
96 /// Number of variables the batch binds, and so the number of rounds.
97 pub num_rounds: usize,
98 /// Grinding difficulty guarding each round's challenge, or zero to omit grinding.
99 pub pow_bits: usize,
100 /// Basis the two transmitted values are read in.
101 pub basis: Basis,
102}
103
104impl SumcheckShape {
105 /// Collect the numbers that fix one batch of rounds.
106 ///
107 /// # Arguments
108 ///
109 /// - `num_rounds`: number of variables the batch binds.
110 /// - `pow_bits`: grinding difficulty per round, or zero to omit grinding.
111 /// - `basis`: how the two transmitted values are read.
112 #[must_use]
113 pub const fn new(num_rounds: usize, pow_bits: usize, basis: Basis) -> Self {
114 Self {
115 num_rounds,
116 pow_bits,
117 basis,
118 }
119 }
120
121 /// Describe the transcript this shape fixes.
122 ///
123 /// # Panics
124 ///
125 /// Never in practice.
126 /// A flat sequence of leaf steps always passes structural validation.
127 #[must_use]
128 pub fn pattern<F, EF>(&self) -> InteractionPattern
129 where
130 F: TranscriptField,
131 EF: ExtensionField<F>,
132 {
133 // Both readings send two values per round, so both describe the same steps.
134 //
135 // evaluation basis: [h(0), h(inf)]
136 // projective basis: [s(1), s(inf)]
137 //
138 // Matching every variant forces a new reading to be given a width here.
139 //
140 // That is the whole of what the match buys:
141 //
142 // another width -> this line must change, so the description changes
143 // another step sequence -> a copied arm compiles and shares this description
144 //
145 // Readings that agree on width and sequence are separated only by the instance byte.
146 let values_per_round = match self.basis {
147 Basis::Evaluation => VALUES_PER_ROUND,
148 Basis::Projective => VALUES_PER_ROUND,
149 };
150
151 // Up to three steps per round.
152 let mut steps = Vec::with_capacity(3 * self.num_rounds);
153
154 for _ in 0..self.num_rounds {
155 // The round polynomial is one step carrying both transmitted values.
156 steps.push(Interaction::algebra::<F, EF>(
157 Hierarchy::Atomic,
158 Kind::Message,
159 ROUND_POLY,
160 Length::Fixed(values_per_round),
161 ));
162
163 // Grinding sits between the polynomial and the challenge it protects.
164 //
165 // The difficulty travels inside the step.
166 // A verifier expecting a cheaper grind therefore fails the shape check.
167 if self.pow_bits > 0 {
168 steps.push(Interaction::algebra::<F, F>(
169 Hierarchy::Atomic,
170 Kind::Pow,
171 ROUND_POW,
172 Length::Fixed(self.pow_bits),
173 ));
174 }
175
176 // The challenge binds the variable this round reduces away.
177 steps.push(Interaction::algebra::<F, EF>(
178 Hierarchy::Atomic,
179 Kind::Challenge,
180 ROUND_CHALLENGE,
181 Length::Scalar,
182 ));
183 }
184
185 InteractionPattern::new(steps).expect("a flat sequence of leaf steps is always well formed")
186 }
187
188 /// Bind the protocol identity, this shape, and the basis.
189 ///
190 /// The round count and the difficulty both move the step sequence.
191 /// The fingerprint therefore carries them.
192 ///
193 /// The basis does not move it.
194 ///
195 /// ```text
196 /// evaluation basis: the pair is [h(0), h(inf)]
197 /// projective basis: the pair is [s(1), s(inf)]
198 /// ```
199 ///
200 /// Both are two extension elements followed by one challenge.
201 /// Only the instance label separates two runs that disagree on which reading is meant.
202 ///
203 /// Every shipped driver picks the evaluation reading, so no live pair can disagree today.
204 /// The label is what keeps the two apart once a projective driver exists.
205 #[must_use]
206 pub fn domain_separator<F, EF>(&self) -> DomainSeparator<Alphabet<F>>
207 where
208 F: TranscriptField,
209 EF: ExtensionField<F>,
210 {
211 let mut separator = DomainSeparator::new(VERSION, NAME, self.pattern::<F, EF>());
212
213 separator.instance(&[match self.basis {
214 Basis::Evaluation => 0,
215 Basis::Projective => 1,
216 }]);
217
218 separator
219 }
220}
221
222/// Prover-side transcript of one batch of quadratic sumcheck rounds.
223///
224/// # Overview
225///
226/// Holds the only definition of what a prover writes per round.
227///
228/// Every prover of these rounds drives them through this type.
229/// No two prover loops can then drift from each other, or from the verifier.
230///
231/// # Borrowing
232///
233/// The challenger is borrowed, not consumed.
234///
235/// A sumcheck runs inside a larger protocol.
236/// That protocol's own transcript continues where this one stops.
237pub struct ProverTranscript<'a, C, F: TranscriptField, EF> {
238 /// Driver walking the description and holding the borrowed sponge.
239 ///
240 /// Absent when the batch has no rounds, because then there is nothing to describe.
241 state: Option<ProverState<&'a mut C, Alphabet<F>>>,
242 /// The numbers this batch was described with.
243 shape: SumcheckShape,
244 /// Marker for the extension field the rounds carry.
245 _ef: PhantomData<EF>,
246}
247
248impl<'a, C, F, EF> ProverTranscript<'a, C, F, EF>
249where
250 F: TranscriptField,
251 EF: ExtensionField<F>,
252 C: CanObserve<F> + CanSample<F> + GrindingChallenger<Witness = F>,
253{
254 /// Seed the transcript from the shape.
255 ///
256 /// A batch of zero rounds leaves the sponge exactly as it was found.
257 ///
258 /// # Arguments
259 ///
260 /// - `challenger`: sponge of the surrounding protocol, borrowed for the batch.
261 /// - `shape`: the numbers that fix this batch's transcript.
262 pub fn new(challenger: &'a mut C, shape: SumcheckShape) -> Self {
263 let state = (shape.num_rounds > 0).then(|| {
264 // Seeding folds the shape fingerprint into the sponge before any step.
265 let separator = shape.domain_separator::<F, EF>();
266 ProverState::new(challenger, &separator)
267 });
268
269 Self {
270 state,
271 shape,
272 _ef: PhantomData,
273 }
274 }
275
276 /// Play one round: bind the two transmitted values, grind, and draw the challenge.
277 ///
278 /// # Returns
279 ///
280 /// - The challenge that binds this round's variable.
281 /// - The grinding witness, when the difficulty is positive.
282 ///
283 /// The caller stores both in its own proof.
284 ///
285 /// # Panics
286 ///
287 /// When the batch was described with no rounds at all.
288 pub fn round(&mut self, c_a: EF, c_inf: EF) -> (EF, Option<F>) {
289 let state = self
290 .state
291 .as_mut()
292 .expect("a batch described with no rounds has no round to play");
293
294 // Bind both values before the challenge that will be evaluated against them.
295 state.observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_POLY, &[c_a, c_inf]);
296
297 // Grinding raises the cost of searching for a favourable challenge.
298 let witness =
299 (self.shape.pow_bits > 0).then(|| state.observe_pow(ROUND_POW, self.shape.pow_bits));
300
301 // Draw the challenge for the caller to fold with.
302 let challenge = state
303 .challenge_extension::<F, EF, FieldToFieldCodec<F>>(ROUND_CHALLENGE)
304 .into_inner();
305
306 (challenge, witness)
307 }
308
309 /// Close the transcript once every described step has been played.
310 ///
311 /// # Panics
312 ///
313 /// When fewer rounds were played than the batch was described with.
314 pub fn finish(self) {
315 if let Some(state) = self.state {
316 // Nothing was written to the driver's own buffer.
317 // Closing is therefore purely the check that the description was consumed.
318 assert!(
319 state.finalize().is_empty(),
320 "the quadratic sumcheck carries every value in its own proof",
321 );
322 }
323 }
324}
325
326/// Verifier-side transcript of one batch of quadratic sumcheck rounds.
327///
328/// Mirrors the prover side call for call, over the same description.
329///
330/// The values come from the proof rather than from a wire.
331/// The caller must therefore have counted them against the described shape first.
332pub struct VerifierTranscript<'a, C, F: TranscriptField, EF> {
333 /// Driver walking the description and holding the borrowed sponge.
334 ///
335 /// The proof carries every value, so the driver reads an empty wire.
336 ///
337 /// Absent when the batch has no rounds, because then there is nothing to describe.
338 state: Option<VerifierState<'static, &'a mut C, Alphabet<F>>>,
339 /// The numbers this batch was described with.
340 shape: SumcheckShape,
341 /// Index of the next round to play, used to place a round failure.
342 round: usize,
343 /// Marker for the extension field the rounds carry.
344 _ef: PhantomData<EF>,
345}
346
347impl<'a, C, F, EF> VerifierTranscript<'a, C, F, EF>
348where
349 F: TranscriptField,
350 EF: ExtensionField<F>,
351 C: CanObserve<F> + CanSample<F> + GrindingChallenger<Witness = F>,
352{
353 /// Seed the transcript from the shape.
354 ///
355 /// The argument matches the prover's, so both sides seed identically.
356 pub fn new(challenger: &'a mut C, shape: SumcheckShape) -> Self {
357 let state = (shape.num_rounds > 0).then(|| {
358 // Seeding folds the shape fingerprint into the sponge before any step.
359 let separator = shape.domain_separator::<F, EF>();
360 VerifierState::new(challenger, &separator, &[])
361 });
362
363 Self {
364 state,
365 shape,
366 round: 0,
367 _ef: PhantomData,
368 }
369 }
370
371 /// Replay one round: bind both values, re-check the grind, draw the challenge.
372 ///
373 /// Every value comes from the proof, so every disagreement is a rejection.
374 ///
375 /// # Errors
376 ///
377 /// - Grinding is enabled and the round carries no witness.
378 /// - The witness misses the required difficulty.
379 ///
380 /// # Panics
381 ///
382 /// When the batch was described with no rounds at all.
383 pub fn round(&mut self, c_a: EF, c_inf: EF, witness: Option<F>) -> Result<EF, SumcheckError> {
384 let round = self.round;
385 self.round += 1;
386
387 let state = self
388 .state
389 .as_mut()
390 .expect("a batch described with no rounds has no round to play");
391
392 // Bind both values before the challenge that will be evaluated against them.
393 //
394 // A round always sends exactly two, so this step cannot disagree on its width.
395 state
396 .observe_extensions::<F, EF, FieldToFieldCodec<F>>(ROUND_POLY, &[c_a, c_inf])
397 .expect("a round always carries exactly the two values the step describes");
398
399 // Re-run the prover's grinding step on the witness it committed to.
400 if self.shape.pow_bits > 0 {
401 // With no witness the described step cannot be played at all.
402 //
403 // Releasing the completeness check keeps this rejection the only failure.
404 let Some(witness) = witness else {
405 state.abort();
406 return Err(SumcheckError::MissingPowWitness {
407 round,
408 difficulty: self.shape.pow_bits,
409 });
410 };
411 // A failing check releases the completeness check on its own way out.
412 state
413 .observe_pow(ROUND_POW, self.shape.pow_bits, witness)
414 .map_err(|_| SumcheckError::InvalidPowWitness {
415 round,
416 difficulty: self.shape.pow_bits,
417 })?;
418 }
419
420 // Draw the same challenge the prover saw.
421 Ok(state
422 .challenge_extension::<F, EF, FieldToFieldCodec<F>>(ROUND_CHALLENGE)
423 .into_inner())
424 }
425
426 /// Close the transcript once every described step has been played.
427 ///
428 /// # Panics
429 ///
430 /// When fewer rounds were played than the batch was described with.
431 pub fn finish(self) {
432 if let Some(state) = self.state {
433 // The proof carries every value, so no unread wire bytes can remain.
434 state
435 .finalize()
436 .expect("the quadratic sumcheck reads an empty wire");
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use alloc::vec::Vec;
444
445 use p3_baby_bear::{BabyBear, Poseidon2BabyBear};
446 use p3_challenger::testing::pow_difficulties;
447 use p3_challenger::{CanSample, DuplexChallenger};
448 use p3_field::PrimeCharacteristicRing;
449 use p3_field::extension::BinomialExtensionField;
450 use rand::SeedableRng;
451 use rand::rngs::SmallRng;
452
453 use super::*;
454
455 type F = BabyBear;
456 type EF = BinomialExtensionField<F, 4>;
457 type Perm = Poseidon2BabyBear<16>;
458 type Ch = DuplexChallenger<F, Perm, 16, 8>;
459
460 fn fresh_challenger() -> Ch {
461 // Fixed seed so two runs differ only where the transcript makes them differ.
462 let mut rng = SmallRng::seed_from_u64(0xDEADBEEF);
463 Ch::new(Perm::new_from_rng_128(&mut rng))
464 }
465
466 /// The first challenge a shape's seed produces.
467 fn first_challenge(shape: SumcheckShape) -> F {
468 let mut challenger = fresh_challenger();
469 shape.domain_separator::<F, EF>().seed(&mut challenger);
470 challenger.sample()
471 }
472
473 #[test]
474 fn every_number_that_shapes_a_batch_reaches_the_seed() {
475 // Fixture state: 4 rounds, no grinding, values read in the evaluation basis.
476 let base = SumcheckShape::new(4, 0, Basis::Evaluation);
477
478 // One more round appends two more steps.
479 assert_ne!(
480 first_challenge(base),
481 first_challenge(SumcheckShape::new(5, 0, Basis::Evaluation))
482 );
483
484 // Enabling grinding inserts a step per round.
485 assert_ne!(
486 first_challenge(base),
487 first_challenge(SumcheckShape::new(4, 8, Basis::Evaluation))
488 );
489
490 // Two positive difficulties differ only inside the grinding steps.
491 assert_ne!(
492 first_challenge(SumcheckShape::new(4, 8, Basis::Evaluation)),
493 first_challenge(SumcheckShape::new(4, 9, Basis::Evaluation))
494 );
495
496 // The basis leaves the step sequence identical, so only the label separates the two.
497 assert_ne!(
498 first_challenge(base),
499 first_challenge(SumcheckShape::new(4, 0, Basis::Projective))
500 );
501 }
502
503 #[test]
504 fn a_batch_of_no_rounds_leaves_the_sponge_untouched() {
505 // Invariant: a batch with nothing to absorb is not a transcript.
506 //
507 // A caller whose configuration reduces to zero rounds runs no steps and seeds nothing.
508 // Its counterpart on the other side, which may not call in at all, therefore stays in step.
509 let mut untouched = fresh_challenger();
510 let expected: F = untouched.sample();
511
512 let mut challenger = fresh_challenger();
513 let transcript = ProverTranscript::<Ch, F, EF>::new(
514 &mut challenger,
515 SumcheckShape::new(0, 0, Basis::Evaluation),
516 );
517 transcript.finish();
518
519 assert_eq!(CanSample::<F>::sample(&mut challenger), expected);
520 }
521
522 #[test]
523 fn a_round_missing_its_grinding_witness_is_rejected() {
524 // Described batch: 2 rounds guarded by 4 bits of grinding each.
525 //
526 // A described grinding step cannot be replayed with no witness to feed it.
527 //
528 // This is the path a downstream crate reaches with a proof whose witness list is short.
529 // A panic here would compound with the driver's own drop check and abort the process.
530 let mut challenger = fresh_challenger();
531 let mut transcript = VerifierTranscript::<Ch, F, EF>::new(
532 &mut challenger,
533 SumcheckShape::new(2, 4, Basis::Evaluation),
534 );
535
536 let err = transcript
537 .round(EF::ONE, EF::ONE, None)
538 .expect_err("a described grinding step with no witness must error");
539
540 assert_eq!(
541 err,
542 SumcheckError::MissingPowWitness {
543 round: 0,
544 difficulty: 4
545 }
546 );
547 }
548
549 #[test]
550 fn a_grinding_witness_below_the_difficulty_is_rejected() {
551 // Described batch: 1 round guarded by 12 bits of grinding.
552 //
553 // Mutation: hand the round a witness that was never ground at all.
554 let mut challenger = fresh_challenger();
555 let mut transcript = VerifierTranscript::<Ch, F, EF>::new(
556 &mut challenger,
557 SumcheckShape::new(1, 12, Basis::Evaluation),
558 );
559
560 let err = transcript
561 .round(EF::ONE, EF::ONE, Some(F::ZERO))
562 .expect_err("a witness below the required difficulty must error");
563
564 assert_eq!(
565 err,
566 SumcheckError::InvalidPowWitness {
567 round: 0,
568 difficulty: 12
569 }
570 );
571 }
572
573 /// The challenges a verifier redraws from one recorded pair of values, per round.
574 fn replay(rounds: &[[EF; 2]]) -> Vec<EF> {
575 let mut challenger = fresh_challenger();
576 let shape = SumcheckShape::new(rounds.len(), 0, Basis::Evaluation);
577 let mut transcript = VerifierTranscript::<Ch, F, EF>::new(&mut challenger, shape);
578 let replayed = rounds
579 .iter()
580 .map(|&[c_a, c_inf]| transcript.round(c_a, c_inf, None).unwrap())
581 .collect();
582 transcript.finish();
583 replayed
584 }
585
586 #[test]
587 fn a_perturbed_finite_point_value_moves_every_later_challenge() {
588 // Invariant: the pair is bound before the challenge that is evaluated against it.
589 //
590 // Without that, a prover could pick the round polynomial to suit the challenge.
591 //
592 // Fixture state: two rounds, no grinding.
593 //
594 // Mutation: change the first round's finite-point value.
595 //
596 // honest: [[1, 2], [2, 1]]
597 // tampered: [[3, 2], [2, 1]]
598 let honest = [[EF::ONE, EF::TWO], [EF::TWO, EF::ONE]];
599 let mut tampered = honest;
600 tampered[0][0] = EF::from_u8(3);
601
602 assert_ne!(replay(&honest), replay(&tampered));
603 }
604
605 #[test]
606 fn a_perturbed_leading_coefficient_moves_every_later_challenge() {
607 // Same invariant for the other half of the pair.
608 //
609 // Fixture state: two rounds, no grinding.
610 //
611 // Mutation: change the first round's leading coefficient.
612 //
613 // honest: [[1, 2], [2, 1]]
614 // tampered: [[1, 3], [2, 1]]
615 let honest = [[EF::ONE, EF::TWO], [EF::TWO, EF::ONE]];
616 let mut tampered = honest;
617 tampered[0][1] = EF::from_u8(3);
618
619 assert_ne!(replay(&honest), replay(&tampered));
620 }
621
622 #[test]
623 fn a_recorded_grinding_witness_replays_to_the_same_challenge() {
624 // Completeness of the guarded path: the witness is absorbed, not merely checked.
625 //
626 // The verifier redraws the challenge only after feeding back the witness the prover found.
627 //
628 // Fixture state: one round guarded by 4 bits.
629 let shape = SumcheckShape::new(1, 4, Basis::Evaluation);
630
631 let mut challenger = fresh_challenger();
632 let mut prover = ProverTranscript::<Ch, F, EF>::new(&mut challenger, shape);
633 let (challenge, witness) = prover.round(EF::ONE, EF::TWO);
634 prover.finish();
635 let witness = witness.expect("a positive difficulty always yields a witness");
636
637 // Replaying the recorded witness reaches the recorded challenge.
638 let mut challenger = fresh_challenger();
639 let mut verifier = VerifierTranscript::<Ch, F, EF>::new(&mut challenger, shape);
640 let replayed = verifier.round(EF::ONE, EF::TWO, Some(witness)).unwrap();
641 verifier.finish();
642
643 assert_eq!(challenge, replayed);
644 }
645
646 #[test]
647 fn both_sides_of_a_round_draw_the_same_challenge() {
648 // Completeness: the two drivers walk one description and land on one challenge.
649 let shape = SumcheckShape::new(2, 0, Basis::Evaluation);
650
651 let mut prover_challenger = fresh_challenger();
652 let mut prover = ProverTranscript::<Ch, F, EF>::new(&mut prover_challenger, shape);
653 let (first, _) = prover.round(EF::ONE, EF::TWO);
654 let (second, _) = prover.round(EF::TWO, EF::ONE);
655 prover.finish();
656
657 let mut verifier_challenger = fresh_challenger();
658 let mut verifier = VerifierTranscript::<Ch, F, EF>::new(&mut verifier_challenger, shape);
659 let replayed_first = verifier.round(EF::ONE, EF::TWO, None).unwrap();
660 let replayed_second = verifier.round(EF::TWO, EF::ONE, None).unwrap();
661 verifier.finish();
662
663 assert_eq!(first, replayed_first);
664 assert_eq!(second, replayed_second);
665
666 // The sponge is handed back in one state, so the surrounding protocol stays in step.
667 assert_eq!(
668 CanSample::<F>::sample(&mut prover_challenger),
669 CanSample::<F>::sample(&mut verifier_challenger)
670 );
671 }
672
673 #[test]
674 fn the_described_grinding_matches_the_configured_difficulty() {
675 // Invariant: a grinding difficulty lives in two places.
676 //
677 // transcript -> the bits the pattern describes
678 // model -> the bits this protocol's own report credits
679 //
680 // Both read the same configuration field, so they must agree round for round.
681 //
682 // This protocol prices its own grinding, so the shared budget never compares it.
683 //
684 // Fixture state: four rounds, ground and unground.
685 for bits in [0, 4] {
686 let shape = SumcheckShape::new(4, bits, Basis::Evaluation);
687 let described = pow_difficulties(&shape.pattern::<F, EF>());
688
689 // A zero difficulty describes no step.
690 //
691 // The ground case is therefore the only one that records anything.
692 let expected = if bits == 0 { 0 } else { shape.num_rounds };
693 assert_eq!(described.len(), expected);
694
695 assert!(
696 described
697 .iter()
698 .all(|&(label, got)| label == ROUND_POW && got == bits)
699 );
700 }
701 }
702}