Skip to main content

agent_uri_dht/
key.rs

1//! DHT key derivation and distance metrics.
2
3use std::fmt;
4
5use agent_uri::{CapabilityPath, TrustRoot};
6use sha2::{Digest, Sha256};
7
8/// DHT key derived from trust root and capability path.
9///
10/// A 256-bit hash used as the key in a Kademlia-style DHT.
11/// Keys are derived deterministically from trust root and capability path,
12/// enabling capability-based discovery.
13///
14/// # Key Derivation
15///
16/// ```text
17/// key = SHA256(trust_root || "/" || capability_path)
18/// ```
19///
20/// # Examples
21///
22/// ```
23/// use agent_uri::{TrustRoot, CapabilityPath};
24/// use agent_uri_dht::DhtKey;
25///
26/// let trust_root = TrustRoot::parse("anthropic.com").unwrap();
27/// let path = CapabilityPath::parse("assistant/chat").unwrap();
28/// let key = DhtKey::derive(&trust_root, &path);
29///
30/// // Keys are deterministic
31/// let key2 = DhtKey::derive(&trust_root, &path);
32/// assert_eq!(key, key2);
33/// ```
34#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub struct DhtKey([u8; 32]);
36
37impl DhtKey {
38    /// Creates a `DhtKey` from raw bytes.
39    ///
40    /// # Arguments
41    ///
42    /// * `bytes` - 32-byte array representing the key
43    #[must_use]
44    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
45        Self(bytes)
46    }
47
48    /// Returns the key as a byte slice.
49    #[must_use]
50    pub const fn as_bytes(&self) -> &[u8; 32] {
51        &self.0
52    }
53
54    /// Derives a DHT key from trust root and capability path.
55    ///
56    /// The key is computed as: `SHA256(trust_root || "/" || capability_path)`
57    ///
58    /// # Arguments
59    ///
60    /// * `trust_root` - The trust root (authority)
61    /// * `capability_path` - The capability path
62    ///
63    /// # Returns
64    ///
65    /// A deterministic 256-bit key for DHT lookup.
66    #[must_use]
67    pub fn derive(trust_root: &TrustRoot, capability_path: &CapabilityPath) -> Self {
68        let mut hasher = Sha256::new();
69        hasher.update(trust_root.as_str().as_bytes());
70        hasher.update(b"/");
71        hasher.update(capability_path.as_str().as_bytes());
72
73        let result = hasher.finalize();
74        let mut bytes = [0u8; 32];
75        bytes.copy_from_slice(&result);
76        Self(bytes)
77    }
78
79    /// Derives a DHT key for a specific path depth (prefix query support).
80    ///
81    /// This enables hierarchical discovery by deriving keys for path prefixes.
82    ///
83    /// # Arguments
84    ///
85    /// * `trust_root` - The trust root (authority)
86    /// * `capability_path` - The full capability path
87    /// * `depth` - The number of path segments to include (1-indexed)
88    ///
89    /// # Returns
90    ///
91    /// A key for the path truncated to the specified depth.
92    /// Returns `None` if depth is 0 or exceeds the path depth.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use agent_uri::{TrustRoot, CapabilityPath};
98    /// use agent_uri_dht::DhtKey;
99    ///
100    /// let trust_root = TrustRoot::parse("anthropic.com").unwrap();
101    /// let path = CapabilityPath::parse("assistant/chat/streaming").unwrap();
102    ///
103    /// // Key for "assistant" only
104    /// let key1 = DhtKey::derive_at_depth(&trust_root, &path, 1).unwrap();
105    ///
106    /// // Key for "assistant/chat"
107    /// let key2 = DhtKey::derive_at_depth(&trust_root, &path, 2).unwrap();
108    ///
109    /// // Full path key
110    /// let key3 = DhtKey::derive_at_depth(&trust_root, &path, 3).unwrap();
111    /// assert_eq!(key3, DhtKey::derive(&trust_root, &path));
112    /// ```
113    #[must_use]
114    pub fn derive_at_depth(
115        trust_root: &TrustRoot,
116        capability_path: &CapabilityPath,
117        depth: usize,
118    ) -> Option<Self> {
119        if depth == 0 || depth > capability_path.depth() {
120            return None;
121        }
122
123        let mut hasher = Sha256::new();
124        hasher.update(trust_root.as_str().as_bytes());
125        hasher.update(b"/");
126
127        // Build truncated path
128        let segments = capability_path.segments();
129        let prefix_path: String = segments[..depth]
130            .iter()
131            .map(agent_uri::PathSegment::as_str)
132            .collect::<Vec<_>>()
133            .join("/");
134
135        hasher.update(prefix_path.as_bytes());
136
137        let result = hasher.finalize();
138        let mut bytes = [0u8; 32];
139        bytes.copy_from_slice(&result);
140        Some(Self(bytes))
141    }
142
143    /// Computes the XOR distance to another key.
144    ///
145    /// This is the fundamental distance metric for Kademlia routing.
146    /// Closer keys (lower XOR distance) are more likely to be stored
147    /// on the same or nearby nodes.
148    ///
149    /// # Arguments
150    ///
151    /// * `other` - The key to compute distance to
152    ///
153    /// # Returns
154    ///
155    /// A `DhtKey` representing the XOR distance (interpretable as a 256-bit unsigned integer).
156    #[must_use]
157    pub fn distance(&self, other: &Self) -> Self {
158        let mut result = [0u8; 32];
159        for (i, (a, b)) in self.0.iter().zip(other.0.iter()).enumerate() {
160            result[i] = a ^ b;
161        }
162        Self(result)
163    }
164
165    /// Returns the leading zero bits in the key.
166    ///
167    /// Useful for Kademlia bucket calculations.
168    #[must_use]
169    pub fn leading_zeros(&self) -> u32 {
170        let mut count = 0u32;
171        for byte in &self.0 {
172            if *byte == 0 {
173                count += 8;
174            } else {
175                count += byte.leading_zeros();
176                break;
177            }
178        }
179        count
180    }
181}
182
183impl fmt::Debug for DhtKey {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        write!(f, "DhtKey({self})")
186    }
187}
188
189impl fmt::Display for DhtKey {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        // Display as hex, truncated for readability
192        for byte in &self.0[..8] {
193            write!(f, "{byte:02x}")?;
194        }
195        write!(f, "...")
196    }
197}
198
199impl AsRef<[u8]> for DhtKey {
200    fn as_ref(&self) -> &[u8] {
201        &self.0
202    }
203}
204
205#[cfg(feature = "serde")]
206impl serde::Serialize for DhtKey {
207    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
208    where
209        S: serde::Serializer,
210    {
211        // Serialize as hex string
212        use core::fmt::Write as _;
213
214        let mut hex = String::with_capacity(self.0.len() * 2);
215        for byte in &self.0 {
216            let _ = write!(&mut hex, "{byte:02x}");
217        }
218        serializer.serialize_str(&hex)
219    }
220}
221
222#[cfg(feature = "serde")]
223impl<'de> serde::Deserialize<'de> for DhtKey {
224    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
225    where
226        D: serde::Deserializer<'de>,
227    {
228        let hex = String::deserialize(deserializer)?;
229        if hex.len() != 64 {
230            return Err(serde::de::Error::custom(
231                "DhtKey hex string must be 64 characters",
232            ));
233        }
234        let mut bytes = [0u8; 32];
235        for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
236            let s = std::str::from_utf8(chunk).map_err(serde::de::Error::custom)?;
237            bytes[i] = u8::from_str_radix(s, 16).map_err(serde::de::Error::custom)?;
238        }
239        Ok(Self(bytes))
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    fn hex(key: &DhtKey) -> String {
248        use std::fmt::Write as _;
249        key.as_bytes().iter().fold(String::new(), |mut out, byte| {
250            let _ = write!(out, "{byte:02x}");
251            out
252        })
253    }
254
255    #[test]
256    fn derivation_matches_the_specs_test_vectors() {
257        // SPECIFICATION.md Appendix B.4. A key derivation is an interoperability
258        // surface: two implementations that disagree on one byte cannot see each
259        // other's registrations at all. Pinning the published digests here is
260        // what stops a refactor from moving them quietly.
261        let anthropic = TrustRoot::parse("anthropic.com").unwrap();
262        let chat = CapabilityPath::parse("assistant/chat").unwrap();
263        assert_eq!(
264            hex(&DhtKey::derive(&anthropic, &chat)),
265            "ee7f343128163eec1164fb5afc0a019df215fc73decb14bc58fef1a4966e8262"
266        );
267
268        let openai = TrustRoot::parse("openai.com").unwrap();
269        assert_eq!(
270            hex(&DhtKey::derive(&openai, &chat)),
271            "c5a97797f98cc507b8604ebd16a27071e87056b047c8f2625182287d14b31f53"
272        );
273
274        let acme = TrustRoot::parse("acme.com").unwrap();
275        let invoice = CapabilityPath::parse("workflow/approval/invoice").unwrap();
276        let expected = [
277            "16889f14c0da9c42cae8063d495e33b4fa1b12cabfdd019c1491b217a56c857a",
278            "b15b22d3c95b3091743a071ed616d9715038a7afd559a7dc28f3d7a1f9eec03e",
279            "d9786664a610a9aaa2799a65c6bd3f9baa44a067f7511cb179c63041021f25f2",
280        ];
281        for (depth, want) in (1..=3).zip(expected) {
282            let key = DhtKey::derive_at_depth(&acme, &invoice, depth).unwrap();
283            assert_eq!(hex(&key), want, "depth {depth}");
284        }
285    }
286
287    #[test]
288    fn derive_is_deterministic() {
289        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
290        let path = CapabilityPath::parse("assistant/chat").unwrap();
291
292        let key1 = DhtKey::derive(&trust_root, &path);
293        let key2 = DhtKey::derive(&trust_root, &path);
294
295        assert_eq!(key1, key2);
296    }
297
298    #[test]
299    fn different_inputs_produce_different_keys() {
300        let trust_root1 = TrustRoot::parse("anthropic.com").unwrap();
301        let trust_root2 = TrustRoot::parse("openai.com").unwrap();
302        let path = CapabilityPath::parse("assistant/chat").unwrap();
303
304        let key1 = DhtKey::derive(&trust_root1, &path);
305        let key2 = DhtKey::derive(&trust_root2, &path);
306
307        assert_ne!(key1, key2);
308    }
309
310    #[test]
311    fn different_paths_produce_different_keys() {
312        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
313        let path1 = CapabilityPath::parse("assistant/chat").unwrap();
314        let path2 = CapabilityPath::parse("assistant/code").unwrap();
315
316        let key1 = DhtKey::derive(&trust_root, &path1);
317        let key2 = DhtKey::derive(&trust_root, &path2);
318
319        assert_ne!(key1, key2);
320    }
321
322    #[test]
323    fn derive_at_depth_zero_returns_none() {
324        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
325        let path = CapabilityPath::parse("assistant/chat").unwrap();
326
327        assert!(DhtKey::derive_at_depth(&trust_root, &path, 0).is_none());
328    }
329
330    #[test]
331    fn derive_at_depth_exceeding_path_returns_none() {
332        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
333        let path = CapabilityPath::parse("assistant/chat").unwrap();
334
335        assert!(DhtKey::derive_at_depth(&trust_root, &path, 3).is_none());
336    }
337
338    #[test]
339    fn derive_at_full_depth_equals_derive() {
340        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
341        let path = CapabilityPath::parse("assistant/chat").unwrap();
342
343        let key1 = DhtKey::derive(&trust_root, &path);
344        let key2 = DhtKey::derive_at_depth(&trust_root, &path, 2).unwrap();
345
346        assert_eq!(key1, key2);
347    }
348
349    #[test]
350    fn derive_at_partial_depth_produces_different_key() {
351        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
352        let path = CapabilityPath::parse("assistant/chat/streaming").unwrap();
353
354        let key1 = DhtKey::derive_at_depth(&trust_root, &path, 1).unwrap();
355        let key2 = DhtKey::derive_at_depth(&trust_root, &path, 2).unwrap();
356        let key3 = DhtKey::derive_at_depth(&trust_root, &path, 3).unwrap();
357
358        assert_ne!(key1, key2);
359        assert_ne!(key2, key3);
360        assert_ne!(key1, key3);
361    }
362
363    #[test]
364    fn distance_to_self_is_zero() {
365        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
366        let path = CapabilityPath::parse("assistant/chat").unwrap();
367        let key = DhtKey::derive(&trust_root, &path);
368
369        let distance = key.distance(&key);
370
371        assert_eq!(distance, DhtKey::from_bytes([0u8; 32]));
372    }
373
374    #[test]
375    fn distance_is_symmetric() {
376        let trust_root = TrustRoot::parse("anthropic.com").unwrap();
377        let path1 = CapabilityPath::parse("assistant/chat").unwrap();
378        let path2 = CapabilityPath::parse("assistant/code").unwrap();
379
380        let key1 = DhtKey::derive(&trust_root, &path1);
381        let key2 = DhtKey::derive(&trust_root, &path2);
382
383        assert_eq!(key1.distance(&key2), key2.distance(&key1));
384    }
385
386    #[test]
387    fn leading_zeros_for_zero_key() {
388        let key = DhtKey::from_bytes([0u8; 32]);
389        assert_eq!(key.leading_zeros(), 256);
390    }
391
392    #[test]
393    fn leading_zeros_for_first_byte_set() {
394        let mut bytes = [0u8; 32];
395        bytes[0] = 0x80; // 10000000 in binary
396        let key = DhtKey::from_bytes(bytes);
397        assert_eq!(key.leading_zeros(), 0);
398    }
399
400    #[test]
401    fn leading_zeros_for_partial_byte() {
402        let mut bytes = [0u8; 32];
403        bytes[0] = 0x08; // 00001000 in binary
404        let key = DhtKey::from_bytes(bytes);
405        assert_eq!(key.leading_zeros(), 4);
406    }
407
408    #[test]
409    fn display_shows_truncated_hex() {
410        let key = DhtKey::from_bytes([0xab; 32]);
411        let display = format!("{key}");
412        assert!(display.starts_with("abababab"));
413        assert!(display.ends_with("..."));
414    }
415
416    #[test]
417    fn debug_includes_display() {
418        let key = DhtKey::from_bytes([0xab; 32]);
419        let debug = format!("{key:?}");
420        assert!(debug.starts_with("DhtKey("));
421        assert!(debug.contains("abababab"));
422    }
423}