use alloy::primitives::keccak256;
use rand::Rng;
use std::sync::LazyLock;
pub static TAG: LazyLock<[u8; 4]> = LazyLock::new(|| {
let hash = keccak256(b"mpp");
[hash[0], hash[1], hash[2], hash[3]]
});
pub const VERSION: u8 = 0x01;
pub const ANONYMOUS: [u8; 10] = [0u8; 10];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedMemo {
pub version: u8,
pub server_fingerprint: [u8; 10],
pub client_fingerprint: Option<[u8; 10]>,
pub nonce: [u8; 7],
}
fn fingerprint(value: &str) -> [u8; 10] {
let hash = keccak256(value.as_bytes());
let mut fp = [0u8; 10];
fp.copy_from_slice(&hash[..10]);
fp
}
pub fn encode(server_id: &str, client_id: Option<&str>) -> [u8; 32] {
let mut buf = [0u8; 32];
buf[..4].copy_from_slice(&*TAG);
buf[4] = VERSION;
buf[5..15].copy_from_slice(&fingerprint(server_id));
if let Some(cid) = client_id {
buf[15..25].copy_from_slice(&fingerprint(cid));
}
let mut rng = rand::rng();
rng.fill(&mut buf[25..32]);
buf
}
pub fn encode_hex(server_id: &str, client_id: Option<&str>) -> String {
hex::encode(encode(server_id, client_id))
}
pub fn is_mpp_memo(memo: &[u8; 32]) -> bool {
memo[..4] == *TAG && memo[4] == VERSION
}
pub fn verify_server(memo: &[u8; 32], server_id: &str) -> bool {
if !is_mpp_memo(memo) {
return false;
}
memo[5..15] == fingerprint(server_id)
}
pub fn decode(memo: &[u8; 32]) -> Option<DecodedMemo> {
if !is_mpp_memo(memo) {
return None;
}
let version = memo[4];
let mut server_fingerprint = [0u8; 10];
server_fingerprint.copy_from_slice(&memo[5..15]);
let mut client_bytes = [0u8; 10];
client_bytes.copy_from_slice(&memo[15..25]);
let client_fingerprint = if client_bytes == ANONYMOUS {
None
} else {
Some(client_bytes)
};
let mut nonce = [0u8; 7];
nonce.copy_from_slice(&memo[25..32]);
Some(DecodedMemo {
version,
server_fingerprint,
client_fingerprint,
nonce,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tag_matches_keccak() {
let hash = keccak256(b"mpp");
assert_eq!(*TAG, hash[..4]);
}
#[test]
fn test_encode_is_32_bytes() {
let memo = encode("api.example.com", None);
assert_eq!(memo.len(), 32);
}
#[test]
fn test_is_mpp_memo() {
let memo = encode("api.example.com", None);
assert!(is_mpp_memo(&memo));
let zeros = [0u8; 32];
assert!(!is_mpp_memo(&zeros));
}
#[test]
fn test_verify_server() {
let memo = encode("api.example.com", None);
assert!(verify_server(&memo, "api.example.com"));
assert!(!verify_server(&memo, "other.example.com"));
}
#[test]
fn test_decode_without_client() {
let memo = encode("api.example.com", None);
let decoded = decode(&memo).unwrap();
assert_eq!(decoded.version, VERSION);
assert_eq!(decoded.server_fingerprint, fingerprint("api.example.com"));
assert_eq!(decoded.client_fingerprint, None);
}
#[test]
fn test_decode_with_client() {
let memo = encode("api.example.com", Some("my-app"));
let decoded = decode(&memo).unwrap();
assert_eq!(decoded.version, VERSION);
assert_eq!(decoded.server_fingerprint, fingerprint("api.example.com"));
assert_eq!(decoded.client_fingerprint, Some(fingerprint("my-app")));
}
#[test]
fn test_decode_invalid_memo() {
let zeros = [0u8; 32];
assert!(decode(&zeros).is_none());
}
#[test]
fn test_encode_hex() {
let hex_str = encode_hex("api.example.com", None);
assert_eq!(hex_str.len(), 64);
}
#[test]
fn test_round_trip() {
let memo = encode("srv", Some("cli"));
assert!(is_mpp_memo(&memo));
assert!(verify_server(&memo, "srv"));
let decoded = decode(&memo).unwrap();
assert_eq!(decoded.server_fingerprint, fingerprint("srv"));
assert_eq!(decoded.client_fingerprint, Some(fingerprint("cli")));
}
}