1use 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
35pub struct Eidos;
36
37impl Eidos {
38 #[inline]
45 pub fn compress(cv: Word, block: [Felt; BLOCK_LEN]) -> Word {
46 compression::compress_felt_block(cv, block)
47 }
48
49 #[inline]
53 pub fn compress_packed(cv: PackedChainingValue, block: PackedBlock) -> PackedChainingValue {
54 compression::compress_packed_felt_cv(&cv, &block)
55 }
56
57 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
130 pub fn hash(bytes: &[u8]) -> Word {
131 Self::hash_in_domain(bytes, GENERIC_BYTE_STRING)
132 }
133
134 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 #[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 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 #[inline]
213 pub fn merge(values: &[Word; 2]) -> Word {
214 compress_digest_pair(values, MERKLE_NODE_INIT_CV)
215 }
216
217 #[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 #[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 #[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 #[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 #[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 #[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}