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