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 framing::{self, GENERIC_FELT_TAG, MERKLE_NODE_INIT_CV},
15};
16use crate::{Felt, Word, field::BasedVectorSpace};
17
18#[derive(Debug, Copy, Clone, Eq, PartialEq)]
32pub struct Eidos;
33
34impl Eidos {
35 #[inline]
42 pub fn compress(cv: Word, block: [Felt; BLOCK_LEN]) -> Word {
43 compression::compress_felt_block(cv, block)
44 }
45
46 #[inline]
50 pub fn compress_packed(cv: PackedChainingValue, block: PackedBlock) -> PackedChainingValue {
51 compression::compress_packed_felt_cv(&cv, &block)
52 }
53
54 #[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 #[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 #[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 #[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 #[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 #[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 #[inline]
127 pub fn hash(bytes: &[u8]) -> Word {
128 Self::hash_in_domain(bytes, GENERIC_BYTE_STRING)
129 }
130
131 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 #[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 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 #[inline]
210 pub fn merge(values: &[Word; 2]) -> Word {
211 compress_digest_pair(values, MERKLE_NODE_INIT_CV)
212 }
213
214 #[inline]
220 pub fn merkle_node_init_chaining_word() -> Word {
221 encoding::output_cv_to_word(MERKLE_NODE_INIT_CV)
222 }
223
224 #[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 #[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 #[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}