1use std::fmt;
4
5use agent_uri::{CapabilityPath, TrustRoot};
6use sha2::{Digest, Sha256};
7
8#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
35pub struct DhtKey([u8; 32]);
36
37impl DhtKey {
38 #[must_use]
44 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
45 Self(bytes)
46 }
47
48 #[must_use]
50 pub const fn as_bytes(&self) -> &[u8; 32] {
51 &self.0
52 }
53
54 #[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 #[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 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 #[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 #[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 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 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 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; 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; 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}