feat(brahman-handshake): Fase 2 — discovery remoto via DHT por flow type
Tercer paso del plan "el encuentro entre Entes no se restringe a
local". Cuando un Init local acepta una sesion cuya Card declara
outputs, anuncia al DHT (Kademlia, via brahman-net) que el provee
esos flow types. Cualquier nodo conectado al mismo DHT puede
consultar y obtener la lista de PeerId's que sirven el flow.
API nueva en brahman_handshake::network:
- flow_dht_key(flow_name, type_ref) -> [u8; 32]: blake3 hash de
"brahman-flow|v1|{flow}|{type_canon}". Determinista cross-host.
Cambiar la canonicalizacion rompe compatibilidad — el prefijo v1
documenta version del esquema y obliga a bump al modificar.
- announce_outputs(net, card): start_providing por cada flow.output.
Idempotente, fire-and-forget.
- find_remote_providers(net, flow_name, type_ref) -> Vec<PeerId>:
query DHT. Lista vacia si nadie anuncia.
Wire en el server:
- ServerConfig gana pub net: Option<Arc<BrahmanNet>>. Si esta set,
cada Card registrada con outputs se anuncia automaticamente al DHT
desde register_session. None = server "ciego al DHT".
- Debug manual de ServerConfig (BrahmanNet no es Debug).
Canonicalizacion del TypeRef:
- Primitive { name } -> "prim:{name}"
- Wit { package, interface, name } -> "wit:{package}#{interface_or_empty}#{name}"
Tests: 2 nuevos en tests/network_discovery.rs:
- dht_discovery_finds_remote_provider: 2 nodos, A registra Card con
flow.output = monad-list:json, B dial-ea a A, B llama
find_remote_providers y descubre el peer_id de A.
- dht_discovery_negative_unknown_flow: B busca flow inexistente,
devuelve [] sin colgarse.
Callers actualizados con net: None: tests existentes + ente-zero
(arje aun no expone red; pasar Some(Arc<BrahmanNet>) cuando quiera
publicar al DHT remoto).
Lo que esto desbloquea: un nouser daemon en maquina A puede ser
descubierto por nouser-explorer en maquina B sin conocimiento previo
del peer — solo necesitan compartir DHT (via bootstrap inicial).
Pendiente para Fase 3: trust (firma Ed25519 en Cards remotas) +
stop_providing al cleanup de sesion.
This commit is contained in:
@@ -43,11 +43,11 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use brahman_card::{Card, WitInterface};
|
||||
use brahman_card::{Card, TypeRef, WitInterface};
|
||||
use brahman_net::{BrahmanNet, OpenStreamError, PeerId, Stream, StreamProtocol};
|
||||
use futures::StreamExt;
|
||||
use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt};
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::client::{Client, ClientError};
|
||||
use crate::server::Server;
|
||||
@@ -138,3 +138,92 @@ pub async fn connect_libp2p(
|
||||
let client = Client::connect_with_stream(stream.compat(), card, wit).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Discovery remoto via DHT — Fase 2
|
||||
// =====================================================================
|
||||
//
|
||||
// Cuando un Ente registra una Card con outputs en el Init local, el
|
||||
// Init anuncia al DHT (`net.start_providing(key)`) bajo una key
|
||||
// derivada de `(flow_name, TypeRef)`. Cualquier nodo conectado al
|
||||
// mismo DHT puede consultar `find_remote_providers(flow_name, type)`
|
||||
// y obtener la lista de `PeerId`s que dijeron proveer ese flow.
|
||||
//
|
||||
// La key es **estable y libre de colisiones** entre versiones del
|
||||
// monorepo: usa blake3 sobre un canon textual `brahman-flow|{name}|{type_canon}`.
|
||||
// Cambiar la canonicalización rompe el discovery cross-version, así
|
||||
// que cualquier modificación requiere bump de versión documentado.
|
||||
|
||||
/// Prefijo de namespace para todas las keys DHT del subprotocolo
|
||||
/// brahman. Discrimina contra otros usos del mismo DHT (sync minga,
|
||||
/// futuros) — protege contra colisiones accidentales.
|
||||
const FLOW_KEY_PREFIX: &str = "brahman-flow|v1|";
|
||||
|
||||
/// Canonicaliza un `TypeRef` a string estable. Cambios aquí rompen
|
||||
/// la compatibilidad de discovery cross-version; bump documentado
|
||||
/// en `FLOW_KEY_PREFIX` al modificar.
|
||||
fn canonicalize_type(t: &TypeRef) -> String {
|
||||
match t {
|
||||
TypeRef::Primitive { name } => format!("prim:{}", name),
|
||||
TypeRef::Wit {
|
||||
package,
|
||||
interface,
|
||||
name,
|
||||
} => format!(
|
||||
"wit:{}#{}#{}",
|
||||
package,
|
||||
interface.as_deref().unwrap_or(""),
|
||||
name
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deriva la key del DHT para un `(flow_name, type_ref)` específico.
|
||||
/// blake3-32B determinístico — la misma tupla en cualquier máquina
|
||||
/// produce la misma key.
|
||||
pub fn flow_dht_key(flow_name: &str, type_ref: &TypeRef) -> [u8; 32] {
|
||||
let canon = format!(
|
||||
"{}{}|{}",
|
||||
FLOW_KEY_PREFIX,
|
||||
flow_name,
|
||||
canonicalize_type(type_ref)
|
||||
);
|
||||
*blake3::hash(canon.as_bytes()).as_bytes()
|
||||
}
|
||||
|
||||
/// Anuncia al DHT que este nodo provee cada output flow declarado
|
||||
/// en `card`. Llamarlo tras `register_session` propaga la
|
||||
/// disponibilidad a todos los peers que comparten DHT con éste.
|
||||
///
|
||||
/// Idempotente: re-anunciar la misma key actualiza el TTL del record
|
||||
/// en el DHT. Best-effort: si `start_providing` falla por falta de
|
||||
/// peers cercanos (DHT vacío), el record vive en el store local
|
||||
/// hasta que llegue una conexión.
|
||||
pub fn announce_outputs(net: &BrahmanNet, card: &Card) {
|
||||
for flow in &card.flow.output {
|
||||
let key = flow_dht_key(&flow.name, &flow.ty);
|
||||
debug!(
|
||||
target: "brahman_handshake::network",
|
||||
flow = %flow.name,
|
||||
"announce_output → DHT"
|
||||
);
|
||||
net.start_providing(&key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Consulta el DHT por peers que han anunciado proveer el flow
|
||||
/// `(flow_name, type_ref)`. Devuelve la lista resuelta de `PeerId`s.
|
||||
/// Lista vacía si nadie anuncia, si la query timeout-ea, o si el
|
||||
/// DHT no ha encontrado providers.
|
||||
///
|
||||
/// Para cada `PeerId` devuelto, el caller puede luego dial-ar al
|
||||
/// peer (a sus addrs conocidas vía Identify) y abrir un sub-handshake
|
||||
/// remoto con [`connect_libp2p`].
|
||||
pub async fn find_remote_providers(
|
||||
net: &BrahmanNet,
|
||||
flow_name: &str,
|
||||
type_ref: &TypeRef,
|
||||
) -> Vec<PeerId> {
|
||||
let key = flow_dht_key(flow_name, type_ref);
|
||||
net.find_providers(&key).await
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use brahman_broker::{Broker, Endpoint};
|
||||
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
|
||||
use brahman_net::BrahmanNet;
|
||||
use tokio::io::{split, AsyncRead, AsyncWrite, WriteHalf};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
@@ -39,7 +40,7 @@ type LastMatches = Arc<Mutex<HashMap<SessionId, HashMap<String, Endpoint>>>>;
|
||||
const PUSH_CHANNEL_CAPACITY: usize = 32;
|
||||
|
||||
/// Configuración del servidor.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ServerConfig {
|
||||
/// `true` si el Init está atado al servidor (se reporta en `HelloAck`).
|
||||
pub init_attached: bool,
|
||||
@@ -47,6 +48,27 @@ pub struct ServerConfig {
|
||||
/// `register` tras un Hello aceptado y `unregister` al cerrar la
|
||||
/// sesión (Farewell o EOF). Si es `None`, el broker no se usa.
|
||||
pub broker: Option<SharedBroker>,
|
||||
/// Capa P2P compartida. Si está presente, cada Card registrada
|
||||
/// con outputs se anuncia automáticamente al DHT vía
|
||||
/// [`brahman_handshake::network::announce_outputs`], permitiendo
|
||||
/// que un consumer remoto los descubra con
|
||||
/// [`brahman_handshake::network::find_remote_providers`]. Si es
|
||||
/// `None`, el server queda "ciego al DHT" — sólo matchea sesiones
|
||||
/// locales (lo cual es correcto cuando no hay conectividad o no
|
||||
/// se desea exponer al exterior).
|
||||
pub net: Option<Arc<BrahmanNet>>,
|
||||
}
|
||||
|
||||
// Manual Debug porque BrahmanNet no implementa Debug (libp2p Swarm
|
||||
// no es Debug). Sólo loggeamos los campos relevantes para tracing.
|
||||
impl std::fmt::Debug for ServerConfig {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ServerConfig")
|
||||
.field("init_attached", &self.init_attached)
|
||||
.field("broker", &self.broker.as_ref().map(|_| "<broker>"))
|
||||
.field("net", &self.net.as_ref().map(|_| "<net>"))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Servidor de handshake escuchando en un Unix socket.
|
||||
@@ -508,6 +530,12 @@ async fn register_session(
|
||||
.await
|
||||
.register(session_id, &card, wit.clone());
|
||||
}
|
||||
// Si el server tiene net configurado, anunciar los outputs al
|
||||
// DHT para que peers remotos puedan descubrirlos. Idempotente
|
||||
// y best-effort — fallos de Kad no propagan al handshake.
|
||||
if let Some(net) = &config.net {
|
||||
crate::network::announce_outputs(net, &card);
|
||||
}
|
||||
let resolved = match wit {
|
||||
Some(w) => ResolvedCard::from_conscious(card, w),
|
||||
None => ResolvedCard::from_agnostic(card),
|
||||
|
||||
Reference in New Issue
Block a user