miden_crypto/hash/eidos/
frame.rs1use super::{
4 domain::{DomainTag, EidosDomain},
5 encoding, framing,
6};
7use crate::{Felt, Word};
8
9#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
15pub struct EidosFrame {
16 domain: DomainTag,
17 params: [u32; 3],
18}
19
20impl EidosFrame {
21 pub const FELT_LEN: usize = 4;
23
24 pub const fn new(domain: DomainTag, params: [u32; 3]) -> Self {
26 Self { domain, params }
27 }
28
29 pub const fn for_domain<D: EidosDomain>(_: D, params: [u32; 3]) -> Self {
31 Self::new(D::TAG, params)
32 }
33
34 pub const fn domain(self) -> DomainTag {
36 self.domain
37 }
38
39 pub const fn params(self) -> [u32; 3] {
41 self.params
42 }
43
44 pub const fn as_u32s(self) -> [u32; Self::FELT_LEN] {
46 [self.domain.as_u32(), self.params[0], self.params[1], self.params[2]]
47 }
48
49 pub const fn as_word(self) -> Word {
51 let values = self.as_u32s();
52 Word::new([
53 Felt::new_unchecked(values[0] as u64),
54 Felt::new_unchecked(values[1] as u64),
55 Felt::new_unchecked(values[2] as u64),
56 Felt::new_unchecked(values[3] as u64),
57 ])
58 }
59
60 pub fn from_word(word: Word) -> Option<Self> {
65 let [domain, param0, param1, param2] = word.into_elements().map(felt_to_u32);
66 Some(Self::new(DomainTag::from_u32(domain?)?, [param0?, param1?, param2?]))
67 }
68
69 #[inline]
71 pub fn initial_chaining_word(self) -> Word {
72 Word::new(framing::init_u64_cv(self.domain.as_u32(), self.params).map(Felt::new_unchecked))
73 }
74
75 pub fn from_initial_chaining_word(word: Word) -> Option<Self> {
80 let lanes = encoding::word_to_cv(word);
81 let expected = framing::init_cv(lanes[0], [lanes[2], lanes[4], lanes[6]]);
82 if lanes != expected {
83 return None;
84 }
85
86 Some(Self::new(DomainTag::from_u32(lanes[0])?, [lanes[2], lanes[4], lanes[6]]))
87 }
88}
89
90impl From<EidosFrame> for Word {
91 fn from(frame: EidosFrame) -> Self {
92 frame.as_word()
93 }
94}
95
96fn felt_to_u32(value: Felt) -> Option<u32> {
97 u32::try_from(value.as_canonical_u64()).ok()
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::hash::eidos::{
104 DomainVersion,
105 domain::namespace,
106 domains::{GENERIC_BYTE_STRING, GENERIC_FELT_SEQUENCE},
107 };
108
109 #[test]
110 fn frame_round_trips_through_words_and_initial_chaining_words() {
111 for frame in [
112 EidosFrame::for_domain(GENERIC_FELT_SEQUENCE, [0; 3]),
113 EidosFrame::for_domain(GENERIC_BYTE_STRING, [1, 2, 3]),
114 EidosFrame::new(
115 DomainTag::new(namespace::MIDEN_VM, u16::MAX, DomainVersion::numbered(u8::MAX)),
116 [u32::MAX; 3],
117 ),
118 ] {
119 assert_eq!(EidosFrame::from_word(frame.as_word()), Some(frame));
120 assert_eq!(
121 EidosFrame::from_initial_chaining_word(frame.initial_chaining_word()),
122 Some(frame)
123 );
124 }
125 }
126
127 #[test]
128 fn frame_word_rejects_non_u32_values_and_invalid_domains() {
129 let too_large = Felt::new_unchecked(u32::MAX as u64 + 1);
130 assert_eq!(
131 EidosFrame::from_word(Word::new([too_large, Felt::ZERO, Felt::ZERO, Felt::ZERO,])),
132 None
133 );
134 assert_eq!(EidosFrame::from_word(Word::default()), None);
135
136 let valid = EidosFrame::for_domain(GENERIC_FELT_SEQUENCE, [0; 3]).as_word();
137 for index in 1..4 {
138 let mut values = valid.into_elements();
139 values[index] = too_large;
140 assert_eq!(EidosFrame::from_word(Word::new(values)), None);
141 }
142 }
143
144 #[test]
145 fn initial_chaining_word_rejects_wrong_fixed_lanes() {
146 let frame = EidosFrame::for_domain(GENERIC_FELT_SEQUENCE, [1, 2, 3]);
147 for index in 0..4 {
148 let mut values = frame.initial_chaining_word().into_elements();
149 values[index] = Felt::new_unchecked(values[index].as_canonical_u64() ^ (1 << 32));
150 assert_eq!(EidosFrame::from_initial_chaining_word(Word::new(values)), None);
151 }
152 }
153
154 #[test]
155 fn initial_chaining_word_rejects_masked_iv() {
156 let frame = EidosFrame::for_domain(GENERIC_FELT_SEQUENCE, [0; 3]);
157 let masked =
158 encoding::mask_and_pack_word(encoding::word_to_cv(frame.initial_chaining_word()));
159 assert_eq!(EidosFrame::from_initial_chaining_word(masked), None);
160 }
161}