Skip to main content

agent_store/
fingerprint.rs

1//! Writer identity.
2//!
3//! A [`Fingerprint`] is a 32-byte BLAKE3 digest of an ed25519 public key —
4//! the same derivation `agent-mesh-protocol` uses for its `Fingerprint`, so
5//! the two are **wire-compatible**: the hex string a newt writes here is the
6//! hex string its mesh identity announces.
7//!
8//! It is kept local (rather than depending on `agent-mesh-protocol`) so the
9//! substrate carries no hard dependency on a specific mesh-protocol release.
10//! A future `mesh` feature can add `From`/`Into` conversions once a consumer
11//! needs them.
12
13use std::fmt;
14
15/// A 32-byte writer identity (BLAKE3 of an ed25519 public key).
16#[derive(Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Fingerprint([u8; 32]);
18
19impl Fingerprint {
20    /// Wrap 32 raw bytes that are already a fingerprint.
21    pub fn from_bytes(bytes: [u8; 32]) -> Self {
22        Self(bytes)
23    }
24
25    /// Derive a fingerprint from an ed25519 public key (BLAKE3 of the key
26    /// bytes). Deterministic and wire-compatible with the mesh.
27    pub fn from_ed25519_pubkey(pubkey: &[u8]) -> Self {
28        Self(*blake3::hash(pubkey).as_bytes())
29    }
30
31    /// The raw 32 bytes.
32    pub fn as_bytes(&self) -> &[u8; 32] {
33        &self.0
34    }
35
36    /// Lower-case hex (64 chars) — the canonical string form used as the
37    /// `writer` column in [`crate::WriterLog`].
38    pub fn to_hex(&self) -> String {
39        blake3::Hash::from_bytes(self.0).to_hex().to_string()
40    }
41
42    /// Parse a 64-char lower-case hex string back into a fingerprint.
43    pub fn from_hex(s: &str) -> Option<Self> {
44        blake3::Hash::from_hex(s).ok().map(|h| Self(*h.as_bytes()))
45    }
46}
47
48impl fmt::Display for Fingerprint {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str(&self.to_hex())
51    }
52}
53
54impl fmt::Debug for Fingerprint {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "Fingerprint({})", self.to_hex())
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn derivation_is_deterministic() {
66        let a = Fingerprint::from_ed25519_pubkey(b"some-ed25519-pubkey-bytes");
67        let b = Fingerprint::from_ed25519_pubkey(b"some-ed25519-pubkey-bytes");
68        assert_eq!(a, b);
69    }
70
71    #[test]
72    fn different_keys_differ() {
73        let a = Fingerprint::from_ed25519_pubkey(b"key-one");
74        let b = Fingerprint::from_ed25519_pubkey(b"key-two");
75        assert_ne!(a, b);
76    }
77
78    #[test]
79    fn hex_round_trips() {
80        let fp = Fingerprint::from_ed25519_pubkey(b"round-trip-me");
81        let hex = fp.to_hex();
82        assert_eq!(hex.len(), 64);
83        assert_eq!(Fingerprint::from_hex(&hex), Some(fp));
84    }
85
86    #[test]
87    fn from_hex_rejects_garbage() {
88        assert_eq!(Fingerprint::from_hex("not-hex"), None);
89        assert_eq!(Fingerprint::from_hex("dead"), None); // wrong length
90    }
91}