feat: introduce NodeIdentity and store_actor in lattice-core, and implement mesh networking in lattice-net while removing unicast.
This commit is contained in:
@@ -17,7 +17,6 @@ hex = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
shlex = "1"
|
||||
hostname = "0.4"
|
||||
serde_json = "1"
|
||||
iroh = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
//! Accept handler for incoming Iroh connections
|
||||
|
||||
use lattice_net::{MessageSink, MessageStream};
|
||||
use crate::node::{StoreHandle, PeerStatus};
|
||||
use iroh::Endpoint;
|
||||
use iroh::endpoint::Connection;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use lattice_core::proto::{PeerMessage, peer_message, JoinResponse};
|
||||
|
||||
/// Spawn the accept loop for incoming connections.
|
||||
pub fn spawn_accept_loop(
|
||||
endpoint: Endpoint,
|
||||
shared_store: Arc<RwLock<Option<StoreHandle>>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
if let Some(incoming) = endpoint.accept().await {
|
||||
match incoming.await {
|
||||
Ok(conn) => {
|
||||
let store = shared_store.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(conn, store).await {
|
||||
eprintln!("[Accept] Error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => eprintln!("[Accept] Handshake error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Handle a single incoming connection
|
||||
async fn handle_connection(
|
||||
conn: Connection,
|
||||
shared_store: Arc<RwLock<Option<StoreHandle>>>,
|
||||
) -> Result<(), String> {
|
||||
let remote_id = conn.remote_id();
|
||||
let remote_hex = hex::encode(remote_id.as_bytes());
|
||||
println!("\n[Incoming] {} (ALPN: {})", remote_id.fmt_short(), String::from_utf8_lossy(conn.alpn()));
|
||||
|
||||
let store = {
|
||||
let guard = shared_store.read().await;
|
||||
match &*guard {
|
||||
Some(s) => s.clone(),
|
||||
None => return Err("No store available".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
let (send, recv) = conn.accept_bi().await
|
||||
.map_err(|e| format!("Accept stream error: {}", e))?;
|
||||
|
||||
// Wrap in framed message streams
|
||||
let mut sink = MessageSink::new(send);
|
||||
let mut stream = MessageStream::new(recv);
|
||||
|
||||
// Read first message
|
||||
let msg = stream.recv().await?
|
||||
.ok_or_else(|| "Peer closed stream".to_string())?;
|
||||
|
||||
match msg.message {
|
||||
Some(peer_message::Message::JoinRequest(req)) => {
|
||||
// For join: verify peer is invited
|
||||
verify_peer_status(&store, &remote_hex, PeerStatusCheck::Exactly(PeerStatus::Invited)).await?;
|
||||
println!("[Peer] Verified as invited");
|
||||
|
||||
println!("[Join] Got JoinRequest from {}", hex::encode(&req.node_pubkey));
|
||||
|
||||
let resp = PeerMessage {
|
||||
message: Some(peer_message::Message::JoinResponse(JoinResponse {
|
||||
store_uuid: store.id().as_bytes().to_vec(),
|
||||
inviter_pubkey: vec![],
|
||||
})),
|
||||
};
|
||||
sink.send(&resp).await?;
|
||||
sink.finish().await?;
|
||||
|
||||
// Set peer status to 'active' now that they've joined
|
||||
let status_key = format!("/nodes/{}/status", remote_hex);
|
||||
if let Err(e) = store.put(status_key.as_bytes(), PeerStatus::Active.as_str().as_bytes()).await {
|
||||
eprintln!("[Join] Warning: Failed to set peer status: {}", e);
|
||||
}
|
||||
|
||||
println!("[Join] Sent JoinResponse, peer now active");
|
||||
Ok(())
|
||||
}
|
||||
Some(peer_message::Message::SyncRequest(req)) => {
|
||||
// For sync: verify peer is active (or invited for first sync after join)
|
||||
verify_peer_status(&store, &remote_hex, PeerStatusCheck::ActiveOrInvited).await?;
|
||||
println!("[Peer] Verified for sync");
|
||||
|
||||
handle_sync_request(sink, stream, req, &store).await
|
||||
}
|
||||
_ => Err("Unexpected message type".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Expected peer status check mode
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum PeerStatusCheck {
|
||||
Exactly(PeerStatus),
|
||||
ActiveOrInvited,
|
||||
}
|
||||
|
||||
/// Verify a peer has the expected status
|
||||
async fn verify_peer_status(store: &StoreHandle, remote_hex: &str, expected: PeerStatusCheck) -> Result<(), String> {
|
||||
let status_key = format!("/nodes/{}/status", remote_hex);
|
||||
let status = match store.get(status_key.as_bytes()).await {
|
||||
Ok(Some(s)) => String::from_utf8_lossy(&s).to_string(),
|
||||
Ok(None) => return Err(format!("Peer not found")),
|
||||
Err(e) => return Err(format!("Error checking peer status: {}", e)),
|
||||
};
|
||||
|
||||
let valid = match expected {
|
||||
PeerStatusCheck::Exactly(ps) => status == ps.as_str(),
|
||||
PeerStatusCheck::ActiveOrInvited => status == PeerStatus::Active.as_str() || status == PeerStatus::Invited.as_str(),
|
||||
};
|
||||
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Peer status is '{}', expected {:?}", status, expected))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a sync request - bidirectional exchange of entries
|
||||
async fn handle_sync_request(
|
||||
mut sink: MessageSink,
|
||||
mut stream: MessageStream,
|
||||
peer_request: lattice_core::proto::SyncRequest,
|
||||
store: &StoreHandle,
|
||||
) -> Result<(), String> {
|
||||
println!("[Sync] Received SyncRequest");
|
||||
|
||||
// Get our sync state
|
||||
let my_state = store.sync_state().await
|
||||
.map_err(|e| format!("Failed to get sync state: {}", e))?;
|
||||
|
||||
// 1. Send our sync state as response
|
||||
let resp = PeerMessage {
|
||||
message: Some(peer_message::Message::SyncResponse(lattice_core::proto::SyncResponse {
|
||||
state: Some(my_state.to_proto()),
|
||||
})),
|
||||
};
|
||||
sink.send(&resp).await?;
|
||||
|
||||
// 2. Send entries peer is missing
|
||||
let peer_state = peer_request.state
|
||||
.map(|s| lattice_core::sync_state::SyncState::from_proto(&s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let entries_sent = crate::sync_protocol::send_missing_entries(&mut sink, store, &my_state, &peer_state).await?;
|
||||
println!("[Sync] Sent {} entries, now receiving from peer...", entries_sent);
|
||||
|
||||
// 3. Receive entries from requester (bidirectional)
|
||||
let (entries_applied, _) = crate::sync_protocol::receive_entries(&mut stream, store).await?;
|
||||
|
||||
sink.finish().await?;
|
||||
|
||||
println!("[Sync] Applied {} entries from peer", entries_applied);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+56
-46
@@ -1,7 +1,7 @@
|
||||
//! CLI command handlers
|
||||
|
||||
use crate::node::{LatticeNode, StoreHandle, PeerStatus};
|
||||
use lattice_core::Uuid;
|
||||
use lattice_core::{Node, StoreHandle};
|
||||
use lattice_core::{Uuid, PeerStatus};
|
||||
use lattice_net::LatticeEndpoint;
|
||||
use chrono::DateTime;
|
||||
use std::time::Instant;
|
||||
@@ -21,7 +21,7 @@ fn block_async<F: std::future::Future>(f: F) -> F::Output {
|
||||
})
|
||||
}
|
||||
|
||||
pub type Handler = fn(&LatticeNode, Option<&StoreHandle>, Option<&LatticeEndpoint>, &[String]) -> CommandResult;
|
||||
pub type Handler = fn(&Node, Option<&StoreHandle>, Option<&LatticeEndpoint>, &[String]) -> CommandResult;
|
||||
|
||||
pub struct Command {
|
||||
pub name: &'static str,
|
||||
@@ -167,11 +167,11 @@ pub fn commands() -> Vec<Command> {
|
||||
|
||||
// --- Store management ---
|
||||
|
||||
fn cmd_init(node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
fn cmd_init(node: &Node, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
match block_async(node.init()) {
|
||||
Ok((store_id, handle)) => {
|
||||
println!("Initialized with root store: {}", store_id);
|
||||
println!("Node pubkey stored in /nodes/{}/info", hex::encode(node.node_id()));
|
||||
println!("Node info stored in /nodes/{}/*", hex::encode(node.node_id()));
|
||||
CommandResult::SwitchTo(handle)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -181,7 +181,7 @@ fn cmd_init(node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Option<
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_create_store(node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
fn cmd_create_store(node: &Node, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
match node.create_store() {
|
||||
Ok(store_id) => {
|
||||
println!("Created store: {}", store_id);
|
||||
@@ -203,7 +203,7 @@ fn cmd_create_store(node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint:
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_use_store(node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_use_store(node: &Node, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let store_id = match Uuid::parse_str(&args[0]) {
|
||||
Ok(id) => id,
|
||||
Err(_) => {
|
||||
@@ -229,7 +229,7 @@ fn cmd_use_store(node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Op
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_list_stores(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
fn cmd_list_stores(node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
let stores = match node.list_stores() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
@@ -252,7 +252,7 @@ fn cmd_list_stores(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: O
|
||||
|
||||
// --- Info ---
|
||||
|
||||
fn cmd_help(_node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
fn cmd_help(_node: &Node, _store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
println!("\nCommands:");
|
||||
for cmd in commands() {
|
||||
if cmd.args.is_empty() {
|
||||
@@ -266,7 +266,7 @@ fn cmd_help(_node: &LatticeNode, _store: Option<&StoreHandle>, _endpoint: Option
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_status(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
fn cmd_status(node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
println!("Node ID: {}", hex::encode(node.node_id()));
|
||||
println!("Data: {}", node.data_path().display());
|
||||
match node.root_store() {
|
||||
@@ -320,7 +320,7 @@ fn cmd_status(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option
|
||||
|
||||
// --- KV ---
|
||||
|
||||
fn cmd_put(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_put(_node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let Some(h) = store else {
|
||||
println!("No store selected. Use 'init' or 'use <uuid>'");
|
||||
return CommandResult::Ok;
|
||||
@@ -333,7 +333,7 @@ fn cmd_put(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_get(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_get(_node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let Some(h) = store else {
|
||||
println!("No store selected. Use 'init' or 'use <uuid>'");
|
||||
return CommandResult::Ok;
|
||||
@@ -384,7 +384,7 @@ fn cmd_get(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_delete(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_delete(_node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let Some(h) = store else {
|
||||
println!("No store selected. Use 'init' or 'use <uuid>'");
|
||||
return CommandResult::Ok;
|
||||
@@ -397,7 +397,7 @@ fn cmd_delete(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Optio
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_list(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_list(_node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let Some(h) = store else {
|
||||
println!("No store selected. Use 'init' or 'use <uuid>'");
|
||||
return CommandResult::Ok;
|
||||
@@ -448,7 +448,7 @@ fn format_value(v: &[u8]) -> String {
|
||||
std::str::from_utf8(v).map(String::from).unwrap_or_else(|_| format!("0x{}", hex::encode(v)))
|
||||
}
|
||||
|
||||
fn cmd_author_state(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_author_state(node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let store = match store {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -492,7 +492,7 @@ fn cmd_author_state(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint:
|
||||
|
||||
// --- Peer management ---
|
||||
|
||||
fn cmd_invite(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_invite(node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let store = match store {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -510,22 +510,27 @@ fn cmd_invite(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option
|
||||
}
|
||||
};
|
||||
|
||||
// Write /nodes/{pubkey}/info with inviter info
|
||||
let info_key = format!("/nodes/{}/info", pubkey_hex);
|
||||
// Write peer info as separate keys
|
||||
let inviter_hex = hex::encode(node.node_id());
|
||||
let added_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let info = serde_json::json!({
|
||||
"added_by": inviter_hex,
|
||||
"added_at": added_at
|
||||
});
|
||||
|
||||
match block_async(store.put(info_key.as_bytes(), info.to_string().as_bytes())) {
|
||||
let added_by_key = format!("/nodes/{}/added_by", pubkey_hex);
|
||||
match block_async(store.put(added_by_key.as_bytes(), inviter_hex.as_bytes())) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Error writing info: {}", e);
|
||||
eprintln!("Error writing added_by: {}", e);
|
||||
return CommandResult::Ok;
|
||||
}
|
||||
}
|
||||
|
||||
let added_at_key = format!("/nodes/{}/added_at", pubkey_hex);
|
||||
match block_async(store.put(added_at_key.as_bytes(), added_at.to_string().as_bytes())) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
eprintln!("Error writing added_at: {}", e);
|
||||
return CommandResult::Ok;
|
||||
}
|
||||
}
|
||||
@@ -541,12 +546,13 @@ fn cmd_invite(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option
|
||||
}
|
||||
|
||||
println!("Invited peer: {}", pubkey_hex);
|
||||
println!(" /nodes/{}/info", pubkey_hex);
|
||||
println!(" /nodes/{}/added_by", pubkey_hex);
|
||||
println!(" /nodes/{}/added_at", pubkey_hex);
|
||||
println!(" /nodes/{}/status = {} (will become active after sync)", pubkey_hex, PeerStatus::Invited.as_str());
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_peers(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
fn cmd_peers(_node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, _args: &[String]) -> CommandResult {
|
||||
let store = match store {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -586,23 +592,27 @@ fn cmd_peers(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for (pubkey, status) in &peers {
|
||||
// Try to get info for name/added_at
|
||||
let info_key = format!("/nodes/{}/info", pubkey);
|
||||
let mut name = String::new();
|
||||
let mut added = String::new();
|
||||
// Try to get name and added_at from separate keys
|
||||
let name_key = format!("/nodes/{}/name", pubkey);
|
||||
let added_at_key = format!("/nodes/{}/added_at", pubkey);
|
||||
|
||||
if let Ok(Some(info_bytes)) = block_async(store.get(info_key.as_bytes())) {
|
||||
if let Ok(info) = serde_json::from_slice::<serde_json::Value>(&info_bytes) {
|
||||
if let Some(n) = info.get("name").and_then(|v| v.as_str()) {
|
||||
name = n.to_string();
|
||||
}
|
||||
if let Some(ts) = info.get("added_at").and_then(|v| v.as_u64()) {
|
||||
if let Some(dt) = DateTime::from_timestamp(ts as i64, 0) {
|
||||
added = dt.format("%Y-%m-%d").to_string();
|
||||
}
|
||||
let name = match block_async(store.get(name_key.as_bytes())) {
|
||||
Ok(Some(bytes)) => String::from_utf8_lossy(&bytes).to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
let added = match block_async(store.get(added_at_key.as_bytes())) {
|
||||
Ok(Some(bytes)) => {
|
||||
if let Ok(ts) = String::from_utf8_lossy(&bytes).parse::<i64>() {
|
||||
DateTime::from_timestamp(ts, 0)
|
||||
.map(|dt| dt.format("%Y-%m-%d").to_string())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
by_status.entry(*status)
|
||||
.or_default()
|
||||
@@ -631,7 +641,7 @@ fn cmd_peers(_node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_remove(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_remove(node: &Node, store: Option<&StoreHandle>, _endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let store = match store {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
@@ -683,7 +693,7 @@ fn cmd_remove(node: &LatticeNode, store: Option<&StoreHandle>, _endpoint: Option
|
||||
CommandResult::Ok
|
||||
}
|
||||
|
||||
fn cmd_join(node: &LatticeNode, store: Option<&StoreHandle>, endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_join(node: &Node, store: Option<&StoreHandle>, endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let endpoint = match endpoint {
|
||||
Some(ep) => ep,
|
||||
None => {
|
||||
@@ -707,7 +717,7 @@ fn cmd_join(node: &LatticeNode, store: Option<&StoreHandle>, endpoint: Option<&L
|
||||
|
||||
println!("Joining mesh via {}...", peer_id.fmt_short());
|
||||
|
||||
match block_async(crate::sync::join_mesh(node, endpoint, peer_id)) {
|
||||
match block_async(lattice_net::join_mesh(node, endpoint, peer_id)) {
|
||||
Ok(handle) => {
|
||||
println!("Joined mesh! Use 'sync' command to sync entries.");
|
||||
CommandResult::SwitchTo(handle)
|
||||
@@ -719,7 +729,7 @@ fn cmd_join(node: &LatticeNode, store: Option<&StoreHandle>, endpoint: Option<&L
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_sync(node: &LatticeNode, store: Option<&StoreHandle>, endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
fn cmd_sync(node: &Node, store: Option<&StoreHandle>, endpoint: Option<&LatticeEndpoint>, args: &[String]) -> CommandResult {
|
||||
let endpoint = match endpoint {
|
||||
Some(ep) => ep,
|
||||
None => {
|
||||
@@ -738,7 +748,7 @@ fn cmd_sync(node: &LatticeNode, store: Option<&StoreHandle>, endpoint: Option<&L
|
||||
|
||||
if args.is_empty() {
|
||||
// Sync with all active peers
|
||||
match block_async(crate::sync::sync_all(node, endpoint, store)) {
|
||||
match block_async(lattice_net::sync_all(node, endpoint, store)) {
|
||||
Ok(results) => {
|
||||
if results.is_empty() {
|
||||
println!("No peers to sync with.");
|
||||
@@ -760,7 +770,7 @@ fn cmd_sync(node: &LatticeNode, store: Option<&StoreHandle>, endpoint: Option<&L
|
||||
};
|
||||
|
||||
println!("Syncing with {}...", peer_id.fmt_short());
|
||||
match block_async(crate::sync::sync_with_peer(node, endpoint, store, peer_id)) {
|
||||
match block_async(lattice_net::sync_with_peer(node, endpoint, store, peer_id)) {
|
||||
Ok(result) => {
|
||||
println!("Sync complete! Applied {} entries (peer sent {})",
|
||||
result.entries_applied, result.entries_sent_by_peer);
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
//! Lattice Interactive CLI
|
||||
|
||||
mod accept_handler;
|
||||
mod node;
|
||||
mod commands;
|
||||
mod store_actor;
|
||||
mod sync_protocol;
|
||||
mod sync;
|
||||
|
||||
use accept_handler::spawn_accept_loop;
|
||||
use lattice_net::spawn_accept_loop;
|
||||
use commands::CommandResult;
|
||||
use node::{LatticeNodeBuilder, StoreHandle};
|
||||
use lattice_core::{NodeBuilder, StoreHandle};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::DefaultEditor;
|
||||
use std::sync::Arc;
|
||||
@@ -20,7 +15,7 @@ async fn main() {
|
||||
println!("Lattice CLI v{}", env!("CARGO_PKG_VERSION"));
|
||||
println!("Type 'help' for commands, 'quit' to exit.\n");
|
||||
|
||||
let node = match LatticeNodeBuilder::new().build() {
|
||||
let node = match NodeBuilder::new().build() {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialize: {}", e);
|
||||
|
||||
@@ -1,551 +0,0 @@
|
||||
//! Local Lattice node API with multi-store support
|
||||
|
||||
use lattice_core::{
|
||||
DataDir, MetaStore, Node, SigChain, Store, Uuid,
|
||||
log::LogError,
|
||||
meta_store::MetaStoreError,
|
||||
sigchain::SigChainError,
|
||||
store::StoreError,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NodeError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Store error: {0}")]
|
||||
Store(#[from] StoreError),
|
||||
|
||||
#[error("MetaStore error: {0}")]
|
||||
MetaStore(#[from] MetaStoreError),
|
||||
|
||||
#[error("SigChain error: {0}")]
|
||||
SigChain(#[from] SigChainError),
|
||||
|
||||
#[error("Log error: {0}")]
|
||||
Log(#[from] LogError),
|
||||
|
||||
#[error("Node error: {0}")]
|
||||
Node(#[from] lattice_core::node::NodeError),
|
||||
|
||||
#[error("Already initialized")]
|
||||
AlreadyInitialized,
|
||||
|
||||
#[error("Channel closed")]
|
||||
ChannelClosed,
|
||||
|
||||
#[error("Actor error: {0}")]
|
||||
Actor(String),
|
||||
}
|
||||
|
||||
/// Peer status values used across the system
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PeerStatus {
|
||||
/// Peer has been invited but hasn't joined yet
|
||||
Invited,
|
||||
/// Peer is active and can sync
|
||||
Active,
|
||||
/// Peer has been removed from the mesh
|
||||
Removed,
|
||||
}
|
||||
|
||||
impl PeerStatus {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
PeerStatus::Invited => "invited",
|
||||
PeerStatus::Active => "active",
|
||||
PeerStatus::Removed => "removed",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<PeerStatus> {
|
||||
match s {
|
||||
"invited" => Some(PeerStatus::Invited),
|
||||
"active" => Some(PeerStatus::Active),
|
||||
"removed" => Some(PeerStatus::Removed),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeInfo {
|
||||
pub node_id: String,
|
||||
pub data_path: String,
|
||||
pub stores: Vec<Uuid>,
|
||||
}
|
||||
|
||||
pub struct StoreInfo {
|
||||
pub store_id: Uuid,
|
||||
pub entries_replayed: u64,
|
||||
}
|
||||
|
||||
pub struct LatticeNodeBuilder {
|
||||
pub data_dir: DataDir,
|
||||
}
|
||||
|
||||
impl LatticeNodeBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self { data_dir: DataDir::default() }
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<LatticeNode, NodeError> {
|
||||
self.data_dir.ensure_dirs()?;
|
||||
|
||||
let key_path = self.data_dir.identity_key();
|
||||
let node = if key_path.exists() {
|
||||
Node::load(&key_path)?
|
||||
} else {
|
||||
let node = Node::generate();
|
||||
node.save(&key_path)?;
|
||||
node
|
||||
};
|
||||
|
||||
let meta = MetaStore::open(self.data_dir.meta_db())?;
|
||||
|
||||
Ok(LatticeNode {
|
||||
data_dir: self.data_dir,
|
||||
node: Rc::new(node),
|
||||
meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LatticeNodeBuilder {
|
||||
fn default() -> Self { Self::new() }
|
||||
}
|
||||
|
||||
/// A local Lattice node (manages identity and store registry)
|
||||
pub struct LatticeNode {
|
||||
data_dir: DataDir,
|
||||
node: Rc<Node>,
|
||||
meta: MetaStore,
|
||||
}
|
||||
|
||||
impl LatticeNode {
|
||||
pub fn info(&self) -> NodeInfo {
|
||||
NodeInfo {
|
||||
node_id: hex::encode(self.node.public_key_bytes()),
|
||||
data_path: self.data_dir.base().display().to_string(),
|
||||
stores: self.meta.list_stores().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> [u8; 32] {
|
||||
self.node.public_key_bytes()
|
||||
}
|
||||
|
||||
/// Get the secret key bytes for Iroh integration (same Ed25519 key)
|
||||
pub fn secret_key_bytes(&self) -> [u8; 32] {
|
||||
self.node.secret_key_bytes()
|
||||
}
|
||||
|
||||
pub fn data_path(&self) -> &Path {
|
||||
self.data_dir.base()
|
||||
}
|
||||
|
||||
/// Get the root store ID
|
||||
pub fn root_store(&self) -> Result<Option<Uuid>, NodeError> {
|
||||
Ok(self.meta.root_store()?)
|
||||
}
|
||||
/// Open the root store if set
|
||||
pub fn open_root_store(&self) -> Result<Option<(StoreHandle, StoreInfo)>, NodeError> {
|
||||
match self.meta.root_store()? {
|
||||
Some(id) => Ok(Some(self.open_store(id)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the node with a root store (fails if already initialized).
|
||||
/// Writes the node's pubkey to `/nodes/{pubkey}/info` in the root store.
|
||||
pub async fn init(&self) -> Result<(Uuid, StoreHandle), NodeError> {
|
||||
if self.meta.root_store()?.is_some() {
|
||||
return Err(NodeError::AlreadyInitialized);
|
||||
}
|
||||
let store_id = self.create_store()?;
|
||||
self.meta.set_root_store(store_id)?;
|
||||
|
||||
// Open the store and write our node info
|
||||
let (handle, _) = self.open_store(store_id)?;
|
||||
let pubkey_hex = hex::encode(self.node.public_key_bytes());
|
||||
let key = format!("/nodes/{}/info", pubkey_hex);
|
||||
|
||||
// Store node metadata: name (hostname), added_at (timestamp)
|
||||
let hostname = hostname::get()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
let added_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
let info = serde_json::json!({
|
||||
"name": hostname,
|
||||
"added_at": added_at
|
||||
});
|
||||
handle.put(key.as_bytes(), info.to_string().as_bytes()).await?;
|
||||
|
||||
// Write status = active
|
||||
let status_key = format!("/nodes/{}/status", pubkey_hex);
|
||||
handle.put(status_key.as_bytes(), PeerStatus::Active.as_str().as_bytes()).await?;
|
||||
|
||||
Ok((store_id, handle))
|
||||
}
|
||||
|
||||
pub fn list_stores(&self) -> Result<Vec<Uuid>, NodeError> {
|
||||
Ok(self.meta.list_stores()?)
|
||||
}
|
||||
|
||||
pub fn create_store(&self) -> Result<Uuid, NodeError> {
|
||||
let store_id = Uuid::new_v4();
|
||||
self.create_store_internal(store_id)
|
||||
}
|
||||
|
||||
/// Create a store with a specific UUID (for joining existing mesh)
|
||||
pub fn create_store_with_uuid(&self, store_id: Uuid) -> Result<Uuid, NodeError> {
|
||||
self.create_store_internal(store_id)
|
||||
}
|
||||
|
||||
/// Set a store as the root store
|
||||
pub fn set_root_store(&self, store_id: Uuid) -> Result<(), NodeError> {
|
||||
self.meta.set_root_store(store_id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_store_internal(&self, store_id: Uuid) -> Result<Uuid, NodeError> {
|
||||
self.data_dir.ensure_store_dirs(store_id)?;
|
||||
let _ = Store::open(self.data_dir.store_state_db(store_id))?;
|
||||
self.meta.add_store(store_id)?;
|
||||
Ok(store_id)
|
||||
}
|
||||
|
||||
pub fn open_store(&self, store_id: Uuid) -> Result<(StoreHandle, StoreInfo), NodeError> {
|
||||
self.data_dir.ensure_store_dirs(store_id)?;
|
||||
|
||||
let author_id_hex = hex::encode(self.node.public_key_bytes());
|
||||
let log_path = self.data_dir.store_log_file(store_id, &author_id_hex);
|
||||
|
||||
let sigchain = if log_path.exists() {
|
||||
SigChain::from_log(&log_path, *store_id.as_bytes(), self.node.public_key_bytes())?
|
||||
} else {
|
||||
SigChain::new(&log_path, *store_id.as_bytes(), self.node.public_key_bytes())
|
||||
};
|
||||
|
||||
let store = Store::open(self.data_dir.store_state_db(store_id))?;
|
||||
let entries_replayed = if log_path.exists() {
|
||||
store.replay_log(&log_path)?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let info = StoreInfo { store_id, entries_replayed };
|
||||
|
||||
// Spawn actor thread - actor owns store, sigchain, and node copy
|
||||
let (tx, actor_handle) = crate::store_actor::spawn_store_actor(
|
||||
store_id,
|
||||
store,
|
||||
sigchain,
|
||||
(*self.node).clone(),
|
||||
);
|
||||
|
||||
let handle = StoreHandle {
|
||||
store_id,
|
||||
tx,
|
||||
actor_handle: Some(actor_handle),
|
||||
};
|
||||
|
||||
Ok((handle, info))
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle to a specific store - wraps channel to actor thread
|
||||
pub struct StoreHandle {
|
||||
store_id: Uuid,
|
||||
tx: tokio::sync::mpsc::Sender<crate::store_actor::StoreCmd>,
|
||||
actor_handle: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Clone for StoreHandle {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
store_id: self.store_id,
|
||||
tx: self.tx.clone(),
|
||||
actor_handle: None, // Clones don't own the actor thread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreHandle {
|
||||
pub fn id(&self) -> Uuid { self.store_id }
|
||||
|
||||
pub async fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::Get { key: key.to_vec(), resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn get_heads(&self, key: &[u8]) -> Result<Vec<lattice_core::HeadInfo>, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::GetHeads { key: key.to_vec(), resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<Vec<(Vec<u8>, Vec<u8>)>, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::List { resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn log_seq(&self) -> u64 {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
let _ = self.tx.send(StoreCmd::LogSeq { resp: resp_tx }).await;
|
||||
resp_rx.await.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub async fn applied_seq(&self) -> Result<u64, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::AppliedSeq { resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn author_state(&self, author: &[u8; 32]) -> Result<Option<lattice_core::proto::AuthorState>, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::AuthorState { author: *author, resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn sync_state(&self) -> Result<lattice_core::sync_state::SyncState, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::SyncState { resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn read_entries_after(&self, author: &[u8; 32], from_hash: Option<[u8; 32]>) -> Result<Vec<lattice_core::proto::SignedEntry>, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::ReadEntriesAfter { author: *author, from_hash, resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn apply_entry(&self, entry: lattice_core::proto::SignedEntry) -> Result<(), NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::ApplyEntry { entry, resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(NodeError::Store)
|
||||
}
|
||||
|
||||
pub async fn put(&self, key: &[u8], value: &[u8]) -> Result<u64, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::Put { key: key.to_vec(), value: value.to_vec(), resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(|e| NodeError::Actor(e.to_string()))
|
||||
}
|
||||
|
||||
pub async fn delete(&self, key: &[u8]) -> Result<u64, NodeError> {
|
||||
use crate::store_actor::StoreCmd;
|
||||
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
|
||||
self.tx.send(StoreCmd::Delete { key: key.to_vec(), resp: resp_tx }).await
|
||||
.map_err(|_| NodeError::ChannelClosed)?;
|
||||
resp_rx.await
|
||||
.map_err(|_| NodeError::ChannelClosed)?
|
||||
.map_err(|e| NodeError::Actor(e.to_string()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Drop for StoreHandle {
|
||||
fn drop(&mut self) {
|
||||
// Only send shutdown if we own the actor (non-cloned handle)
|
||||
if let Some(handle) = self.actor_handle.take() {
|
||||
let _ = self.tx.try_send(crate::store_actor::StoreCmd::Shutdown);
|
||||
let _ = handle.join();
|
||||
}
|
||||
// Clones (actor_handle = None) don't send shutdown - actor keeps running
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env::temp_dir;
|
||||
|
||||
fn temp_data_dir(name: &str) -> DataDir {
|
||||
let path = temp_dir().join(format!("lattice_node_test_{}", name));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
DataDir::new(path)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_and_open_store() {
|
||||
let data_dir = temp_data_dir("meta_store");
|
||||
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("Failed to create node");
|
||||
|
||||
assert!(node.info().stores.is_empty());
|
||||
|
||||
let store_id = node.create_store().expect("Failed to create store");
|
||||
|
||||
// Verify it's in the list
|
||||
let stores = node.list_stores().expect("list failed");
|
||||
assert!(stores.contains(&store_id));
|
||||
|
||||
let (handle, _) = node.open_store(store_id).expect("Failed to open store");
|
||||
handle.put(b"/key", b"value").await.expect("put failed");
|
||||
assert_eq!(handle.get(b"/key").await.unwrap(), Some(b"value".to_vec()));
|
||||
|
||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_isolation() {
|
||||
let data_dir = temp_data_dir("meta_isolation");
|
||||
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("Failed to create node");
|
||||
|
||||
let store_a = node.create_store().expect("create A");
|
||||
let store_b = node.create_store().expect("create B");
|
||||
|
||||
let (handle_a, _) = node.open_store(store_a).expect("open A");
|
||||
handle_a.put(b"/key", b"from A").await.expect("put A");
|
||||
|
||||
let (handle_b, _) = node.open_store(store_b).expect("open B");
|
||||
assert_eq!(handle_b.get(b"/key").await.unwrap(), None);
|
||||
|
||||
assert_eq!(handle_a.get(b"/key").await.unwrap(), Some(b"from A".to_vec()));
|
||||
|
||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_init_creates_root_store() {
|
||||
let data_dir = temp_data_dir("init_root");
|
||||
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("create node");
|
||||
|
||||
// Initially no root store
|
||||
assert!(node.root_store().unwrap().is_none());
|
||||
|
||||
// Init creates root store
|
||||
let (root_id, _handle) = node.init().await.expect("init failed");
|
||||
assert_eq!(node.root_store().unwrap(), Some(root_id));
|
||||
|
||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_duplicate_init_fails() {
|
||||
let data_dir = temp_data_dir("init_dup");
|
||||
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("create node");
|
||||
|
||||
node.init().await.expect("first init");
|
||||
|
||||
// Second init should fail
|
||||
match node.init().await {
|
||||
Ok(_) => panic!("Expected AlreadyInitialized error"),
|
||||
Err(e) => match e {
|
||||
NodeError::AlreadyInitialized => (),
|
||||
_ => panic!("Expected AlreadyInitialized, got {:?}", e),
|
||||
},
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_root_store_in_info_after_init() {
|
||||
let data_dir = temp_data_dir("init_info");
|
||||
|
||||
// First session: init
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("create node");
|
||||
let (root_id, _) = node.init().await.expect("init");
|
||||
drop(node); // End first session
|
||||
|
||||
// Second session: root_store should persist
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("reload node");
|
||||
|
||||
assert_eq!(node.root_store().unwrap(), Some(root_id));
|
||||
|
||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_idempotent_put_and_delete() {
|
||||
let data_dir = temp_data_dir("idempotent");
|
||||
|
||||
let node = LatticeNodeBuilder { data_dir: data_dir.clone() }
|
||||
.build()
|
||||
.expect("create node");
|
||||
let (_, store) = node.init().await.expect("init");
|
||||
|
||||
// Get baseline seq after init
|
||||
let baseline = store.log_seq().await;
|
||||
|
||||
// Put twice with same value - second should be idempotent
|
||||
let seq1 = store.put(b"/key", b"value").await.expect("put 1");
|
||||
assert_eq!(seq1, baseline + 1);
|
||||
|
||||
let seq2 = store.put(b"/key", b"value").await.expect("put 2");
|
||||
assert_eq!(seq2, baseline + 1, "Second put should be idempotent (no new entry)");
|
||||
|
||||
assert_eq!(store.log_seq().await, baseline + 1);
|
||||
|
||||
// Delete twice - second should be idempotent
|
||||
let seq3 = store.delete(b"/key").await.expect("delete 1");
|
||||
assert_eq!(seq3, baseline + 2);
|
||||
|
||||
let seq4 = store.delete(b"/key").await.expect("delete 2");
|
||||
assert_eq!(seq4, baseline + 2, "Second delete should be idempotent (no new entry)");
|
||||
|
||||
assert_eq!(store.log_seq().await, baseline + 2);
|
||||
|
||||
let _ = std::fs::remove_dir_all(data_dir.base());
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
//! Store Actor - dedicated thread that owns Store and processes commands via channel
|
||||
|
||||
use lattice_core::{
|
||||
EntryBuilder, HeadInfo, Node, SigChain, SigChainManager, Store, Uuid,
|
||||
hlc::HLC,
|
||||
proto::AuthorState,
|
||||
sigchain::SigChainError,
|
||||
store::StoreError,
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
/// Commands sent to the store actor
|
||||
pub enum StoreCmd {
|
||||
Get {
|
||||
key: Vec<u8>,
|
||||
resp: oneshot::Sender<Result<Option<Vec<u8>>, StoreError>>,
|
||||
},
|
||||
GetHeads {
|
||||
key: Vec<u8>,
|
||||
resp: oneshot::Sender<Result<Vec<HeadInfo>, StoreError>>,
|
||||
},
|
||||
List {
|
||||
resp: oneshot::Sender<Result<Vec<(Vec<u8>, Vec<u8>)>, StoreError>>,
|
||||
},
|
||||
Put {
|
||||
key: Vec<u8>,
|
||||
value: Vec<u8>,
|
||||
resp: oneshot::Sender<Result<u64, StoreActorError>>,
|
||||
},
|
||||
Delete {
|
||||
key: Vec<u8>,
|
||||
resp: oneshot::Sender<Result<u64, StoreActorError>>,
|
||||
},
|
||||
LogSeq {
|
||||
resp: oneshot::Sender<u64>,
|
||||
},
|
||||
AppliedSeq {
|
||||
resp: oneshot::Sender<Result<u64, StoreError>>,
|
||||
},
|
||||
AuthorState {
|
||||
author: [u8; 32],
|
||||
resp: oneshot::Sender<Result<Option<AuthorState>, StoreError>>,
|
||||
},
|
||||
// Sync-related commands
|
||||
SyncState {
|
||||
resp: oneshot::Sender<Result<lattice_core::sync_state::SyncState, StoreError>>,
|
||||
},
|
||||
ReadEntriesAfter {
|
||||
author: [u8; 32],
|
||||
from_hash: Option<[u8; 32]>,
|
||||
resp: oneshot::Sender<Result<Vec<lattice_core::proto::SignedEntry>, StoreError>>,
|
||||
},
|
||||
ApplyEntry {
|
||||
entry: lattice_core::proto::SignedEntry,
|
||||
resp: oneshot::Sender<Result<(), StoreError>>,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StoreActorError {
|
||||
Store(StoreError),
|
||||
SigChain(SigChainError),
|
||||
}
|
||||
|
||||
impl From<StoreError> for StoreActorError {
|
||||
fn from(e: StoreError) -> Self {
|
||||
StoreActorError::Store(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SigChainError> for StoreActorError {
|
||||
fn from(e: SigChainError) -> Self {
|
||||
StoreActorError::SigChain(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StoreActorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StoreActorError::Store(e) => write!(f, "Store error: {}", e),
|
||||
StoreActorError::SigChain(e) => write!(f, "SigChain error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StoreActorError {}
|
||||
|
||||
/// The store actor - runs in its own thread, owns Store and SigChainManager
|
||||
pub struct StoreActor {
|
||||
store_id: Uuid,
|
||||
store: Store,
|
||||
chain_manager: SigChainManager, // Manages all authors' sigchains
|
||||
node: Node,
|
||||
rx: mpsc::Receiver<StoreCmd>,
|
||||
}
|
||||
|
||||
impl StoreActor {
|
||||
/// Create a new store actor (but don't start the thread yet)
|
||||
pub fn new(
|
||||
store_id: Uuid,
|
||||
store: Store,
|
||||
sigchain: SigChain,
|
||||
node: Node,
|
||||
rx: mpsc::Receiver<StoreCmd>,
|
||||
) -> Self {
|
||||
// Derive logs_dir from sigchain's log file path
|
||||
let logs_dir = sigchain.log_path()
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Create chain manager and register the local node's sigchain
|
||||
let mut chain_manager = SigChainManager::new(&logs_dir, *store_id.as_bytes());
|
||||
let local_author = node.public_key_bytes();
|
||||
chain_manager.get_or_create(local_author); // Pre-initialize local chain
|
||||
|
||||
Self {
|
||||
store_id,
|
||||
store,
|
||||
chain_manager,
|
||||
node,
|
||||
rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the actor loop - processes commands until Shutdown received
|
||||
/// Uses blocking_recv since redb is sync and we run in spawn_blocking
|
||||
pub fn run(mut self) {
|
||||
while let Some(cmd) = self.rx.blocking_recv() {
|
||||
match cmd {
|
||||
StoreCmd::Get { key, resp } => {
|
||||
let _ = resp.send(self.store.get(&key));
|
||||
}
|
||||
StoreCmd::GetHeads { key, resp } => {
|
||||
let _ = resp.send(self.store.get_heads(&key));
|
||||
}
|
||||
StoreCmd::List { resp } => {
|
||||
let _ = resp.send(self.store.list_all());
|
||||
}
|
||||
StoreCmd::Put { key, value, resp } => {
|
||||
let result = self.do_put(&key, &value);
|
||||
let _ = resp.send(result);
|
||||
}
|
||||
StoreCmd::Delete { key, resp } => {
|
||||
let result = self.do_delete(&key);
|
||||
let _ = resp.send(result);
|
||||
}
|
||||
StoreCmd::LogSeq { resp } => {
|
||||
let local_author = self.node.public_key_bytes();
|
||||
let len = self.chain_manager.get(&local_author)
|
||||
.map(|c| c.len())
|
||||
.unwrap_or(0);
|
||||
let _ = resp.send(len);
|
||||
}
|
||||
StoreCmd::AppliedSeq { resp } => {
|
||||
let author = self.node.public_key_bytes();
|
||||
let result = self.store.author_state(&author)
|
||||
.map(|s| s.map(|a| a.seq).unwrap_or(0));
|
||||
let _ = resp.send(result);
|
||||
}
|
||||
StoreCmd::AuthorState { author, resp } => {
|
||||
let _ = resp.send(self.store.author_state(&author));
|
||||
}
|
||||
StoreCmd::SyncState { resp } => {
|
||||
let _ = resp.send(self.store.sync_state());
|
||||
}
|
||||
StoreCmd::ReadEntriesAfter { author, from_hash, resp } => {
|
||||
// Read entries from the log file for this author
|
||||
let result = self.do_read_entries_after(&author, from_hash);
|
||||
let _ = resp.send(result);
|
||||
}
|
||||
StoreCmd::ApplyEntry { entry, resp } => {
|
||||
// Use SigChainManager to append to the correct author's log
|
||||
if let Err(e) = self.chain_manager.append_entry(&entry) {
|
||||
let _ = resp.send(Err(StoreError::from(e)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Then apply to store
|
||||
let result = self.store.apply_entry(&entry);
|
||||
let _ = resp.send(result);
|
||||
}
|
||||
StoreCmd::Shutdown => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_put(&mut self, key: &[u8], value: &[u8]) -> Result<u64, StoreActorError> {
|
||||
let heads = self.store.get_heads(key)?;
|
||||
|
||||
// Idempotency check (pure function)
|
||||
if !Store::needs_put(&heads, value) {
|
||||
let local_author = self.node.public_key_bytes();
|
||||
return Ok(self.chain_manager.get(&local_author).map(|c| c.len()).unwrap_or(0));
|
||||
}
|
||||
|
||||
let parent_hashes: Vec<Vec<u8>> = heads.iter().map(|h| h.hash.clone()).collect();
|
||||
self.commit_entry(parent_hashes, |b| b.put(key.to_vec(), value.to_vec()))
|
||||
}
|
||||
|
||||
fn do_delete(&mut self, key: &[u8]) -> Result<u64, StoreActorError> {
|
||||
let heads = self.store.get_heads(key)?;
|
||||
|
||||
// Idempotency check (pure function)
|
||||
if !Store::needs_delete(&heads) {
|
||||
let local_author = self.node.public_key_bytes();
|
||||
return Ok(self.chain_manager.get(&local_author).map(|c| c.len()).unwrap_or(0));
|
||||
}
|
||||
|
||||
let parent_hashes: Vec<Vec<u8>> = heads.iter().map(|h| h.hash.clone()).collect();
|
||||
self.commit_entry(parent_hashes, |b| b.delete(key.to_vec()))
|
||||
}
|
||||
|
||||
fn commit_entry<F>(&mut self, parent_hashes: Vec<Vec<u8>>, build: F) -> Result<u64, StoreActorError>
|
||||
where
|
||||
F: FnOnce(EntryBuilder) -> EntryBuilder,
|
||||
{
|
||||
let local_author = self.node.public_key_bytes();
|
||||
let sigchain = self.chain_manager.get_or_create(local_author);
|
||||
|
||||
let seq = sigchain.len() + 1;
|
||||
let prev_hash = *sigchain.last_hash();
|
||||
|
||||
let builder = EntryBuilder::new(seq, HLC::now())
|
||||
.store_id(self.store_id.as_bytes().to_vec())
|
||||
.prev_hash(prev_hash.to_vec())
|
||||
.parent_hashes(parent_hashes);
|
||||
let entry = build(builder).sign(&self.node);
|
||||
|
||||
// Append to local sigchain
|
||||
let sigchain = self.chain_manager.get_or_create(local_author);
|
||||
sigchain.append(&entry)?;
|
||||
self.store.apply_entry(&entry)?;
|
||||
|
||||
Ok(seq)
|
||||
}
|
||||
|
||||
fn do_read_entries_after(
|
||||
&self,
|
||||
author: &[u8; 32],
|
||||
from_hash: Option<[u8; 32]>,
|
||||
) -> Result<Vec<lattice_core::proto::SignedEntry>, StoreError> {
|
||||
// Build log path for this author
|
||||
let author_hex = hex::encode(author);
|
||||
let log_path = self.chain_manager.logs_dir().join(format!("{}.log", author_hex));
|
||||
|
||||
if !log_path.exists() {
|
||||
return Ok(Vec::new()); // No log file for this author
|
||||
}
|
||||
|
||||
// Use lattice_core's read_entries_after
|
||||
lattice_core::log::read_entries_after(&log_path, from_hash)
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a store actor in a new thread, returns (sender, join_handle)
|
||||
/// Uses std::thread since redb is blocking
|
||||
pub fn spawn_store_actor(
|
||||
store_id: Uuid,
|
||||
store: Store,
|
||||
sigchain: SigChain,
|
||||
node: Node,
|
||||
) -> (mpsc::Sender<StoreCmd>, JoinHandle<()>) {
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let actor = StoreActor::new(store_id, store, sigchain, node, rx);
|
||||
let handle = thread::spawn(move || actor.run());
|
||||
(tx, handle)
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
//! Sync networking operations for LatticeNode
|
||||
//!
|
||||
//! Provides async methods for joining meshes and syncing with peers.
|
||||
|
||||
use lattice_net::{MessageSink, MessageStream};
|
||||
use crate::node::{LatticeNode, NodeError, StoreHandle, PeerStatus};
|
||||
use lattice_core::proto::{peer_message, PeerMessage, JoinRequest, SignedEntry};
|
||||
use lattice_net::LatticeEndpoint;
|
||||
use prost::Message;
|
||||
|
||||
/// Result of a sync operation with a peer
|
||||
pub struct SyncResult {
|
||||
pub entries_applied: u64,
|
||||
pub entries_sent_by_peer: u64,
|
||||
}
|
||||
|
||||
/// Join an existing mesh by connecting to a peer.
|
||||
/// Returns the new StoreHandle on success.
|
||||
/// After joining, automatically syncs with the peer to get initial data.
|
||||
pub async fn join_mesh(
|
||||
node: &LatticeNode,
|
||||
endpoint: &LatticeEndpoint,
|
||||
peer_id: iroh::PublicKey,
|
||||
) -> Result<StoreHandle, NodeError> {
|
||||
// Connect to peer
|
||||
let conn = endpoint.connect(peer_id).await
|
||||
.map_err(|e| NodeError::Actor(format!("Connection failed: {}", e)))?;
|
||||
|
||||
// Open stream
|
||||
let (send, recv) = conn.open_bi().await
|
||||
.map_err(|e| NodeError::Actor(format!("Failed to open stream: {}", e)))?;
|
||||
|
||||
let mut sink = MessageSink::new(send);
|
||||
let mut stream = MessageStream::new(recv);
|
||||
|
||||
// Send JoinRequest
|
||||
let req = PeerMessage {
|
||||
message: Some(peer_message::Message::JoinRequest(JoinRequest {
|
||||
node_pubkey: node.node_id().to_vec(),
|
||||
})),
|
||||
};
|
||||
sink.send(&req).await
|
||||
.map_err(|e| NodeError::Actor(format!("Failed to send: {}", e)))?;
|
||||
sink.finish().await
|
||||
.map_err(|e| NodeError::Actor(format!("Failed to finish: {}", e)))?;
|
||||
|
||||
// Receive JoinResponse
|
||||
let msg = stream.recv().await
|
||||
.map_err(|e| NodeError::Actor(format!("Recv error: {}", e)))?
|
||||
.ok_or_else(|| NodeError::Actor("Peer closed stream".to_string()))?;
|
||||
|
||||
match msg.message {
|
||||
Some(peer_message::Message::JoinResponse(resp)) => {
|
||||
let store_uuid = lattice_core::Uuid::from_slice(&resp.store_uuid)
|
||||
.map_err(|_| NodeError::Actor("Invalid UUID from peer".to_string()))?;
|
||||
|
||||
// Create local store with that UUID
|
||||
node.create_store_with_uuid(store_uuid)?;
|
||||
node.set_root_store(store_uuid)?;
|
||||
|
||||
let (handle, _) = node.open_store(store_uuid)?;
|
||||
|
||||
// Immediately sync with the peer to get initial data
|
||||
println!("[Join] Syncing with peer to get initial data...");
|
||||
match sync_with_peer(node, endpoint, &handle, peer_id).await {
|
||||
Ok(result) => {
|
||||
println!("[Join] Initial sync complete: applied {} entries", result.entries_applied);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[Join] Warning: Initial sync failed: {}", e);
|
||||
// Don't fail join, just warn - peer might not have data yet
|
||||
}
|
||||
}
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
_ => Err(NodeError::Actor("Unexpected response message type".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync with a specific peer (bidirectional).
|
||||
/// Both sides exchange states and send missing entries to each other.
|
||||
pub async fn sync_with_peer(
|
||||
node: &LatticeNode,
|
||||
endpoint: &LatticeEndpoint,
|
||||
store: &StoreHandle,
|
||||
peer_id: iroh::PublicKey,
|
||||
) -> Result<SyncResult, NodeError> {
|
||||
|
||||
// Connect
|
||||
let conn = endpoint.connect(peer_id).await
|
||||
.map_err(|e| NodeError::Actor(format!("Connection failed: {}", e)))?;
|
||||
|
||||
// Open stream
|
||||
let (send, recv) = conn.open_bi().await
|
||||
.map_err(|e| NodeError::Actor(format!("Failed to open stream: {}", e)))?;
|
||||
|
||||
let mut sink = MessageSink::new(send);
|
||||
let mut stream = MessageStream::new(recv);
|
||||
|
||||
// Get our sync state
|
||||
let my_state = store.sync_state().await?;
|
||||
|
||||
// 1. Send SyncRequest with our state (don't finish yet - we'll send entries later)
|
||||
let req = PeerMessage {
|
||||
message: Some(peer_message::Message::SyncRequest(lattice_core::proto::SyncRequest {
|
||||
state: Some(my_state.to_proto()),
|
||||
full_sync: false,
|
||||
})),
|
||||
};
|
||||
sink.send(&req).await
|
||||
.map_err(|e| NodeError::Actor(format!("Failed to send: {}", e)))?;
|
||||
|
||||
// 2. Receive SyncResponse (peer's state) and entries until SyncDone
|
||||
let mut entries_applied = 0u64;
|
||||
let mut entries_sent_by_peer = 0u64;
|
||||
let mut peer_state = lattice_core::sync_state::SyncState::default();
|
||||
|
||||
loop {
|
||||
match stream.recv().await {
|
||||
Ok(Some(msg)) => match msg.message {
|
||||
Some(peer_message::Message::SyncResponse(resp)) => {
|
||||
// Peer's sync state - we'll use this to compute what to send
|
||||
if let Some(s) = resp.state {
|
||||
peer_state = lattice_core::sync_state::SyncState::from_proto(&s);
|
||||
}
|
||||
}
|
||||
Some(peer_message::Message::SyncEntry(entry)) => {
|
||||
if let Ok(signed) = SignedEntry::decode(&entry.signed_entry[..]) {
|
||||
if store.apply_entry(signed).await.is_ok() {
|
||||
entries_applied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(peer_message::Message::SyncDone(done)) => {
|
||||
entries_sent_by_peer = done.entries_sent;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Send entries peer is missing (using shared protocol)
|
||||
let entries_sent = crate::sync_protocol::send_missing_entries(&mut sink, store, &my_state, &peer_state).await
|
||||
.map_err(|e| NodeError::Actor(e))?;
|
||||
|
||||
sink.finish().await
|
||||
.map_err(|e| NodeError::Actor(format!("Failed to finish: {}", e)))?;
|
||||
|
||||
// Update own node info if we applied entries
|
||||
if entries_applied > 0 {
|
||||
let pubkey_hex = hex::encode(node.node_id());
|
||||
let info_key = format!("/nodes/{}/info", pubkey_hex);
|
||||
let info_val = serde_json::json!({
|
||||
"name": hostname::get().map(|h| h.to_string_lossy().to_string()).unwrap_or_default(),
|
||||
"added_at": std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}).to_string();
|
||||
let _ = store.put(info_key.as_bytes(), info_val.as_bytes()).await;
|
||||
|
||||
// Set own status to 'active' (we're now a fully synced peer)
|
||||
let status_key = format!("/nodes/{}/status", pubkey_hex);
|
||||
let _ = store.put(status_key.as_bytes(), PeerStatus::Active.as_str().as_bytes()).await;
|
||||
}
|
||||
|
||||
println!("[Sync] Applied {} entries, sent {} entries", entries_applied, entries_sent);
|
||||
|
||||
Ok(SyncResult {
|
||||
entries_applied,
|
||||
entries_sent_by_peer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sync with all active peers from the store.
|
||||
pub async fn sync_all(
|
||||
node: &LatticeNode,
|
||||
endpoint: &LatticeEndpoint,
|
||||
store: &StoreHandle,
|
||||
) -> Result<Vec<SyncResult>, NodeError> {
|
||||
let my_pubkey = hex::encode(node.node_id());
|
||||
|
||||
// Get all active peers (invited peers haven't joined yet)
|
||||
let all_entries = store.list().await?;
|
||||
let mut peer_ids = Vec::new();
|
||||
|
||||
for (key, value) in &all_entries {
|
||||
let key_str = String::from_utf8_lossy(key);
|
||||
if key_str.ends_with("/status") && value == PeerStatus::Active.as_str().as_bytes() {
|
||||
if let Some(pubkey) = key_str.strip_prefix("/nodes/").and_then(|s| s.strip_suffix("/status")) {
|
||||
if pubkey != my_pubkey {
|
||||
if let Ok(id) = lattice_net::parse_node_id(pubkey) {
|
||||
peer_ids.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync with each peer
|
||||
let mut results = Vec::new();
|
||||
for peer_id in peer_ids {
|
||||
match sync_with_peer(node, endpoint, store, peer_id).await {
|
||||
Ok(result) => results.push(result),
|
||||
Err(e) => {
|
||||
// Log error but continue with other peers
|
||||
eprintln!("Sync with {} failed: {}", peer_id.fmt_short(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
//! Sync Protocol - shared logic for bidirectional sync
|
||||
//!
|
||||
//! Provides reusable functions for sending and receiving entries during sync.
|
||||
//! Used by both accept_handler (incoming sync) and sync (outgoing sync).
|
||||
|
||||
use crate::node::StoreHandle;
|
||||
use lattice_core::proto::{peer_message, PeerMessage, SignedEntry};
|
||||
use lattice_core::sync_state::SyncState;
|
||||
use lattice_net::{MessageSink, MessageStream};
|
||||
use prost::Message;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Send entries that peer is missing based on state diff.
|
||||
/// Returns (entries_sent, optional_error).
|
||||
pub async fn send_missing_entries(
|
||||
sink: &mut MessageSink,
|
||||
store: &StoreHandle,
|
||||
my_state: &SyncState,
|
||||
peer_state: &SyncState,
|
||||
) -> Result<u64, String> {
|
||||
let missing = peer_state.diff(my_state);
|
||||
|
||||
// Build queues for each author's entries
|
||||
let mut author_entries: Vec<VecDeque<SignedEntry>> = Vec::new();
|
||||
for range in missing {
|
||||
let from_hash = if range.from_hash == [0u8; 32] { None } else { Some(range.from_hash) };
|
||||
let entries = store.read_entries_after(&range.author, from_hash).await
|
||||
.map_err(|e| format!("Failed to read entries: {}", e))?;
|
||||
if !entries.is_empty() {
|
||||
author_entries.push(entries.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Stream entries in HLC (causal) order
|
||||
let mut entries_sent = 0u64;
|
||||
for entry in lattice_core::CausalEntryIter::new(author_entries) {
|
||||
let sync_msg = PeerMessage {
|
||||
message: Some(peer_message::Message::SyncEntry(lattice_core::proto::SyncEntry {
|
||||
signed_entry: entry.encode_to_vec(),
|
||||
hash: vec![],
|
||||
})),
|
||||
};
|
||||
sink.send(&sync_msg).await?;
|
||||
entries_sent += 1;
|
||||
}
|
||||
|
||||
// Send SyncDone
|
||||
let done = PeerMessage {
|
||||
message: Some(peer_message::Message::SyncDone(lattice_core::proto::SyncDone {
|
||||
entries_sent,
|
||||
})),
|
||||
};
|
||||
sink.send(&done).await?;
|
||||
|
||||
Ok(entries_sent)
|
||||
}
|
||||
|
||||
/// Receive and apply entries until SyncDone is received.
|
||||
/// Returns (entries_applied, entries_reported_by_peer).
|
||||
pub async fn receive_entries(
|
||||
stream: &mut MessageStream,
|
||||
store: &StoreHandle,
|
||||
) -> Result<(u64, u64), String> {
|
||||
let mut entries_applied = 0u64;
|
||||
let mut entries_reported = 0u64;
|
||||
|
||||
loop {
|
||||
match stream.recv().await {
|
||||
Ok(Some(msg)) => match msg.message {
|
||||
Some(peer_message::Message::SyncEntry(entry)) => {
|
||||
if let Ok(signed) = SignedEntry::decode(&entry.signed_entry[..]) {
|
||||
if store.apply_entry(signed).await.is_ok() {
|
||||
entries_applied += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(peer_message::Message::SyncDone(done)) => {
|
||||
entries_reported = done.entries_sent;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(None) => break,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok((entries_applied, entries_reported))
|
||||
}
|
||||
Reference in New Issue
Block a user