Skip to main content

p3_challenger/
multi_field_challenger.rs

1use alloc::string::String;
2use alloc::vec;
3use alloc::vec::Vec;
4use core::array;
5
6use p3_field::{
7    BasedVectorSpace, PrimeField, PrimeField32, absorb_radix_bits, max_absorb_injective_limbs,
8    reduce_packed, split_pf_to_field_order_limbs, squeeze_field_order_num_limbs,
9};
10use p3_symmetric::{CryptographicPermutation, Hash, MerkleCap};
11
12use crate::{
13    CanFinalizeDigest, CanObserve, CanSample, CanSampleBits, DuplexChallenger, FieldChallenger,
14};
15
16/// A challenger that samples in `F: PrimeField32` while the transcript sponge lives in `PF`.
17///
18/// Wraps [`DuplexChallenger<PF>`](DuplexChallenger): all permutations and `PF` rate state are
19/// exactly those of `inner`. This type only adapts
20///
21/// - **`F` → `PF`**: pending scalars are packed with [`reduce_packed`] (radix
22///   $2^{\texttt{absorb\_radix\_bits::<F>()}}$) into up to `RATE` `PF` rate slots, then
23///   [`DuplexChallenger::absorb_rate_padded_with_tag`](DuplexChallenger::absorb_rate_padded_with_tag)
24///   runs (zero-padded tail, length tag = number of `F`s absorbed).
25/// - **`PF` → `F`**: after each duplex, each rate cell is split with
26///   [`split_pf_to_field_order_limbs`] (base `|F|`, [`squeeze_field_order_num_limbs`] limbs per
27///   cell) into a flat queue consumed by [`CanSample::sample`]. Each extracted limb is uniform
28///   over the **entire** `F` domain (bias `< 1/|F|`). The inner `output_buffer` is then cleared
29///   so the next empty batch triggers a new duplex like [`DuplexChallenger::sample`].
30///
31/// **`observe(Hash)` / `observe(MerkleCap)`** flush pending `F`s through that packed absorb, then
32/// absorb digest words natively via the same `absorb_rate_padded_with_tag` (length tag = number of
33/// `PF` words in the block)—no PF → `F` → repack detour.
34#[derive(Clone, Debug)]
35pub struct MultiField32Challenger<F, PF, P, const WIDTH: usize, const RATE: usize>
36where
37    F: PrimeField32,
38    PF: PrimeField,
39    P: CryptographicPermutation<[PF; WIDTH]>,
40{
41    /// The underlying `PF` duplex sponge.
42    inner: DuplexChallenger<PF, P, WIDTH, RATE>,
43    f_buffer: Vec<F>,
44    /// Expanded `F` limbs from `inner.output_buffer` (same pop order as the pre-wrapper design).
45    f_squeeze_buffer: Vec<F>,
46}
47
48impl<F, PF, P, const WIDTH: usize, const RATE: usize> MultiField32Challenger<F, PF, P, WIDTH, RATE>
49where
50    F: PrimeField32,
51    PF: PrimeField,
52    P: CryptographicPermutation<[PF; WIDTH]>,
53{
54    /// Radix bit-width $b$ for packing observed `F` values via [`reduce_packed`]: the smallest
55    /// `b` with `F::ORDER_U32 - 1 < 2^b` (see [`p3_field::absorb_radix_bits`]).
56    #[inline]
57    #[must_use]
58    pub const fn absorb_radix_bits(&self) -> u32 {
59        absorb_radix_bits::<F>()
60    }
61
62    /// Maximum number of `F` elements packed into a single `PF` rate slot injectively (see
63    /// [`p3_field::max_absorb_injective_limbs`]). Pending scalars are absorbed in chunks of this
64    /// size; at most `RATE` such packed words are written per duplex step.
65    #[inline]
66    #[must_use]
67    pub fn absorb_num_f_elms(&self) -> usize {
68        max_absorb_injective_limbs::<F, PF>()
69    }
70
71    /// Number of base-`|F|` limbs taken from each squeezed `PF` rate cell when refilling the
72    /// `F` queue (see [`p3_field::squeeze_field_order_num_limbs`] and
73    /// [`p3_field::split_pf_to_field_order_limbs`]). Chooses near-uniform limbs over `F` for
74    /// uniform `PF`.
75    #[inline]
76    #[must_use]
77    pub fn squeeze_num_f_elms(&self) -> usize {
78        squeeze_field_order_num_limbs::<PF, F>()
79    }
80
81    /// Number of `F` challenges still queued from the current squeeze batch (after `sample` pops).
82    #[inline]
83    #[must_use]
84    pub const fn pending_f_squeeze_len(&self) -> usize {
85        self.f_squeeze_buffer.len()
86    }
87
88    pub fn new(permutation: P) -> Result<Self, String> {
89        if F::order() >= PF::order() {
90            return Err(String::from("F::order() must be less than PF::order()"));
91        }
92        if RATE >= WIDTH {
93            return Err(String::from("RATE must be less than WIDTH"));
94        }
95        // A full flush stamps up to limbs-per-slot * RATE scalars into a byte-sized length tag.
96        // Past 255, lengths differing by 256 would share a tag and collide in the transcript.
97        if max_absorb_injective_limbs::<F, PF>() * RATE > u8::MAX as usize {
98            return Err(String::from(
99                "absorb length tag must fit in a u8: max_absorb_injective_limbs * RATE must be at most 255",
100            ));
101        }
102
103        Ok(Self {
104            inner: DuplexChallenger::new(permutation),
105            f_buffer: vec![],
106            f_squeeze_buffer: vec![],
107        })
108    }
109
110    fn flush_f_if_non_empty(&mut self) {
111        if self.f_buffer.is_empty() {
112            return;
113        }
114        let n_in = self.f_buffer.len();
115        let absorb_n = self.absorb_num_f_elms();
116        assert!(n_in <= absorb_n * RATE);
117        let rb = self.absorb_radix_bits();
118        let packed: Vec<PF> = self
119            .f_buffer
120            .chunks(absorb_n)
121            .map(|chunk| reduce_packed(chunk, rb))
122            .collect();
123        // Invariant: the constructor bounds a full flush at 255 scalars, so this never truncates.
124        let tag = u8::try_from(n_in).expect("absorb length tag must fit in a u8");
125        self.inner.absorb_rate_padded_with_tag(&packed, tag);
126        self.f_buffer.clear();
127        self.f_squeeze_buffer.clear();
128    }
129
130    fn refill_f_squeeze_from_inner(&mut self) {
131        self.f_squeeze_buffer.clear();
132        let squeeze_n = self.squeeze_num_f_elms();
133        for &pf in &self.inner.output_buffer {
134            self.f_squeeze_buffer
135                .extend(split_pf_to_field_order_limbs::<PF, F>(pf, squeeze_n));
136        }
137        // Match `DuplexChallenger` semantics: squeezing consumes the current rate row. Until these
138        // `F` limbs are exhausted, `inner.output_buffer` must read as empty so the next `sample`
139        // triggers a fresh duplex when needed.
140        self.inner.output_buffer.clear();
141    }
142}
143
144impl<F, PF, P, const WIDTH: usize, const RATE: usize> FieldChallenger<F>
145    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
146where
147    F: PrimeField32,
148    PF: PrimeField,
149    P: CryptographicPermutation<[PF; WIDTH]>,
150{
151}
152
153impl<F, PF, P, const WIDTH: usize, const RATE: usize> CanObserve<F>
154    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
155where
156    F: PrimeField32,
157    PF: PrimeField,
158    P: CryptographicPermutation<[PF; WIDTH]>,
159{
160    fn observe(&mut self, value: F) {
161        self.inner.output_buffer.clear();
162        self.f_squeeze_buffer.clear();
163        self.f_buffer.push(value);
164        if self.f_buffer.len() == self.absorb_num_f_elms() * RATE {
165            self.flush_f_if_non_empty();
166        }
167    }
168}
169
170impl<F, PF, const N: usize, P, const WIDTH: usize, const RATE: usize> CanObserve<[F; N]>
171    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
172where
173    F: PrimeField32,
174    PF: PrimeField,
175    P: CryptographicPermutation<[PF; WIDTH]>,
176{
177    fn observe(&mut self, values: [F; N]) {
178        for value in values {
179            self.observe(value);
180        }
181    }
182}
183
184impl<F, PF, const N: usize, P, const WIDTH: usize, const RATE: usize> CanObserve<Hash<F, PF, N>>
185    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
186where
187    F: PrimeField32,
188    PF: PrimeField,
189    P: CryptographicPermutation<[PF; WIDTH]>,
190{
191    fn observe(&mut self, values: Hash<F, PF, N>) {
192        self.inner.output_buffer.clear();
193        self.f_squeeze_buffer.clear();
194        self.flush_f_if_non_empty();
195
196        let words: &[PF; N] = values.as_ref();
197
198        for chunk in words.chunks(RATE) {
199            // Invariant: each block holds at most RATE words, bounded at 255 by the constructor.
200            let tag = u8::try_from(chunk.len()).expect("absorb length tag must fit in a u8");
201            self.inner.absorb_rate_padded_with_tag(chunk, tag);
202            self.f_squeeze_buffer.clear();
203        }
204    }
205}
206
207impl<F, PF, const N: usize, P, const WIDTH: usize, const RATE: usize>
208    CanObserve<&MerkleCap<F, [PF; N]>> for MultiField32Challenger<F, PF, P, WIDTH, RATE>
209where
210    F: PrimeField32,
211    PF: PrimeField,
212    P: CryptographicPermutation<[PF; WIDTH]>,
213{
214    fn observe(&mut self, cap: &MerkleCap<F, [PF; N]>) {
215        for digest in cap.roots() {
216            self.observe(Hash::<F, PF, N>::from(*digest));
217        }
218    }
219}
220
221impl<F, PF, const N: usize, P, const WIDTH: usize, const RATE: usize>
222    CanObserve<MerkleCap<F, [PF; N]>> for MultiField32Challenger<F, PF, P, WIDTH, RATE>
223where
224    F: PrimeField32,
225    PF: PrimeField,
226    P: CryptographicPermutation<[PF; WIDTH]>,
227{
228    fn observe(&mut self, cap: MerkleCap<F, [PF; N]>) {
229        self.observe(&cap);
230    }
231}
232
233impl<F, PF, P, const WIDTH: usize, const RATE: usize> CanObserve<Vec<Vec<F>>>
234    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
235where
236    F: PrimeField32,
237    PF: PrimeField,
238    P: CryptographicPermutation<[PF; WIDTH]>,
239{
240    fn observe(&mut self, valuess: Vec<Vec<F>>) {
241        for values in valuess {
242            for value in values {
243                self.observe(value);
244            }
245        }
246    }
247}
248
249impl<F, EF, PF, P, const WIDTH: usize, const RATE: usize> CanSample<EF>
250    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
251where
252    F: PrimeField32,
253    EF: BasedVectorSpace<F>,
254    PF: PrimeField,
255    P: CryptographicPermutation<[PF; WIDTH]>,
256{
257    fn sample(&mut self) -> EF {
258        EF::from_basis_coefficients_fn(|_| {
259            self.flush_f_if_non_empty();
260            if self.f_squeeze_buffer.is_empty() {
261                if !self.inner.input_buffer.is_empty() || self.inner.output_buffer.is_empty() {
262                    self.inner.duplexing();
263                }
264                self.refill_f_squeeze_from_inner();
265            }
266            self.f_squeeze_buffer
267                .pop()
268                .expect("Output buffer should be non-empty")
269        })
270    }
271}
272
273impl<F, PF, P, const WIDTH: usize, const RATE: usize> CanSampleBits<usize>
274    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
275where
276    F: PrimeField32,
277    PF: PrimeField,
278    P: CryptographicPermutation<[PF; WIDTH]>,
279{
280    /// The sampled bits are not perfectly uniform, but we can bound the error: every sequence
281    /// appears with probability 1/p-close to uniform (1/2^b).
282    ///
283    /// Proof:
284    /// We denote p = F::ORDER_U32, and b = `bits`.
285    /// If X follows a uniform distribution over F, if we consider the first b bits of X, each
286    /// sequence appears either with probability P1 = ⌊p / 2^b⌋ / p or P2 = (1 + ⌊p / 2^b⌋) / p.
287    /// We have 1/2^b - 1/p ≤ P1, P2 ≤ 1/2^b + 1/p
288    fn sample_bits(&mut self, bits: usize) -> usize {
289        assert!(bits < (usize::BITS as usize));
290        // Evaluate the bound in `u64` to keep the shift within its type width.
291        // A `u32` shift by `bits >= 32` would wrap and zero the mask, accepting any witness.
292        assert!(
293            (1u64 << bits) < F::ORDER_U64,
294            "requested bit count must fit within the field order"
295        );
296        let rand_f: F = self.sample();
297        let rand_usize = rand_f.as_canonical_u32() as usize;
298        rand_usize & ((1 << bits) - 1)
299    }
300}
301
302impl<F, PF, P, const WIDTH: usize, const RATE: usize> MultiField32Challenger<F, PF, P, WIDTH, RATE>
303where
304    F: PrimeField32,
305    PF: PrimeField,
306    P: CryptographicPermutation<[PF; WIDTH]>,
307{
308    /// Build the proof-of-work acceptance predicate used by
309    /// [`GrindingChallenger::grind`](crate::GrindingChallenger::grind).
310    ///
311    /// The predicate replays `check_witness` = `observe(witness)` + `sample_bits(bits)`
312    /// on a stack copy of the sponge state:
313    ///
314    /// ```text
315    ///     pending scalars | witness  ->  packed rate slots, zero tail, length tag
316    ///                                ->  permute
317    ///                                ->  last limb of last rate cell
318    ///                                ->  accept iff low `bits` are zero
319    /// ```
320    ///
321    /// Everything except the witness digit is candidate-independent and computed once.
322    /// Each call then costs one state copy, one digit write, one permutation,
323    /// and one limb split — no clone, no heap allocation.
324    pub(crate) fn pow_check_fn(&self, bits: usize) -> impl Fn(u32) -> bool + Sync + '_ {
325        let rb = self.absorb_radix_bits();
326        let absorb_n = self.absorb_num_f_elms();
327        let squeeze_n = self.squeeze_num_f_elms();
328
329        // The witness joins the pending scalars at the next free position.
330        let n_pending = self.f_buffer.len();
331        // Invariant: the constructor bounds a full flush at 255 scalars, so this never truncates.
332        let tag = u8::try_from(n_pending + 1).expect("absorb length tag must fit in a u8");
333
334        // Packed-slot coordinates of the witness digit.
335        let chunk_idx = n_pending / absorb_n;
336        let pos_in_chunk = n_pending % absorb_n;
337
338        // The packing is little-endian Horner: digit `j` of a chunk weighs `2^(rb * j)`.
339        let shift = PF::from_u64(1u64 << rb).exp_u64(pos_in_chunk as u64);
340
341        // Digits below the witness in its own chunk.
342        let const_tail: PF = reduce_packed(&self.f_buffer[chunk_idx * absorb_n..], rb);
343
344        // Candidate-independent pre-permutation state.
345        // Mirrors `absorb_rate_padded_with_tag(packed_chunks, tag)` on the inner sponge.
346        let base_state: [PF; WIDTH] = array::from_fn(|i| {
347            if i < chunk_idx {
348                // Full constant chunks fill the leading rate slots.
349                reduce_packed(&self.f_buffer[i * absorb_n..(i + 1) * absorb_n], rb)
350            } else if i < RATE {
351                // The witness chunk (rebuilt per candidate) and the unused rate slots are zeroed.
352                PF::ZERO
353            } else if i == RATE {
354                // The first capacity element carries the length tag.
355                self.inner.sponge_state[RATE] + PF::from_u8(tag)
356            } else {
357                // The rest of the capacity carries forward unchanged.
358                self.inner.sponge_state[i]
359            }
360        });
361
362        // Accept when the low `bits` of the checked challenge are zero.
363        let mask = (1u64 << bits) - 1;
364
365        move |candidate| {
366            // One stack copy, one digit write, one permutation per candidate.
367            let mut state = base_state;
368            state[chunk_idx] = const_tail + shift * PF::from_u32(candidate);
369            self.inner.permutation.permute_mut(&mut state);
370
371            // `sample_bits` pops the last limb split from the last rate cell.
372            let limbs = split_pf_to_field_order_limbs::<PF, F>(state[RATE - 1], squeeze_n);
373            (u64::from(limbs[squeeze_n - 1].as_canonical_u32()) & mask) == 0
374        }
375    }
376}
377
378impl<F, PF, P, const WIDTH: usize, const RATE: usize> CanFinalizeDigest
379    for MultiField32Challenger<F, PF, P, WIDTH, RATE>
380where
381    F: PrimeField32,
382    PF: PrimeField,
383    P: CryptographicPermutation<[PF; WIDTH]>,
384{
385    type Digest = [PF; RATE];
386
387    fn finalize(mut self) -> [PF; RATE] {
388        // Match the previous single `duplexing()` in `finalize`: if there was pending `F`, the
389        // absorb+permute happens in `flush_f_if_non_empty` only; otherwise run one empty absorb
390        // round (permute), like `duplexing` with `n_in == 0`.
391        let had_pending_f = !self.f_buffer.is_empty();
392        self.flush_f_if_non_empty();
393        if !had_pending_f {
394            self.inner.duplexing();
395        }
396        self.inner.sponge_state[..RATE].try_into().unwrap()
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use p3_baby_bear::BabyBear;
403    use p3_field::{
404        Field, PrimeCharacteristicRing, PrimeField, injective_pack_bits, split_pf_to_packed_limbs,
405        squeeze_field_order_num_limbs,
406    };
407    use p3_goldilocks::Goldilocks;
408    use p3_symmetric::Permutation;
409    use proptest::prelude::*;
410
411    use super::*;
412    use crate::grinding_challenger::GrindingChallenger;
413
414    const WIDTH: usize = 8;
415    const RATE: usize = 4;
416
417    type F = BabyBear;
418    type PF = Goldilocks;
419
420    #[derive(Clone)]
421    struct TestPermutation;
422
423    impl Permutation<[PF; WIDTH]> for TestPermutation {
424        fn permute_mut(&self, input: &mut [PF; WIDTH]) {
425            for (i, val) in input.iter_mut().enumerate() {
426                *val = PF::from_u8((i + 1) as u8);
427            }
428        }
429    }
430
431    impl CryptographicPermutation<[PF; WIDTH]> for TestPermutation {}
432
433    /// A permutation where each output depends on all inputs, suitable for
434    /// tests that need to detect state changes (e.g. finalize).
435    #[derive(Clone)]
436    struct MixingPermutation;
437
438    impl Permutation<[PF; WIDTH]> for MixingPermutation {
439        fn permute_mut(&self, input: &mut [PF; WIDTH]) {
440            let sum: PF = input.iter().copied().sum();
441            for (i, val) in input.iter_mut().enumerate() {
442                *val = sum + PF::from_u8((i + 1) as u8);
443            }
444        }
445    }
446
447    impl CryptographicPermutation<[PF; WIDTH]> for MixingPermutation {}
448
449    /// A no-op permutation generic over the state width.
450    /// Lets tests instantiate challengers at widths the fixed-width permutations cannot reach.
451    #[derive(Clone)]
452    struct WideIdentityPermutation;
453
454    impl<const W: usize> Permutation<[PF; W]> for WideIdentityPermutation {
455        fn permute_mut(&self, _input: &mut [PF; W]) {}
456    }
457
458    impl<const W: usize> CryptographicPermutation<[PF; W]> for WideIdentityPermutation {}
459
460    #[test]
461    fn test_new_rejects_length_tag_overflow() {
462        // The capacity length tag is a single byte stamped per padded absorb.
463        // A full flush absorbs up to limbs-per-slot * RATE scalars at once.
464        //
465        // Fixture state: BabyBear packs 2 limbs per Goldilocks rate slot.
466        assert_eq!(max_absorb_injective_limbs::<F, PF>(), 2);
467
468        // Mutation: push RATE past the byte boundary.
469        //
470        //     RATE = 128 → 2 * 128 = 256 > 255 → reject
471        //     RATE = 127 → 2 * 127 = 254 ≤ 255 → accept
472        let too_wide = MultiField32Challenger::<F, PF, _, 129, 128>::new(WideIdentityPermutation);
473        assert_eq!(
474            too_wide.err().as_deref(),
475            Some(
476                "absorb length tag must fit in a u8: max_absorb_injective_limbs * RATE must be at most 255"
477            )
478        );
479
480        let in_range = MultiField32Challenger::<F, PF, _, 128, 127>::new(WideIdentityPermutation);
481        assert!(in_range.is_ok());
482    }
483
484    #[test]
485    fn test_packing() {
486        let c = MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
487        assert_eq!(c.absorb_radix_bits(), 31);
488        assert_eq!(c.absorb_num_f_elms(), 2);
489        assert_eq!(c.squeeze_num_f_elms(), 1);
490        assert_eq!(squeeze_field_order_num_limbs::<PF, F>(), 1);
491    }
492
493    #[test]
494    fn test_output_buffer_excludes_capacity() {
495        let permutation = TestPermutation;
496        let mut challenger =
497            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(permutation).unwrap();
498
499        let squeeze_n = challenger.squeeze_num_f_elms();
500
501        let _: F = challenger.sample();
502
503        let expected_output_size = RATE * squeeze_n;
504
505        assert_eq!(
506            challenger.pending_f_squeeze_len(),
507            expected_output_size - 1,
508            "Pending F squeeze should be RATE * squeeze_num_f_elms minus one sample"
509        );
510        assert_eq!(
511            challenger.inner.output_buffer.len(),
512            0,
513            "After refill, inner PF output buffer is drained like popped F outputs"
514        );
515    }
516
517    #[test]
518    fn test_finalize() {
519        let new_chal =
520            || MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
521
522        // Deterministic: same observations produce same digest.
523        let mut c1 = new_chal();
524        let mut c2 = new_chal();
525        for i in 0..5u8 {
526            c1.observe(F::from_u8(i));
527            c2.observe(F::from_u8(i));
528        }
529        assert_eq!(c1.finalize(), c2.finalize());
530
531        // Different observations produce different digests.
532        let mut c1 = new_chal();
533        let mut c2 = new_chal();
534        for i in 0..5u8 {
535            c1.observe(F::from_u8(i));
536            c2.observe(F::from_u8(i + 1));
537        }
538        assert_ne!(c1.finalize(), c2.finalize());
539    }
540
541    /// Document how sampling interacts with finalize.
542    ///
543    /// Same principle as DuplexChallenger: sampling only pops from the
544    /// output buffer without modifying sponge state. The digest changes
545    /// when a sample triggers a new duplexing. Each duplexing produces
546    /// `num_f_elms * RATE` output elements (here 1 * 4 = 4 BabyBear
547    /// elements for Goldilocks/BabyBear), so the digest is stable within
548    /// each batch of that many samples.
549    #[test]
550    fn test_finalize_sample_interaction() {
551        let batch_size = {
552            let c =
553                MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
554            c.squeeze_num_f_elms() * RATE
555        };
556
557        let digest = |n_samples: usize| {
558            let mut c =
559                MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
560            for i in 0..3u8 {
561                c.observe(F::from_u8(i));
562            }
563            for _ in 0..n_samples {
564                let _: F = c.sample();
565            }
566            c.finalize()
567        };
568
569        // The first sample triggers duplexing (absorbs pending input),
570        // so finalize's duplexing is an extra permutation — different digest.
571        assert_ne!(digest(0), digest(1));
572
573        // Samples within the same batch don't trigger another duplexing.
574        assert_eq!(digest(1), digest(2));
575        assert_eq!(digest(1), digest(batch_size));
576
577        // Exhausting the output buffer triggers a fresh duplexing.
578        assert_ne!(digest(batch_size), digest(batch_size + 1));
579
580        // Stable within the second batch.
581        assert_eq!(digest(batch_size + 1), digest(batch_size + 2));
582    }
583
584    #[test]
585    fn test_partial_absorb_length_distinct_from_padded_equivalent() {
586        let ne = {
587            let c =
588                MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
589            c.absorb_num_f_elms()
590        };
591        assert_eq!(ne, 2);
592
593        let mut a =
594            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
595        a.observe(F::ONE);
596
597        let mut b =
598            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
599        b.observe(F::ONE);
600        for _ in 1..ne {
601            b.observe(F::ZERO);
602        }
603
604        assert_ne!(a.finalize(), b.finalize());
605    }
606
607    #[test]
608    fn test_absorb_no_radix_overflow_collision() {
609        let mut a =
610            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
611        a.observe(F::from_u32(1 << 30));
612        a.observe(F::ZERO);
613
614        let mut b =
615            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
616        b.observe(F::ZERO);
617        b.observe(F::ONE);
618
619        assert_ne!(a.finalize(), b.finalize());
620    }
621
622    #[test]
623    fn test_duplexing_respects_rate() {
624        let permutation = TestPermutation;
625        let mut challenger =
626            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(permutation).unwrap();
627
628        let absorb_n = challenger.absorb_num_f_elms();
629
630        for i in 0..(absorb_n * RATE) {
631            challenger.observe(F::from_u8(i as u8));
632        }
633
634        assert_eq!(
635            challenger.inner.output_buffer.len(),
636            RATE,
637            "After a full F batch flush, inner holds one rate row of PF elements"
638        );
639        assert_eq!(
640            challenger.pending_f_squeeze_len(),
641            0,
642            "F limbs are produced on sample() via split_pf_to_packed_limbs, not on observe"
643        );
644    }
645
646    #[test]
647    fn test_squeeze_covers_full_f_range() {
648        // With base-2^30, challenges are confined to [0, 2^30) ≈ 50% of BabyBear.
649        // With base-|F|, the c0 limb = v mod p_BB is near-uniform over all of BabyBear.
650        // Verify that values above 2^30 can appear as challenges by constructing a Goldilocks
651        // rate output whose canonical form mod p_BB exceeds 2^30.
652        //
653        // injective_pack_bits::<BabyBear>() = 30, so [2^30, p_BB) was previously unreachable.
654        use p3_field::split_pf_to_field_order_limbs;
655        let pack_bits = injective_pack_bits::<F>();
656        let threshold = 1u32 << pack_bits; // 2^30
657
658        // Build a Goldilocks value v such that v mod p_BB > 2^30.
659        // p_BB + 2^30 < Goldilocks::ORDER (since p_BB ≈ 2^30.9 and p_GL ≈ 2^64),
660        // so v = p_BB + threshold + 1 is a valid small Goldilocks element.
661        let v_raw = F::ORDER_U32 as u64 + threshold as u64 + 1;
662        let pf_val = PF::from_u64(v_raw);
663        let limbs = split_pf_to_field_order_limbs::<PF, F>(pf_val, 1);
664        // c0 = v_raw mod p_BB = threshold + 1 (since v_raw = p_BB + threshold + 1 ≡ threshold + 1).
665        assert_eq!(limbs[0].as_canonical_u32(), threshold + 1);
666        assert!(
667            limbs[0].as_canonical_u32() > threshold,
668            "c0 must exceed the old base-2^30 ceiling"
669        );
670    }
671
672    #[test]
673    fn test_observe_hash_native_pf_high_bits_distinct() {
674        use num_bigint::BigUint;
675        use p3_bn254::Bn254;
676        use p3_field::split_pf_to_packed_limbs;
677        use p3_symmetric::Hash;
678
679        type PF254 = Bn254;
680
681        #[derive(Clone)]
682        struct Bn254MixingPermutation;
683
684        impl Permutation<[PF254; WIDTH]> for Bn254MixingPermutation {
685            fn permute_mut(&self, input: &mut [PF254; WIDTH]) {
686                let sum: PF254 = input.iter().copied().sum();
687                for (i, val) in input.iter_mut().enumerate() {
688                    *val = sum + PF254::from_u8((i + 1) as u8);
689                }
690            }
691        }
692
693        impl CryptographicPermutation<[PF254; WIDTH]> for Bn254MixingPermutation {}
694
695        let pack_bits = injective_pack_bits::<F>();
696        let observe_n = PF254::bits().div_ceil(pack_bits as usize);
697
698        let a = PF254::from_biguint(BigUint::from(1u32)).unwrap();
699        let b = PF254::from_biguint(BigUint::from(1u32) + (BigUint::from(1u32) << 200)).unwrap();
700        assert_ne!(a, b);
701
702        let digest = |h: PF254| {
703            let mut c =
704                MultiField32Challenger::<F, PF254, _, WIDTH, RATE>::new(Bn254MixingPermutation)
705                    .unwrap();
706            c.observe(Hash::<F, PF254, 1>::from([h]));
707            c.finalize()
708        };
709
710        assert_ne!(digest(a), digest(b));
711
712        let limbs_a = split_pf_to_packed_limbs::<PF254, F>(a, observe_n, pack_bits);
713        let limbs_b = split_pf_to_packed_limbs::<PF254, F>(b, observe_n, pack_bits);
714        assert_ne!(limbs_a, limbs_b);
715
716        let d_a = a.as_canonical_biguint().to_u64_digits();
717        let d_b = b.as_canonical_biguint().to_u64_digits();
718        let take3 = |d: &[u64]| {
719            let mut v = [0u64; 3];
720            for (i, x) in d.iter().take(3).enumerate() {
721                v[i] = *x;
722            }
723            v
724        };
725        assert_eq!(take3(&d_a), take3(&d_b));
726    }
727
728    #[test]
729    fn test_observe_hash_native_vs_expanded_f_not_equal() {
730        use p3_symmetric::Hash;
731
732        let g = PF::from_u64(123456789);
733        let mut native =
734            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
735        native.observe(Hash::<F, PF, 1>::from([g]));
736
737        let mut via_f =
738            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
739        let pb = injective_pack_bits::<F>();
740        let n = PF::bits().div_ceil(pb as usize);
741        for f in split_pf_to_packed_limbs::<PF, F>(g, n, pb) {
742            via_f.observe(f);
743        }
744
745        assert_ne!(native.finalize(), via_f.finalize());
746    }
747
748    #[test]
749    fn test_inner_sponge_matches_manual_absorb_chain() {
750        let mut m =
751            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
752        for i in 0..8u8 {
753            m.observe(F::from_u8(i));
754        }
755        let d_m = m.inner.sponge_state;
756
757        let mut inner = DuplexChallenger::<PF, _, WIDTH, RATE>::new(MixingPermutation);
758        let packed: Vec<PF> = (0..8)
759            .step_by(2)
760            .map(|j| {
761                reduce_packed::<F, PF>(
762                    &[F::from_u8(j), F::from_u8(j + 1)],
763                    absorb_radix_bits::<F>(),
764                )
765            })
766            .collect();
767        inner.absorb_rate_padded_with_tag(&packed, 8);
768        assert_eq!(d_m, inner.sponge_state);
769    }
770
771    #[test]
772    fn test_grind_zero_bits_returns_zero() {
773        // bits == 0: must short-circuit to ZERO without touching state.
774        let mut challenger =
775            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
776
777        // Snapshot to detect any transcript mutation.
778        let before = challenger.clone();
779
780        let witness = challenger.grind(0);
781
782        assert_eq!(witness, F::ZERO);
783        assert_eq!(challenger.inner.input_buffer, before.inner.input_buffer);
784        assert_eq!(challenger.inner.output_buffer, before.inner.output_buffer);
785        assert_eq!(challenger.inner.sponge_state, before.inner.sponge_state);
786    }
787
788    #[test]
789    #[should_panic = "requested bit count must fit within the field order"]
790    fn test_sample_bits_rejects_oversized_request() {
791        // Sampled field is BabyBear, order ~2^30.9, so a 32-bit request must be rejected.
792        // The bound is evaluated in u64, so the guard fires in every build profile.
793        let mut challenger =
794            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
795        let _ = challenger.sample_bits(32);
796    }
797
798    #[test]
799    #[should_panic = "requested bit count must fit within the field order"]
800    fn test_grind_rejects_oversized_request() {
801        // Same guard, reached through the proof-of-work entry point.
802        let mut challenger =
803            MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
804        let _ = challenger.grind(32);
805    }
806
807    #[test]
808    fn test_grind_advances_state_like_direct_check() {
809        // The fast grind precomputes the pre-permutation state once per search.
810        // Its result and final transcript must match a direct `check_witness`.
811        //
812        // Pending counts cover every packing geometry (absorb_n = 2, RATE = 4):
813        //
814        //     0  ->  witness opens chunk 0          (chunk_idx 0, pos 0)
815        //     1  ->  witness completes chunk 0      (chunk_idx 0, pos 1)
816        //     4  ->  witness opens chunk 2          (chunk_idx 2, pos 0)
817        //     7  ->  witness fills the whole batch  (flush fires inside observe)
818        for pending in [0usize, 1, 4, 7] {
819            let mut challenger =
820                MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
821            for i in 0..pending {
822                challenger.observe(F::from_u8(i as u8));
823            }
824
825            // Shadow challenger consumes the witness through the plain verifier path.
826            let mut direct = challenger.clone();
827
828            let bits = 2;
829            let witness = challenger.grind(bits);
830
831            // The witness validates, and both transcripts land on the same state.
832            assert!(direct.check_witness(bits, witness), "pending = {pending}");
833            assert_eq!(challenger.inner.sponge_state, direct.inner.sponge_state);
834            assert_eq!(challenger.inner.input_buffer, direct.inner.input_buffer);
835            assert_eq!(challenger.inner.output_buffer, direct.inner.output_buffer);
836            assert_eq!(challenger.f_buffer, direct.f_buffer);
837            assert_eq!(challenger.f_squeeze_buffer, direct.f_squeeze_buffer);
838        }
839    }
840
841    #[test]
842    fn test_grind_advances_state_like_direct_check_bn254() {
843        // Bn254 splits each squeezed cell into several BabyBear limbs,
844        // so this pins the limb-extraction path the Goldilocks fixture cannot reach.
845        use p3_bn254::Bn254;
846
847        type PF254 = Bn254;
848
849        #[derive(Clone)]
850        struct Bn254MixingPermutation;
851
852        impl Permutation<[PF254; WIDTH]> for Bn254MixingPermutation {
853            fn permute_mut(&self, input: &mut [PF254; WIDTH]) {
854                let sum: PF254 = input.iter().copied().sum();
855                for (i, val) in input.iter_mut().enumerate() {
856                    *val = sum + PF254::from_u8((i + 1) as u8);
857                }
858            }
859        }
860
861        impl CryptographicPermutation<[PF254; WIDTH]> for Bn254MixingPermutation {}
862
863        let mut challenger =
864            MultiField32Challenger::<F, PF254, _, WIDTH, RATE>::new(Bn254MixingPermutation)
865                .unwrap();
866        assert!(challenger.squeeze_num_f_elms() > 1);
867        for i in 0..3u8 {
868            challenger.observe(F::from_u8(i));
869        }
870
871        let mut direct = challenger.clone();
872
873        let bits = 2;
874        let witness = challenger.grind(bits);
875
876        assert!(direct.check_witness(bits, witness));
877        assert_eq!(challenger.inner.sponge_state, direct.inner.sponge_state);
878        assert_eq!(challenger.f_buffer, direct.f_buffer);
879        assert_eq!(challenger.f_squeeze_buffer, direct.f_squeeze_buffer);
880    }
881
882    proptest! {
883        #[test]
884        fn prop_pow_check_matches_check_witness(
885            pending_values in prop::collection::vec(0u32..F::ORDER_U32, 0..8),
886            candidate in 0u32..F::ORDER_U32,
887            bits in 1usize..8,
888        ) {
889            // Specialization-vs-reference pin:
890            // the precomputed-state predicate must agree with the plain
891            // `observe + sample_bits` verifier path on every candidate.
892            let mut challenger =
893                MultiField32Challenger::<F, PF, _, WIDTH, RATE>::new(MixingPermutation).unwrap();
894            for &v in &pending_values {
895                challenger.observe(F::from_u32(v));
896            }
897
898            let fast = challenger.pow_check_fn(bits)(candidate);
899
900            // Reference path on a clone; `candidate` is canonical by the range above.
901            let witness = F::from_u32(candidate);
902            let reference = challenger.clone().check_witness(bits, witness);
903
904            prop_assert_eq!(fast, reference);
905        }
906    }
907}