guardian_shared/
lookup_auth_message.rs1use miden_protocol::crypto::hash::rpo::Rpo256;
2use miden_protocol::{Felt, Word};
3use std::sync::OnceLock;
4
5const DOMAIN_TAG_BYTES: &[u8] = b"guardian.lookup.v1";
14
15fn domain_tag() -> Word {
17 static TAG: OnceLock<Word> = OnceLock::new();
18 *TAG.get_or_init(|| {
19 let mut elements = Vec::with_capacity(DOMAIN_TAG_BYTES.len().div_ceil(8));
20 for chunk in DOMAIN_TAG_BYTES.chunks(8) {
21 let mut chunk_bytes = [0u8; 8];
22 chunk_bytes[..chunk.len()].copy_from_slice(chunk);
23 elements.push(Felt::new(u64::from_le_bytes(chunk_bytes)));
24 }
25 Rpo256::hash_elements(&elements)
26 })
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct LookupAuthMessage {
39 timestamp_ms: i64,
40 key_commitment: Word,
41}
42
43impl LookupAuthMessage {
44 pub fn new(timestamp_ms: i64, key_commitment: Word) -> Self {
45 Self {
46 timestamp_ms,
47 key_commitment,
48 }
49 }
50
51 pub fn timestamp_ms(&self) -> i64 {
52 self.timestamp_ms
53 }
54
55 pub fn key_commitment(&self) -> Word {
56 self.key_commitment
57 }
58
59 pub fn to_word(&self) -> Word {
71 let tag = domain_tag();
72 let tag_elements = tag.as_elements();
73 let kc_elements = self.key_commitment.as_elements();
74 let timestamp_felt = Felt::new(self.timestamp_ms as u64);
75 let message_elements: [Felt; 9] = [
76 tag_elements[0],
77 tag_elements[1],
78 tag_elements[2],
79 tag_elements[3],
80 timestamp_felt,
81 kc_elements[0],
82 kc_elements[1],
83 kc_elements[2],
84 kc_elements[3],
85 ];
86 Rpo256::hash_elements(&message_elements)
87 }
88}
89
90pub fn lookup_domain_tag() -> Word {
93 domain_tag()
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::auth_request_message::AuthRequestMessage;
100 use crate::auth_request_payload::AuthRequestPayload;
101 use miden_protocol::account::AccountId;
102
103 fn sample_commitment(seed: u32) -> Word {
104 Word::from([
105 seed,
106 seed.wrapping_add(1),
107 seed.wrapping_add(2),
108 seed.wrapping_add(3),
109 ])
110 }
111
112 #[test]
113 fn domain_tag_is_stable_across_calls() {
114 let a = lookup_domain_tag();
115 let b = lookup_domain_tag();
116 assert_eq!(a, b);
117 }
118
119 #[test]
120 fn domain_tag_is_not_zero_word() {
121 let tag = lookup_domain_tag();
122 assert_ne!(tag, Word::from([Felt::ZERO; 4]));
123 }
124
125 #[test]
126 fn digest_changes_with_commitment() {
127 let timestamp = 1_700_000_000_000i64;
128 let left = LookupAuthMessage::new(timestamp, sample_commitment(1)).to_word();
129 let right = LookupAuthMessage::new(timestamp, sample_commitment(2)).to_word();
130 assert_ne!(left, right);
131 }
132
133 #[test]
134 fn digest_changes_with_timestamp() {
135 let commitment = sample_commitment(7);
136 let left = LookupAuthMessage::new(1_700_000_000_000, commitment).to_word();
137 let right = LookupAuthMessage::new(1_700_000_000_001, commitment).to_word();
138 assert_ne!(left, right);
139 }
140
141 #[test]
142 fn digest_is_deterministic() {
143 let msg = LookupAuthMessage::new(1_700_000_000_000, sample_commitment(42));
144 assert_eq!(msg.to_word(), msg.to_word());
145 }
146
147 #[test]
148 fn digest_handles_extreme_timestamps() {
149 let commitment = sample_commitment(99);
150 let zero = LookupAuthMessage::new(0, commitment).to_word();
151 let large = LookupAuthMessage::new(i64::MAX, commitment).to_word();
152 assert_ne!(zero, large);
153 let negative = LookupAuthMessage::new(-1, commitment).to_word();
156 assert_ne!(negative, zero);
157 }
158
159 #[test]
160 fn lookup_digest_is_distinct_from_auth_request_digest() {
161 let timestamp = 1_700_000_000_000i64;
165 let commitment = sample_commitment(123);
166
167 let lookup_digest = LookupAuthMessage::new(timestamp, commitment).to_word();
168
169 let account_id =
170 AccountId::from_hex("0x8a65fc5a39e4cd106d648e3eb4ab5f").expect("valid account id");
171 let payload = AuthRequestPayload::from_bytes(&commitment.as_bytes());
172 let request_digest = AuthRequestMessage::new(account_id, timestamp, payload).to_word();
173
174 assert_ne!(lookup_digest, request_digest);
175 }
176
177 #[test]
178 fn domain_tag_is_known_constant() {
179 let tag = lookup_domain_tag();
183 let mut elements: Vec<Felt> = Vec::new();
187 for chunk in DOMAIN_TAG_BYTES.chunks(8) {
188 let mut bytes = [0u8; 8];
189 bytes[..chunk.len()].copy_from_slice(chunk);
190 elements.push(Felt::new(u64::from_le_bytes(bytes)));
191 }
192 let expected = Rpo256::hash_elements(&elements);
193 assert_eq!(tag, expected);
194 }
195}