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>
This commit is contained in:
Generated
+11
@@ -1746,6 +1746,17 @@ dependencies = [
|
|||||||
"ulid",
|
"ulid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "brahman-dht"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"blake3",
|
||||||
|
"brahman-net",
|
||||||
|
"libp2p",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brahman-handshake"
|
name = "brahman-handshake"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ members = [
|
|||||||
"crates/protocol/brahman-admin",
|
"crates/protocol/brahman-admin",
|
||||||
"crates/protocol/brahman-sidecar",
|
"crates/protocol/brahman-sidecar",
|
||||||
"crates/protocol/brahman-net",
|
"crates/protocol/brahman-net",
|
||||||
|
"crates/protocol/brahman-dht",
|
||||||
"crates/protocol/arje-card",
|
"crates/protocol/arje-card",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user