Skip to main content

p3_challenger/
duplex_challenger.rs

1use alloc::vec;
2use alloc::vec::Vec;
3use core::error::Error;
4use core::fmt::{Display, Formatter};
5
6use p3_field::{BasedVectorSpace, Field, PrimeField, PrimeField64};
7use p3_monty_31::{MontyField31, MontyParameters};
8use p3_symmetric::{CryptographicPermutation, Hash, MerkleCap};
9
10use crate::{
11    CanFinalizeDigest, CanObserve, CanSample, CanSampleBits, CanSampleUniformBits, FieldChallenger,
12};
13
14/// A generic duplex sponge challenger over a finite field, used for generating deterministic
15/// challenges from absorbed inputs.
16///
17/// This structure implements a duplex sponge that alternates between:
18/// - Absorbing inputs into the sponge state,
19/// - Applying a cryptographic permutation over the state,
20/// - Squeezing outputs from the state as challenges.
21///
22/// The sponge operates over a state of `WIDTH` elements, divided into:
23/// - A rate of `RATE` elements (the portion exposed to input/output),
24/// - A capacity of `WIDTH - RATE` elements (the hidden part ensuring security).
25///
26/// The challenger buffers observed inputs until the rate is full, applies the permutation,
27/// and then produces challenge outputs from the permuted state. It supports:
28/// - Observing single values, arrays, hashes, or nested vectors,
29/// - Sampling fresh challenges as field elements or bitstrings.
30#[derive(Clone, Debug)]
31pub struct DuplexChallenger<F, P, const WIDTH: usize, const RATE: usize>
32where
33    F: Clone,
34    P: CryptographicPermutation<[F; WIDTH]>,
35{
36    /// The internal sponge state, consisting of `WIDTH` field elements.
37    ///
38    /// The first `RATE` elements form the rate section, where input values are absorbed
39    /// and output values are squeezed.
40    /// The remaining `WIDTH - RATE` elements form the capacity, which provides hidden
41    /// entropy and security against attacks.
42    pub sponge_state: [F; WIDTH],
43
44    /// A buffer holding field elements that have been observed but not yet absorbed.
45    ///
46    /// Inputs added via `observe` are collected here.
47    /// Once the buffer reaches `RATE` elements, the sponge performs a duplexing step:
48    /// it absorbs the inputs into the state and applies the permutation.
49    pub input_buffer: Vec<F>,
50
51    /// A buffer holding field elements that have been squeezed from the sponge state.
52    ///
53    /// Outputs are produced by `duplexing` and stored here.
54    /// Calls to `sample` or `sample_bits` pop values from this buffer.
55    /// When the buffer is empty (or new inputs were absorbed), a new duplexing step is triggered.
56    pub output_buffer: Vec<F>,
57
58    /// The cryptographic permutation applied to the sponge state.
59    ///
60    /// This permutation must provide strong pseudorandomness and collision resistance,
61    /// ensuring that squeezed outputs are indistinguishable from random and securely
62    /// bound to the absorbed inputs.
63    pub permutation: P,
64}
65
66impl<F, P, const WIDTH: usize, const RATE: usize> DuplexChallenger<F, P, WIDTH, RATE>
67where
68    F: Copy,
69    P: CryptographicPermutation<[F; WIDTH]>,
70{
71    pub fn new(permutation: P) -> Self
72    where
73        F: Default,
74    {
75        const {
76            assert!(RATE > 0 && RATE < WIDTH);
77        }
78        Self {
79            sponge_state: [F::default(); WIDTH],
80            input_buffer: vec![],
81            output_buffer: vec![],
82            permutation,
83        }
84    }
85
86    pub(crate) fn duplexing(&mut self) {
87        assert!(self.input_buffer.len() <= RATE);
88
89        // Overwrite the first r elements with the inputs.
90        for (i, val) in self.input_buffer.drain(..).enumerate() {
91            self.sponge_state[i] = val;
92        }
93
94        // Apply the permutation.
95        self.permutation.permute_mut(&mut self.sponge_state);
96
97        self.output_buffer.clear();
98        self.output_buffer.extend(&self.sponge_state[..RATE]);
99    }
100}
101
102impl<F, P, const WIDTH: usize, const RATE: usize> DuplexChallenger<F, P, WIDTH, RATE>
103where
104    F: Copy + Default + PrimeField,
105    P: CryptographicPermutation<[F; WIDTH]>,
106{
107    /// Absorb `values.len()` elements into the rate, zero remaining rate slots, add `length_tag`
108    /// to `sponge_state[RATE]`, permute, and refill `output_buffer`.
109    ///
110    /// Clears `input_buffer` and `output_buffer` before absorbing. Used by
111    /// [`MultiField32Challenger`](crate::MultiField32Challenger) so packed scalar and native-digest
112    /// absorbs share this [`DuplexChallenger`]'s sponge state without queuing through `observe`.
113    pub fn absorb_rate_padded_with_tag(&mut self, values: &[F], length_tag: u8) {
114        const {
115            assert!(
116                RATE < WIDTH,
117                "RATE must be less than WIDTH for capacity length slot"
118            );
119        }
120        assert!(values.len() <= RATE);
121        self.input_buffer.clear();
122        self.output_buffer.clear();
123        for (i, &v) in values.iter().enumerate() {
124            self.sponge_state[i] = v;
125        }
126        self.sponge_state[values.len()..RATE].fill(F::ZERO);
127        self.sponge_state[RATE] += F::from_u8(length_tag);
128        self.permutation.permute_mut(&mut self.sponge_state);
129        self.output_buffer
130            .extend_from_slice(&self.sponge_state[..RATE]);
131    }
132}
133
134impl<F, P, const WIDTH: usize, const RATE: usize> FieldChallenger<F>
135    for DuplexChallenger<F, P, WIDTH, RATE>
136where
137    F: PrimeField64,
138    P: CryptographicPermutation<[F; WIDTH]>,
139{
140}
141
142impl<F, P, const WIDTH: usize, const RATE: usize> CanObserve<F>
143    for DuplexChallenger<F, P, WIDTH, RATE>
144where
145    F: Copy,
146    P: CryptographicPermutation<[F; WIDTH]>,
147{
148    fn observe(&mut self, value: F) {
149        // Any buffered output is now invalid.
150        self.output_buffer.clear();
151
152        self.input_buffer.push(value);
153
154        if self.input_buffer.len() == RATE {
155            self.duplexing();
156        }
157    }
158}
159
160impl<F, P, const N: usize, const WIDTH: usize, const RATE: usize> CanObserve<[F; N]>
161    for DuplexChallenger<F, P, WIDTH, RATE>
162where
163    F: Copy,
164    P: CryptographicPermutation<[F; WIDTH]>,
165{
166    fn observe(&mut self, values: [F; N]) {
167        for value in values {
168            self.observe(value);
169        }
170    }
171}
172
173impl<F, P, const N: usize, const WIDTH: usize, const RATE: usize> CanObserve<Hash<F, F, N>>
174    for DuplexChallenger<F, P, WIDTH, RATE>
175where
176    F: Copy,
177    P: CryptographicPermutation<[F; WIDTH]>,
178{
179    fn observe(&mut self, values: Hash<F, F, N>) {
180        for value in values {
181            self.observe(value);
182        }
183    }
184}
185
186impl<F, P, const N: usize, const WIDTH: usize, const RATE: usize> CanObserve<&MerkleCap<F, [F; N]>>
187    for DuplexChallenger<F, P, WIDTH, RATE>
188where
189    F: Copy,
190    P: CryptographicPermutation<[F; WIDTH]>,
191{
192    fn observe(&mut self, cap: &MerkleCap<F, [F; N]>) {
193        for digest in cap.roots() {
194            for value in digest {
195                self.observe(*value);
196            }
197        }
198    }
199}
200
201impl<F, P, const N: usize, const WIDTH: usize, const RATE: usize> CanObserve<MerkleCap<F, [F; N]>>
202    for DuplexChallenger<F, P, WIDTH, RATE>
203where
204    F: Copy,
205    P: CryptographicPermutation<[F; WIDTH]>,
206{
207    fn observe(&mut self, cap: MerkleCap<F, [F; N]>) {
208        self.observe(&cap);
209    }
210}
211
212// for TrivialPcs
213impl<F, P, const WIDTH: usize, const RATE: usize> CanObserve<Vec<Vec<F>>>
214    for DuplexChallenger<F, P, WIDTH, RATE>
215where
216    F: Copy,
217    P: CryptographicPermutation<[F; WIDTH]>,
218{
219    fn observe(&mut self, valuess: Vec<Vec<F>>) {
220        for values in valuess {
221            for value in values {
222                self.observe(value);
223            }
224        }
225    }
226}
227
228impl<F, EF, P, const WIDTH: usize, const RATE: usize> CanSample<EF>
229    for DuplexChallenger<F, P, WIDTH, RATE>
230where
231    F: Field,
232    EF: BasedVectorSpace<F>,
233    P: CryptographicPermutation<[F; WIDTH]>,
234{
235    fn sample(&mut self) -> EF {
236        EF::from_basis_coefficients_fn(|_| {
237            // If we have buffered inputs, we must perform a duplexing so that the challenge will
238            // reflect them. Or if we've run out of outputs, we must perform a duplexing to get more.
239            if !self.input_buffer.is_empty() || self.output_buffer.is_empty() {
240                self.duplexing();
241            }
242
243            self.output_buffer
244                .pop()
245                .expect("Output buffer should be non-empty")
246        })
247    }
248}
249
250impl<F, P, const WIDTH: usize, const RATE: usize> CanSampleBits<usize>
251    for DuplexChallenger<F, P, WIDTH, RATE>
252where
253    F: PrimeField64,
254    P: CryptographicPermutation<[F; WIDTH]>,
255{
256    /// The sampled bits are not perfectly uniform, but we can bound the error: every sequence
257    /// appears with probability 1/p-close to uniform (1/2^b).
258    ///
259    /// Proof:
260    /// We denote p = F::ORDER_U64, and b = `bits`.
261    /// If X follows a uniform distribution over F, if we consider the first b bits of X, each
262    /// sequence appears either with probability P1 = ⌊p / 2^b⌋ / p or P2 = (1 + ⌊p / 2^b⌋) / p.
263    /// We have 1/2^b - 1/p ≤ P1, P2 ≤ 1/2^b + 1/p
264    fn sample_bits(&mut self, bits: usize) -> usize {
265        assert!(bits < (usize::BITS as usize));
266        assert!((1 << bits) < F::ORDER_U64);
267        let rand_f: F = self.sample();
268        let rand_usize = rand_f.as_canonical_u64() as usize;
269        rand_usize & ((1 << bits) - 1)
270    }
271}
272
273/// Trait for fields that support uniform bit sampling optimizations
274pub trait UniformSamplingField {
275    /// Maximum number of bits we can sample at negligible (~1/field prime) probability of
276    /// triggering an error / requiring a resample.
277    const MAX_SINGLE_SAMPLE_BITS: usize;
278    /// An array storing the largest value `m_k` for each `k` in [0, 31], such that `m_k`
279    /// is a multiple of `2^k` and less than P. `m_k` is defined as:
280    ///
281    /// \( m_k = ⌊P / 2^k⌋ · 2^k \)
282    ///
283    /// This is used as a rejection sampling threshold (or error trigger), when sampling
284    /// random bits from uniformly sampled field elements. As long as we sample up to the `k`
285    /// least significant bits in the range [0, m_k), we sample from exactly `m_k` elements. As
286    /// `m_k` is divisible by 2^k, each of the least significant `k` bits has exactly the same
287    /// number of zeroes and ones, leading to a uniform sampling.
288    const SAMPLING_BITS_M: [u64; 64];
289}
290
291// Provide a blanket implementation for Monty31 fields here, which forwards the
292// implementation of the variables to the generic argument `<Field>Parameter`,
293// for which we implement the trait (KoalaBear, BabyBear).
294impl<MP> UniformSamplingField for MontyField31<MP>
295where
296    MP: UniformSamplingField + MontyParameters,
297{
298    const MAX_SINGLE_SAMPLE_BITS: usize = MP::MAX_SINGLE_SAMPLE_BITS;
299    const SAMPLING_BITS_M: [u64; 64] = MP::SAMPLING_BITS_M;
300}
301
302// Set of different strategies we currently support for sampling
303// Implementations for each are below.
304/// A zero-sized struct representing the "resample" strategy.
305pub(super) struct ResampleOnRejection;
306/// A zero-sized struct representing the "error" strategy.
307pub(super) struct ErrorOnRejection;
308
309/// Custom error raised when resampling is required for uniform bits but disabled
310/// via `ErrorOnRejection` strategy.
311#[derive(Debug)]
312pub struct ResamplingError {
313    /// The sampled value
314    value: u64,
315    /// The target value we need to be smaller than
316    m: u64,
317}
318
319impl Display for ResamplingError {
320    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
321        write!(
322            f,
323            "Encountered value {0}, which requires resampling for uniform bits as it not smaller than {1}. But resampling is not enabled.",
324            self.value, self.m
325        )
326    }
327}
328
329impl Error for ResamplingError {}
330
331/// A trait that defines a strategy for handling out-of-range samples.
332pub(super) trait BitSamplingStrategy<F, P, const W: usize, const R: usize>
333where
334    F: PrimeField64,
335    P: CryptographicPermutation<[F; W]>,
336{
337    /// Whether to error instead of resampling when a drawn value is too large.
338    const ERROR_ON_REJECTION: bool;
339
340    #[inline]
341    fn sample_value(
342        challenger: &mut DuplexChallenger<F, P, W, R>,
343        m: u64,
344    ) -> Result<F, ResamplingError> {
345        let mut result: F = challenger.sample();
346        if Self::ERROR_ON_REJECTION {
347            if result.as_canonical_u64() >= m {
348                return Err(ResamplingError {
349                    value: result.as_canonical_u64(),
350                    m,
351                });
352            }
353        } else {
354            while result.as_canonical_u64() >= m {
355                result = challenger.sample();
356            }
357        }
358        Ok(result)
359    }
360}
361
362/// Implement rejection sampling
363impl<F, P, const W: usize, const R: usize> BitSamplingStrategy<F, P, W, R> for ResampleOnRejection
364where
365    F: PrimeField64,
366    P: CryptographicPermutation<[F; W]>,
367{
368    const ERROR_ON_REJECTION: bool = false;
369}
370
371/// Implement erroring on a required rejection
372impl<F, P, const W: usize, const R: usize> BitSamplingStrategy<F, P, W, R> for ErrorOnRejection
373where
374    F: PrimeField64,
375    P: CryptographicPermutation<[F; W]>,
376{
377    const ERROR_ON_REJECTION: bool = true;
378}
379
380impl<F, P, const WIDTH: usize, const RATE: usize> DuplexChallenger<F, P, WIDTH, RATE>
381where
382    F: UniformSamplingField + PrimeField64,
383    P: CryptographicPermutation<[F; WIDTH]>,
384{
385    /// Generic implementation for uniform bit sampling, parameterized by a strategy.
386    #[inline]
387    fn sample_uniform_bits_with_strategy<S>(
388        &mut self,
389        bits: usize,
390    ) -> Result<usize, ResamplingError>
391    where
392        S: BitSamplingStrategy<F, P, WIDTH, RATE>,
393    {
394        if bits == 0 {
395            return Ok(0);
396        };
397        assert!(bits < usize::BITS as usize, "bit count must be valid");
398        assert!(
399            (1u64 << bits) < F::ORDER_U64,
400            "bit count exceeds field order"
401        );
402        let m = F::SAMPLING_BITS_M[bits];
403        if bits <= F::MAX_SINGLE_SAMPLE_BITS {
404            // Fast path: Only one sample is needed for sufficient uniformity.
405            let rand_f = S::sample_value(self, m);
406            Ok(rand_f?.as_canonical_u64() as usize & ((1 << bits) - 1))
407        } else {
408            // Slow path: Sample twice to construct the required number of bits.
409            // This reduces the bias introduced by a single, larger sample.
410            let half_bits1 = bits / 2;
411            let half_bits2 = bits - half_bits1;
412            // Sample the first chunk of bits.
413            let rand1 = S::sample_value(self, F::SAMPLING_BITS_M[half_bits1]);
414            let chunk1 = rand1?.as_canonical_u64() as usize & ((1 << half_bits1) - 1);
415            // Sample the second chunk of bits.
416            let rand2 = S::sample_value(self, F::SAMPLING_BITS_M[half_bits2]);
417            let chunk2 = rand2?.as_canonical_u64() as usize & ((1 << half_bits2) - 1);
418
419            // Combine the chunks.
420            Ok(chunk1 | (chunk2 << half_bits1))
421        }
422    }
423}
424
425impl<F, P, const WIDTH: usize, const RATE: usize> CanSampleUniformBits<F>
426    for DuplexChallenger<F, P, WIDTH, RATE>
427where
428    F: UniformSamplingField + PrimeField64,
429    P: CryptographicPermutation<[F; WIDTH]>,
430{
431    fn sample_uniform_bits<const RESAMPLE: bool>(
432        &mut self,
433        bits: usize,
434    ) -> Result<usize, ResamplingError> {
435        if RESAMPLE {
436            self.sample_uniform_bits_with_strategy::<ResampleOnRejection>(bits)
437        } else {
438            self.sample_uniform_bits_with_strategy::<ErrorOnRejection>(bits)
439        }
440    }
441}
442
443impl<F, P, const WIDTH: usize, const RATE: usize> CanFinalizeDigest
444    for DuplexChallenger<F, P, WIDTH, RATE>
445where
446    F: Copy,
447    P: CryptographicPermutation<[F; WIDTH]>,
448{
449    type Digest = [F; RATE];
450
451    fn finalize(mut self) -> [F; RATE] {
452        // Unconditionally duplex: absorb any pending input and permute.
453        //
454        // Note: sampling only pops from the output buffer without modifying
455        // sponge state, so it does not necessarily affect the digest (e.g.
456        // when the last observe already triggered auto-duplexing).
457        self.duplexing();
458        self.sponge_state[..RATE].try_into().unwrap()
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use core::iter;
465
466    use p3_baby_bear::BabyBear;
467    use p3_field::PrimeCharacteristicRing;
468    use p3_field::extension::BinomialExtensionField;
469    use p3_goldilocks::Goldilocks;
470    use p3_symmetric::Permutation;
471
472    use super::*;
473    use crate::grinding_challenger::GrindingChallenger;
474
475    const WIDTH: usize = 24;
476    const RATE: usize = 16;
477
478    type G = Goldilocks;
479    type EF2G = BinomialExtensionField<G, 2>;
480
481    type BB = BabyBear;
482
483    #[derive(Clone)]
484    struct TestPermutation {}
485
486    impl<F: Clone> Permutation<[F; WIDTH]> for TestPermutation {
487        fn permute_mut(&self, input: &mut [F; WIDTH]) {
488            input.reverse();
489        }
490    }
491
492    impl<F: Clone> CryptographicPermutation<[F; WIDTH]> for TestPermutation {}
493
494    #[test]
495    fn test_duplex_challenger() {
496        type Chal = DuplexChallenger<G, TestPermutation, WIDTH, RATE>;
497        let permutation = TestPermutation {};
498        let mut duplex_challenger = DuplexChallenger::new(permutation);
499
500        // Observe 12 elements.
501        (0..12).for_each(|element| duplex_challenger.observe(G::from_u8(element as u8)));
502
503        let state_after_duplexing: Vec<_> = iter::repeat_n(G::ZERO, 12)
504            .chain((0..12).map(G::from_u8).rev())
505            .collect();
506
507        let expected_samples: Vec<G> = state_after_duplexing[..16].iter().copied().rev().collect();
508        let samples = <Chal as CanSample<G>>::sample_vec(&mut duplex_challenger, 16);
509        assert_eq!(samples, expected_samples);
510    }
511
512    #[test]
513    #[should_panic]
514    fn test_duplex_challenger_sample_bits_security() {
515        type GoldilocksChal = DuplexChallenger<G, TestPermutation, WIDTH, RATE>;
516        let permutation = TestPermutation {};
517        let mut duplex_challenger = GoldilocksChal::new(permutation);
518
519        for _ in 0..100 {
520            assert!(duplex_challenger.sample_bits(129) < 4);
521        }
522    }
523
524    #[test]
525    #[should_panic]
526    fn test_duplex_challenger_sample_bits_security_small_field() {
527        type BabyBearChal = DuplexChallenger<BB, TestPermutation, WIDTH, RATE>;
528        let permutation = TestPermutation {};
529        let mut duplex_challenger = BabyBearChal::new(permutation);
530
531        for _ in 0..100 {
532            assert!(duplex_challenger.sample_bits(40) < 1 << 31);
533        }
534    }
535
536    #[test]
537    #[should_panic]
538    fn test_duplex_challenger_grind_security() {
539        type GoldilocksChal = DuplexChallenger<G, TestPermutation, WIDTH, RATE>;
540        let permutation = TestPermutation {};
541        let mut duplex_challenger = GoldilocksChal::new(permutation);
542
543        // This should cause sample_bits (and hence grind and check_witness) to
544        // panic. If bit sizes were not constrained correctly inside the
545        // challenger, (1 << too_many_bits) would loop around, incorrectly
546        // grinding and accepting a 1-bit PoW.
547        let too_many_bits = usize::BITS as usize;
548
549        let witness = duplex_challenger.grind(too_many_bits);
550        assert!(duplex_challenger.check_witness(too_many_bits, witness));
551    }
552
553    #[test]
554    fn test_observe_single_value() {
555        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
556        chal.observe(G::from_u8(42));
557        assert_eq!(chal.input_buffer, vec![G::from_u8(42)]);
558        assert!(chal.output_buffer.is_empty());
559    }
560
561    #[test]
562    fn test_observe_array_of_values() {
563        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
564        chal.observe([G::from_u8(1), G::from_u8(2), G::from_u8(3)]);
565        assert_eq!(
566            chal.input_buffer,
567            vec![G::from_u8(1), G::from_u8(2), G::from_u8(3)]
568        );
569        assert!(chal.output_buffer.is_empty());
570    }
571
572    #[test]
573    fn test_observe_hash_array() {
574        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
575        let hash = Hash::<G, G, 4>::from([G::from_u8(10); 4]);
576        chal.observe(hash);
577        assert_eq!(chal.input_buffer, vec![G::from_u8(10); 4]);
578    }
579
580    #[test]
581    fn test_observe_nested_vecs() {
582        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
583        chal.observe(vec![
584            vec![G::from_u8(1), G::from_u8(2)],
585            vec![G::from_u8(3)],
586        ]);
587        assert_eq!(
588            chal.input_buffer,
589            vec![G::from_u8(1), G::from_u8(2), G::from_u8(3)]
590        );
591    }
592
593    #[test]
594    fn test_sample_triggers_duplex() {
595        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
596        chal.observe(G::from_u8(5));
597        assert!(chal.output_buffer.is_empty());
598        let _sample: G = chal.sample();
599        assert!(!chal.output_buffer.is_empty());
600    }
601
602    #[test]
603    fn test_sample_multiple_extension_field() {
604        use p3_field::extension::BinomialExtensionField;
605        type EF = BinomialExtensionField<G, 2>;
606        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
607
608        chal.observe(G::from_u8(1));
609        chal.observe(G::from_u8(2));
610        let _: EF = chal.sample();
611        let _: EF = chal.sample();
612    }
613
614    #[test]
615    fn test_sample_bits_within_bounds() {
616        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
617        for i in 0..RATE {
618            chal.observe(G::from_u8(i as u8));
619        }
620
621        // With RATE=16 and input = 0..15, the reversed sponge_state will be 15..0
622        // The first RATE elements of that, i.e. output_buffer, are 15..0
623        // sample_bits(3) will sample the last of those: G::from_u8(0)
624
625        let bits = 3;
626        let value = chal.sample_bits(bits);
627        let expected = G::ZERO.as_canonical_u64() as usize & ((1 << bits) - 1);
628        assert_eq!(value, expected);
629    }
630
631    #[test]
632    fn test_sample_bits_trigger_duplex_when_empty() {
633        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
634        // Force empty buffers
635        assert_eq!(chal.input_buffer.len(), 0);
636        assert_eq!(chal.output_buffer.len(), 0);
637
638        // sampling bits should not panic, should return 0
639        let bits = 2;
640        let sample = chal.sample_bits(bits);
641        let expected = G::ZERO.as_canonical_u64() as usize & ((1 << bits) - 1);
642        assert_eq!(sample, expected);
643    }
644
645    #[test]
646    fn test_output_buffer_pops_correctly() {
647        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
648
649        // Observe RATE elements, causing a duplexing
650        for i in 0..RATE {
651            chal.observe(G::from_u8(i as u8));
652        }
653
654        // we expect the output buffer to be reversed
655        let expected = [
656            G::from_u8(0),
657            G::from_u8(0),
658            G::from_u8(0),
659            G::from_u8(0),
660            G::from_u8(0),
661            G::from_u8(0),
662            G::from_u8(0),
663            G::from_u8(0),
664            G::from_u8(15),
665            G::from_u8(14),
666            G::from_u8(13),
667            G::from_u8(12),
668            G::from_u8(11),
669            G::from_u8(10),
670            G::from_u8(9),
671            G::from_u8(8),
672        ]
673        .to_vec();
674
675        assert_eq!(chal.output_buffer, expected);
676
677        let first: G = chal.sample();
678        let second: G = chal.sample();
679
680        // sampling pops from end of output buffer
681        assert_eq!(first, G::from_u8(8));
682        assert_eq!(second, G::from_u8(9));
683    }
684
685    #[test]
686    fn test_duplexing_only_when_needed() {
687        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
688        chal.output_buffer = vec![G::from_u8(10), G::from_u8(20)];
689
690        // Sample should not call duplexing; just pop from the buffer
691        let sample: G = chal.sample();
692        assert_eq!(sample, G::from_u8(20));
693        assert_eq!(chal.output_buffer, vec![G::from_u8(10)]);
694    }
695
696    #[test]
697    fn test_flush_when_input_full() {
698        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
699
700        // Observe RATE elements, causing a duplexing
701        for i in 0..RATE {
702            chal.observe(G::from_u8(i as u8));
703        }
704
705        // We expect the output buffer to be reversed
706        let expected_output = [
707            G::from_u8(0),
708            G::from_u8(0),
709            G::from_u8(0),
710            G::from_u8(0),
711            G::from_u8(0),
712            G::from_u8(0),
713            G::from_u8(0),
714            G::from_u8(0),
715            G::from_u8(15),
716            G::from_u8(14),
717            G::from_u8(13),
718            G::from_u8(12),
719            G::from_u8(11),
720            G::from_u8(10),
721            G::from_u8(9),
722            G::from_u8(8),
723        ]
724        .to_vec();
725
726        // Input buffer should be drained after duplexing
727        assert!(chal.input_buffer.is_empty());
728
729        // Output buffer should match expected state from duplexing
730        assert_eq!(chal.output_buffer, expected_output);
731    }
732
733    #[test]
734    fn test_observe_base_as_algebra_element_consistency_with_direct_observe() {
735        // Create two identical challengers to verify behavior equivalence
736        let mut chal1 =
737            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
738        let mut chal2 =
739            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
740
741        let base_val = G::from_u8(99);
742
743        // Method 1: Use the convenience method for base-to-extension observation
744        chal1.observe_base_as_algebra_element::<EF2G>(base_val);
745
746        // Method 2: Manually convert to extension field then observe
747        let ext_val = EF2G::from(base_val);
748        chal2.observe_algebra_element(ext_val);
749
750        // Both methods must produce identical internal state
751        assert_eq!(chal1.input_buffer, chal2.input_buffer);
752        assert_eq!(chal1.output_buffer, chal2.output_buffer);
753        assert_eq!(chal1.sponge_state, chal2.sponge_state);
754    }
755
756    #[test]
757    fn test_observe_base_as_algebra_element_stream_consistency() {
758        // Create two identical challengers for stream observation test
759        let mut chal1 =
760            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
761        let mut chal2 =
762            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
763
764        // Define a base value vector
765        let base_values: Vec<_> = (0u8..25).map(G::from_u8).collect();
766
767        // Method 1: Observe stream using convenience method
768        for &val in &base_values {
769            chal1.observe_base_as_algebra_element::<EF2G>(val);
770        }
771
772        // Method 2: Manually convert each element before observing
773        for &val in &base_values {
774            let ext_val = EF2G::from(val);
775            chal2.observe_algebra_element(ext_val);
776        }
777
778        // Verify identical state through sequential observations and duplexing.
779        assert_eq!(chal1.input_buffer, chal2.input_buffer);
780        assert_eq!(chal1.output_buffer, chal2.output_buffer);
781        assert_eq!(chal1.sponge_state, chal2.sponge_state);
782
783        // Verify sampling produces identical challenges
784        let sample1: EF2G = chal1.sample_algebra_element();
785        let sample2: EF2G = chal2.sample_algebra_element();
786        assert_eq!(sample1, sample2);
787
788        // Verify state consistency is maintained after sampling
789        assert_eq!(chal1.input_buffer, chal2.input_buffer);
790        assert_eq!(chal1.output_buffer, chal2.output_buffer);
791        assert_eq!(chal1.sponge_state, chal2.sponge_state);
792    }
793
794    #[test]
795    fn test_observe_algebra_elements_equivalence() {
796        // Test that the two following paths give the same results:
797        // - `observe_algebra_slice`
798        // - `observe_algebra_element` in a loop
799        let mut chal1 =
800            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
801        let mut chal2 =
802            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
803
804        // Create a slice of extension field elements
805        let ext_values: Vec<EF2G> = (0u8..10).map(|i| EF2G::from(G::from_u8(i))).collect();
806
807        // Method 1: Use observe_algebra_slice with slice
808        chal1.observe_algebra_slice(&ext_values);
809
810        // Method 2: Call observe_algebra_element individually
811        for ext_val in &ext_values {
812            chal2.observe_algebra_element(*ext_val);
813        }
814
815        // Verify identical internal state
816        assert_eq!(chal1.input_buffer, chal2.input_buffer);
817        assert_eq!(chal1.output_buffer, chal2.output_buffer);
818        assert_eq!(chal1.sponge_state, chal2.sponge_state);
819
820        // Verify sampling produces identical challenges
821        let sample1: EF2G = chal1.sample_algebra_element();
822        let sample2: EF2G = chal2.sample_algebra_element();
823        assert_eq!(sample1, sample2);
824    }
825
826    #[test]
827    fn test_observe_algebra_elements_empty_slice() {
828        // Test that observing an empty slice does not change state
829        let mut chal1 =
830            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
831        let mut chal2 =
832            DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
833
834        // Observe some values first to have non-trivial state
835        chal1.observe(G::from_u8(42));
836        chal2.observe(G::from_u8(42));
837
838        // Observe empty slice
839        let empty: Vec<EF2G> = vec![];
840        chal1.observe_algebra_slice(&empty);
841
842        // Verify state unchanged
843        assert_eq!(chal1.input_buffer, chal2.input_buffer);
844        assert_eq!(chal1.output_buffer, chal2.output_buffer);
845        assert_eq!(chal1.sponge_state, chal2.sponge_state);
846    }
847
848    #[test]
849    fn test_observe_algebra_elements_triggers_duplexing() {
850        // Test that observing enough elements triggers duplexing
851        let mut chal = DuplexChallenger::<G, TestPermutation, WIDTH, RATE>::new(TestPermutation {});
852
853        // EF2G has dimension 2, so we need RATE/2 elements to fill the buffer
854        //
855        // With RATE=16, we need 8 EF2G elements to trigger duplexing
856        let ext_values: Vec<EF2G> = (0u8..8).map(|i| EF2G::from(G::from_u8(i))).collect();
857
858        assert!(chal.input_buffer.is_empty());
859        assert!(chal.output_buffer.is_empty());
860
861        chal.observe_algebra_slice(&ext_values);
862
863        // After observing 8 EF2G elements (16 base field elements), duplexing should occur
864        assert!(chal.input_buffer.is_empty());
865        assert!(!chal.output_buffer.is_empty());
866    }
867
868    #[test]
869    fn test_finalize() {
870        let new_chal = || DuplexChallenger::<G, _, WIDTH, RATE>::new(TestPermutation {});
871
872        // Deterministic: same observations produce same digest.
873        let mut c1 = new_chal();
874        let mut c2 = new_chal();
875        for i in 0..5u8 {
876            c1.observe(G::from_u8(i));
877            c2.observe(G::from_u8(i));
878        }
879        assert_eq!(c1.finalize(), c2.finalize());
880
881        // Different observations produce different digests.
882        let mut c1 = new_chal();
883        let mut c2 = new_chal();
884        for i in 0..10u8 {
885            c1.observe(G::from_u8(i));
886            c2.observe(G::from_u8(i + 1));
887        }
888        assert_ne!(c1.finalize(), c2.finalize());
889    }
890
891    /// Document how sampling interacts with finalize.
892    ///
893    /// Sampling does not modify the sponge state — it only pops from the
894    /// output buffer. This means the digest only changes when a sample
895    /// triggers a new duplexing (because the output buffer was empty or
896    /// there was pending input). Within one "batch" of RATE outputs, all
897    /// sample counts produce the same digest.
898    #[test]
899    fn test_finalize_sample_interaction() {
900        let digest = |n_samples: usize| {
901            let mut c = DuplexChallenger::<G, _, WIDTH, RATE>::new(TestPermutation {});
902            for i in 0..5u8 {
903                c.observe(G::from_u8(i));
904            }
905            for _ in 0..n_samples {
906                let _: G = c.sample();
907            }
908            c.finalize()
909        };
910
911        // The first sample triggers duplexing (absorbs pending input),
912        // so finalize's duplexing is now an extra permutation on an
913        // already-duplexed state — different from the 0-sample case.
914        assert_ne!(digest(0), digest(1));
915
916        // Samples 1 through RATE all come from the same output batch.
917        // They don't trigger another duplexing, so the sponge state
918        // (and thus the digest) is identical.
919        assert_eq!(digest(1), digest(2));
920        assert_eq!(digest(1), digest(RATE));
921
922        // The (RATE+1)-th sample exhausts the output buffer and triggers
923        // a fresh duplexing, changing the sponge state again.
924        assert_ne!(digest(RATE), digest(RATE + 1));
925
926        // Within the second batch, the digest is again stable.
927        assert_eq!(digest(RATE + 1), digest(RATE + 2));
928    }
929}