Skip to main content

miden_crypto/hash/eidos/
frame.rs

1//! Eidos framing values and their initial chaining words.
2
3use super::{
4    domain::{DomainTag, EidosDomain},
5    encoding, framing,
6};
7use crate::{Felt, Word};
8
9/// A structurally valid Eidos frame.
10///
11/// The domain defines the meaning of the three parameters and the payload schedule. This type
12/// checks the numeric framing representation; registry membership and domain-specific parameter
13/// rules remain the responsibility of the owning registry.
14#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
15pub struct EidosFrame {
16    domain: DomainTag,
17    params: [u32; 3],
18}
19
20impl EidosFrame {
21    /// Number of field elements in the canonical frame representation.
22    pub const FELT_LEN: usize = 4;
23
24    /// Creates a frame from a structurally valid domain tag and its domain-defined parameters.
25    pub const fn new(domain: DomainTag, params: [u32; 3]) -> Self {
26        Self { domain, params }
27    }
28
29    /// Creates a frame for a typed domain.
30    pub const fn for_domain<D: EidosDomain>(_: D, params: [u32; 3]) -> Self {
31        Self::new(D::TAG, params)
32    }
33
34    /// Returns the frame's domain tag.
35    pub const fn domain(self) -> DomainTag {
36        self.domain
37    }
38
39    /// Returns the three parameters whose meanings are defined by the domain.
40    pub const fn params(self) -> [u32; 3] {
41        self.params
42    }
43
44    /// Returns the canonical four-u32 representation `(domain, param0, param1, param2)`.
45    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    /// Returns the canonical four-Felt representation used by protocol encodings.
50    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    /// Parses a canonical four-Felt frame.
61    ///
62    /// Every element must fit in a `u32`, and the first element must encode a structurally valid
63    /// domain tag. This does not check membership in an owner registry.
64    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    /// Derives the initial chaining word for this frame.
70    #[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    /// Recovers a frame from an initial chaining word produced by Eidos framing.
76    ///
77    /// The fixed high lanes must match those set by [`Self::initial_chaining_word`]. Registry
78    /// membership and domain-specific parameter rules are not checked.
79    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}