use crate::{agents::ForAgent, Db, Storelike};
use iroh::{protocol::Router, Endpoint, NodeId};
use std::sync::OnceLock;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn io_err(e: impl std::fmt::Display) -> crate::errors::AtomicError {
format!("Iroh I/O error: {e}").into()
}
pub(crate) const ATOMIC_ALPN: &[u8] = b"atomic/1";
pub fn normalize_node_id(id: &str) -> String {
let mut s = id.trim().to_string();
if let Some(rest) = s.strip_prefix("did:ad:node:") {
s = rest.split(':').next().unwrap_or(rest).to_string();
} else if let Some(rest) = s.strip_prefix("iroh:") {
s = rest.to_string();
}
s.to_lowercase()
}
static NODE_ID: OnceLock<String> = OnceLock::new();
pub fn effective_device_name(store: &Db) -> String {
let from_db = get_device_name(store);
let raw = if !from_db.trim().is_empty() {
from_db
} else {
hostname::get()
.ok()
.and_then(|os| os.into_string().ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "Unknown".to_string())
};
let max = super::protocol::HELLO_MAX_CHARS;
if raw.chars().count() > max {
raw.chars().take(max).collect()
} else {
raw
}
}
pub fn get_node_id() -> Option<&'static str> {
NODE_ID.get().map(|s| s.as_str())
}
const IROH_SECRET_KEY: &[u8] = b"_iroh_secret_key";
const DEVICE_NAME_KEY: &[u8] = b"_device_name";
pub fn get_device_name(store: &Db) -> String {
store
.kv
.get(crate::db::trees::Tree::PluginMeta, DEVICE_NAME_KEY)
.ok()
.flatten()
.and_then(|b| String::from_utf8(b).ok())
.unwrap_or_default()
}
pub fn set_device_name(store: &Db, name: &str) {
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
DEVICE_NAME_KEY,
name.as_bytes(),
);
}
fn load_or_create_secret_key(store: &Db) -> iroh::SecretKey {
if let Ok(Some(bytes)) = store
.kv
.get(crate::db::trees::Tree::PluginMeta, IROH_SECRET_KEY)
{
if bytes.len() == 32 {
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
return iroh::SecretKey::from_bytes(&arr);
}
}
let key = iroh::SecretKey::generate(rand::rngs::OsRng);
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
IROH_SECRET_KEY,
&key.to_bytes(),
);
let _ = store.flush();
key
}
pub async fn start(store: Db) -> anyhow::Result<(NodeId, Router)> {
let secret_key = load_or_create_secret_key(&store);
let endpoint: Endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery_n0()
.discovery_local_network()
.bind()
.await?;
let node_id = endpoint.node_id();
NODE_ID.set(node_id.to_string()).ok();
ENDPOINT.set(endpoint.clone()).ok();
let relay = endpoint.home_relay();
tracing::info!("Iroh NodeID: {node_id}, waiting for relay...");
let relay_url =
tokio::time::timeout(std::time::Duration::from_secs(10), wait_for_relay(relay)).await;
match relay_url {
Ok(Some(url)) => tracing::info!("Iroh relay connected: {url}"),
Ok(None) => tracing::warn!("Iroh relay: none (direct connections only)"),
Err(_) => tracing::warn!("Iroh relay: timed out after 10s (connections may fail)"),
}
let bg_store = store.clone();
let router = Router::builder(endpoint)
.accept(ATOMIC_ALPN, AtomicHandler { store })
.spawn();
ROUTER.set(router.clone()).ok();
start_live_sync(bg_store.clone());
let auto_store = bg_store;
tokio::spawn(async move {
let my_id = normalize_node_id(get_node_id().unwrap_or_default());
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
loop {
let drive = match auto_store.get_active_drive() {
Some(d) => d,
None => {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
continue;
}
};
let peers = get_known_peers(&auto_store);
if peers.is_empty() {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
continue;
}
let mut all_connected = true;
for peer in &peers {
if normalize_node_id(&peer.node_id) == my_id {
continue;
}
let peer_key = normalize_node_id(&peer.node_id);
if live_peer_ids().contains(&peer_key) {
continue;
}
if normalize_node_id(&my_id) > peer_key {
if live_peer_ids().contains(&peer_key) {
continue;
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
if live_peer_ids().contains(&peer_key) {
continue;
}
}
all_connected = false;
tracing::info!(
"[auto_connect] connecting to {}",
&peer.node_id[..peer.node_id.len().min(12)]
);
match sync_drive_with_peer_if_needed(&peer.node_id, &drive, &auto_store).await {
Ok(count) => {
tracing::info!(
"[auto_connect] synced {count} resources, live connection established"
);
}
Err(e) => {
tracing::debug!("[auto_connect] failed: {e}");
}
}
}
if all_connected {
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
} else {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
});
Ok((node_id, router))
}
async fn wait_for_relay(
mut watcher: iroh::watchable::Watcher<Option<iroh::RelayUrl>>,
) -> Option<iroh::RelayUrl> {
if let Ok(Some(url)) = watcher.get() {
return Some(url);
}
loop {
if watcher.updated().await.is_err() {
return None;
}
if let Ok(Some(url)) = watcher.get() {
return Some(url);
}
}
}
#[derive(Debug, Clone)]
struct AtomicHandler {
store: Db,
}
impl iroh::protocol::ProtocolHandler for AtomicHandler {
fn accept(
&self,
connection: iroh::endpoint::Connection,
) -> futures::future::BoxFuture<'static, anyhow::Result<()>> {
let store = self.store.clone();
Box::pin(async move {
let remote = connection.remote_node_id()?;
let remote_str = normalize_node_id(&remote.to_string());
tracing::info!("[accept] incoming connection from {remote_str}");
let (send, recv) = match connection.accept_bi().await {
Ok(pair) => pair,
Err(e) => {
tracing::info!("[accept] connection closed from {remote}: {e}");
return Ok(());
}
};
let store_clone = store.clone();
let remote_id = remote_str.clone();
match handle_stream(send, recv, store_clone, remote_id).await {
Ok(imported) => {
push_sync_event(&remote_str, imported);
}
Err(e) => {
tracing::warn!("[accept] stream error: {e}");
}
}
Ok(())
})
}
}
static ENDPOINT: OnceLock<Endpoint> = OnceLock::new();
static ROUTER: OnceLock<Router> = OnceLock::new();
use std::collections::HashMap;
use std::sync::Mutex;
static LIVE_PEERS: Mutex<Option<HashMap<String, (u64, tokio::sync::mpsc::Sender<Vec<u8>>)>>> =
Mutex::new(None);
static LIVE_PEER_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static LIVE_CONNECTIONS: Mutex<Option<Vec<iroh::endpoint::Connection>>> = Mutex::new(None);
static LIVE_PEER_NAMES: Mutex<Option<HashMap<String, String>>> = Mutex::new(None);
pub fn set_live_peer_name(peer_id: &str, name: &str) {
if name.is_empty() {
return;
}
if let Ok(mut guard) = LIVE_PEER_NAMES.lock() {
guard
.get_or_insert_with(HashMap::new)
.insert(normalize_node_id(peer_id), name.to_string());
}
}
pub fn live_peer_name(peer_id: &str) -> Option<String> {
LIVE_PEER_NAMES
.lock()
.ok()
.and_then(|guard| guard.as_ref()?.get(&normalize_node_id(peer_id)).cloned())
}
pub fn live_peer_count() -> usize {
LIVE_PEERS
.lock()
.ok()
.and_then(|map| map.as_ref().map(|m| m.len()))
.unwrap_or(0)
}
pub fn live_peer_ids() -> Vec<String> {
LIVE_PEERS
.lock()
.ok()
.and_then(|map| map.as_ref().map(|m| m.keys().cloned().collect()))
.unwrap_or_default()
}
pub fn remove_live_peer(peer_id: &str, generation: u64) {
remove_live_peer_inner(peer_id, Some(generation), true);
}
fn remove_live_peer_any_quiet(peer_id: &str) {
remove_live_peer_inner(peer_id, None, false);
}
pub fn remove_live_peer_any(peer_id: &str) {
remove_live_peer_inner(peer_id, None, true);
}
fn remove_live_peer_inner(peer_id: &str, generation: Option<u64>, notify: bool) {
let key = normalize_node_id(peer_id);
let mut removed = false;
if let Ok(mut guard) = LIVE_PEERS.lock() {
if let Some(map) = guard.as_mut() {
let is_current = match generation {
Some(generation) => map.get(&key).is_some_and(|(gen, _)| *gen == generation),
None => true,
};
if is_current {
removed = map.remove(&key).is_some();
} else if map.contains_key(&key) {
tracing::debug!(
"[live] stale connection for {} tried to deregister a newer one — ignored",
&key[..key.len().min(12)]
);
}
}
}
if removed {
tracing::info!("[live] removed peer {}", &key[..key.len().min(12)]);
if notify {
push_event(&key, 0, "disconnected");
}
}
}
static LAST_EVENT_MS: OnceLock<std::sync::Mutex<std::collections::HashMap<(String, String), u64>>> =
OnceLock::new();
const EVENT_DEBOUNCE_MS: u64 = 15_000;
fn last_event_ms() -> &'static std::sync::Mutex<std::collections::HashMap<(String, String), u64>> {
LAST_EVENT_MS.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
}
pub fn is_importing() -> bool {
super::ws_apply::is_importing()
}
fn encode_live_update_wire_msg(subject_key: &str, loro_bytes: &[u8]) -> Vec<u8> {
let frame = super::protocol::encode_update(0, 0, subject_key, None, loro_bytes);
let len = frame.len() as u32;
let mut msg = Vec::with_capacity(4 + frame.len());
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&frame);
msg
}
fn own_agent_update_frame(store: &Db) -> Option<Vec<u8>> {
let agent = store.get_default_agent().ok()?;
let key = crate::Subject::from_raw(
&agent.subject.to_string(),
store.get_base_domain().as_deref(),
)
.pure_id();
let snapshot = store
.kv
.get(crate::db::trees::Tree::LoroSnapshots, key.as_bytes())
.ok()
.flatten()
.filter(|b| !b.is_empty())?;
Some(encode_live_update_wire_msg(&key, &snapshot))
}
fn send_live_update_wire_msg_except(msg: Vec<u8>, skip_peer: Option<&str>) {
let mut dead_peers = Vec::new();
let peers = LIVE_PEERS.lock().unwrap();
if let Some(map) = peers.as_ref() {
for (peer_id, (generation, tx)) in map {
if skip_peer.is_some_and(|skip| normalize_node_id(skip) == *peer_id) {
continue;
}
match tx.try_send(msg.clone()) {
Ok(_) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(m)) => {
let peer = peer_id.clone();
let tx_retry = tx.clone();
tokio::spawn(async move {
if tx_retry.send(m).await.is_err() {
tracing::warn!(
"[live_sync] retry send failed for {}",
&peer[..peer.len().min(12)]
);
}
});
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
dead_peers.push((peer_id.clone(), *generation));
}
}
}
}
drop(peers);
for (peer_id, generation) in dead_peers {
remove_live_peer(&peer_id, generation);
}
}
fn send_live_update_wire_msg(msg: Vec<u8>) {
send_live_update_wire_msg_except(msg, None);
}
pub fn broadcast_ephemeral(
kind: u8,
drive: &str,
agent: &str,
payload: &[u8],
skip_peer: Option<&str>,
) {
if payload.is_empty() || payload.len() > super::protocol::max_payload_for_kind(kind) {
return;
}
let frame = super::protocol::encode_ephemeral(kind, drive, agent, payload);
let len = frame.len() as u32;
let mut msg = Vec::with_capacity(4 + frame.len());
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&frame);
send_live_update_wire_msg_except(msg, skip_peer);
}
pub fn broadcast_live_update(subject_key: &str, loro_bytes: &[u8]) {
if loro_bytes.is_empty() || super::ws_apply::is_importing() {
return;
}
let msg = encode_live_update_wire_msg(subject_key, loro_bytes);
send_live_update_wire_msg(msg);
}
fn start_live_sync(store: Db) {
{
let mut map = LIVE_PEERS.lock().unwrap();
if map.is_none() {
*map = Some(HashMap::new());
}
}
{
let mut conns = LIVE_CONNECTIONS.lock().unwrap();
if conns.is_none() {
*conns = Some(Vec::new());
}
}
tokio::spawn(async move {
let mut rx = store.subscribe_events();
tracing::info!("[live_sync] push loop started");
loop {
let event = match rx.recv().await {
Ok(e) => e,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
rx = store.subscribe_events();
continue;
}
};
if super::ws_apply::is_importing() {
continue;
}
let subject_key = match &event {
crate::DbEvent::Changed { subject, .. }
| crate::DbEvent::Destroyed { subject, .. } => subject.pure_id(),
_ => continue,
};
let from_peer: Option<String> = match &event {
crate::DbEvent::Changed { source_id, .. }
| crate::DbEvent::Destroyed { source_id, .. } => source_id.clone(),
_ => None,
};
let loro_bytes: Option<Vec<u8>> = match &event {
crate::DbEvent::Changed {
delta: Some(delta), ..
} if !delta.is_empty() => Some(delta.clone()),
crate::DbEvent::Changed { .. } => store
.kv
.get(
crate::db::trees::Tree::LoroSnapshots,
subject_key.as_bytes(),
)
.ok()
.flatten()
.filter(|b| !b.is_empty()),
crate::DbEvent::Destroyed { .. } => {
let frame = super::protocol::encode_destroy(0, &subject_key);
let len = frame.len() as u32;
let mut msg = Vec::with_capacity(4 + frame.len());
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(&frame);
send_live_update_wire_msg_except(msg, from_peer.as_deref());
continue;
}
_ => None,
};
if let Some(bytes) = loro_bytes {
let msg = encode_live_update_wire_msg(&subject_key, &bytes);
send_live_update_wire_msg_except(msg, from_peer.as_deref());
}
}
});
}
#[cfg(test)]
pub(crate) async fn admitted_for_drive_for_test(
store: &Db,
agent: &ForAgent,
drive_subject: &str,
trust_owned: bool,
cache: &mut std::collections::HashMap<String, bool>,
) -> bool {
admitted_for_drive(store, agent, drive_subject, trust_owned, cache).await
}
async fn admitted_for_drive(
store: &Db,
agent: &ForAgent,
drive_subject: &str,
trust_owned: bool,
cache: &mut std::collections::HashMap<String, bool>,
) -> bool {
if let Some(&verdict) = cache.get(drive_subject) {
return verdict;
}
let policy = store.sync_policy();
if !policy.admit_drive_write(drive_subject) {
let drive_subj =
crate::Subject::from_raw(drive_subject, store.get_base_domain().as_deref());
let is_new_here = store.get_resource(&drive_subj).await.is_err();
if is_new_here && policy.may_enroll_drive(drive_subject, agent) {
policy.enroll_drive(drive_subject);
} else {
cache.insert(drive_subject.to_string(), false);
return false;
}
}
let drive_subj = crate::Subject::from_raw(drive_subject, store.get_base_domain().as_deref());
let verdict = match store.get_resource(&drive_subj).await {
Ok(drive_resource) => {
super::engine::may_accept_drive_write(store, &drive_resource, agent, trust_owned).await
}
Err(_) => true,
};
cache.insert(drive_subject.to_string(), verdict);
verdict
}
async fn apply_peer_remove(
store: &Db,
agent: &ForAgent,
subject: &str,
trust_owned: bool,
drive_cache: &mut std::collections::HashMap<String, bool>,
) {
match super::ws_apply::resolve_destroy_drive(store, subject).await {
Some(drive_subject) => {
if admitted_for_drive(store, agent, &drive_subject, trust_owned, drive_cache).await {
let _ = super::ws_apply::apply_destroy_checked(store, subject).await;
} else {
tracing::warn!(
"[sync] rejected SYNC_DIFF remove for {} from peer: not admitted for drive {}",
&subject[..subject.len().min(30)],
&drive_subject[..drive_subject.len().min(30)]
);
}
}
None => {
let _ = super::ws_apply::apply_destroy_checked(store, subject).await;
}
}
}
fn invalidate_drive_cache_on_identity_change(
agent: &ForAgent,
previous: &ForAgent,
drive_cache: &mut std::collections::HashMap<String, bool>,
) {
if agent != previous {
drive_cache.clear();
}
}
fn register_live_peer(
peer_id: String,
mut send: iroh::endpoint::SendStream,
mut recv: iroh::endpoint::RecvStream,
store: Db,
agent: ForAgent,
initiated_by_us: bool,
) {
let key = normalize_node_id(&peer_id);
let (tx, mut rx) = tokio::sync::mpsc::channel::<Vec<u8>>(64);
let tx_for_read = tx.clone();
let generation = LIVE_PEER_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
let is_new_peer = {
let mut map = LIVE_PEERS.lock().unwrap();
if let Some(m) = map.as_mut() {
let replacing = m.contains_key(&key);
if replacing {
tracing::info!(
"[live] replacing existing connection to {}",
&key[..key.len().min(12)]
);
}
m.insert(key.clone(), (generation, tx));
!replacing
} else {
false
}
};
let peer_short = key[..key.len().min(12)].to_string();
tracing::info!("[live] registered peer {peer_short} (new={is_new_peer})");
push_event(&key, 0, "connected");
if let Some(frame) = own_agent_update_frame(&store) {
let _ = tx_for_read.try_send(frame);
}
let write_peer_id = key.clone();
tokio::spawn(async move {
tracing::info!(
"[live] write loop started for {}",
&write_peer_id[..write_peer_id.len().min(12)]
);
loop {
let msg =
match tokio::time::timeout(super::protocol::KEEPALIVE_INTERVAL, rx.recv()).await {
Ok(Some(msg)) => msg,
Ok(None) => break,
Err(_) => super::protocol::encode_keepalive_wire_msg(),
};
match send.write_all(&msg).await {
Ok(_) => {
tracing::trace!(
"[live] wrote {} bytes to {}",
msg.len(),
&write_peer_id[..write_peer_id.len().min(12)]
);
}
Err(e) => {
tracing::warn!(
"[live] write failed to {}: {e}",
&write_peer_id[..write_peer_id.len().min(12)]
);
break;
}
}
}
tracing::info!(
"[live] write loop ended for {}",
&write_peer_id[..write_peer_id.len().min(12)]
);
remove_live_peer(&write_peer_id, generation);
});
let read_peer_id = key.clone();
tokio::spawn(async move {
tracing::info!(
"[live] read loop started for {} as {agent:?}",
&read_peer_id[..read_peer_id.len().min(12)]
);
let mut agent = agent;
let mut drive_cache: std::collections::HashMap<String, bool> =
std::collections::HashMap::new();
let mut peer_sends_keepalives = false;
loop {
let read =
tokio::time::timeout(super::protocol::LIVENESS_TIMEOUT, recv.read_u32()).await;
let len = match read {
Ok(Ok(n)) => {
tracing::trace!(
"[live] received frame {} bytes from {}",
n,
&read_peer_id[..read_peer_id.len().min(12)]
);
n as usize
}
Ok(Err(e)) => {
tracing::info!(
"[live] read error from {}: {e}",
&read_peer_id[..read_peer_id.len().min(12)]
);
break;
}
Err(_) if peer_sends_keepalives => {
tracing::info!(
"[live] no traffic from {} for {:?} — treating the link as dead",
&read_peer_id[..read_peer_id.len().min(12)],
super::protocol::LIVENESS_TIMEOUT
);
break;
}
Err(_) => {
tracing::debug!(
"[live] {} is quiet and sends no keepalives — not assuming it is dead",
&read_peer_id[..read_peer_id.len().min(12)]
);
continue;
}
};
let frame_cap = if matches!(agent, ForAgent::Public) {
super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES
} else {
super::protocol::IROH_FRAME_MAX_BYTES
};
if len == 0 || len > frame_cap {
break;
}
let mut buf = vec![0u8; len];
if recv.read_exact(&mut buf).await.is_err() {
break;
}
if buf.is_empty() {
continue;
}
if buf[0] == super::protocol::tag::KEEPALIVE {
peer_sends_keepalives = true;
continue;
}
if !initiated_by_us
&& matches!(agent, ForAgent::Public)
&& buf[0] != super::protocol::tag::AUTH
{
tracing::warn!(
"[live] closing: 0x{:02x} from {} before AUTH",
buf[0],
&read_peer_id[..read_peer_id.len().min(12)]
);
let _ = tx_for_read
.send(frame_with_len(&auth_required_error(buf[0])))
.await;
break;
}
if buf[0] == super::protocol::tag::ERROR {
let msg = std::str::from_utf8(buf.get(5..).unwrap_or(&[])).unwrap_or("(non-utf8)");
tracing::warn!(
"[live] {} answered with an error: {msg}",
&read_peer_id[..read_peer_id.len().min(12)]
);
continue;
}
if buf[0] == super::protocol::tag::EPHEMERAL {
if let Some(decoded) = super::protocol::decode_ephemeral(&buf[1..]) {
let scope_subj = crate::Subject::from_raw(
&decoded.drive,
store.get_base_domain().as_deref(),
);
let admitted = if decoded.kind == super::protocol::ephemeral_kind::DOC {
match super::ws_apply::resolve_destroy_drive(&store, &decoded.drive).await {
Some(drive_subject) => {
admitted_for_drive(
&store,
&agent,
&drive_subject,
initiated_by_us,
&mut drive_cache,
)
.await
}
None => false,
}
} else {
match store.get_resource(&scope_subj).await {
Ok(scope_resource) => {
crate::hierarchy::check_read(&store, &scope_resource, &agent)
.await
.is_ok()
}
Err(_) => false,
}
};
if admitted {
store.publish_ephemeral(crate::db::EphemeralEvent {
kind: decoded.kind,
drive: decoded.drive,
agent: decoded.agent,
payload: decoded.payload,
from_peer: read_peer_id.clone(),
});
} else {
tracing::debug!(
"[live] dropped a kind-{} frame {} is not admitted for",
decoded.kind,
&read_peer_id[..read_peer_id.len().min(12)]
);
}
}
continue;
}
if buf[0] == super::protocol::tag::DESTROY {
if buf.len() > 3 {
let subject = std::str::from_utf8(&buf[3..])
.unwrap_or_default()
.to_string();
match super::ws_apply::resolve_destroy_drive(&store, &subject).await {
Some(drive_subject) => {
if admitted_for_drive(
&store,
&agent,
&drive_subject,
initiated_by_us,
&mut drive_cache,
)
.await
{
let _ =
super::ws_apply::apply_destroy_checked(&store, &subject).await;
} else {
tracing::warn!(
"[live] rejected DESTROY for {} from {}: not admitted for drive {}",
&subject[..subject.len().min(20)],
&read_peer_id[..read_peer_id.len().min(12)],
&drive_subject[..drive_subject.len().min(20)]
);
}
}
None => {
let _ = super::ws_apply::apply_destroy_checked(&store, &subject).await;
}
}
}
continue;
}
if buf[0] == super::protocol::tag::UPDATE {
if let Some(decoded) = super::protocol::decode_update(&buf[1..]) {
if !decoded.loro_bytes.is_empty() {
if let Some(resolved) = super::ws_apply::resolve_update(
&store,
&decoded.subject,
&decoded.loro_bytes,
)
.await
{
if admitted_for_drive(
&store,
&agent,
&resolved.drive_subject,
initiated_by_us,
&mut drive_cache,
)
.await
{
super::ws_apply::set_import_source(Some(read_peer_id.clone()));
super::ws_apply::set_importing(true);
let _ = super::ws_apply::persist_update(
&store,
&decoded.subject,
resolved,
)
.await;
super::ws_apply::set_importing(false);
super::ws_apply::set_import_source(None);
tracing::trace!(
"[live] imported update for {} from {}",
&decoded.subject[..decoded.subject.len().min(20)],
&read_peer_id[..read_peer_id.len().min(12)]
);
} else {
tracing::warn!(
"[live] rejected UPDATE for {} from {}: not admitted for drive {}",
&decoded.subject[..decoded.subject.len().min(20)],
&read_peer_id[..read_peer_id.len().min(12)],
&resolved.drive_subject[..resolved.drive_subject.len().min(20)]
);
}
}
}
}
continue;
}
let agent_before_frame = agent.clone();
let responses = super::engine::handle_frame(&buf, &store, &mut agent).await;
invalidate_drive_cache_on_identity_change(
&agent,
&agent_before_frame,
&mut drive_cache,
);
for response in responses {
let mut framed = Vec::with_capacity(4 + response.len());
framed.extend_from_slice(&(response.len() as u32).to_be_bytes());
framed.extend_from_slice(&response);
if tx_for_read.send(framed).await.is_err() {
tracing::warn!(
"[live] response channel closed for {}, dropping responses",
&read_peer_id[..read_peer_id.len().min(12)]
);
break;
}
}
}
remove_live_peer(&read_peer_id, generation);
});
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SyncEvent {
pub remote_node_id: String,
pub resources_imported: usize,
pub timestamp: u64,
#[serde(default = "default_event_kind")]
pub kind: String,
}
#[allow(dead_code)] fn default_event_kind() -> String {
"sync".into()
}
static SYNC_EVENT_TX: OnceLock<tokio::sync::broadcast::Sender<SyncEvent>> = OnceLock::new();
fn get_event_tx() -> &'static tokio::sync::broadcast::Sender<SyncEvent> {
SYNC_EVENT_TX.get_or_init(|| tokio::sync::broadcast::channel(32).0)
}
fn push_sync_event(remote_node_id: &str, resources_imported: usize) {
push_event(remote_node_id, resources_imported, "sync");
}
fn push_event(remote_node_id: &str, resources_imported: usize, kind: &str) {
let now = crate::utils::now() as u64;
let key = (remote_node_id.to_string(), kind.to_string());
if kind != "connected" {
if let Ok(mut last) = last_event_ms().lock() {
if let Some(&prev) = last.get(&key) {
if now.saturating_sub(prev) < EVENT_DEBOUNCE_MS {
tracing::debug!(
"[live] debounced {kind} for {}",
&remote_node_id[..remote_node_id.len().min(12)]
);
return;
}
}
last.insert(key, now);
}
}
let event = SyncEvent {
remote_node_id: remote_node_id.to_string(),
resources_imported,
timestamp: now,
kind: kind.to_string(),
};
let _ = get_event_tx().send(event);
}
pub fn poll_sync_events() -> Vec<SyncEvent> {
let mut rx = get_event_tx().subscribe();
let mut events = Vec::new();
while let Ok(e) = rx.try_recv() {
events.push(e);
}
events
}
pub async fn wait_for_sync_event() -> SyncEvent {
let mut rx = get_event_tx().subscribe();
loop {
match rx.recv().await {
Ok(event) => return event,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(_) => {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
rx = get_event_tx().subscribe();
}
}
}
}
pub async fn wait_for_peer_count_change(current: usize) -> usize {
let mut rx = get_event_tx().subscribe();
loop {
let count = live_peer_count();
if count != current {
return count;
}
match tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv()).await {
Ok(Ok(_)) => {
let count = live_peer_count();
if count != current {
return count;
}
}
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
Ok(Err(_)) => {
rx = get_event_tx().subscribe();
}
Err(_) => {} }
}
}
pub async fn sync_drive_with_peer(
remote_node_id: &str,
drive: &str,
store: &Db,
) -> crate::errors::AtomicResult<usize> {
sync_drive_with_peer_forced(remote_node_id, drive, store, true).await
}
pub async fn sync_drive_with_peer_outcome(
remote_node_id: &str,
drive: &str,
store: &Db,
) -> crate::errors::AtomicResult<PeerSyncOutcome> {
let endpoint = ENDPOINT
.get()
.ok_or("Iroh peer not started. Call start() first.")?;
sync_drive_with_peer_using_outcome(endpoint, remote_node_id, drive, store, true).await
}
pub async fn sync_drive_with_peer_if_needed(
remote_node_id: &str,
drive: &str,
store: &Db,
) -> crate::errors::AtomicResult<usize> {
sync_drive_with_peer_forced(remote_node_id, drive, store, false).await
}
async fn sync_drive_with_peer_forced(
remote_node_id: &str,
drive: &str,
store: &Db,
force: bool,
) -> crate::errors::AtomicResult<usize> {
let endpoint = ENDPOINT
.get()
.ok_or("Iroh peer not started. Call start() first.")?;
sync_drive_with_peer_using(endpoint, remote_node_id, drive, store, force).await
}
#[derive(Debug, Clone)]
pub struct PeerSyncOutcome {
pub count: usize,
pub pushed: usize,
pub in_sync: bool,
pub peer_name: Option<String>,
}
pub async fn sync_drive_with_peer_using(
endpoint: &Endpoint,
remote_node_id: &str,
drive: &str,
store: &Db,
force: bool,
) -> crate::errors::AtomicResult<usize> {
sync_drive_with_peer_using_outcome(endpoint, remote_node_id, drive, store, force)
.await
.map(|o| o.count)
}
pub async fn sync_drive_with_peer_using_outcome(
endpoint: &Endpoint,
remote_node_id: &str,
drive: &str,
store: &Db,
force: bool,
) -> crate::errors::AtomicResult<PeerSyncOutcome> {
let remote_key = normalize_node_id(remote_node_id);
let node_id: NodeId = remote_key
.parse()
.map_err(|e| format!("Invalid NodeID '{remote_node_id}': {e}"))?;
if !force && live_peer_ids().contains(&remote_key) {
tracing::debug!(
"[sync] already live with {}, skipping bulk reconnect",
&remote_key[..remote_key.len().min(12)]
);
return Ok(PeerSyncOutcome {
count: 0,
pushed: 0,
in_sync: true,
peer_name: None,
});
}
if force && live_peer_ids().contains(&remote_key) {
remove_live_peer_any_quiet(&remote_key);
}
let my_node_id = endpoint.node_id();
let my_relay = endpoint.home_relay();
tracing::info!(
"[sync] my NodeID: {}, relay: {:?}, connecting to: {}, drive: {}",
&my_node_id.to_string()[..16],
my_relay.get(),
&node_id.to_string()[..node_id.to_string().len().min(16)],
&drive[..drive.len().min(20)],
);
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20);
let remote_short = &node_id.to_string()[..node_id.to_string().len().min(16)];
let dial = dial_target(store, node_id);
let conn =
match tokio::time::timeout(CONNECT_TIMEOUT, endpoint.connect(dial, ATOMIC_ALPN)).await {
Ok(Ok(c)) => c,
Ok(Err(e)) => {
tracing::error!("[sync] connect failed to {remote_short}: {e}");
return Err(format!("Iroh connect to {remote_short} failed: {e}").into());
}
Err(_) => {
tracing::error!("[sync] connect timed out to {remote_short}");
return Err(format!(
"Iroh connect to {remote_short} timed out after {}s. \
Is the other device online, on the network, and running the app?",
CONNECT_TIMEOUT.as_secs()
)
.into());
}
};
tracing::info!("[sync] connected! Opening bi stream...");
let (mut send, mut recv) = conn.open_bi().await.map_err(|e| {
tracing::error!("[sync] open_bi failed: {e}");
format!("Iroh open_bi failed: {e}")
})?;
tracing::info!("[sync] bi stream open, sending AUTH...");
let agent = store.get_default_agent()?;
let auth_frame = super::protocol::encode_auth(&agent, drive)?;
send.write_u32(auth_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&auth_frame).await.map_err(io_err)?;
let auth_len = match recv.read_u32().await {
Ok(n) => n as usize,
Err(e) => return Err(format!("Failed to read auth response: {e}").into()),
};
if auth_len > super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES {
return Err(format!(
"Auth response frame too large: {auth_len} bytes (max {})",
super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES
)
.into());
}
let mut auth_buf = vec![0u8; auth_len];
recv.read_exact(&mut auth_buf).await.map_err(io_err)?;
if auth_buf.is_empty() || auth_buf[0] != super::protocol::tag::AUTH_OK {
let msg = if auth_buf.len() > 5 {
std::str::from_utf8(&auth_buf[5..]).unwrap_or("unknown error")
} else {
"auth rejected"
};
return Err(format!("Authentication failed: {msg}").into());
}
tracing::info!("Authenticated with peer");
let hello_frame = super::protocol::encode_hello(&effective_device_name(store));
send.write_u32(hello_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&hello_frame).await.map_err(io_err)?;
let mut peer_display_name: Option<String> = None;
let mut remote_agent = ForAgent::Public;
let mut drive_cache: std::collections::HashMap<String, bool> = std::collections::HashMap::new();
let drive_subject = crate::Subject::from_raw(drive, store.get_base_domain().as_deref());
let drive_subjects = super::engine::collect_drive_subjects(store, &drive_subject).await;
let vvs = super::engine::build_drive_vvs(store, &drive_subjects);
let drive_hash = super::engine::compute_drive_hash(&vvs);
let mut peer_set = std::collections::BTreeSet::new();
for vv in vvs.values() {
for peer_id in vv.keys() {
peer_set.insert(peer_id.clone());
}
}
let peers: Vec<String> = peer_set.into_iter().collect();
let peer_index: std::collections::HashMap<&str, usize> = peers
.iter()
.enumerate()
.map(|(i, p)| (p.as_str(), i))
.collect();
let mut resources: std::collections::HashMap<String, Vec<i32>> =
std::collections::HashMap::new();
for (subject, vv) in &vvs {
let mut counters = vec![0i32; peers.len()];
for (pid, &counter) in vv {
if let Some(&idx) = peer_index.get(pid.as_str()) {
counters[idx] = counter;
}
}
resources.insert(subject.clone(), counters);
}
let sync_frame = super::protocol::encode_sync(drive, &drive_hash, &peers, &resources);
send.write_u32(sync_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&sync_frame).await.map_err(io_err)?;
let mut total_imported = 0;
let mut total_pushed = 0usize;
let mut acked_in_sync = false;
let mut pull_subjects: Vec<String> = Vec::new();
while let Ok(n) = recv.read_u32().await {
let len = n as usize;
if len == 0 || len > super::protocol::IROH_FRAME_MAX_BYTES {
break;
}
let mut buf = vec![0u8; len];
recv.read_exact(&mut buf).await.map_err(io_err)?;
if buf.is_empty() {
break;
}
let tag = buf[0];
let payload = &buf[1..];
match tag {
super::protocol::tag::HELLO => {
if peer_display_name.is_none() {
peer_display_name = super::protocol::decode_hello(payload);
if let Some(name) = &peer_display_name {
tracing::info!(
"[sync] peer {} introduced itself as \"{}\"",
&remote_key[..remote_key.len().min(12)],
name
);
if !name.is_empty() {
add_known_peer(store, &remote_key, name);
}
}
}
continue;
}
super::protocol::tag::SYNC_OK => {
tracing::info!("Peer says drive {drive} is in sync");
acked_in_sync = true;
break;
}
super::protocol::tag::SYNC_DIFF => {
if let Some(diff) = super::protocol::decode_sync_diff(payload) {
tracing::info!(
"SYNC_DIFF: server pushes {}, server pulls {}, remove {}",
diff.push.len(),
diff.pull.len(),
diff.remove.len()
);
for subject in &diff.remove {
apply_peer_remove(store, &remote_agent, subject, true, &mut drive_cache)
.await;
}
pull_subjects = diff.pull.clone();
if diff.push.is_empty() {
if !diff.pull.is_empty() {
let entries = super::engine::collect_readable_snapshots(
store,
&remote_agent,
&diff.pull,
Some(&remote_key),
)
.await;
if !entries.is_empty() {
let refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(s, b)| (s.as_str(), b.as_slice()))
.collect();
for chunk in super::protocol::encode_sync_push_chunks(drive, &refs)
{
send.write_u32(chunk.len() as u32).await.map_err(io_err)?;
send.write_all(&chunk).await.map_err(io_err)?;
}
total_pushed += entries.len();
tracing::info!("Pushed {} resources to peer", entries.len());
}
}
break;
}
}
}
super::protocol::tag::SYNC_PUSH => {
let mut last_chunk = false;
if let Some(push) = super::protocol::decode_sync_push(payload) {
last_chunk = push.last;
match super::engine::import_sync_push(&push, store, &remote_agent, true).await {
Ok((count, blob_requests)) => {
total_imported += count;
for req_frame in blob_requests {
send.write_u32(req_frame.len() as u32)
.await
.map_err(io_err)?;
send.write_all(&req_frame).await.map_err(io_err)?;
}
}
Err(rejected) => {
tracing::warn!("[sync] dropped incoming push: {rejected}");
}
}
}
if !last_chunk {
continue;
}
if !pull_subjects.is_empty() {
let entries = super::engine::collect_readable_snapshots(
store,
&remote_agent,
&pull_subjects,
Some(&remote_key),
)
.await;
if !entries.is_empty() {
let refs: Vec<(&str, &[u8])> = entries
.iter()
.map(|(s, b)| (s.as_str(), b.as_slice()))
.collect();
for chunk in super::protocol::encode_sync_push_chunks(drive, &refs) {
send.write_u32(chunk.len() as u32).await.map_err(io_err)?;
send.write_all(&chunk).await.map_err(io_err)?;
}
total_pushed += entries.len();
tracing::info!("Pushed {} resources back to peer", entries.len());
}
}
break;
}
super::protocol::tag::ERROR => {
let code = payload
.get(2..4)
.map(|b| u16::from_be_bytes([b[0], b[1]]))
.unwrap_or(super::protocol::error_code::UNKNOWN);
let msg =
std::str::from_utf8(payload.get(4..).unwrap_or(&[])).unwrap_or("unknown error");
if code == super::protocol::error_code::SYNC_REJECTED
|| code == super::protocol::error_code::AUTH_REQUIRED
{
tracing::warn!("[sync] peer refused: {msg}");
return Err(format!("Peer refused to sync: {msg}").into());
}
tracing::warn!("Peer returned error: {msg}");
break;
}
super::protocol::tag::AUTH => {
if let Ok(json) = std::str::from_utf8(payload) {
match serde_json::from_str::<crate::authentication::AuthValues>(json) {
Ok(auth) => {
match crate::authentication::get_agent_from_auth_values_and_check(
Some(auth),
store,
)
.await
{
Ok(a) => {
tracing::info!(
"[sync] peer {} authenticated back as {a:?}",
&remote_key[..remote_key.len().min(12)]
);
invalidate_drive_cache_on_identity_change(
&a,
&remote_agent,
&mut drive_cache,
);
remote_agent = a;
}
Err(e) => {
tracing::debug!("[sync] peer's auth-back rejected: {e}");
}
}
}
Err(e) => {
tracing::debug!("[sync] invalid auth-back JSON from peer: {e}");
}
}
}
}
_ => {
tracing::debug!("Unexpected frame tag from peer: 0x{tag:02x}");
}
}
}
tracing::info!(
"sync_drive_with_peer: imported {total_imported} resources from {remote_node_id}"
);
if total_imported == 0 && total_pushed == 0 && !acked_in_sync {
return Err(format!(
"Connected to that device, but nothing synced: it shared nothing \
readable with you and took nothing of yours ({remote_agent})."
)
.into());
}
mark_peer_synced(
store,
&remote_key,
Some(total_pushed as u32),
Some(total_imported as u32),
);
if let Some(info) = endpoint.remote_info(node_id) {
let addr: iroh::NodeAddr = info.into();
remember_peer_addr(
store,
&remote_key,
addr.relay_url.map(|u| u.to_string()),
addr.direct_addresses
.iter()
.map(|a| a.to_string())
.collect(),
);
}
tracing::info!(
"[live] transitioning to live mode with {}",
&remote_node_id[..remote_node_id.len().min(12)]
);
{
let mut conns = LIVE_CONNECTIONS.lock().unwrap();
if let Some(v) = conns.as_mut() {
v.push(conn);
}
}
register_live_peer(
remote_key.clone(),
send,
recv,
store.clone(),
remote_agent,
true,
);
if store.get_active_drive().as_deref() != Some(drive) {
if let Err(e) = store.set_active_drive(drive) {
tracing::warn!("[sync] could not remember the drive to reconnect to: {e}");
}
}
Ok(PeerSyncOutcome {
count: total_imported,
pushed: total_pushed,
in_sync: acked_in_sync,
peer_name: peer_display_name,
})
}
const KNOWN_PEERS_KEY: &[u8] = b"_iroh_known_peers_v2";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct KnownPeer {
pub node_id: String,
pub name: String,
#[serde(default)]
pub last_sent: Option<u32>,
#[serde(default)]
pub last_received: Option<u32>,
#[serde(default)]
pub last_synced: Option<i64>,
#[serde(default)]
pub relay_url: Option<String>,
#[serde(default)]
pub direct_addrs: Vec<String>,
}
pub fn is_paired_peer(store: &Db, node_id: &str) -> bool {
let key = normalize_node_id(node_id);
get_known_peers(store)
.iter()
.any(|p| normalize_node_id(&p.node_id) == key)
}
pub fn get_known_peers(store: &Db) -> Vec<KnownPeer> {
if let Ok(Some(bytes)) = store
.kv
.get(crate::db::trees::Tree::PluginMeta, KNOWN_PEERS_KEY)
{
serde_json::from_slice(&bytes).unwrap_or_default()
} else {
vec![]
}
}
pub fn add_known_peer(store: &Db, node_id: &str, name: &str) {
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
if let Some(existing) = peers
.iter_mut()
.find(|p| normalize_node_id(&p.node_id) == key)
{
if !name.is_empty() {
existing.name = name.to_string();
}
} else {
peers.push(KnownPeer {
node_id: key,
last_sent: None,
last_received: None,
name: name.to_string(),
last_synced: None,
relay_url: None,
direct_addrs: Vec::new(),
});
}
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
pub fn remember_peer_addr(
store: &Db,
node_id: &str,
relay_url: Option<String>,
direct_addrs: Vec<String>,
) {
if relay_url.is_none() && direct_addrs.is_empty() {
return;
}
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
let entry = if let Some(existing) = peers
.iter_mut()
.find(|p| normalize_node_id(&p.node_id) == key)
{
existing
} else {
peers.push(KnownPeer {
node_id: key.clone(),
last_sent: None,
last_received: None,
name: String::new(),
last_synced: None,
relay_url: None,
direct_addrs: Vec::new(),
});
peers.last_mut().unwrap()
};
if relay_url.is_some() {
entry.relay_url = relay_url;
}
if !direct_addrs.is_empty() {
entry.direct_addrs = direct_addrs;
}
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
fn dial_target(store: &Db, node_id: NodeId) -> iroh::NodeAddr {
let key = node_id.to_string();
for peer in get_known_peers(store) {
if normalize_node_id(&peer.node_id) != normalize_node_id(&key) {
continue;
}
let mut addr = iroh::NodeAddr::new(node_id);
if let Some(relay) = peer.relay_url.as_deref().and_then(|s| s.parse().ok()) {
addr = addr.with_relay_url(relay);
}
let socks: Vec<std::net::SocketAddr> = peer
.direct_addrs
.iter()
.filter_map(|s| s.parse().ok())
.collect();
if !socks.is_empty() {
addr = addr.with_direct_addresses(socks);
}
return addr;
}
iroh::NodeAddr::new(node_id)
}
pub fn mark_known_peer_synced(store: &Db, node_id: &str, sent: Option<u32>, received: Option<u32>) {
if !is_paired_peer(store, node_id) {
return;
}
mark_peer_synced(store, node_id, sent, received);
}
pub fn mark_peer_synced(store: &Db, node_id: &str, sent: Option<u32>, received: Option<u32>) {
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
let now = crate::utils::now();
if let Some(existing) = peers
.iter_mut()
.find(|p| normalize_node_id(&p.node_id) == key)
{
existing.last_synced = Some(now);
existing.last_sent = sent;
existing.last_received = received;
} else {
peers.push(KnownPeer {
node_id: key,
name: String::new(),
last_sent: sent,
last_received: received,
last_synced: Some(now),
relay_url: None,
direct_addrs: Vec::new(),
});
}
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
pub fn remove_known_peer(store: &Db, node_id: &str) {
let key = normalize_node_id(node_id);
let mut peers = get_known_peers(store);
peers.retain(|p| normalize_node_id(&p.node_id) != key);
let _ = store.kv.insert(
crate::db::trees::Tree::PluginMeta,
KNOWN_PEERS_KEY,
&serde_json::to_vec(&peers).unwrap_or_default(),
);
let _ = store.flush();
}
fn auth_required_error(tag: u8) -> Vec<u8> {
super::protocol::encode_error(
0,
super::protocol::error_code::AUTH_REQUIRED,
&format!("AUTH required before frame 0x{tag:02x}"),
)
}
fn frame_with_len(frame: &[u8]) -> Vec<u8> {
let mut framed = Vec::with_capacity(4 + frame.len());
framed.extend_from_slice(&(frame.len() as u32).to_be_bytes());
framed.extend_from_slice(frame);
framed
}
async fn refuse_stream(send: &mut iroh::endpoint::SendStream, frames: &[Vec<u8>]) {
for frame in frames {
let _ = send.write_all(&frame_with_len(frame)).await;
}
let _ = send.finish();
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), send.stopped()).await;
}
fn auth_requested_subject(payload: &[u8]) -> Option<String> {
let json = std::str::from_utf8(payload).ok()?;
let auth: crate::authentication::AuthValues = serde_json::from_str(json).ok()?;
Some(auth.requested_subject)
}
fn handshake_frame_drive(buf: &[u8]) -> Option<String> {
match *buf.first()? {
super::protocol::tag::SYNC => super::protocol::decode_sync(&buf[1..]).map(|s| s.drive),
super::protocol::tag::SYNC_PUSH => {
super::protocol::decode_sync_push(&buf[1..]).map(|p| p.drive)
}
_ => None,
}
}
async fn handle_stream(
mut send: iroh::endpoint::SendStream,
mut recv: iroh::endpoint::RecvStream,
store: Db,
remote_id: String,
) -> anyhow::Result<usize> {
let remote_key = normalize_node_id(&remote_id);
let mut agent = ForAgent::Public;
let mut bound_drive: Option<crate::Subject> = None;
let mut total_imported = 0;
let mut sent_sync_ok = false;
let mut hello_sent = false;
let mut auth_sent = false;
let mut peer_display_name: Option<String> = None;
while let Ok(n) = recv.read_u32().await {
let len = n as usize;
let frame_cap = if matches!(agent, ForAgent::Public) {
super::protocol::IROH_PREAUTH_FRAME_MAX_BYTES
} else {
super::protocol::IROH_FRAME_MAX_BYTES
};
if len == 0 || len > frame_cap {
break;
}
let mut buf = vec![0u8; len];
recv.read_exact(&mut buf).await.map_err(io_err)?;
let Some(&tag) = buf.first() else {
continue;
};
if matches!(agent, ForAgent::Public) && tag != super::protocol::tag::AUTH {
tracing::warn!(
"[accept] closing: 0x{tag:02x} from {} before AUTH",
&remote_key[..remote_key.len().min(12)]
);
refuse_stream(&mut send, &[auth_required_error(tag)]).await;
return Ok(total_imported);
}
if let (Some(bound), Some(named)) = (&bound_drive, handshake_frame_drive(&buf)) {
let named = crate::Subject::from_raw(&named, store.get_base_domain().as_deref());
if &named != bound {
tracing::warn!(
"[accept] closing: {} authenticated for {bound} but sent 0x{tag:02x} for {named}",
&remote_key[..remote_key.len().min(12)]
);
refuse_stream(
&mut send,
&[super::protocol::encode_error(
0,
super::protocol::error_code::AUTH_REQUIRED,
&format!("AUTH was for {bound}, not {named}"),
)],
)
.await;
return Ok(total_imported);
}
}
if tag == super::protocol::tag::HELLO {
if peer_display_name.is_none() {
peer_display_name = super::protocol::decode_hello(&buf[1..]);
if let Some(name) = &peer_display_name {
tracing::info!(
"[accept] peer {} introduced itself as \"{}\"",
&remote_key[..remote_key.len().min(12)],
name
);
set_live_peer_name(&remote_key, name);
}
}
continue;
}
let responses = super::engine::handle_frame(&buf, &store, &mut agent).await;
if tag == super::protocol::tag::SYNC_PUSH
&& responses
.iter()
.any(|r| r.first() == Some(&super::protocol::tag::SYNC_OK))
{
if let Some(push) = super::protocol::decode_sync_push(&buf[1..]) {
total_imported += push.entries.len();
}
}
let just_authed = tag == super::protocol::tag::AUTH
&& responses
.iter()
.any(|r| !r.is_empty() && r[0] == super::protocol::tag::AUTH_OK);
if tag == super::protocol::tag::AUTH {
if just_authed {
if bound_drive.is_none() {
bound_drive = auth_requested_subject(&buf[1..])
.map(|s| crate::Subject::from_raw(&s, store.get_base_domain().as_deref()));
}
} else {
tracing::warn!(
"[accept] closing: AUTH from {} failed",
&remote_key[..remote_key.len().min(12)]
);
refuse_stream(&mut send, &responses).await;
return Ok(total_imported);
}
}
if just_authed {
tracing::info!(
"[accept] {} authenticated as {agent} — serving what they may read",
&remote_key[..remote_key.len().min(12)]
);
}
let client_pushed = tag == super::protocol::tag::SYNC_PUSH;
let sync_ok = responses
.iter()
.any(|r| !r.is_empty() && r[0] == super::protocol::tag::SYNC_OK);
if sync_ok {
sent_sync_ok = true;
}
let sync_diff_needs_no_pushback = responses.iter().any(|r| {
!r.is_empty()
&& r[0] == super::protocol::tag::SYNC_DIFF
&& super::protocol::decode_sync_diff(&r[1..])
.is_some_and(|diff| diff.pull.is_empty())
});
for response in responses {
if let Err(e) = send.write_u32(response.len() as u32).await {
tracing::warn!(
"[accept] failed to write response header to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
break;
}
if let Err(e) = send.write_all(&response).await {
tracing::warn!(
"[accept] failed to write response body to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
break;
}
}
if just_authed && !hello_sent {
hello_sent = true;
let hello = super::protocol::encode_hello(&effective_device_name(&store));
let header_ok = send.write_u32(hello.len() as u32).await;
if let Err(e) = header_ok {
tracing::warn!(
"[accept] failed to write HELLO header to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
} else if let Err(e) = send.write_all(&hello).await {
tracing::warn!(
"[accept] failed to write HELLO body to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
}
}
if just_authed && !auth_sent {
auth_sent = true;
if let Ok(our_agent) = store.get_default_agent() {
if let Ok(auth_frame) = super::protocol::encode_auth(&our_agent, &remote_key) {
if let Err(e) = send.write_u32(auth_frame.len() as u32).await {
tracing::warn!(
"[accept] failed to write auth-back header to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
} else if let Err(e) = send.write_all(&auth_frame).await {
tracing::warn!(
"[accept] failed to write auth-back body to {}: {e}",
&remote_key[..remote_key.len().min(12)]
);
}
}
}
}
if sync_ok || client_pushed || sync_diff_needs_no_pushback {
tracing::info!(
"[accept] sync complete, transitioning to live mode with {}",
&remote_key[..remote_key.len().min(12)]
);
mark_known_peer_synced(&store, &remote_key, None, Some(total_imported as u32));
register_live_peer(remote_key, send, recv, store, agent, false);
return Ok(total_imported);
}
}
if sent_sync_ok {
tracing::info!(
"[accept] SYNC_OK sent, entering live mode with {}",
&remote_key[..remote_key.len().min(12)]
);
register_live_peer(remote_key, send, recv, store, agent, false);
}
Ok(total_imported)
}
#[cfg(test)]
mod live_write_admission_tests {
use super::*;
use crate::Db;
use std::collections::HashMap;
#[tokio::test]
async fn relayed_write_to_our_own_drive_accepted_only_when_we_dialed() {
let db = Db::init_temp("relay_owned_drive").await.unwrap();
let (_alice, drive) = db.setup("Alice").await.unwrap();
let server = db.create_agent(Some("Server")).await.unwrap();
let server_agent = ForAgent::AgentSubject(server.subject.clone());
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(&db, &server_agent, &drive, false, &mut cache).await,
"a peer that dialed us may not write our drive on its own identity"
);
let mut cache = HashMap::new();
assert!(
admitted_for_drive(&db, &server_agent, &drive, true, &mut cache).await,
"a server we dialed must be able to relay updates to our own drive"
);
}
#[tokio::test]
async fn owner_admitted_stranger_rejected() {
let db = Db::init_temp("live_admission_owner_vs_stranger")
.await
.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mallory = db.create_agent(Some("Mallory")).await.unwrap();
let mut cache = HashMap::new();
assert!(
admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache
)
.await,
"the drive's own owner must be admitted"
);
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(
&db,
&ForAgent::AgentSubject(mallory.subject.clone()),
&drive,
false,
&mut cache
)
.await,
"an unrelated agent with no rights to the drive must be rejected"
);
}
#[tokio::test]
async fn owner_rejected_when_drive_not_admitted() {
let db = Db::init_temp("live_admission_policy_gate").await.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let policy = std::sync::Arc::new(crate::sync::policy::AllowlistPolicy::new());
policy.set_grace(std::time::Duration::ZERO);
db.set_sync_policy(policy);
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache
)
.await,
"the owner's ACL rights don't matter if the drive isn't admitted by policy"
);
}
#[tokio::test]
async fn verdict_is_cached_after_first_check() {
let db = Db::init_temp("live_admission_cache").await.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mut cache = HashMap::new();
assert!(cache.get(&drive).is_none());
admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache,
)
.await;
assert_eq!(cache.get(&drive), Some(&true));
}
#[tokio::test]
async fn stale_public_verdict_cleared_after_late_auth_upgrades_identity() {
let db = Db::init_temp("live_admission_cache_identity_change")
.await
.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mut cache = HashMap::new();
assert!(
!admitted_for_drive(&db, &ForAgent::Public, &drive, false, &mut cache).await,
"Public should not be admitted for Alice's drive"
);
assert_eq!(cache.get(&drive), Some(&false));
let previous = ForAgent::Public;
let upgraded = ForAgent::AgentSubject(alice.subject.clone());
invalidate_drive_cache_on_identity_change(&upgraded, &previous, &mut cache);
assert!(
cache.get(&drive).is_none(),
"the stale Public verdict must be gone after the identity change"
);
assert!(
admitted_for_drive(&db, &upgraded, &drive, false, &mut cache).await,
"Alice must be admitted for her own drive once the stale cache is cleared"
);
}
#[tokio::test]
async fn cache_survives_when_identity_is_unchanged() {
let db = Db::init_temp("live_admission_cache_identity_stable")
.await
.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let mut cache = HashMap::new();
admitted_for_drive(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&drive,
false,
&mut cache,
)
.await;
assert_eq!(cache.get(&drive), Some(&true));
let same = ForAgent::AgentSubject(alice.subject.clone());
invalidate_drive_cache_on_identity_change(&same, &same, &mut cache);
assert_eq!(
cache.get(&drive),
Some(&true),
"an unchanged identity must not clear the cache"
);
}
}
#[cfg(test)]
mod initiator_trust_tests {
use super::*;
use crate::Db;
use std::collections::HashMap;
pub(super) async fn private_drive_with_child(
db: &Db,
owner: &crate::agents::Agent,
) -> (String, String) {
let mut builder = crate::commit::CommitBuilder::new("placeholder".into());
builder.set(
crate::urls::IS_A.into(),
crate::Value::ResourceArray(vec![crate::urls::DRIVE.into()]),
);
builder.set(
crate::urls::NAME.into(),
crate::Value::String("Private Drive".into()),
);
builder.set(
crate::urls::WRITE.into(),
crate::Value::ResourceArray(vec![owner.subject.to_string().into()]),
);
builder.set(
crate::urls::READ.into(),
crate::Value::ResourceArray(vec![owner.subject.to_string().into()]),
);
let commit = crate::commit::Commit::create_did(builder, owner, db)
.await
.unwrap();
let drive_did = commit.subject.to_string();
let opts = crate::commit::CommitOpts {
validate_signature: true,
validate_timestamp: false,
validate_previous_commit: false,
validate_rights: false,
update_index: true,
..crate::commit::CommitOpts::no_validations_no_index()
};
db.apply_commit(commit, &opts).await.unwrap();
db.set_active_drive(&drive_did).unwrap();
let child = db
.create_resource(
crate::urls::CLASS,
&drive_did,
"Secret Doc",
Some(vec![(
crate::urls::DESCRIPTION,
crate::Value::String("top secret".into()),
)]),
)
.await
.unwrap();
(drive_did, child)
}
#[tokio::test]
async fn pull_serving_refuses_unreadable_subjects_for_public_peer() {
let db = Db::init_temp("initiator_pull_public").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let served = crate::sync::engine::collect_readable_snapshots(
&db,
&ForAgent::Public,
&[child.clone()],
None,
)
.await;
assert!(
served.is_empty(),
"a Public peer must not be served a snapshot for a subject it can't read"
);
let served_owner = crate::sync::engine::collect_readable_snapshots(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&[child.clone()],
None,
)
.await;
assert_eq!(
served_owner.len(),
1,
"the owner must still be served the snapshot it can read"
);
assert_eq!(served_owner[0].0, child);
}
#[tokio::test]
async fn pull_serving_refuses_unreadable_subjects_for_stranger() {
let db = Db::init_temp("initiator_pull_stranger").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let mallory = db.create_agent(Some("Mallory")).await.unwrap();
let served = crate::sync::engine::collect_readable_snapshots(
&db,
&ForAgent::AgentSubject(mallory.subject.clone()),
&[child],
None,
)
.await;
assert!(
served.is_empty(),
"a peer that authenticated as an unrelated agent must not receive unreadable snapshots"
);
}
#[tokio::test]
async fn a_paired_replica_receives_what_the_owner_can_read() {
let db = Db::init_temp("paired_replica_serves").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let replica_node = "aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999";
let replica_agent = db.create_agent(Some("Replica")).await.unwrap();
let as_replica = ForAgent::AgentSubject(replica_agent.subject.clone());
let unpaired = crate::sync::engine::collect_readable_snapshots(
&db,
&as_replica,
&[child.clone()],
Some(replica_node),
)
.await;
assert!(
unpaired.is_empty(),
"a node the owner never dialled must not be served"
);
add_known_peer(&db, replica_node, "Replica");
let paired = crate::sync::engine::collect_readable_snapshots(
&db,
&as_replica,
&[child],
Some(replica_node),
)
.await;
assert_eq!(
paired.len(),
1,
"a paired replica must receive what the owner can read"
);
}
#[tokio::test]
async fn remove_rejected_for_unadmitted_peer_known_subject() {
let db = Db::init_temp("initiator_remove_stranger").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let mallory = db.create_agent(Some("Mallory")).await.unwrap();
let mut cache = HashMap::new();
apply_peer_remove(
&db,
&ForAgent::AgentSubject(mallory.subject.clone()),
&child,
false,
&mut cache,
)
.await;
assert!(
db.get_resource(&child.as_str().into()).await.is_ok(),
"an unadmitted peer's remove[] entry must NOT delete a subject we hold"
);
assert!(
!crate::sync::tombstones::is_tombstoned(&db, &child),
"an unadmitted peer's remove[] entry must NOT tombstone a subject we hold"
);
}
#[tokio::test]
async fn remove_applied_for_admitted_owner() {
let db = Db::init_temp("initiator_remove_owner").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (_drive, child) = private_drive_with_child(&db, &alice).await;
let mut cache = HashMap::new();
apply_peer_remove(
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
&child,
false,
&mut cache,
)
.await;
assert!(
db.get_resource(&child.as_str().into()).await.is_err(),
"the owner's remove[] entry must delete the subject"
);
assert!(
crate::sync::tombstones::is_tombstoned(&db, &child),
"the owner's remove[] entry must record a tombstone"
);
}
}
#[cfg(all(test, feature = "db-redb"))]
mod accept_gate_tests {
use super::initiator_trust_tests::private_drive_with_child;
use super::*;
use crate::sync::protocol::{self, error_code, tag};
use crate::Db;
fn parse_error(frame: &[u8]) -> Option<(u16, u16, String)> {
if frame.first() != Some(&tag::ERROR) || frame.len() < 5 {
return None;
}
let request_id = u16::from_be_bytes([frame[1], frame[2]]);
let code = u16::from_be_bytes([frame[3], frame[4]]);
let message = String::from_utf8_lossy(&frame[5..]).into_owned();
Some((request_id, code, message))
}
async fn push_frame_for(
db: &Db,
owner: &crate::agents::Agent,
drive: &str,
child: &str,
) -> Vec<u8> {
let snapshots = crate::sync::engine::collect_readable_snapshots(
db,
&ForAgent::AgentSubject(owner.subject.clone()),
&[child.to_string()],
None,
)
.await;
assert_eq!(snapshots.len(), 1, "owner can read its own child");
let entries: Vec<(&str, &[u8])> = snapshots
.iter()
.map(|(s, b)| (s.as_str(), b.as_slice()))
.collect();
protocol::encode_sync_push(drive, &entries, true)
}
#[tokio::test]
async fn import_sync_push_refuses_agent_without_write_right() {
let db = Db::init_temp("gate_import_stranger").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (drive, child) = private_drive_with_child(&db, &alice).await;
let mallory = crate::agents::Agent::new(Some("Mallory")).unwrap();
let frame = push_frame_for(&db, &alice, &drive, &child).await;
let push = protocol::decode_sync_push(&frame[1..]).unwrap();
for (who, agent) in [
("Public", ForAgent::Public),
(
"a stranger",
ForAgent::AgentSubject(mallory.subject.clone()),
),
] {
let rejected = crate::sync::engine::import_sync_push(&push, &db, &agent, false)
.await
.expect_err(&format!(
"{who} must not be able to push into a private drive"
));
assert_eq!(rejected.drive, drive);
assert!(
rejected.reason.contains("no write right"),
"reason names the cause: {}",
rejected.reason
);
}
crate::sync::engine::import_sync_push(
&push,
&db,
&ForAgent::AgentSubject(alice.subject.clone()),
false,
)
.await
.expect("the owner may push into its own drive");
}
#[tokio::test]
async fn rejected_sync_push_is_answered_with_error_not_sync_ok() {
let db = Db::init_temp("gate_push_error_frame").await.unwrap();
let alice = crate::agents::Agent::new(Some("Alice")).unwrap();
db.set_default_agent(alice.clone());
let (drive, child) = private_drive_with_child(&db, &alice).await;
let mallory = crate::agents::Agent::new(Some("Mallory")).unwrap();
let frame = push_frame_for(&db, &alice, &drive, &child).await;
let mut as_mallory = ForAgent::AgentSubject(mallory.subject.clone());
let responses = crate::sync::engine::handle_frame(&frame, &db, &mut as_mallory).await;
assert!(
!responses.iter().any(|f| f.first() == Some(&tag::SYNC_OK)),
"a rejected push must never be acknowledged with SYNC_OK"
);
let (request_id, code, message) = responses
.iter()
.find_map(|f| parse_error(f))
.expect("a rejected push is answered with an ERROR frame");
assert_eq!(
request_id, 0,
"connection-level error, not tied to a request"
);
assert_eq!(code, error_code::SYNC_REJECTED);
assert!(
message.contains(&drive),
"the message names the drive so the sender can act on it: {message}"
);
let mut as_alice = ForAgent::AgentSubject(alice.subject.clone());
let responses = crate::sync::engine::handle_frame(&frame, &db, &mut as_alice).await;
assert!(
responses.iter().any(|f| f.first() == Some(&tag::SYNC_OK)),
"the owner's push is acknowledged"
);
}
async fn raw_stream(
router: &Router,
node_id: &NodeId,
) -> (
iroh::Endpoint,
iroh::endpoint::SendStream,
iroh::endpoint::RecvStream,
) {
let ep = iroh::Endpoint::builder().bind().await.unwrap();
let addr = router.endpoint().node_addr().await.unwrap();
ep.add_node_addr(addr).unwrap();
let conn = ep.connect(*node_id, ATOMIC_ALPN).await.unwrap();
let (send, recv) = conn.open_bi().await.unwrap();
(ep, send, recv)
}
async fn write_frame(send: &mut iroh::endpoint::SendStream, frame: &[u8]) {
send.write_all(&frame_with_len(frame)).await.unwrap();
}
async fn read_frame(recv: &mut iroh::endpoint::RecvStream) -> Option<Vec<u8>> {
loop {
let n = tokio::time::timeout(std::time::Duration::from_secs(10), recv.read_u32())
.await
.expect("accept side answers within 10s")
.ok()?;
let mut buf = vec![0u8; n as usize];
recv.read_exact(&mut buf).await.ok()?;
if !matches!(buf.first(), Some(&tag::HELLO) | Some(&tag::AUTH)) {
return Some(buf);
}
}
}
#[tokio::test]
async fn iroh_sync_before_auth_is_refused_and_stream_closed() {
let db = Db::init_temp("gate_iroh_preauth").await.unwrap();
let (_alice, drive) = db.setup("Alice").await.unwrap();
let (node_id, router) = start(db.clone()).await.unwrap();
let (_ep, mut send, mut recv) = raw_stream(&router, &node_id).await;
let sync = protocol::encode_sync(&drive, "", &[], &std::collections::HashMap::new());
write_frame(&mut send, &sync).await;
let reply = read_frame(&mut recv)
.await
.expect("an ERROR frame, not silence");
let (_, code, message) = parse_error(&reply).expect("ERROR frame");
assert_eq!(code, error_code::AUTH_REQUIRED, "{message}");
assert!(
read_frame(&mut recv).await.is_none(),
"the accept side closes the stream after refusing"
);
}
#[tokio::test]
async fn iroh_auth_for_one_drive_does_not_open_another() {
let db = Db::init_temp("gate_iroh_binding").await.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let (node_id, router) = start(db.clone()).await.unwrap();
let (_ep, mut send, mut recv) = raw_stream(&router, &node_id).await;
let auth = protocol::encode_auth(&alice, "did:key:z6MkSomeOtherDrive").unwrap();
write_frame(&mut send, &auth).await;
let reply = read_frame(&mut recv).await.expect("AUTH_OK");
assert_eq!(reply.first(), Some(&tag::AUTH_OK), "AUTH itself is valid");
let sync = protocol::encode_sync(&drive, "", &[], &std::collections::HashMap::new());
write_frame(&mut send, &sync).await;
let reply = read_frame(&mut recv)
.await
.expect("an ERROR frame, not a SYNC_DIFF");
let (_, code, message) = parse_error(&reply).expect("ERROR frame");
assert_eq!(code, error_code::AUTH_REQUIRED, "{message}");
assert!(message.contains(&drive), "{message}");
assert!(read_frame(&mut recv).await.is_none(), "stream closed");
}
#[tokio::test]
async fn iroh_auth_then_sync_for_same_drive_is_served() {
let db = Db::init_temp("gate_iroh_happy").await.unwrap();
let (alice, drive) = db.setup("Alice").await.unwrap();
let (node_id, router) = start(db.clone()).await.unwrap();
let (_ep, mut send, mut recv) = raw_stream(&router, &node_id).await;
write_frame(&mut send, &protocol::encode_auth(&alice, &drive).unwrap()).await;
assert_eq!(
read_frame(&mut recv).await.unwrap().first(),
Some(&tag::AUTH_OK)
);
let sync = protocol::encode_sync(&drive, "", &[], &std::collections::HashMap::new());
write_frame(&mut send, &sync).await;
let reply = read_frame(&mut recv).await.expect("a handshake answer");
assert!(
matches!(
reply.first(),
Some(&tag::SYNC_DIFF) | Some(&tag::SYNC_PUSH) | Some(&tag::SYNC_OK)
),
"expected a sync answer, got 0x{:02x}",
reply[0]
);
}
}
#[cfg(test)]
mod live_peer_registry_tests {
use super::*;
fn register(key: &str) -> u64 {
let generation =
LIVE_PEER_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
let (tx, _rx) = tokio::sync::mpsc::channel(4);
let mut guard = LIVE_PEERS.lock().unwrap();
if guard.is_none() {
*guard = Some(HashMap::new());
}
guard
.as_mut()
.unwrap()
.insert(normalize_node_id(key), (generation, tx));
generation
}
fn is_registered(key: &str) -> bool {
LIVE_PEERS
.lock()
.unwrap()
.as_ref()
.is_some_and(|m| m.contains_key(&normalize_node_id(key)))
}
#[test]
fn a_stale_connection_does_not_deregister_its_replacement() {
let peer = "test-peer-stale-vs-replacement";
let old = register(peer);
let new = register(peer);
assert_ne!(old, new);
remove_live_peer(peer, old);
assert!(
is_registered(peer),
"the replacement connection must survive the old one's teardown"
);
remove_live_peer(peer, new);
assert!(
!is_registered(peer),
"the current connection must still be able to deregister itself"
);
}
#[test]
fn a_deliberate_reconnect_evicts_whoever_is_current() {
let peer = "test-peer-forced-reconnect";
register(peer);
remove_live_peer_any_quiet(peer);
assert!(!is_registered(peer));
}
}
#[cfg(all(test, feature = "db-redb"))]
mod peer_sync_volume_tests {
use super::*;
#[tokio::test]
async fn a_completed_sync_records_what_it_moved() {
let db = Db::init_temp("peer_sync_volume").await.unwrap();
let node = "1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff";
mark_peer_synced(&db, node, Some(49), Some(1));
let peer = get_known_peers(&db)
.into_iter()
.find(|p| normalize_node_id(&p.node_id) == normalize_node_id(node))
.expect("the sync must record the peer");
assert_eq!(peer.last_sent, Some(49));
assert_eq!(peer.last_received, Some(1));
assert!(peer.last_synced.is_some());
mark_peer_synced(&db, node, Some(0), Some(2));
let peer = get_known_peers(&db)
.into_iter()
.find(|p| normalize_node_id(&p.node_id) == normalize_node_id(node))
.unwrap();
assert_eq!(peer.last_sent, Some(0));
assert_eq!(peer.last_received, Some(2));
}
}