feat: Implement Iroh-based peer networking, join protocol, and bidirectional store synchronization.

This commit is contained in:
2025-12-22 21:11:34 +01:00
parent 7c8e5cfa3d
commit e942da49ff
22 changed files with 1933 additions and 57 deletions
+3
View File
@@ -9,10 +9,13 @@ license.workspace = true
lattice-core = { workspace = true }
iroh = { workspace = true }
iroh-gossip = { workspace = true }
prost = { workspace = true }
tokio = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
bytes = { workspace = true }
tokio-util = { workspace = true }
futures-util = { workspace = true }
[dev-dependencies]
tokio-test = { workspace = true }
+57
View File
@@ -0,0 +1,57 @@
//! Iroh endpoint for network connectivity
//!
//! Creates an Iroh endpoint from the node's Ed25519 secret key,
//! ensuring the same identity is used for both Lattice and Iroh.
//!
//! Discovery: Uses both DNS (default) and mDNS (local network)
use iroh::{Endpoint, endpoint::{BindError, Connection, ConnectError}};
use iroh::discovery::mdns::MdnsDiscovery;
pub use iroh::PublicKey;
/// ALPN protocol identifier for Lattice sync
pub const LATTICE_ALPN: &[u8] = b"lattice-sync/1";
/// Wrapper around Iroh endpoint with Lattice integration
pub struct LatticeEndpoint {
endpoint: Endpoint,
}
impl LatticeEndpoint {
/// Create a new endpoint from Ed25519 secret key bytes (from identity.key)
/// Enables both DNS discovery (internet) and mDNS discovery (local network)
pub async fn new(secret_key_bytes: [u8; 32]) -> Result<Self, BindError> {
let secret_key = iroh::SecretKey::from_bytes(&secret_key_bytes);
// mDNS for local network discovery
let mdns = MdnsDiscovery::builder();
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.alpns(vec![LATTICE_ALPN.to_vec()])
.discovery(mdns) // Add mDNS on top of default DNS
.bind()
.await?;
Ok(Self { endpoint })
}
/// Get the public key (same as Lattice pubkey, can be shared with peers)
pub fn public_key(&self) -> PublicKey {
self.endpoint.secret_key().public()
}
/// Connect to a peer by their public key
pub async fn connect(&self, peer: PublicKey) -> Result<Connection, ConnectError> {
self.endpoint.connect(peer, LATTICE_ALPN).await
}
/// Accept an incoming connection
pub async fn accept(&self) -> Option<iroh::endpoint::Incoming> {
self.endpoint.accept().await
}
/// Get the underlying endpoint
pub fn endpoint(&self) -> &Endpoint {
&self.endpoint
}
}
+63
View File
@@ -0,0 +1,63 @@
//! Message framing for Iroh streams using tokio-util LengthDelimitedCodec
//!
//! Provides a clean interface for sending/receiving length-prefixed PeerMessage
//! over QUIC streams without manual buffer management.
use futures_util::{SinkExt, StreamExt};
use lattice_core::proto::PeerMessage;
use prost::Message;
use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
/// Framed writer for sending PeerMessage over an Iroh SendStream
pub struct MessageSink {
inner: FramedWrite<iroh::endpoint::SendStream, LengthDelimitedCodec>,
}
impl MessageSink {
pub fn new(stream: iroh::endpoint::SendStream) -> Self {
Self {
inner: FramedWrite::new(stream, LengthDelimitedCodec::new()),
}
}
/// Send a PeerMessage (length-prefixed)
pub async fn send(&mut self, msg: &PeerMessage) -> Result<(), String> {
let bytes = msg.encode_to_vec();
self.inner.send(bytes.into()).await
.map_err(|e| format!("Send error: {}", e))
}
/// Finish the stream (signal we're done sending)
pub async fn finish(self) -> Result<(), String> {
let mut stream = self.inner.into_inner();
let _ = stream.finish();
stream.stopped().await.ok();
Ok(())
}
}
/// Framed reader for receiving PeerMessage from an Iroh RecvStream
pub struct MessageStream {
inner: FramedRead<iroh::endpoint::RecvStream, LengthDelimitedCodec>,
}
impl MessageStream {
pub fn new(stream: iroh::endpoint::RecvStream) -> Self {
Self {
inner: FramedRead::new(stream, LengthDelimitedCodec::new()),
}
}
/// Receive next PeerMessage (or None if stream closed)
pub async fn recv(&mut self) -> Result<Option<PeerMessage>, String> {
match self.inner.next().await {
Some(Ok(bytes)) => {
PeerMessage::decode(&bytes[..])
.map(Some)
.map_err(|e| format!("Decode error: {}", e))
}
Some(Err(e)) => Err(format!("Read error: {}", e)),
None => Ok(None),
}
}
}
+13
View File
@@ -1,8 +1,21 @@
//! Lattice Networking
//!
//! Networking layer using Iroh:
//! - **Endpoint**: Network identity and connection management
//! - **Gossip**: Broadcasting changes across the mesh
//! - **Unicast**: Point-to-point communication for reconciliation
//! - **Framing**: Length-delimited message framing for QUIC streams
pub mod endpoint;
pub mod gossip;
pub mod unicast;
pub mod framing;
pub use endpoint::{LatticeEndpoint, PublicKey};
pub use framing::{MessageSink, MessageStream};
pub use lattice_core::proto::{SyncRequest, SyncResponse, SyncEntry, SyncDone, SyncState, Frontier};
/// Parse a PublicKey (NodeId) from hex or base32 string
pub fn parse_node_id(s: &str) -> Result<PublicKey, String> {
s.parse().map_err(|e| format!("{}", e))
}