use miden_protocol::crypto::hash::rpo::Rpo256;
use miden_protocol::{Felt, Word};
use std::sync::OnceLock;
const DOMAIN_TAG_BYTES: &[u8] = b"guardian.lookup.v1";
fn domain_tag() -> Word {
static TAG: OnceLock<Word> = OnceLock::new();
*TAG.get_or_init(|| {
let mut elements = Vec::with_capacity(DOMAIN_TAG_BYTES.len().div_ceil(8));
for chunk in DOMAIN_TAG_BYTES.chunks(8) {
let mut chunk_bytes = [0u8; 8];
chunk_bytes[..chunk.len()].copy_from_slice(chunk);
elements.push(Felt::new(u64::from_le_bytes(chunk_bytes)));
}
Rpo256::hash_elements(&elements)
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LookupAuthMessage {
timestamp_ms: i64,
key_commitment: Word,
}
impl LookupAuthMessage {
pub fn new(timestamp_ms: i64, key_commitment: Word) -> Self {
Self {
timestamp_ms,
key_commitment,
}
}
pub fn timestamp_ms(&self) -> i64 {
self.timestamp_ms
}
pub fn key_commitment(&self) -> Word {
self.key_commitment
}
pub fn to_word(&self) -> Word {
let tag = domain_tag();
let tag_elements = tag.as_elements();
let kc_elements = self.key_commitment.as_elements();
let timestamp_felt = Felt::new(self.timestamp_ms as u64);
let message_elements: [Felt; 9] = [
tag_elements[0],
tag_elements[1],
tag_elements[2],
tag_elements[3],
timestamp_felt,
kc_elements[0],
kc_elements[1],
kc_elements[2],
kc_elements[3],
];
Rpo256::hash_elements(&message_elements)
}
}
pub fn lookup_domain_tag() -> Word {
domain_tag()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth_request_message::AuthRequestMessage;
use crate::auth_request_payload::AuthRequestPayload;
use miden_protocol::account::AccountId;
fn sample_commitment(seed: u32) -> Word {
Word::from([
seed,
seed.wrapping_add(1),
seed.wrapping_add(2),
seed.wrapping_add(3),
])
}
#[test]
fn domain_tag_is_stable_across_calls() {
let a = lookup_domain_tag();
let b = lookup_domain_tag();
assert_eq!(a, b);
}
#[test]
fn domain_tag_is_not_zero_word() {
let tag = lookup_domain_tag();
assert_ne!(tag, Word::from([Felt::ZERO; 4]));
}
#[test]
fn digest_changes_with_commitment() {
let timestamp = 1_700_000_000_000i64;
let left = LookupAuthMessage::new(timestamp, sample_commitment(1)).to_word();
let right = LookupAuthMessage::new(timestamp, sample_commitment(2)).to_word();
assert_ne!(left, right);
}
#[test]
fn digest_changes_with_timestamp() {
let commitment = sample_commitment(7);
let left = LookupAuthMessage::new(1_700_000_000_000, commitment).to_word();
let right = LookupAuthMessage::new(1_700_000_000_001, commitment).to_word();
assert_ne!(left, right);
}
#[test]
fn digest_is_deterministic() {
let msg = LookupAuthMessage::new(1_700_000_000_000, sample_commitment(42));
assert_eq!(msg.to_word(), msg.to_word());
}
#[test]
fn digest_handles_extreme_timestamps() {
let commitment = sample_commitment(99);
let zero = LookupAuthMessage::new(0, commitment).to_word();
let large = LookupAuthMessage::new(i64::MAX, commitment).to_word();
assert_ne!(zero, large);
let negative = LookupAuthMessage::new(-1, commitment).to_word();
assert_ne!(negative, zero);
}
#[test]
fn lookup_digest_is_distinct_from_auth_request_digest() {
let timestamp = 1_700_000_000_000i64;
let commitment = sample_commitment(123);
let lookup_digest = LookupAuthMessage::new(timestamp, commitment).to_word();
let account_id =
AccountId::from_hex("0x8a65fc5a39e4cd106d648e3eb4ab5f").expect("valid account id");
let payload = AuthRequestPayload::from_bytes(&commitment.as_bytes());
let request_digest = AuthRequestMessage::new(account_id, timestamp, payload).to_word();
assert_ne!(lookup_digest, request_digest);
}
#[test]
fn domain_tag_is_known_constant() {
let tag = lookup_domain_tag();
let mut elements: Vec<Felt> = Vec::new();
for chunk in DOMAIN_TAG_BYTES.chunks(8) {
let mut bytes = [0u8; 8];
bytes[..chunk.len()].copy_from_slice(chunk);
elements.push(Felt::new(u64::from_le_bytes(bytes)));
}
let expected = Rpo256::hash_elements(&elements);
assert_eq!(tag, expected);
}
}