use std::fmt;
use agent_uri::{CapabilityPath, TrustRoot};
use sha2::{Digest, Sha256};
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DhtKey([u8; 32]);
impl DhtKey {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn derive(trust_root: &TrustRoot, capability_path: &CapabilityPath) -> Self {
let mut hasher = Sha256::new();
hasher.update(trust_root.as_str().as_bytes());
hasher.update(b"/");
hasher.update(capability_path.as_str().as_bytes());
let result = hasher.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&result);
Self(bytes)
}
#[must_use]
pub fn derive_at_depth(
trust_root: &TrustRoot,
capability_path: &CapabilityPath,
depth: usize,
) -> Option<Self> {
if depth == 0 || depth > capability_path.depth() {
return None;
}
let mut hasher = Sha256::new();
hasher.update(trust_root.as_str().as_bytes());
hasher.update(b"/");
let segments = capability_path.segments();
let prefix_path: String = segments[..depth]
.iter()
.map(agent_uri::PathSegment::as_str)
.collect::<Vec<_>>()
.join("/");
hasher.update(prefix_path.as_bytes());
let result = hasher.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&result);
Some(Self(bytes))
}
#[must_use]
pub fn distance(&self, other: &Self) -> Self {
let mut result = [0u8; 32];
for (i, (a, b)) in self.0.iter().zip(other.0.iter()).enumerate() {
result[i] = a ^ b;
}
Self(result)
}
#[must_use]
pub fn leading_zeros(&self) -> u32 {
let mut count = 0u32;
for byte in &self.0 {
if *byte == 0 {
count += 8;
} else {
count += byte.leading_zeros();
break;
}
}
count
}
}
impl fmt::Debug for DhtKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DhtKey({self})")
}
}
impl fmt::Display for DhtKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in &self.0[..8] {
write!(f, "{byte:02x}")?;
}
write!(f, "...")
}
}
impl AsRef<[u8]> for DhtKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for DhtKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let hex: String = self.0.iter().map(|b| format!("{b:02x}")).collect();
serializer.serialize_str(&hex)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for DhtKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Deserialize;
let hex = String::deserialize(deserializer)?;
if hex.len() != 64 {
return Err(serde::de::Error::custom(
"DhtKey hex string must be 64 characters",
));
}
let mut bytes = [0u8; 32];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let s = std::str::from_utf8(chunk).map_err(serde::de::Error::custom)?;
bytes[i] = u8::from_str_radix(s, 16).map_err(serde::de::Error::custom)?;
}
Ok(Self(bytes))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_is_deterministic() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path = CapabilityPath::parse("assistant/chat").unwrap();
let key1 = DhtKey::derive(&trust_root, &path);
let key2 = DhtKey::derive(&trust_root, &path);
assert_eq!(key1, key2);
}
#[test]
fn different_inputs_produce_different_keys() {
let trust_root1 = TrustRoot::parse("anthropic.com").unwrap();
let trust_root2 = TrustRoot::parse("openai.com").unwrap();
let path = CapabilityPath::parse("assistant/chat").unwrap();
let key1 = DhtKey::derive(&trust_root1, &path);
let key2 = DhtKey::derive(&trust_root2, &path);
assert_ne!(key1, key2);
}
#[test]
fn different_paths_produce_different_keys() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path1 = CapabilityPath::parse("assistant/chat").unwrap();
let path2 = CapabilityPath::parse("assistant/code").unwrap();
let key1 = DhtKey::derive(&trust_root, &path1);
let key2 = DhtKey::derive(&trust_root, &path2);
assert_ne!(key1, key2);
}
#[test]
fn derive_at_depth_zero_returns_none() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path = CapabilityPath::parse("assistant/chat").unwrap();
assert!(DhtKey::derive_at_depth(&trust_root, &path, 0).is_none());
}
#[test]
fn derive_at_depth_exceeding_path_returns_none() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path = CapabilityPath::parse("assistant/chat").unwrap();
assert!(DhtKey::derive_at_depth(&trust_root, &path, 3).is_none());
}
#[test]
fn derive_at_full_depth_equals_derive() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path = CapabilityPath::parse("assistant/chat").unwrap();
let key1 = DhtKey::derive(&trust_root, &path);
let key2 = DhtKey::derive_at_depth(&trust_root, &path, 2).unwrap();
assert_eq!(key1, key2);
}
#[test]
fn derive_at_partial_depth_produces_different_key() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path = CapabilityPath::parse("assistant/chat/streaming").unwrap();
let key1 = DhtKey::derive_at_depth(&trust_root, &path, 1).unwrap();
let key2 = DhtKey::derive_at_depth(&trust_root, &path, 2).unwrap();
let key3 = DhtKey::derive_at_depth(&trust_root, &path, 3).unwrap();
assert_ne!(key1, key2);
assert_ne!(key2, key3);
assert_ne!(key1, key3);
}
#[test]
fn distance_to_self_is_zero() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path = CapabilityPath::parse("assistant/chat").unwrap();
let key = DhtKey::derive(&trust_root, &path);
let distance = key.distance(&key);
assert_eq!(distance, DhtKey::from_bytes([0u8; 32]));
}
#[test]
fn distance_is_symmetric() {
let trust_root = TrustRoot::parse("anthropic.com").unwrap();
let path1 = CapabilityPath::parse("assistant/chat").unwrap();
let path2 = CapabilityPath::parse("assistant/code").unwrap();
let key1 = DhtKey::derive(&trust_root, &path1);
let key2 = DhtKey::derive(&trust_root, &path2);
assert_eq!(key1.distance(&key2), key2.distance(&key1));
}
#[test]
fn leading_zeros_for_zero_key() {
let key = DhtKey::from_bytes([0u8; 32]);
assert_eq!(key.leading_zeros(), 256);
}
#[test]
fn leading_zeros_for_first_byte_set() {
let mut bytes = [0u8; 32];
bytes[0] = 0x80; let key = DhtKey::from_bytes(bytes);
assert_eq!(key.leading_zeros(), 0);
}
#[test]
fn leading_zeros_for_partial_byte() {
let mut bytes = [0u8; 32];
bytes[0] = 0x08; let key = DhtKey::from_bytes(bytes);
assert_eq!(key.leading_zeros(), 4);
}
#[test]
fn display_shows_truncated_hex() {
let key = DhtKey::from_bytes([0xab; 32]);
let display = format!("{key}");
assert!(display.starts_with("abababab"));
assert!(display.ends_with("..."));
}
#[test]
fn debug_includes_display() {
let key = DhtKey::from_bytes([0xab; 32]);
let debug = format!("{key:?}");
assert!(debug.starts_with("DhtKey("));
assert!(debug.contains("abababab"));
}
}