tawasuyu

tawasuyu/brahman

Rust 332TOML 127JSON 17Markdown 154710 Definiciones

feat(brahman-dht): B3 — discovery typed sobre el Kademlia compartido

brahman-net corre un único Kademlia para todo el ecosistema.
brahman-dht le pone arriba un esquema de claves namespaced para que
distintos dominios coexistan sin colisión en la misma malla.

- key — RecordKind (Code/Card/Persona/Service/Custom) + DhtKey.
  Wire: [kind_tag] ++ blake3(id) = 33 bytes longitud fija. Custom(n)
  usa 0x80|n: nunca choca con los kinds estándar.
- Dht — wrapper sobre BrahmanNet: announce/withdraw/find (modelo de
  provider records).

Consumidores: minga (Code), brahman-card-discovery (Card), agorapura
(Persona). 5 tests verdes (incl. smoke async sobre un nodo libp2p real).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

27603c906df4258a10ba0f8e05b36126b481f6f5 · por sergio · (hace 5 meses) · padre dc8554d12 · ver el árbol en este commit

Delta semántico · 25

crates/protocol/brahman-dht/src/key.rs

crates/protocol/brahman-dht/src/lib.rs

5 archivos · +226 −0

diff --git a/Cargo.lock b/Cargo.lock
index 042b4a4..e0556cb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1746,6 +1746,17 @@ dependencies = [
  "ulid",
 ]
 
