⎇ e6d21c60273e028ce20801c3287895cc7777c43a
shared/card/card-net/src/key.rs
Historia
Quién tocó qué
crudo
feat: card standalone — primitiva de identidad soberana (hoja, autocontenida)
1//! Claves namespaced del DHT compartido — la **primitiva unificadora** de
2//! Brahman (Capa 1).
3//!
4//! El ecosistema brahman corre UN solo Kademlia (en este crate, `card-net`).
5//! Para que distintos dominios — código indexado (minga), Cards
6//! (card-discovery), Personas (ágora), servicios — coexistan sin colisión,
7//! cada clave lleva un byte de `kind` como prefijo. La representación en wire
8//! es de longitud fija: `[kind_tag] ++ blake3(id)` = 33 bytes.
9//!
10//! Vive en `card-net` (y no en un dominio concreto) precisamente porque es el
11//! namespace COMÚN: minga, agora y card-discovery la comparten. `minga-dht`
12//! la re-exporta por compatibilidad histórica.
13
14use serde::{Deserialize, Serialize};
15
16/// Tipo de registro — el namespace de una clave en el DHT.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18pub enum RecordKind {
19 /// Bloque de código indexado (minga).
20 Code,
21 /// `Card` brahman (módulo, ente, etc.).
22 Card,
23 /// `Persona` de ágora (identidad humana federada).
24 Persona,
25 /// Endpoint de servicio.
26 Service,
27 /// Dominio definido por el consumidor.
28 Custom(u8),
29}
30
31impl RecordKind {
32 /// Byte de etiqueta. `Custom(n)` ocupa `0x80 | n` (top bit) para no
33 /// chocar nunca con los kinds estándar (`0x00..`).
34 pub fn tag(&self) -> u8 {
35 match self {
36 RecordKind::Code => 0x01,
37 RecordKind::Card => 0x02,
38 RecordKind::Persona => 0x03,
39 RecordKind::Service => 0x04,
40 RecordKind::Custom(n) => 0x80 | (n & 0x7f),
41 }
42 }
43}
44
45/// Longitud fija de la clave en wire: 1 byte de kind + 32 de hash.
46pub const DHT_KEY_LEN: usize = 33;
47
48/// Clave de DHT namespaced. Dos formas de construcción:
49///
50/// - Con un `id` legible (`new`, `code`, `card`, `persona`) — el wire
51/// hashea el id con blake3. Útil cuando el consumidor publica
52/// identidades simbólicas (nombres de módulos, slugs de personas).
53/// - Con `for_hash` — el wire usa los 32 bytes del hash directamente.
54/// Útil cuando el id YA es un blake3 (como en minga, que indexa
55/// contenido por su α-hash, o ágora, cuyo `IdentityId` ya es
56/// `blake3(pubkey)`) — evita una segunda pasada de blake3.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct DhtKey {
59 kind: RecordKind,
60 /// Representación canónica de los 32 bytes que forman la "id" en wire.
61 /// Si se construyó por nombre simbólico, son `blake3(id_string)`.
62 /// Si se construyó por hash directo, son el hash tal cual.
63 body: [u8; 32],
64 /// `id` legible — para `Display`. `None` si se construyó por hash.
65 label: Option<String>,
66}
67
68impl DhtKey {
69 pub fn new(kind: RecordKind, id: impl Into<String>) -> Self {
70 let id = id.into();
71 let body = *blake3::hash(id.as_bytes()).as_bytes();
72 Self {
73 kind,
74 body,
75 label: Some(id),
76 }
77 }
78
79 /// Clave para un bloque de código (id legible).
80 pub fn code(id: impl Into<String>) -> Self {
81 Self::new(RecordKind::Code, id)
82 }
83
84 /// Clave para una Card.
85 pub fn card(id: impl Into<String>) -> Self {
86 Self::new(RecordKind::Card, id)
87 }
88
89 /// Clave para una Persona.
90 pub fn persona(id: impl Into<String>) -> Self {
91 Self::new(RecordKind::Persona, id)
92 }
93
94 /// Clave a partir de un hash ya computado (32 bytes). El wire usa
95 /// esos bytes directamente, **sin re-hashear**.
96 pub fn for_hash(kind: RecordKind, hash: [u8; 32]) -> Self {
97 Self {
98 kind,
99 body: hash,
100 label: None,
101 }
102 }
103
104 pub fn kind(&self) -> RecordKind {
105 self.kind
106 }
107
108 pub fn id(&self) -> Option<&str> {
109 self.label.as_deref()
110 }
111
112 /// Representación en wire: `[kind_tag] ++ body`, 33 bytes.
113 pub fn to_bytes(&self) -> Vec<u8> {
114 let mut out = Vec::with_capacity(DHT_KEY_LEN);
115 out.push(self.kind.tag());
116 out.extend_from_slice(&self.body);
117 out
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn wire_key_has_fixed_length() {
127 assert_eq!(DhtKey::card("modulo-x").to_bytes().len(), DHT_KEY_LEN);
128 assert_eq!(DhtKey::code("fn-hash").to_bytes().len(), DHT_KEY_LEN);
129 }
130
131 #[test]
132 fn same_id_different_kind_does_not_collide() {
133 let a = DhtKey::card("foo").to_bytes();
134 let b = DhtKey::code("foo").to_bytes();
135 let c = DhtKey::persona("foo").to_bytes();
136 assert_ne!(a, b);
137 assert_ne!(b, c);
138 assert_ne!(a, c);
139 // El hash del id es el mismo; sólo difiere el byte de kind.
140 assert_eq!(a[1..], b[1..]);
141 assert_ne!(a[0], b[0]);
142 }
143
144 #[test]
145 fn same_kind_and_id_is_stable() {
146 assert_eq!(DhtKey::card("x").to_bytes(), DhtKey::card("x").to_bytes());
147 }
148
149 #[test]
150 fn for_hash_no_rehashea() {
151 // for_hash debe usar los bytes tal cual: body = hash, no blake3(hash).
152 let h = [7u8; 32];
153 let k = DhtKey::for_hash(RecordKind::Persona, h);
154 let wire = k.to_bytes();
155 assert_eq!(wire[0], RecordKind::Persona.tag());
156 assert_eq!(&wire[1..], &h);
157 }
158
159 #[test]
160 fn custom_kind_never_collides_with_standard() {
161 for std in [RecordKind::Code, RecordKind::Card, RecordKind::Persona, RecordKind::Service] {
162 for n in 0..=127u8 {
163 assert_ne!(std.tag(), RecordKind::Custom(n).tag());
164 }
165 }
166 }
167}