feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p

Segundo paso del plan "el encuentro entre Entes no se restringe a
local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
MatchEvent / Farewell, frames postcard length-prefixed) ahora tambien
viaja sobre streams libp2p de la malla brahman-net — el mismo Init
acepta sesiones por Unix socket Y por libp2p indistintamente, y un
consumer remoto puede dial-ar al multiaddr y completar handshake.

Cambios:

- Session<S> y Client<S> genericos: ambos dejan de estar atados a
  UnixStream y pasan a ser genericos sobre S: AsyncRead + AsyncWrite
  + Unpin + Send + 'static. El path Unix queda como
  Client = Client<UnixStream> (default generico). Constructores
  nuevos: Server::session_from_stream(stream),
  Client::connect_with_stream(stream, card, wit).

- Refactor del post-handshake con split: tokio::select! sobre
  &mut self.stream requeria S: Sync indirectamente, y libp2p::Stream
  no es Sync. Reemplazado por tokio::io::split(stream) -> reader loop
  principal + writer task separada que drena el push channel. Writer
  compartido bajo Arc<Mutex<WriteHalf<S>>> para serializar Pong/Error
  inline con los MatchEvents pusheados. Cleanup garantizado en todas
  las ramas. La logica del post-handshake migra a funciones libres
  (run_post_handshake, handle_inbound_frame, cleanup,
  broadcast_match_diffs, do_handshake, register_session,
  validate_hello).

- Nuevo modulo brahman-handshake::network:
  - BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0"
  - LibP2pHandshakeStream = Compat<libp2p::Stream>
  - run_libp2p_accept_loop(server, net): accept loop que delega cada
    stream entrante a session_from_stream(stream.compat()). Sesiones
    libp2p y Unix conviven en el mismo Server — comparten broker,
    push table, last_matches.
  - connect_libp2p(net, peer, card, wit): abre stream libp2p al peer
    y arranca handshake.
  - NetworkError tipado.

Deps: brahman-handshake gana brahman-net, futures, tokio-util.
brahman-net re-exporta Multiaddr, PeerId, Stream, StreamProtocol,
Protocol, OpenStreamError para que callers no necesiten dep directa
a libp2p.

Tests: 9 verdes en el path Unix (sin regresion). Nuevo
tests/network_libp2p.rs E2E que arma server con BrahmanNet, hace
listen TCP, monta accept loop; cliente con su propio BrahmanNet
dial-ea al peer_id, completa handshake remoto, ping, farewell.
Verifica que la sesion se registro durante la conversacion y se
removio tras farewell.
This commit is contained in:
Sergio
2026-05-09 12:51:43 +00:00
parent ad0d475a2e
commit 73dadbb166
9 changed files with 723 additions and 281 deletions
@@ -0,0 +1,117 @@
//! Test E2E: handshake brahman remoto sobre libp2p stream.
//!
//! Pipeline:
//! 1. Server: bind Unix socket (necesario aunque no lo use el cliente);
//! crear `BrahmanNet` y escuchar en `/ip4/127.0.0.1/tcp/0`;
//! montar `run_libp2p_accept_loop`.
//! 2. Client: crear su propio `BrahmanNet`; dial al multiaddr del
//! server; `connect_libp2p` con su Card; `ping`; `farewell`.
//! 3. Verificar: el server registró la sesión; sessions.len() == 1
//! durante la sesión, == 0 después del farewell.
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Duration;
use brahman_broker::{Broker, BrokerConfig};
use brahman_card::{
ulid::Ulid, Card, CardKind, Lifecycle, Payload, Priority, Supervision,
CARD_SCHEMA_VERSION,
};
use brahman_handshake::network::{connect_libp2p, run_libp2p_accept_loop};
use brahman_handshake::server::{Server, ServerConfig};
use brahman_net::{BrahmanNet, Multiaddr, PeerId, Protocol};
use tempfile::TempDir;
use tokio::sync::Mutex;
fn sample_card(label: &str) -> Card {
Card {
schema_version: CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: label.into(),
provides: BTreeSet::new(),
requires: BTreeSet::new(),
permissions: Default::default(),
soma: Default::default(),
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::default(),
priority: Priority::default(),
kind: CardKind::Ente,
..Default::default()
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn libp2p_handshake_roundtrip() {
// ---- Server side ----
let tmp = TempDir::new().unwrap();
let unix_socket = tmp.path().join("brahman-init.sock");
let broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default())));
let server = Arc::new(
Server::bind(
&unix_socket,
ServerConfig {
init_attached: true,
broker: Some(broker.clone()),
},
)
.unwrap(),
);
let sessions = server.sessions();
let server_net = Arc::new(BrahmanNet::new().unwrap());
let server_peer_id = server_net.peer_id;
// Listen on a random TCP port.
let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap();
let actual_addr = server_net.listen(listen_addr).await;
// Inject the libp2p PeerId into the multiaddr so the client knows
// who to dial.
let mut full_addr = actual_addr.clone();
full_addr.push(Protocol::P2p(server_peer_id));
// Spawn the libp2p accept loop.
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
// ---- Client side ----
let client_net = BrahmanNet::new().unwrap();
client_net.dial(full_addr.clone());
// Pequeña espera para que el dial conecte. En un entorno real el
// caller usaría un mecanismo de barrier, pero para tests un sleep
// corto es suficiente y deterministic en localhost.
tokio::time::sleep(Duration::from_millis(200)).await;
let card = sample_card("test.remote_ente");
let mut client = connect_libp2p(&client_net, server_peer_id, card, None)
.await
.expect("handshake remoto debería completar");
// Verificación: el server vio la sesión.
{
let s = sessions.lock().await;
assert_eq!(s.len(), 1, "una sesión registrada");
let resolved = s.values().next().unwrap();
assert_eq!(resolved.card.label, "test.remote_ente");
}
// Ping roundtrip.
let ts = client.ping().await.expect("ping debería responder");
assert!(ts > 0, "timestamp del Pong > 0");
// Farewell limpio.
client.farewell().await.expect("farewell debería completar");
// Tras el farewell, el cleanup remueve la sesión.
// Damos un tick para que el handler procese el frame.
tokio::time::sleep(Duration::from_millis(100)).await;
{
let s = sessions.lock().await;
assert_eq!(s.len(), 0, "sesión removida tras farewell");
}
// peer_id no usado aquí, pero validamos que la API existe.
let _ = PeerId::random();
}