feat: Implement Iroh-based peer networking, join protocol, and bidirectional store synchronization.
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user