+[[package]]
+name = "brahman-dht"
+version = "0.1.0"
+dependencies = [
+ "blake3",
+ "brahman-net",
+ "libp2p",
+ "serde",
+ "tokio",
+]
+
 [[package]]
 name = "brahman-handshake"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 14a3afd..e50a9ac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ members = [
     "crates/protocol/brahman-admin",
     "crates/protocol/brahman-sidecar",
     "crates/protocol/brahman-net",
+    "crates/protocol/brahman-dht",
     "crates/protocol/arje-card",
 
     # ============================================================
diff --git a/crates/protocol/brahman-dht/Cargo.toml b/crates/protocol/brahman-dht/Cargo.toml
new file mode 100644
index 0000000..4037d3a
--- /dev/null
+++ b/crates/protocol/brahman-dht/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "brahman-dht"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "Brahman — capa de discovery typed sobre el Kademlia compartido de brahman-net. Claves namespaced por kind (Code/Card/Persona/Service) sin colisión en una sola malla."
+
+[dependencies]
+brahman-net = { path = "../brahman-net" }
+libp2p = { workspace = true }
+blake3 = { workspace = true }
+serde = { workspace = true }
+
+[dev-dependencies]
+tokio = { workspace = true }
diff --git a/crates/protocol/brahman-dht/src/key.rs b/crates/protocol/brahman-dht/src/key.rs
new file mode 100644
index 0000000..8f79600
--- /dev/null
+++ b/crates/protocol/brahman-dht/src/key.rs
@@ -0,0 +1,124 @@
+//! Claves namespaced del DHT compartido.
+//!
+//! El ecosistema brahman corre UN solo Kademlia (en `brahman-net`). Para
+//! que distintos dominios — código indexado (minga), Cards, Personas
+//! (ágora) — coexistan sin colisión, cada clave lleva un byte de `kind`
+//! como prefijo. La representación en wire es de longitud fija:
+//! `[kind_tag] ++ blake3(id)` = 33 bytes.
+
+use serde::{Deserialize, Serialize};
+
+/// Tipo de registro — el namespace de una clave en el DHT.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum RecordKind {
+    /// Bloque de código indexado (minga).
+    Code,
+    /// `Card` brahman (módulo, ente, etc.).
+    Card,
+    /// `Persona` de ágora (identidad humana federada).
+    Persona,
+    /// Endpoint de servicio.
+    Service,
+    /// Dominio definido por el consumidor.
+    Custom(u8),
+}
+
+impl RecordKind {
+    /// Byte de etiqueta. `Custom(n)` ocupa `0x80 | n` (top bit) para no
+    /// chocar nunca con los kinds estándar (`0x00..`).
+    pub fn tag(&self) -> u8 {
+        match self {
+            RecordKind::Code => 0x01,
+            RecordKind::Card => 0x02,
+            RecordKind::Persona => 0x03,
+            RecordKind::Service => 0x04,
+            RecordKind::Custom(n) => 0x80 | (n & 0x7f),
+        }
+    }
+}
+
+/// Longitud fija de la clave en wire: 1 byte de kind + 32 de hash.
+pub const DHT_KEY_LEN: usize = 33;
+
+/// Clave de DHT namespaced. Se construye con un `id` legible; la
+/// representación en wire hashea el `id` con blake3.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DhtKey {
+    kind: RecordKind,
+    id: String,
+}
+
+impl DhtKey {
+    pub fn new(kind: RecordKind, id: impl Into<String>) -> Self {
+        Self { kind, id: id.into() }
+    }
+
+    /// Clave para un bloque de código.
+    pub fn code(id: impl Into<String>) -> Self {
+        Self::new(RecordKind::Code, id)
+    }
+
+    /// Clave para una Card.
+    pub fn card(id: impl Into<String>) -> Self {
+        Self::new(RecordKind::Card, id)
+    }
+
+    /// Clave para una Persona.
+    pub fn persona(id: impl Into<String>) -> Self {
+        Self::new(RecordKind::Persona, id)
+    }
+
+    pub fn kind(&self) -> RecordKind {
+        self.kind
+    }
+
+    pub fn id(&self) -> &str {
+        &self.id
+    }
+
+    /// Representación en wire: `[kind_tag] ++ blake3(id)`, 33 bytes.
+    pub fn to_bytes(&self) -> Vec<u8> {
+        let mut out = Vec::with_capacity(DHT_KEY_LEN);
+        out.push(self.kind.tag());
+        out.extend_from_slice(blake3::hash(self.id.as_bytes()).as_bytes());
+        out
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn wire_key_has_fixed_length() {
+        assert_eq!(DhtKey::card("modulo-x").to_bytes().len(), DHT_KEY_LEN);
+        assert_eq!(DhtKey::code("fn-hash").to_bytes().len(), DHT_KEY_LEN);
+    }
+
+    #[test]
+    fn same_id_different_kind_does_not_collide() {
+        let a = DhtKey::card("foo").to_bytes();
+        let b = DhtKey::code("foo").to_bytes();
+        let c = DhtKey::persona("foo").to_bytes();
+        assert_ne!(a, b);
+        assert_ne!(b, c);
+        assert_ne!(a, c);
+        // El hash del id es el mismo; sólo difiere el byte de kind.
+        assert_eq!(a[1..], b[1..]);
+        assert_ne!(a[0], b[0]);
+    }
+
+    #[test]
+    fn same_kind_and_id_is_stable() {
+        assert_eq!(DhtKey::card("x").to_bytes(), DhtKey::card("x").to_bytes());
+    }
+
+    #[test]
+    fn custom_kind_never_collides_with_standard() {
+        for std in [RecordKind::Code, RecordKind::Card, RecordKind::Persona, RecordKind::Service] {
+            for n in 0..=127u8 {
+                assert_ne!(std.tag(), RecordKind::Custom(n).tag());
+            }
+        }
+    }
+}
diff --git a/crates/protocol/brahman-dht/src/lib.rs b/crates/protocol/brahman-dht/src/lib.rs
new file mode 100644
index 0000000..2eb0a45
--- /dev/null
+++ b/crates/protocol/brahman-dht/src/lib.rs
@@ -0,0 +1,72 @@
+//! `brahman-dht` — capa de discovery typed sobre el Kademlia compartido.
+//!
+//! `brahman-net` corre un único Kademlia para todo el ecosistema. Este
+//! crate le pone arriba un esquema de claves namespaced ([`DhtKey`]):
+//! `minga` publica bloques de código, `brahman-card-discovery` publica
+//! Cards, `agorapura` publica Personas — todo sobre la misma malla sin
+//! colisión, porque cada clave lleva un byte de `kind`.
+//!
+//! El modelo es de **provider records**: un nodo `announce`-a que provee
+//! una clave; otros `find`-an quién la provee y abren un stream directo.
+
+#![forbid(unsafe_code)]
+
+pub mod key;
+
+pub use key::{DhtKey, RecordKind, DHT_KEY_LEN};
+
+use brahman_net::BrahmanNet;
+use libp2p::PeerId;
+use std::sync::Arc;
+
+/// Discovery typed sobre `brahman-net`.
+#[derive(Clone)]
+pub struct Dht {
+    net: Arc<BrahmanNet>,
+}
+
+impl Dht {
+    /// Crea la capa DHT sobre un nodo `brahman-net` ya inicializado.
+    pub fn new(net: Arc<BrahmanNet>) -> Self {
+        Self { net }
+    }
+
+    /// Anuncia que este nodo provee `key`. El registro de provider se
+    /// renueva solo mientras el nodo siga vivo en la malla.
+    pub fn announce(&self, key: &DhtKey) {
+        self.net.start_providing(&key.to_bytes());
+    }
+
+    /// Retira el anuncio de `key`.
+    pub fn withdraw(&self, key: &DhtKey) {
+        self.net.stop_providing(&key.to_bytes());
+    }
+
+    /// Busca los peers que proveen `key`.
+    pub async fn find(&self, key: &DhtKey) -> Vec<PeerId> {
+        self.net.find_providers(&key.to_bytes()).await
+    }
+
+    /// El nodo `brahman-net` subyacente (para abrir streams a un provider).
+    pub fn net(&self) -> &Arc<BrahmanNet> {
+        &self.net
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn announce_find_withdraw_on_a_live_node() {
+        // Smoke: un nodo solo. `find` de una clave que nadie provee
+        // devuelve vacío; announce/withdraw no panickean.
+        let net = Arc::new(BrahmanNet::new().expect("nodo libp2p"));
+        let dht = Dht::new(net);
+        let key = DhtKey::card("modulo-inexistente");
+        dht.announce(&key);
+        dht.withdraw(&key);
+        let found = dht.find(&DhtKey::card("nadie-lo-provee")).await;
+        assert!(found.is_empty());
+    }
+}