Skip to main content

miden_crypto/hash/eidos/
challenger.rs

1//! Fiat-Shamir challenger built from Eidos compression.
2//!
3//! The challenger keeps a four-Felt chaining value and buffers observations in eight-Felt blocks.
4//! A full observation block is compressed immediately. To begin sampling, it zero-pads the pending
5//! block, adds `1 + pending_len` to the fourth CV element in the Goldilocks field, and compresses
6//! once. The resulting four CV elements are returned in order.
7//!
8//! Each additional output word adds `9` in the same field and compresses a block containing a
9//! nonzero `u32` counter followed by seven zeros. Observing after sampling preserves the current
10//! CV, discards any unused output elements, and resets the counter. Finalization also discards
11//! unread output elements and returns a freshly generated word. One uninterrupted squeezing phase
12//! can produce `2^32` words; requesting another word panics when the counter is exhausted. This
13//! makes the absorb/squeeze boundary and the number of pending observations part of the transcript
14//! schedule.
15
16use alloc::vec::Vec;
17
18use p3_challenger::{
19    CanFinalizeDigest, CanObserve, CanSample, CanSampleBits, FieldChallenger, GrindingChallenger,
20};
21use p3_symmetric::{Hash, MerkleCap};
22
23use super::{
24    BLOCK_LEN, DIGEST_WIDTH, Eidos, PACKED_LANES, PackedBlock, PackedChainingValue, PackedFelt,
25};
26use crate::{
27    Felt, Word, ZERO,
28    field::{BasedVectorSpace, PrimeField64},
29    parallel::*,
30};
31
32/// Base value for absorb-to-squeeze transition tags.
33const TRANSITION_TAG_BASE: u32 = 1;
34
35/// Squeeze tag used for counter-mode output extension.
36const SQUEEZE_TAG: Felt = Felt::new_unchecked((TRANSITION_TAG_BASE + BLOCK_LEN as u32) as u64);
37
38#[derive(Debug, Copy, Clone, Eq, PartialEq)]
39enum EidosChallengerMode {
40    Absorbing,
41    Squeezing,
42}
43
44/// Generic Eidos challenger.
45///
46/// This type supports scalar observation for Plonky3 challenger traits.
47/// Each sampled base-field element comes from one packed 63-bit Eidos digest word rather than the
48/// full Goldilocks field range.
49#[derive(Clone, Debug)]
50pub struct EidosChallenger {
51    cv: Word,
52    buffer: [Felt; BLOCK_LEN],
53    buffer_len: usize,
54    mode: EidosChallengerMode,
55    counter: u32,
56    output_word: Word,
57    output_len: usize,
58}
59
60impl EidosChallenger {
61    /// Returns a challenger initialized with the supplied chaining value.
62    ///
63    /// The caller is responsible for domain-separating this value from message hashes and other
64    /// transcript protocols.
65    pub fn new(initial_cv: Word) -> Self {
66        Self {
67            cv: initial_cv,
68            buffer: [ZERO; BLOCK_LEN],
69            buffer_len: 0,
70            mode: EidosChallengerMode::Absorbing,
71            counter: 0,
72            output_word: Word::default(),
73            output_len: 0,
74        }
75    }
76
77    /// Returns the current chaining value.
78    pub fn cv(&self) -> Word {
79        self.cv
80    }
81
82    /// Observes one scalar felt through the generic streaming interface.
83    pub fn observe_felt(&mut self, value: Felt) {
84        self.enter_absorbing_mode();
85
86        self.buffer[self.buffer_len] = value;
87        self.buffer_len += 1;
88        if self.buffer_len == BLOCK_LEN {
89            self.compress_pending_buffer();
90        }
91    }
92
93    /// Samples one base-field element.
94    ///
95    /// Felts are consumed from a freshly squeezed word in natural index order:
96    /// `output_word[0]`, then `[1]`, `[2]`, and `[3]`.
97    pub fn sample_felt(&mut self) -> Felt {
98        if self.output_len == 0 {
99            self.refill_output_word();
100        }
101
102        let idx = DIGEST_WIDTH - self.output_len;
103        self.output_len -= 1;
104        self.output_word[idx]
105    }
106
107    /// Samples `bits` low bits from the next sampled field element.
108    ///
109    /// A zero-bit request consumes one field element and returns zero.
110    pub fn sample_bits(&mut self, bits: usize) -> usize {
111        assert!(bits < usize::BITS as usize, "bit count must be valid");
112        assert!((1u64 << bits) < Felt::ORDER_U64);
113
114        let value = self.sample_felt().as_canonical_u64() as usize;
115        value & ((1usize << bits) - 1)
116    }
117
118    /// Returns the next fresh squeezed word.
119    ///
120    /// This API requires that no partially consumed output word is pending.
121    ///
122    /// # Panics
123    ///
124    /// Panics if a previous word has been only partially consumed through scalar sampling or
125    /// proof-of-work grinding.
126    pub fn squeeze_word(&mut self) -> Word {
127        assert_eq!(self.output_len, 0, "squeeze_word requires word-aligned output");
128        self.refill_output_word();
129        let output = self.output_word;
130        self.output_len = 0;
131        output
132    }
133
134    fn absorb_full_block(&mut self, block: [Felt; BLOCK_LEN]) {
135        assert_eq!(self.buffer_len, 0, "full-block absorb requires an empty scalar buffer");
136        self.enter_absorbing_mode();
137        self.cv = Eidos::compress(self.cv, block);
138    }
139
140    fn enter_absorbing_mode(&mut self) {
141        self.mode = EidosChallengerMode::Absorbing;
142        self.counter = 0;
143        self.output_len = 0;
144    }
145
146    fn compress_pending_buffer(&mut self) {
147        debug_assert_eq!(self.buffer_len, BLOCK_LEN);
148        self.cv = Eidos::compress(self.cv, self.buffer);
149        self.buffer = [ZERO; BLOCK_LEN];
150        self.buffer_len = 0;
151    }
152
153    fn refill_output_word(&mut self) {
154        match self.mode {
155            EidosChallengerMode::Absorbing => {
156                let tag = transition_tag(self.buffer_len);
157                let block = self.buffer;
158                self.cv = Eidos::compress(tweak_cv(self.cv, tag), block);
159                self.buffer = [ZERO; BLOCK_LEN];
160                self.buffer_len = 0;
161                self.mode = EidosChallengerMode::Squeezing;
162                self.counter = 0;
163            },
164            EidosChallengerMode::Squeezing => {
165                self.counter =
166                    self.counter.checked_add(1).expect("squeeze counter exhausted before absorb");
167                self.cv =
168                    Eidos::compress(tweak_cv(self.cv, SQUEEZE_TAG), counter_block(self.counter));
169            },
170        }
171
172        self.output_word = self.cv;
173        self.output_len = DIGEST_WIDTH;
174    }
175}
176
177/// Eidos challenger adapter for Miden transcripts.
178///
179/// The adapter exposes the generic scalar observation and sampling traits required by the STARK
180/// transcript. Scalar observations share an eight-felt buffer. Construction binds the relation
181/// digest as one dedicated full block before that stream begins.
182#[derive(Clone, Debug)]
183pub struct MidenEidosChallenger {
184    inner: EidosChallenger,
185}
186
187impl MidenEidosChallenger {
188    /// Initializes the transcript from a precomputed init CV and relation digest.
189    pub fn new(transcript_init_cv: Word, relation_digest: Word) -> Self {
190        let mut inner = EidosChallenger::new(transcript_init_cv);
191        inner.absorb_full_block([
192            relation_digest[0],
193            relation_digest[1],
194            relation_digest[2],
195            relation_digest[3],
196            ZERO,
197            ZERO,
198            ZERO,
199            ZERO,
200        ]);
201        Self { inner }
202    }
203}
204
205impl<T> CanObserve<T> for MidenEidosChallenger
206where
207    EidosChallenger: CanObserve<T>,
208{
209    fn observe(&mut self, value: T) {
210        self.inner.observe(value);
211    }
212}
213
214impl<T> CanSample<T> for MidenEidosChallenger
215where
216    EidosChallenger: CanSample<T>,
217{
218    fn sample(&mut self) -> T {
219        self.inner.sample()
220    }
221}
222
223impl CanSampleBits<usize> for MidenEidosChallenger {
224    fn sample_bits(&mut self, bits: usize) -> usize {
225        self.inner.sample_bits(bits)
226    }
227}
228
229impl FieldChallenger<Felt> for MidenEidosChallenger {}
230
231impl GrindingChallenger for MidenEidosChallenger {
232    type Witness = Felt;
233
234    fn grind(&mut self, bits: usize) -> Self::Witness {
235        self.inner.grind(bits)
236    }
237}
238
239impl CanFinalizeDigest for MidenEidosChallenger {
240    type Digest = [Felt; DIGEST_WIDTH];
241
242    fn finalize(self) -> Self::Digest {
243        self.inner.finalize()
244    }
245}
246
247impl CanObserve<Felt> for EidosChallenger {
248    fn observe(&mut self, value: Felt) {
249        self.observe_felt(value);
250    }
251}
252
253impl<const N: usize> CanObserve<[Felt; N]> for EidosChallenger {
254    fn observe(&mut self, values: [Felt; N]) {
255        for value in values {
256            self.observe_felt(value);
257        }
258    }
259}
260
261impl<const N: usize> CanObserve<Hash<Felt, Felt, N>> for EidosChallenger {
262    fn observe(&mut self, values: Hash<Felt, Felt, N>) {
263        for value in values {
264            self.observe_felt(value);
265        }
266    }
267}
268
269impl<const N: usize> CanObserve<Hash<Felt, u64, N>> for EidosChallenger {
270    fn observe(&mut self, values: Hash<Felt, u64, N>) {
271        for value in values {
272            self.observe_felt(Felt::new_unchecked(value));
273        }
274    }
275}
276
277impl<const N: usize> CanObserve<&MerkleCap<Felt, [Felt; N]>> for EidosChallenger {
278    fn observe(&mut self, cap: &MerkleCap<Felt, [Felt; N]>) {
279        for digest in cap.roots() {
280            for &value in digest {
281                self.observe_felt(value);
282            }
283        }
284    }
285}
286
287impl<const N: usize> CanObserve<MerkleCap<Felt, [Felt; N]>> for EidosChallenger {
288    fn observe(&mut self, cap: MerkleCap<Felt, [Felt; N]>) {
289        self.observe(&cap);
290    }
291}
292
293impl<const N: usize> CanObserve<&MerkleCap<Felt, [u64; N]>> for EidosChallenger {
294    fn observe(&mut self, cap: &MerkleCap<Felt, [u64; N]>) {
295        for digest in cap.roots() {
296            for &value in digest {
297                self.observe_felt(Felt::new_unchecked(value));
298            }
299        }
300    }
301}
302
303impl<const N: usize> CanObserve<MerkleCap<Felt, [u64; N]>> for EidosChallenger {
304    fn observe(&mut self, cap: MerkleCap<Felt, [u64; N]>) {
305        self.observe(&cap);
306    }
307}
308
309impl CanObserve<Vec<Vec<Felt>>> for EidosChallenger {
310    fn observe(&mut self, rows: Vec<Vec<Felt>>) {
311        for values in rows {
312            for value in values {
313                self.observe_felt(value);
314            }
315        }
316    }
317}
318
319impl<EF: BasedVectorSpace<Felt>> CanSample<EF> for EidosChallenger {
320    fn sample(&mut self) -> EF {
321        EF::from_basis_coefficients_fn(|_| self.sample_felt())
322    }
323}
324
325impl CanSampleBits<usize> for EidosChallenger {
326    fn sample_bits(&mut self, bits: usize) -> usize {
327        EidosChallenger::sample_bits(self, bits)
328    }
329}
330
331impl FieldChallenger<Felt> for EidosChallenger {}
332
333impl GrindingChallenger for EidosChallenger {
334    type Witness = Felt;
335
336    fn grind(&mut self, bits: usize) -> Self::Witness {
337        assert!(bits < usize::BITS as usize, "bit count must be valid");
338        assert!((1u64 << bits) < Felt::ORDER_U64);
339
340        if bits == 0 {
341            return ZERO;
342        }
343
344        let mask = (1u64 << bits) - 1;
345        let cv = self.cv;
346        let buffer = self.buffer;
347        let buffer_len = self.buffer_len;
348
349        // Every candidate shares the same pre-witness challenger snapshot (`observe_felt` always
350        // forces absorbing mode first, so this only ever replays that branch); only the witness
351        // lane varies. One packed compression pass therefore checks `PACKED_LANES` candidates.
352        let witness = (0..WITNESS_BATCHES)
353            .into_par_iter()
354            .map(|batch| {
355                let candidates = witness_batch(batch);
356                let accepted = check_witness_packed(cv, buffer, buffer_len, candidates, mask);
357                (0..PACKED_LANES).find(|&lane| accepted[lane]).map(|lane| candidates[lane])
358            })
359            .find_any(Option::is_some)
360            .flatten()
361            .expect("failed to find proof-of-work witness");
362
363        assert!(self.check_witness(bits, witness));
364        witness
365    }
366}
367
368/// Number of `PACKED_LANES`-wide candidate batches that cover every canonical field element.
369const WITNESS_BATCHES: u64 = Felt::ORDER_U64.div_ceil(PACKED_LANES as u64);
370
371#[inline]
372fn witness_batch(batch: u64) -> PackedFelt {
373    debug_assert!(batch < WITNESS_BATCHES);
374
375    let base = batch * PACKED_LANES as u64;
376    core::array::from_fn(|lane| {
377        let candidate = base + lane as u64;
378        // No canonical candidate exists beyond the field order. Repeat the last in-range
379        // candidate so every lane stays a canonical field element.
380        Felt::new_unchecked(candidate.min(Felt::ORDER_U64 - 1))
381    })
382}
383
384/// Runs the equivalent of `EidosChallenger::check_witness` for `PACKED_LANES` independent
385/// candidate witnesses in one packed compression pass, given a fixed pre-witness challenger
386/// snapshot `(cv, buffer, buffer_len)`.
387///
388/// `observe_felt` always calls `enter_absorbing_mode` first, so absorbing the witness and
389/// sampling the check bits only ever exercises the `Absorbing` branch of `refill_output_word`;
390/// this mirrors that exact sequence with `Felt` replaced by `PackedFelt` throughout.
391fn check_witness_packed(
392    cv: Word,
393    buffer: [Felt; BLOCK_LEN],
394    buffer_len: usize,
395    witnesses: PackedFelt,
396    mask: u64,
397) -> [bool; PACKED_LANES] {
398    debug_assert!(buffer_len < BLOCK_LEN);
399
400    // observe_felt: append the witness at the next free buffer slot.
401    let mut packed_buffer: PackedBlock = core::array::from_fn(|slot| {
402        use core::cmp::Ordering;
403        match slot.cmp(&buffer_len) {
404            Ordering::Less => [buffer[slot]; PACKED_LANES],
405            Ordering::Equal => witnesses,
406            Ordering::Greater => [ZERO; PACKED_LANES],
407        }
408    });
409    let mut packed_cv: PackedChainingValue = core::array::from_fn(|i| [cv[i]; PACKED_LANES]);
410    let mut packed_len = buffer_len + 1;
411
412    // compress_pending_buffer: an untagged compression only if the witness filled the buffer.
413    if packed_len == BLOCK_LEN {
414        packed_cv = Eidos::compress_packed(packed_cv, packed_buffer);
415        packed_buffer = [[ZERO; PACKED_LANES]; BLOCK_LEN];
416        packed_len = 0;
417    }
418
419    // refill_output_word (Absorbing branch): tagged compression producing the fresh output word.
420    let tag = transition_tag(packed_len);
421    packed_cv[3] = packed_cv[3].map(|word| word + tag);
422    let output = Eidos::compress_packed(packed_cv, packed_buffer);
423
424    // sample_felt then sample_bits: the first sample off a freshly refilled word is output[0].
425    core::array::from_fn(|lane| (output[0][lane].as_canonical_u64() & mask) == 0)
426}
427
428impl CanFinalizeDigest for EidosChallenger {
429    type Digest = [Felt; DIGEST_WIDTH];
430
431    fn finalize(mut self) -> Self::Digest {
432        self.output_len = 0;
433        let digest = self.squeeze_word();
434        digest.into()
435    }
436}
437
438fn transition_tag(buffer_len: usize) -> Felt {
439    debug_assert!(buffer_len < BLOCK_LEN);
440    Felt::new_unchecked((TRANSITION_TAG_BASE + buffer_len as u32) as u64)
441}
442
443fn tweak_cv(mut cv: Word, tag: Felt) -> Word {
444    // This is Goldilocks field addition on the packed fourth digest word, not independent addition
445    // on either underlying u32 lane.
446    cv[3] += tag;
447    cv
448}
449
450fn counter_block(counter: u32) -> [Felt; BLOCK_LEN] {
451    let mut block = [ZERO; BLOCK_LEN];
452    block[0] = Felt::from_u32(counter);
453    block
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    const TRANSITION_TAG: Felt = Felt::new_unchecked(TRANSITION_TAG_BASE as u64);
461
462    #[derive(Debug, Copy, Clone, Eq, PartialEq)]
463    struct ChallengerSnapshot {
464        cv: Word,
465        mode: EidosChallengerMode,
466        counter: u32,
467        buffer: [Felt; BLOCK_LEN],
468        buffer_len: usize,
469        output_word: Word,
470        output_len: usize,
471    }
472
473    impl ChallengerSnapshot {
474        const SERIALIZED_LEN: usize = 20;
475
476        fn to_canonical_u64s(self) -> [u64; Self::SERIALIZED_LEN] {
477            let mut row = [0u64; Self::SERIALIZED_LEN];
478            row[0] = self.cv[0].as_canonical_u64();
479            row[1] = self.cv[1].as_canonical_u64();
480            row[2] = self.cv[2].as_canonical_u64();
481            row[3] = self.cv[3].as_canonical_u64();
482            row[4] = mode_as_u64(self.mode);
483            row[5] = self.counter as u64;
484            for (i, value) in self.buffer.iter().enumerate() {
485                row[6 + i] = value.as_canonical_u64();
486            }
487            row[14] = self.buffer_len as u64;
488            row[15] = self.output_word[0].as_canonical_u64();
489            row[16] = self.output_word[1].as_canonical_u64();
490            row[17] = self.output_word[2].as_canonical_u64();
491            row[18] = self.output_word[3].as_canonical_u64();
492            row[19] = self.output_len as u64;
493            row
494        }
495    }
496
497    fn felt(value: u64) -> Felt {
498        Felt::new_unchecked(value)
499    }
500
501    fn word(values: [u64; DIGEST_WIDTH]) -> Word {
502        Word::new(values.map(felt))
503    }
504
505    fn snapshot(challenger: &EidosChallenger) -> ChallengerSnapshot {
506        ChallengerSnapshot {
507            cv: challenger.cv,
508            mode: challenger.mode,
509            counter: challenger.counter,
510            buffer: challenger.buffer,
511            buffer_len: challenger.buffer_len,
512            output_word: challenger.output_word,
513            output_len: challenger.output_len,
514        }
515    }
516
517    fn miden_snapshot(challenger: &MidenEidosChallenger) -> ChallengerSnapshot {
518        snapshot(&challenger.inner)
519    }
520
521    const fn mode_as_u64(mode: EidosChallengerMode) -> u64 {
522        match mode {
523            EidosChallengerMode::Absorbing => 0,
524            EidosChallengerMode::Squeezing => 1,
525        }
526    }
527
528    #[test]
529    fn miden_init_absorbs_relation_digest_as_one_full_block() {
530        let init = word([1, 2, 3, 4]);
531        let relation_digest = word([10, 11, 12, 13]);
532
533        let challenger = MidenEidosChallenger::new(init, relation_digest);
534
535        let expected = Eidos::compress(
536            init,
537            [
538                relation_digest[0],
539                relation_digest[1],
540                relation_digest[2],
541                relation_digest[3],
542                ZERO,
543                ZERO,
544                ZERO,
545                ZERO,
546            ],
547        );
548
549        let snapshot = miden_snapshot(&challenger);
550        assert_eq!(snapshot.cv, expected);
551        assert_eq!(snapshot.mode, EidosChallengerMode::Absorbing);
552        assert_eq!(snapshot.counter, 0);
553        assert_eq!(snapshot.buffer_len, 0);
554        assert_eq!(snapshot.output_len, 0);
555    }
556
557    #[test]
558    fn first_squeeze_uses_zero_len_transition_tweak_and_zero_block() {
559        let init = word([1, 2, 3, 4]);
560        let mut challenger = EidosChallenger::new(init);
561
562        let output = challenger.squeeze_word();
563
564        let expected = Eidos::compress(tweak_cv(init, TRANSITION_TAG), [ZERO; BLOCK_LEN]);
565        assert_eq!(output, expected);
566
567        let snapshot = snapshot(&challenger);
568        assert_eq!(snapshot.mode, EidosChallengerMode::Squeezing);
569        assert_eq!(snapshot.counter, 0);
570        assert_eq!(snapshot.output_len, 0);
571    }
572
573    #[test]
574    fn second_squeeze_uses_counter_block_one() {
575        let init = word([1, 2, 3, 4]);
576        let mut challenger = EidosChallenger::new(init);
577
578        let first = challenger.squeeze_word();
579        let second = challenger.squeeze_word();
580
581        let expected = Eidos::compress(tweak_cv(first, SQUEEZE_TAG), counter_block(1));
582        assert_eq!(second, expected);
583        assert_eq!(snapshot(&challenger).counter, 1);
584    }
585
586    #[test]
587    fn scalar_transition_tag_binds_pending_buffer_length() {
588        let init = word([1, 2, 3, 4]);
589        let value = felt(7);
590
591        let mut one = EidosChallenger::new(init);
592        one.observe_felt(value);
593        let one_output = one.squeeze_word();
594
595        let mut two = EidosChallenger::new(init);
596        two.observe_felt(value);
597        two.observe_felt(ZERO);
598        let two_output = two.squeeze_word();
599
600        let expected_one = Eidos::compress(
601            tweak_cv(init, transition_tag(1)),
602            [value, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO],
603        );
604
605        assert_eq!(one_output, expected_one);
606        assert_ne!(one_output, two_output);
607    }
608
609    #[test]
610    fn sample_felt_consumes_squeezed_word_from_the_front() {
611        let init = word([1, 2, 3, 4]);
612
613        let mut word_challenger = EidosChallenger::new(init);
614        let output = word_challenger.squeeze_word();
615
616        let mut scalar_challenger = EidosChallenger::new(init);
617        assert_eq!(scalar_challenger.sample_felt(), output[0]);
618        assert_eq!(scalar_challenger.sample_felt(), output[1]);
619        assert_eq!(scalar_challenger.sample_felt(), output[2]);
620        assert_eq!(scalar_challenger.sample_felt(), output[3]);
621        assert_eq!(snapshot(&scalar_challenger).output_len, 0);
622    }
623
624    #[test]
625    fn sample_bits_returns_requested_width() {
626        let init = word([1, 2, 3, 4]);
627        let mut challenger = EidosChallenger::new(init);
628
629        let sample = challenger.sample_bits(13);
630
631        assert!(sample < (1 << 13));
632    }
633
634    #[test]
635    fn sample_bits_zero_consumes_one_felt() {
636        let init = word([1, 2, 3, 4]);
637        let mut challenger = EidosChallenger::new(init);
638
639        assert_eq!(challenger.sample_bits(0), 0);
640        assert_eq!(snapshot(&challenger).output_len, DIGEST_WIDTH - 1);
641    }
642
643    #[test]
644    fn zero_bit_grinding_and_verification_leave_the_transcript_unchanged() {
645        let mut challenger = EidosChallenger::new(word([1, 2, 3, 4]));
646        challenger.observe_felt(felt(9));
647        let before = snapshot(&challenger);
648
649        assert_eq!(challenger.grind(0), ZERO);
650        assert_eq!(snapshot(&challenger), before);
651
652        let mut verifier = challenger.clone();
653        assert!(verifier.check_witness(0, ZERO));
654        assert_eq!(snapshot(&verifier), before);
655    }
656
657    #[test]
658    fn observe_after_partial_sample_discards_remaining_output() {
659        let init = word([1, 2, 3, 4]);
660        let value = felt(7);
661
662        let mut challenger = EidosChallenger::new(init);
663        let first_output = challenger.squeeze_word();
664        let expected = Eidos::compress(
665            tweak_cv(first_output, transition_tag(1)),
666            [value, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO],
667        );
668
669        let mut challenger = EidosChallenger::new(init);
670        let _ = challenger.sample_felt();
671        challenger.observe_felt(value);
672
673        assert_eq!(challenger.squeeze_word(), expected);
674    }
675
676    #[test]
677    fn finalize_after_partial_sample_uses_fresh_squeeze() {
678        let init = word([1, 2, 3, 4]);
679        let mut challenger = EidosChallenger::new(init);
680
681        let first = challenger.squeeze_word();
682        let expected = Eidos::compress(tweak_cv(first, SQUEEZE_TAG), counter_block(1));
683
684        let mut challenger = EidosChallenger::new(init);
685        let _ = challenger.sample_felt();
686        let finalized = challenger.finalize();
687
688        assert_eq!(Word::new(finalized), expected);
689    }
690
691    #[test]
692    fn snapshot_serialization_order_is_stable() {
693        let mut challenger = EidosChallenger::new(word([1, 2, 3, 4]));
694        challenger.observe_felt(felt(9));
695        challenger.observe_felt(felt(10));
696
697        let row = snapshot(&challenger).to_canonical_u64s();
698
699        assert_eq!(row.len(), ChallengerSnapshot::SERIALIZED_LEN);
700        assert_eq!(row[0..4], [1, 2, 3, 4]);
701        assert_eq!(row[4], 0);
702        assert_eq!(row[5], 0);
703        assert_eq!(row[6..14], [9, 10, 0, 0, 0, 0, 0, 0]);
704        assert_eq!(row[14], 2);
705        assert_eq!(row[15..20], [0, 0, 0, 0, 0]);
706    }
707
708    #[test]
709    fn witness_batches_cover_the_field_boundary_canonically() {
710        let first = witness_batch(0).map(|candidate| candidate.as_canonical_u64());
711        assert_eq!(first, core::array::from_fn(|lane| lane as u64));
712
713        let penultimate =
714            witness_batch(WITNESS_BATCHES - 2).map(|candidate| candidate.as_canonical_u64());
715        let penultimate_base = Felt::ORDER_U64 - 1 - PACKED_LANES as u64;
716        assert_eq!(penultimate, core::array::from_fn(|lane| penultimate_base + lane as u64));
717
718        let last = witness_batch(WITNESS_BATCHES - 1).map(|candidate| candidate.as_canonical_u64());
719        assert_eq!(last, [Felt::ORDER_U64 - 1; PACKED_LANES]);
720    }
721
722    /// `check_witness_packed` must agree, lane by lane, with the scalar `check_witness` default
723    /// implementation run on an independently cloned challenger, for every pre-witness buffer
724    /// state it can be called with (`buffer_len` in `0..BLOCK_LEN`, covering both the case where
725    /// the witness fills the buffer and the case where it doesn't).
726    #[test]
727    fn check_witness_packed_matches_scalar_check_witness_across_buffer_lengths() {
728        let bits_values = [1usize, 8, 20];
729
730        for buffer_len in 0..BLOCK_LEN {
731            let mut challenger = EidosChallenger::new(word([5, 6, 7, 8]));
732            for i in 0..buffer_len {
733                challenger.observe_felt(felt(100 + i as u64));
734            }
735            assert_eq!(challenger.buffer_len, buffer_len);
736
737            let witnesses: PackedFelt = core::array::from_fn(|lane| felt(1_000_000 + lane as u64));
738
739            for &bits in &bits_values {
740                let mask = (1u64 << bits) - 1;
741                let accepted = check_witness_packed(
742                    challenger.cv,
743                    challenger.buffer,
744                    challenger.buffer_len,
745                    witnesses,
746                    mask,
747                );
748
749                for lane in 0..PACKED_LANES {
750                    let mut scalar = challenger.clone();
751                    let expected = scalar.check_witness(bits, witnesses[lane]);
752                    assert_eq!(
753                        accepted[lane], expected,
754                        "lane {lane} mismatch at buffer_len={buffer_len}, bits={bits}"
755                    );
756                }
757            }
758        }
759    }
760
761    /// `grind` must find a witness from every possible pre-grind buffer state, and that witness
762    /// must independently satisfy `check_witness` on a challenger cloned from the pre-grind
763    /// state (not just the internal assertion `grind` already performs on `self`).
764    #[test]
765    fn grind_finds_a_valid_witness_from_every_buffer_state() {
766        let bits = 4;
767
768        for buffer_len in 0..BLOCK_LEN {
769            let mut challenger = EidosChallenger::new(word([1, 2, 3, 4]));
770            for i in 0..buffer_len {
771                challenger.observe_felt(felt(200 + i as u64));
772            }
773            assert_grind_matches_independent_check(challenger, bits);
774        }
775    }
776
777    #[test]
778    fn grind_from_squeezing_state_matches_independent_check() {
779        let mut challenger = EidosChallenger::new(word([1, 2, 3, 4]));
780        challenger.observe_felt(felt(200));
781        let _ = challenger.sample_felt();
782        assert_eq!(challenger.mode, EidosChallengerMode::Squeezing);
783        assert_ne!(challenger.output_len, 0);
784
785        assert_grind_matches_independent_check(challenger, 4);
786    }
787
788    fn assert_grind_matches_independent_check(mut challenger: EidosChallenger, bits: usize) {
789        let mut verifier = challenger.clone();
790        let witness = challenger.grind(bits);
791
792        assert!(verifier.check_witness(bits, witness));
793        assert_eq!(snapshot(&challenger), snapshot(&verifier));
794    }
795}