Skip to main content

miden_crypto/hash/eidos/
construction.rs

1//! The framed Eidos hash construction.
2
3use alloc::vec::Vec;
4use core::array;
5
6use p3_symmetric::CryptographicHasher;
7
8use super::{
9    BLOCK_LEN, DIGEST_WIDTH, PACKED_LANES, PackedBlock, PackedChainingValue, PackedDigest,
10    PackedFelt, compression,
11    domain::{ByteString, EidosDomain, FeltSequence, Transcript},
12    domains::{GENERIC_BYTE_STRING, GENERIC_FELT_SEQUENCE},
13    encoding,
14    frame::EidosFrame,
15    framing::{self, GENERIC_FELT_TAG, MERKLE_NODE_INIT_CV},
16};
17use crate::{Felt, Word, field::BasedVectorSpace};
18
19/// Eidos hash construction.
20///
21/// Byte strings and field-element strings use distinct typed, registered domains. Both
22/// constructions bind the exact input length into the initial chaining value. The fixed
23/// two-to-one Merkle compression exposed by [`Self::merge`] is the sole reserved zero-tag
24/// construction and is deliberately distinct from ordinary Felt-sequence hashing.
25/// The `CryptographicHasher<u64, _>` implementations are bit-level adapters for the generic
26/// Felt-sequence construction. Canonical Goldilocks encodings produce exactly the same digest as
27/// their `Felt` counterparts. Other `u64` values are split into their two limbs without reduction;
28/// this deterministic extension is not a separate registered message domain.
29///
30/// Digests occupy a 252-bit packed subspace and therefore provide at most 126 bits of generic
31/// collision resistance.
32/// Digest coordinates are below `2^63`; use [`crate::rand::EidosRandomCoin`] when uniform
33/// field samples are required. See the [module documentation](super) for the output contract.
34#[derive(Debug, Copy, Clone, Eq, PartialEq)]
35pub struct Eidos;
36
37impl Eidos {
38    /// Compress one complete block under a caller-supplied chaining value.
39    ///
40    /// This is the raw compression layer underlying the Eidos hash construction. It does not add
41    /// domain separation, length binding, padding, or any other message framing. The input CV may
42    /// contain arbitrary canonical field elements; only the output CV is restricted to Eidos's
43    /// 252-bit packed subspace.
44    #[inline]
45    pub fn compress(cv: Word, block: [Felt; BLOCK_LEN]) -> Word {
46        compression::compress_felt_block(cv, block)
47    }
48
49    /// Compress one complete block in each packed lane.
50    ///
51    /// Each lane is independent. Like [`Self::compress`], this adds no message framing.
52    #[inline]
53    pub fn compress_packed(cv: PackedChainingValue, block: PackedBlock) -> PackedChainingValue {
54        compression::compress_packed_felt_cv(&cv, &block)
55    }
56
57    /// Return all sixteen raw XOF output lanes for one complete block.
58    ///
59    /// Each `u32` lane is embedded as one field element. This is raw XOF material for a
60    /// caller-supplied CV, not an Eidos digest, and no message framing is added.
61    #[inline]
62    pub fn compress_xof(cv: Word, block: [Felt; BLOCK_LEN]) -> [Felt; 16] {
63        Self::compress_xof_lanes(cv, block).map(Felt::from_u32)
64    }
65
66    #[inline]
67    pub(crate) fn compress_xof_lanes(cv: Word, block: [Felt; BLOCK_LEN]) -> [u32; 16] {
68        compression::compress_xof_cv(encoding::word_to_cv(cv), encoding::encode_felt_block(&block))
69    }
70
71    /// Construct the framed initial CV used by a Fiat-Shamir challenger.
72    ///
73    /// The registered transcript tag occupies the domain lane and all three parameter lanes are
74    /// zero. The transcript's subsequent absorb and squeeze schedule is defined by its domain.
75    #[inline]
76    pub fn transcript_init_cv<D>(domain: D) -> Word
77    where
78        D: EidosDomain<Encoding = Transcript>,
79    {
80        Self::init_chaining_word_with_params(domain, [0; 3])
81    }
82
83    /// Construct an initial chaining word with `param0` in the first parameter lane.
84    ///
85    /// The remaining two parameter lanes are zero. Standard Felt- and byte-sequence domains use
86    /// `param0` for the complete logical input length; custom domains define it in their registry
87    /// schema.
88    #[inline]
89    pub fn init_chaining_word<D: EidosDomain>(domain: D, param0: u32) -> Word {
90        Self::init_chaining_word_with_params(domain, [param0, 0, 0])
91    }
92
93    /// Construct an initial chaining value from a registered domain tag and three parameters.
94    ///
95    /// The domain defines the construction and the meaning of its parameters. Every supplied
96    /// value occupies one complete low u32 lane; the corresponding high lane is a fixed IV
97    /// word.
98    #[inline]
99    pub fn init_chaining_word_with_params<D: EidosDomain>(_: D, params: [u32; 3]) -> Word {
100        EidosFrame::new(D::TAG, params).initial_chaining_word()
101    }
102
103    /// Construct an initial chaining value from a structurally valid runtime tag and three
104    /// parameters.
105    ///
106    /// This is the dynamic counterpart of [`Self::init_chaining_word_with_params`] for registries
107    /// such as deferred precompiles, where the concrete domain is selected at runtime. Constructing
108    /// a [`super::DomainTag`] establishes only its structural and namespace rules; the caller
109    /// remains responsible for checking membership in the relevant owner registry and enforcing
110    /// that domain's parameter and payload schema.
111    #[inline]
112    pub fn init_chaining_word_with_tag(tag: super::DomainTag, params: [u32; 3]) -> Word {
113        EidosFrame::new(tag, params).initial_chaining_word()
114    }
115
116    /// Construct the same one-parameter initial chaining word in every packed lane.
117    ///
118    /// The interpretation of `param0` is defined by the registered domain.
119    #[inline]
120    pub fn init_packed_chaining_word<D: EidosDomain>(_: D, param0: u32) -> PackedChainingValue {
121        framing::init_packed_cv(D::TAG.as_u32(), [param0, 0, 0])
122    }
123
124    /// Hash a byte string with the registered generic byte-string domain.
125    ///
126    /// # Panics
127    ///
128    /// Panics if the byte length does not fit in `u32`.
129    #[inline]
130    pub fn hash(bytes: &[u8]) -> Word {
131        Self::hash_in_domain(bytes, GENERIC_BYTE_STRING)
132    }
133
134    /// Hash a byte string under a typed byte-string domain.
135    ///
136    /// A Felt-sequence or custom-schedule domain cannot be passed to this function.
137    ///
138    /// # Panics
139    ///
140    /// Panics if the byte length does not fit in `u32`.
141    pub fn hash_in_domain<D>(bytes: &[u8], _: D) -> Word
142    where
143        D: EidosDomain<Encoding = ByteString>,
144    {
145        let len = u32::try_from(bytes.len()).expect("input too long: byte count must fit in u32");
146        let mut cv = framing::init_cv(D::TAG.as_u32(), [len, 0, 0]);
147
148        if bytes.is_empty() {
149            cv = compression::compress_cv(cv, [0; 16]);
150        } else {
151            for chunk in bytes.chunks(64) {
152                cv = compression::compress_cv(cv, encoding::encode_byte_block(chunk));
153            }
154        }
155
156        encoding::mask_and_pack_word(cv)
157    }
158
159    /// Hash a field-element sequence under the registered generic Felt-sequence domain.
160    ///
161    /// # Panics
162    ///
163    /// Panics if the flattened Felt length overflows `usize` or does not fit in `u32`.
164    #[inline]
165    pub fn hash_elements<E: BasedVectorSpace<Felt>>(elements: &[E]) -> Word {
166        Self::hash_elements_in_domain(elements, GENERIC_FELT_SEQUENCE)
167    }
168
169    /// Hash a field-element sequence under a typed Felt-sequence domain.
170    ///
171    /// A byte-string or custom-schedule domain cannot be passed to this function.
172    ///
173    /// # Panics
174    ///
175    /// Panics if the flattened Felt length overflows `usize` or does not fit in `u32`.
176    ///
177    /// ```compile_fail
178    /// use miden_crypto::{Felt, hash::eidos::{Eidos, domains::GENERIC_BYTE_STRING}};
179    ///
180    /// let values = [Felt::ZERO];
181    /// let _ = Eidos::hash_elements_in_domain(&values, GENERIC_BYTE_STRING);
182    /// ```
183    ///
184    /// Raw field elements are not domain declarations either:
185    ///
186    /// ```compile_fail
187    /// use miden_crypto::{Felt, hash::eidos::Eidos};
188    ///
189    /// let values = [Felt::ZERO];
190    /// let byte_tag = Felt::new_unchecked(0x0000_0301);
191    /// let _ = Eidos::hash_elements_in_domain(&values, byte_tag);
192    /// ```
193    pub fn hash_elements_in_domain<E, D>(elements: &[E], _: D) -> Word
194    where
195        E: BasedVectorSpace<Felt>,
196        D: EidosDomain<Encoding = FeltSequence>,
197    {
198        let len = elements
199            .len()
200            .checked_mul(E::DIMENSION)
201            .expect("input too long: felt count overflowed usize");
202        let iter = elements
203            .iter()
204            .flat_map(|element| E::as_basis_coefficients_slice(element).iter().copied());
205        Word::new(hash_felt_iter_in_domain_with_len(iter, len, D::TAG.as_u32()))
206    }
207
208    /// Compress two digest words as one reserved Merkle inner node.
209    ///
210    /// This fixed, one-block construction uses the reserved all-zero framing tuple and is
211    /// intentionally not equivalent to [`Self::hash_elements`] over the same eight Felts.
212    #[inline]
213    pub fn merge(values: &[Word; 2]) -> Word {
214        compress_digest_pair(values, MERKLE_NODE_INIT_CV)
215    }
216
217    /// Return the initial chaining word reserved for Merkle inner-node compression.
218    ///
219    /// Its four injected framing lanes are all zero. It is exposed for implementations which
220    /// schedule [`Self::merge`] through a separate compression engine; ordinary callers should
221    /// use [`Self::merge`] directly.
222    #[inline]
223    pub fn merkle_node_init_chaining_word() -> Word {
224        Word::new(framing::init_u64_cv(0, [0; 3]).map(Felt::new_unchecked))
225    }
226
227    /// Compress two packed digest words as reserved Merkle inner nodes in every packed lane.
228    ///
229    /// This is the packed equivalent of [`Self::merge`].
230    #[inline]
231    pub fn merge_packed(values: &[PackedDigest; 2]) -> PackedDigest {
232        let block = array::from_fn(|i| values[i / DIGEST_WIDTH][i % DIGEST_WIDTH]);
233        let cv = framing::init_packed_u32_cv(0, [0; 3]);
234        encoding::pack_cv_to_felts(compression::compress_packed_felt_block(&cv, &block))
235    }
236
237    /// Hash two words as one generic eight-Felt sequence.
238    #[inline]
239    pub fn hash_double_word(values: &[Word; 2]) -> Word {
240        Self::hash_double_word_in_domain(values, GENERIC_FELT_SEQUENCE)
241    }
242
243    /// Hash packed pairs of words as generic eight-Felt sequences.
244    #[inline]
245    pub fn hash_double_word_packed(values: &[PackedDigest; 2]) -> PackedDigest {
246        let cv = Self::init_packed_chaining_word(GENERIC_FELT_SEQUENCE, BLOCK_LEN as u32);
247        compress_packed_digest_pair(values, &cv)
248    }
249
250    /// Hash two words as one eight-Felt sequence under a typed Felt-sequence domain.
251    #[inline]
252    pub fn hash_double_word_in_domain<D>(values: &[Word; 2], _: D) -> Word
253    where
254        D: EidosDomain<Encoding = FeltSequence>,
255    {
256        let cv = framing::init_cv(D::TAG.as_u32(), [BLOCK_LEN as u32, 0, 0]);
257        compress_digest_pair(values, cv)
258    }
259
260    /// Hash a sequence of words under the generic Felt-sequence domain.
261    ///
262    /// # Panics
263    ///
264    /// Panics if the flattened Felt length does not fit in `u32`.
265    #[inline]
266    pub fn hash_words(values: &[Word]) -> Word {
267        Self::hash_elements(Word::words_as_elements(values))
268    }
269}
270
271#[inline]
272fn compress_digest_pair(values: &[Word; 2], cv: [u32; 8]) -> Word {
273    let block: [Felt; BLOCK_LEN] = array::from_fn(|i| {
274        if i < DIGEST_WIDTH {
275            values[0][i]
276        } else {
277            values[1][i - DIGEST_WIDTH]
278        }
279    });
280    encoding::mask_and_pack_word(compression::compress_cv(cv, encoding::encode_felt_block(&block)))
281}
282
283#[inline]
284fn compress_packed_digest_pair(
285    values: &[PackedDigest; 2],
286    cv: &PackedChainingValue,
287) -> PackedDigest {
288    let block = array::from_fn(|i| {
289        if i < DIGEST_WIDTH {
290            values[0][i]
291        } else {
292            values[1][i - DIGEST_WIDTH]
293        }
294    });
295    compression::compress_packed_felt_cv(cv, &block)
296}
297
298#[inline]
299fn exact_size_hint<I: Iterator>(iter: &I) -> Option<usize> {
300    let (lower, upper) = iter.size_hint();
301    upper.filter(|&upper| upper == lower)
302}
303
304fn hash_felt_iter_in_domain_with_len<I>(iter: I, len: usize, domain: u32) -> [Felt; DIGEST_WIDTH]
305where
306    I: Iterator<Item = Felt>,
307{
308    let len_u32 = u32::try_from(len).expect("input too long: felt count must fit in u32");
309    let cv = framing::fold_blocks::<BLOCK_LEN, _, _>(
310        iter,
311        len,
312        framing::init_cv(domain, [len_u32, 0, 0]),
313        Felt::ZERO,
314        |cv, block| compression::compress_cv(cv, encoding::encode_felt_block(&block)),
315    );
316    encoding::mask_and_pack_word(cv).into()
317}
318
319fn hash_u64_iter_with_len<I>(iter: I, len: usize) -> [u64; DIGEST_WIDTH]
320where
321    I: Iterator<Item = u64>,
322{
323    let len_u32 = u32::try_from(len).expect("input too long: felt count must fit in u32");
324    let cv = framing::fold_blocks::<BLOCK_LEN, _, _>(
325        iter,
326        len,
327        framing::init_cv(GENERIC_FELT_TAG, [len_u32, 0, 0]),
328        0,
329        compression::compress_u64_cv,
330    );
331    encoding::pack_cv_to_u64s(cv)
332}
333
334fn hash_packed_felt_iter_with_len<I>(iter: I, len: usize) -> PackedDigest
335where
336    I: Iterator<Item = PackedFelt>,
337{
338    let len_u32 = u32::try_from(len).expect("input too long: felt count must fit in u32");
339    let cv = framing::fold_blocks::<BLOCK_LEN, _, _>(
340        iter,
341        len,
342        framing::init_packed_u32_cv(GENERIC_FELT_TAG, [len_u32, 0, 0]),
343        [Felt::ZERO; PACKED_LANES],
344        |cv, block| compression::compress_packed_felt_block(&cv, &block),
345    );
346    encoding::pack_cv_to_felts(cv)
347}
348
349fn hash_packed_u64_iter_with_len<I>(iter: I, len: usize) -> [[u64; PACKED_LANES]; DIGEST_WIDTH]
350where
351    I: Iterator<Item = [u64; PACKED_LANES]>,
352{
353    let len_u32 = u32::try_from(len).expect("input too long: felt count must fit in u32");
354    let cv = framing::fold_blocks::<BLOCK_LEN, _, _>(
355        iter,
356        len,
357        framing::init_packed_u32_cv(GENERIC_FELT_TAG, [len_u32, 0, 0]),
358        [0; PACKED_LANES],
359        |cv, block| compression::compress_packed_u64_block(&cv, &block),
360    );
361    compression::pack_packed_u64_cv(&cv)
362}
363
364impl CryptographicHasher<Felt, [Felt; DIGEST_WIDTH]> for Eidos {
365    fn hash_iter<I>(&self, input: I) -> [Felt; DIGEST_WIDTH]
366    where
367        I: IntoIterator<Item = Felt>,
368    {
369        let iter = input.into_iter();
370        if let Some(len) = exact_size_hint(&iter) {
371            hash_felt_iter_in_domain_with_len(iter, len, GENERIC_FELT_TAG)
372        } else {
373            let elements: Vec<Felt> = iter.collect();
374            let len = elements.len();
375            hash_felt_iter_in_domain_with_len(elements.into_iter(), len, GENERIC_FELT_TAG)
376        }
377    }
378
379    #[inline]
380    fn hash_slice(&self, input: &[Felt]) -> [Felt; DIGEST_WIDTH] {
381        hash_felt_iter_in_domain_with_len(input.iter().copied(), input.len(), GENERIC_FELT_TAG)
382    }
383}
384
385impl CryptographicHasher<u64, [u64; DIGEST_WIDTH]> for Eidos {
386    fn hash_iter<I>(&self, input: I) -> [u64; DIGEST_WIDTH]
387    where
388        I: IntoIterator<Item = u64>,
389    {
390        let iter = input.into_iter();
391        if let Some(len) = exact_size_hint(&iter) {
392            hash_u64_iter_with_len(iter, len)
393        } else {
394            let elements: Vec<u64> = iter.collect();
395            let len = elements.len();
396            hash_u64_iter_with_len(elements.into_iter(), len)
397        }
398    }
399
400    #[inline]
401    fn hash_slice(&self, input: &[u64]) -> [u64; DIGEST_WIDTH] {
402        hash_u64_iter_with_len(input.iter().copied(), input.len())
403    }
404}
405
406impl CryptographicHasher<PackedFelt, PackedDigest> for Eidos {
407    fn hash_iter<I>(&self, input: I) -> PackedDigest
408    where
409        I: IntoIterator<Item = PackedFelt>,
410    {
411        let iter = input.into_iter();
412        if let Some(len) = exact_size_hint(&iter) {
413            hash_packed_felt_iter_with_len(iter, len)
414        } else {
415            let elements: Vec<PackedFelt> = iter.collect();
416            let len = elements.len();
417            hash_packed_felt_iter_with_len(elements.into_iter(), len)
418        }
419    }
420
421    #[inline]
422    fn hash_slice(&self, input: &[PackedFelt]) -> PackedDigest {
423        hash_packed_felt_iter_with_len(input.iter().copied(), input.len())
424    }
425}
426
427impl CryptographicHasher<[u64; PACKED_LANES], [[u64; PACKED_LANES]; DIGEST_WIDTH]> for Eidos {
428    fn hash_iter<I>(&self, input: I) -> [[u64; PACKED_LANES]; DIGEST_WIDTH]
429    where
430        I: IntoIterator<Item = [u64; PACKED_LANES]>,
431    {
432        let iter = input.into_iter();
433        if let Some(len) = exact_size_hint(&iter) {
434            hash_packed_u64_iter_with_len(iter, len)
435        } else {
436            let elements: Vec<[u64; PACKED_LANES]> = iter.collect();
437            let len = elements.len();
438            hash_packed_u64_iter_with_len(elements.into_iter(), len)
439        }
440    }
441
442    #[inline]
443    fn hash_slice(&self, input: &[[u64; PACKED_LANES]]) -> [[u64; PACKED_LANES]; DIGEST_WIDTH] {
444        hash_packed_u64_iter_with_len(input.iter().copied(), input.len())
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::hash::eidos::{
452        DomainTag, DomainVersion, PackedBlock,
453        domain::namespace,
454        domains::{GenericByteStringDomain, GenericFeltSequenceDomain},
455    };
456
457    #[derive(Debug, Copy, Clone)]
458    struct TestTranscriptDomain;
459
460    impl EidosDomain for TestTranscriptDomain {
461        type Encoding = Transcript;
462
463        const NAME: &'static str = "TEST_TRANSCRIPT";
464        const TAG: DomainTag =
465            DomainTag::new(namespace::MIDEN_VM, 0xffff, DomainVersion::numbered(1));
466    }
467
468    struct LooseSizeHint<I>(I);
469
470    impl<I: Iterator> Iterator for LooseSizeHint<I> {
471        type Item = I::Item;
472
473        fn next(&mut self) -> Option<Self::Item> {
474            self.0.next()
475        }
476
477        fn size_hint(&self) -> (usize, Option<usize>) {
478            (0, None)
479        }
480    }
481
482    struct DishonestSizeHint<I> {
483        inner: I,
484        claimed: usize,
485    }
486
487    impl<I: Iterator> Iterator for DishonestSizeHint<I> {
488        type Item = I::Item;
489
490        fn next(&mut self) -> Option<Self::Item> {
491            self.inner.next()
492        }
493
494        fn size_hint(&self) -> (usize, Option<usize>) {
495            (self.claimed, Some(self.claimed))
496        }
497    }
498
499    #[test]
500    fn empty_constructions_each_compress_one_zero_block() {
501        let byte_cv = framing::init_cv(GenericByteStringDomain::TAG.as_u32(), [0; 3]);
502        let felt_cv = framing::init_cv(GenericFeltSequenceDomain::TAG.as_u32(), [0; 3]);
503        assert_eq!(
504            Eidos::hash(&[]),
505            encoding::mask_and_pack_word(compression::compress_cv(byte_cv, [0; 16]))
506        );
507        assert_eq!(
508            Eidos::hash_elements::<Felt>(&[]),
509            encoding::mask_and_pack_word(compression::compress_cv(felt_cv, [0; 16]))
510        );
511        assert_ne!(Eidos::hash(&[]), Eidos::hash_elements::<Felt>(&[]));
512    }
513
514    #[test]
515    fn transcript_init_cv_uses_registered_framing() {
516        assert_eq!(
517            Eidos::transcript_init_cv(TestTranscriptDomain),
518            Eidos::init_chaining_word_with_params(TestTranscriptDomain, [0; 3]),
519        );
520    }
521
522    #[test]
523    fn runtime_tag_initializer_matches_the_typed_initializer() {
524        let params = [1, 2, 3];
525        assert_eq!(
526            Eidos::init_chaining_word_with_tag(TestTranscriptDomain::TAG, params),
527            Eidos::init_chaining_word_with_params(TestTranscriptDomain, params),
528        );
529    }
530
531    #[test]
532    fn framed_full_block_matches_manual_init_then_compress() {
533        let block: [Felt; BLOCK_LEN] =
534            array::from_fn(|i| Felt::new_unchecked((i as u64 + 1) * 0x0101_0101));
535        let cv = Eidos::init_chaining_word(GENERIC_FELT_SEQUENCE, BLOCK_LEN as u32);
536
537        let framed = Eidos::hash_elements_in_domain(&block, GENERIC_FELT_SEQUENCE);
538        assert_eq!(Eidos::compress(cv, block), framed);
539        assert_ne!(Eidos::compress(Word::default(), block), framed);
540    }
541
542    #[test]
543    fn packed_compression_and_two_word_hashes_match_scalar_lanes() {
544        let input_len = (2 * BLOCK_LEN) as u32;
545        let packed_cv = Eidos::init_packed_chaining_word(GENERIC_FELT_SEQUENCE, input_len);
546        let packed_block: PackedBlock = array::from_fn(|element| {
547            array::from_fn(|lane| Felt::new_unchecked((element * 101 + lane * 17 + 3) as u64))
548        });
549        let packed = Eidos::compress_packed(packed_cv, packed_block);
550        let packed_values: [PackedDigest; 2] = [
551            array::from_fn(|word| packed_block[word]),
552            array::from_fn(|word| packed_block[DIGEST_WIDTH + word]),
553        ];
554        let packed_merged = Eidos::merge_packed(&packed_values);
555        let packed_hashed = Eidos::hash_double_word_packed(&packed_values);
556
557        for lane in 0..PACKED_LANES {
558            let scalar_cv = Eidos::init_chaining_word(GENERIC_FELT_SEQUENCE, input_len);
559            let scalar_block = array::from_fn(|element| packed_block[element][lane]);
560            let scalar = Eidos::compress(scalar_cv, scalar_block);
561            let actual = Word::new(array::from_fn(|word| packed[word][lane]));
562            assert_eq!(actual, scalar, "packed lane {lane} diverged");
563
564            let scalar_values = [
565                Word::new(array::from_fn(|word| packed_values[0][word][lane])),
566                Word::new(array::from_fn(|word| packed_values[1][word][lane])),
567            ];
568            let actual = Word::new(array::from_fn(|word| packed_merged[word][lane]));
569            assert_eq!(actual, Eidos::merge(&scalar_values), "packed merge lane {lane} diverged");
570
571            let actual = Word::new(array::from_fn(|word| packed_hashed[word][lane]));
572            assert_eq!(
573                actual,
574                Eidos::hash_double_word(&scalar_values),
575                "packed generic hash lane {lane} diverged"
576            );
577        }
578    }
579
580    #[test]
581    fn all_hasher_representations_match_at_block_boundaries() {
582        for len in [0, 1, 7, 8, 9, 15, 16, 17] {
583            let felts: Vec<Felt> =
584                (0..len).map(|i| Felt::new_unchecked((i as u64 + 1) * 17)).collect();
585            let u64s: Vec<u64> = felts.iter().map(Felt::as_canonical_u64).collect();
586            let felt_digest = <Eidos as CryptographicHasher<Felt, [Felt; DIGEST_WIDTH]>>::hash_iter(
587                &Eidos,
588                felts.iter().copied(),
589            );
590            assert_eq!(felt_digest, Eidos.hash_slice(&felts));
591            let u64_digest = <Eidos as CryptographicHasher<u64, [u64; DIGEST_WIDTH]>>::hash_iter(
592                &Eidos,
593                u64s.iter().copied(),
594            );
595            assert_eq!(u64_digest, Eidos.hash_slice(&u64s));
596            assert_eq!(felt_digest, u64_digest.map(Felt::new_unchecked));
597
598            let packed_felts: Vec<PackedFelt> =
599                felts.iter().map(|felt| [*felt; PACKED_LANES]).collect();
600            let packed_u64s: Vec<[u64; PACKED_LANES]> =
601                u64s.iter().map(|value| [*value; PACKED_LANES]).collect();
602            let packed_felt_digest =
603                <Eidos as CryptographicHasher<PackedFelt, PackedDigest>>::hash_iter(
604                    &Eidos,
605                    packed_felts.iter().copied(),
606                );
607            let packed_u64_digest = <Eidos as CryptographicHasher<
608                [u64; PACKED_LANES],
609                [[u64; PACKED_LANES]; DIGEST_WIDTH],
610            >>::hash_iter(&Eidos, packed_u64s.iter().copied());
611            assert_eq!(packed_felt_digest, Eidos.hash_slice(&packed_felts));
612            assert_eq!(packed_u64_digest, Eidos.hash_slice(&packed_u64s));
613
614            for lane in 0..PACKED_LANES {
615                assert_eq!(
616                    array::from_fn::<_, DIGEST_WIDTH, _>(|word| packed_felt_digest[word][lane]),
617                    felt_digest,
618                );
619                assert_eq!(
620                    array::from_fn::<_, DIGEST_WIDTH, _>(|word| packed_u64_digest[word][lane]),
621                    u64_digest,
622                );
623            }
624        }
625    }
626
627    #[test]
628    fn loose_size_hints_match_exact_iterators_for_all_representations() {
629        let felts: Vec<Felt> = (0..17).map(|i| Felt::new_unchecked((i as u64 + 1) * 17)).collect();
630        let u64s: Vec<u64> = felts.iter().map(Felt::as_canonical_u64).collect();
631        let packed_felts: Vec<PackedFelt> =
632            felts.iter().map(|felt| [*felt; PACKED_LANES]).collect();
633        let packed_u64s: Vec<[u64; PACKED_LANES]> =
634            u64s.iter().map(|value| [*value; PACKED_LANES]).collect();
635
636        assert_eq!(
637            Eidos.hash_iter(felts.iter().copied()),
638            Eidos.hash_iter(LooseSizeHint(felts.into_iter())),
639        );
640        assert_eq!(
641            <Eidos as CryptographicHasher<u64, [u64; DIGEST_WIDTH]>>::hash_iter(
642                &Eidos,
643                u64s.iter().copied(),
644            ),
645            <Eidos as CryptographicHasher<u64, [u64; DIGEST_WIDTH]>>::hash_iter(
646                &Eidos,
647                LooseSizeHint(u64s.into_iter()),
648            ),
649        );
650        assert_eq!(
651            <Eidos as CryptographicHasher<PackedFelt, PackedDigest>>::hash_iter(
652                &Eidos,
653                packed_felts.iter().copied(),
654            ),
655            <Eidos as CryptographicHasher<PackedFelt, PackedDigest>>::hash_iter(
656                &Eidos,
657                LooseSizeHint(packed_felts.into_iter()),
658            ),
659        );
660        assert_eq!(
661            <Eidos as CryptographicHasher<
662                [u64; PACKED_LANES],
663                [[u64; PACKED_LANES]; DIGEST_WIDTH],
664            >>::hash_iter(&Eidos, packed_u64s.iter().copied()),
665            <Eidos as CryptographicHasher<
666                [u64; PACKED_LANES],
667                [[u64; PACKED_LANES]; DIGEST_WIDTH],
668            >>::hash_iter(&Eidos, LooseSizeHint(packed_u64s.into_iter())),
669        );
670    }
671
672    #[test]
673    #[should_panic(expected = "iterator yielded a different length than its size_hint")]
674    fn dishonest_exact_size_hint_is_rejected() {
675        let iter = DishonestSizeHint {
676            inner: [Felt::ONE, Felt::ONE].into_iter(),
677            claimed: 3,
678        };
679        let _: [Felt; DIGEST_WIDTH] = Eidos.hash_iter(iter);
680    }
681}