Skip to main content

agent_mesh_protocol/
fingerprint.rs

1//! BLAKE3-based fingerprint for keys and content-addressed payloads.
2//!
3//! A `Fingerprint` is just the 32-byte BLAKE3 hash of some canonical
4//! byte representation (typically a 32-byte ed25519 public key, but
5//! also used for envelope payloads). It's small, equality-comparable,
6//! and prints as a short hex prefix suitable for log lines.
7
8use crate::MeshError;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12/// A 32-byte BLAKE3 hash, used as the canonical ID for keys and
13/// content-addressed blobs.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct Fingerprint(pub [u8; 32]);
16
17impl Fingerprint {
18    /// Hash an arbitrary byte slice with BLAKE3, returning the
19    /// resulting fingerprint.
20    #[must_use]
21    pub fn of_bytes(data: &[u8]) -> Self {
22        let h = blake3::hash(data);
23        Self(*h.as_bytes())
24    }
25
26    /// 12-character hex prefix, suitable for human display in log
27    /// lines and CLI output. Six bytes is enough collision resistance
28    /// to disambiguate hundreds of agents on a single user.
29    #[must_use]
30    pub fn short(&self) -> String {
31        hex::encode(&self.0[..6])
32    }
33
34    /// Full 64-character hex encoding.
35    #[must_use]
36    pub fn hex(&self) -> String {
37        hex::encode(self.0)
38    }
39}
40
41impl fmt::Display for Fingerprint {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{}", self.short())
44    }
45}
46
47impl std::str::FromStr for Fingerprint {
48    type Err = MeshError;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        let bytes = hex::decode(s).map_err(|e| MeshError::Encoding(e.to_string()))?;
52        if bytes.len() != 32 {
53            return Err(MeshError::Encoding(format!(
54                "expected 32 bytes, got {}",
55                bytes.len()
56            )));
57        }
58        let mut arr = [0u8; 32];
59        arr.copy_from_slice(&bytes);
60        Ok(Self(arr))
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use std::str::FromStr;
68
69    #[test]
70    fn short_truncates_to_12_hex_chars() {
71        let fp = Fingerprint::of_bytes(b"hello world");
72        let s = fp.short();
73        assert_eq!(s.len(), 12, "short should be 12 hex chars (6 bytes)");
74        assert!(s.chars().all(|c| c.is_ascii_hexdigit()));
75    }
76
77    #[test]
78    fn roundtrip_hex() {
79        let fp = Fingerprint::of_bytes(b"some bytes");
80        let h = fp.hex();
81        assert_eq!(h.len(), 64);
82        let parsed = Fingerprint::from_str(&h).expect("parse roundtrip");
83        assert_eq!(fp, parsed);
84    }
85
86    #[test]
87    fn equality() {
88        let a = Fingerprint::of_bytes(b"x");
89        let b = Fingerprint::of_bytes(b"x");
90        let c = Fingerprint::of_bytes(b"y");
91        assert_eq!(a, b);
92        assert_ne!(a, c);
93    }
94
95    #[test]
96    fn from_str_rejects_wrong_length() {
97        let err = Fingerprint::from_str("deadbeef").expect_err("too short");
98        match err {
99            MeshError::Encoding(_) => {}
100            other => panic!("expected Encoding, got {other:?}"),
101        }
102    }
103
104    #[test]
105    fn from_str_rejects_non_hex() {
106        let err = Fingerprint::from_str("zz").expect_err("not hex");
107        match err {
108            MeshError::Encoding(_) => {}
109            other => panic!("expected Encoding, got {other:?}"),
110        }
111    }
112
113    #[test]
114    fn display_matches_short() {
115        let fp = Fingerprint::of_bytes(b"display test");
116        assert_eq!(format!("{fp}"), fp.short());
117    }
118
119    #[test]
120    fn debug_does_not_panic() {
121        let fp = Fingerprint::of_bytes(b"debug");
122        let _ = format!("{fp:?}");
123    }
124
125    #[test]
126    fn hash_in_collection() {
127        use std::collections::HashSet;
128        let mut set = HashSet::new();
129        set.insert(Fingerprint::of_bytes(b"a"));
130        set.insert(Fingerprint::of_bytes(b"a"));
131        set.insert(Fingerprint::of_bytes(b"b"));
132        assert_eq!(set.len(), 2);
133    }
134}