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