Implement spanning tree announcement send/receive protocol

Add TreeAnnounce v1 wire format with versioned encoding (version byte
0x01), slim ancestry entries (32 bytes each, no per-entry signatures),
and transitive trust model where only the direct peer's declaration
signature is verified.

Key changes:
- Enrich TreeCoordinate with CoordEntry metadata (sequence, timestamp)
- TreeAnnounce encode/decode with roundtrip tests
- Per-peer rate limiting (500ms minimum interval) on ActivePeer
- Parent selection: depth-based algorithm with broken-path detection
- New node/tree.rs module: send/receive, periodic refresh, cleanup
- Wire up dispatch, initial announce on promotion, tick integration
- Update gossip protocol design doc with trust model (§2.7)

304 tests pass, clean build.
This commit is contained in:
Johnathan Corgan
2026-02-10 23:35:09 +00:00
parent 9d36977ec8
commit 1c2cda480d
9 changed files with 1260 additions and 126 deletions
+1 -1
View File
@@ -540,7 +540,7 @@ mod tests {
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::new(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
// ===== CacheEntry Tests =====
+1 -1
View File
@@ -29,7 +29,7 @@ pub use identity::{
pub use config::{Config, ConfigError, IdentityConfig, TunConfig, UdpConfig};
// Re-export tree types
pub use tree::{ParentDeclaration, TreeCoordinate, TreeError, TreeState};
pub use tree::{CoordEntry, ParentDeclaration, TreeCoordinate, TreeError, TreeState};
// Re-export bloom filter types
pub use bloom::{BloomError, BloomFilter, BloomState};
+33 -1
View File
@@ -43,6 +43,7 @@ impl Node {
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
self.process_pending_retries(now_ms).await;
self.check_tree_state().await;
}
}
}
@@ -315,6 +316,10 @@ impl Node {
our_index = %our_index,
"Inbound peer promoted to active"
);
// Send initial tree announce to new peer
if let Err(e) = self.send_tree_announce_to_peer(&node_addr).await {
debug!(peer = %node_addr, error = %e, "Failed to send initial TreeAnnounce");
}
}
PromotionResult::CrossConnectionWon { loser_link_id, node_addr } => {
// Clean up the losing connection's link
@@ -324,6 +329,10 @@ impl Node {
loser_link_id = %loser_link_id,
"Inbound cross-connection won, loser link cleaned up"
);
// Send initial tree announce to peer (new or reconnected)
if let Err(e) = self.send_tree_announce_to_peer(&node_addr).await {
debug!(peer = %node_addr, error = %e, "Failed to send initial TreeAnnounce");
}
}
PromotionResult::CrossConnectionLost { winner_link_id } => {
// This connection lost — clean up its link
@@ -435,6 +444,10 @@ impl Node {
node_addr = %node_addr,
"Peer promoted to active"
);
// Send initial tree announce to new peer
if let Err(e) = self.send_tree_announce_to_peer(&node_addr).await {
debug!(peer = %node_addr, error = %e, "Failed to send initial TreeAnnounce");
}
}
PromotionResult::CrossConnectionWon { loser_link_id, node_addr } => {
// Clean up the losing connection's link
@@ -449,6 +462,10 @@ impl Node {
loser_link_id = %loser_link_id,
"Outbound cross-connection won, loser link cleaned up"
);
// Send initial tree announce to peer (new or reconnected)
if let Err(e) = self.send_tree_announce_to_peer(&node_addr).await {
debug!(peer = %node_addr, error = %e, "Failed to send initial TreeAnnounce");
}
}
PromotionResult::CrossConnectionLost { winner_link_id } => {
// This connection lost — clean up its link
@@ -673,7 +690,7 @@ impl Node {
match msg_type {
0x10 => {
// TreeAnnounce
debug!("Received TreeAnnounce (not yet implemented)");
self.handle_tree_announce(from, payload).await;
}
0x20 => {
// FilterAnnounce
@@ -727,6 +744,10 @@ impl Node {
///
/// Frees session index, removes link and address mappings. Used for
/// both graceful disconnect and timeout-based eviction.
///
/// Also handles tree state cleanup: if the removed peer was our parent,
/// selects an alternative or becomes root, and marks remaining peers
/// for pending tree announce (delivered on next tick).
pub(super) fn remove_active_peer(&mut self, node_addr: &NodeAddr) {
let peer = match self.peers.remove(node_addr) {
Some(p) => p,
@@ -747,9 +768,20 @@ impl Node {
// Remove link and address mapping
self.remove_link(&link_id);
// Tree state cleanup
let tree_changed = self.handle_peer_removal_tree_cleanup(node_addr);
if tree_changed {
// Mark all remaining peers for pending tree announce.
// These will be sent on the next tick via check_tree_state().
for peer in self.peers.values_mut() {
peer.mark_tree_announce_pending();
}
}
info!(
node_addr = %node_addr,
link_id = %link_id,
tree_changed = tree_changed,
"Peer removed and state cleaned up"
);
}
+7
View File
@@ -7,6 +7,7 @@
mod handlers;
mod lifecycle;
mod retry;
mod tree;
#[cfg(test)]
mod tests;
@@ -255,6 +256,10 @@ pub struct Node {
/// Rate limiter for msg1 processing (DoS protection).
msg1_rate_limiter: HandshakeRateLimiter,
// === Tree Announce Timing ===
/// Last time we refreshed our root announcement (Unix seconds).
last_root_refresh_secs: u64,
// === Connection Retry ===
/// Retry state for peers whose outbound connections have failed.
/// Keyed by NodeAddr. Entries are created when a handshake times out
@@ -317,6 +322,7 @@ impl Node {
peers_by_index: HashMap::new(),
pending_outbound: HashMap::new(),
msg1_rate_limiter: HandshakeRateLimiter::new(),
last_root_refresh_secs: 0,
retry_pending: HashMap::new(),
})
}
@@ -365,6 +371,7 @@ impl Node {
peers_by_index: HashMap::new(),
pending_outbound: HashMap::new(),
msg1_rate_limiter: HandshakeRateLimiter::new(),
last_root_refresh_secs: 0,
retry_pending: HashMap::new(),
}
}
+284
View File
@@ -0,0 +1,284 @@
//! Spanning Tree Announce send/receive logic.
//!
//! Handles building, sending, and receiving TreeAnnounce messages,
//! including periodic root refresh and rate-limited propagation.
use crate::protocol::TreeAnnounce;
use crate::NodeAddr;
use super::{Node, NodeError};
use tracing::{debug, info, warn};
/// Root nodes re-announce every 30 minutes to keep the tree alive.
const ROOT_REFRESH_INTERVAL_SECS: u64 = 30 * 60;
impl Node {
/// Build a TreeAnnounce from our current tree state.
fn build_tree_announce(&self) -> Result<TreeAnnounce, NodeError> {
let decl = self.tree_state.my_declaration().clone();
let ancestry = self.tree_state.my_coords().clone();
if !decl.is_signed() {
return Err(NodeError::SendFailed {
node_addr: *self.identity.node_addr(),
reason: "declaration not signed".into(),
});
}
Ok(TreeAnnounce::new(decl, ancestry))
}
/// Send a TreeAnnounce to a specific peer, respecting rate limits.
///
/// If the peer is rate-limited, the announce is marked pending for
/// delivery on the next tick cycle.
pub(super) async fn send_tree_announce_to_peer(
&mut self,
peer_addr: &NodeAddr,
) -> Result<(), NodeError> {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
// Check rate limit
let peer = match self.peers.get_mut(peer_addr) {
Some(p) => p,
None => return Err(NodeError::PeerNotFound(*peer_addr)),
};
if !peer.can_send_tree_announce(now_ms) {
peer.mark_tree_announce_pending();
debug!(
peer = %peer_addr,
"TreeAnnounce rate-limited, marking pending"
);
return Ok(());
}
// Build and encode
let announce = self.build_tree_announce()?;
let encoded = announce.encode().map_err(|e| NodeError::SendFailed {
node_addr: *peer_addr,
reason: format!("encode failed: {}", e),
})?;
// Send
self.send_encrypted_link_message(peer_addr, &encoded).await?;
// Record send time
if let Some(peer) = self.peers.get_mut(peer_addr) {
peer.record_tree_announce_sent(now_ms);
}
debug!(peer = %peer_addr, "Sent TreeAnnounce");
Ok(())
}
/// Send a TreeAnnounce to all active peers.
pub(super) async fn send_tree_announce_to_all(&mut self) {
let peer_addrs: Vec<NodeAddr> = self.peers.keys().copied().collect();
for peer_addr in peer_addrs {
if let Err(e) = self.send_tree_announce_to_peer(&peer_addr).await {
debug!(
peer = %peer_addr,
error = %e,
"Failed to send TreeAnnounce"
);
}
}
}
/// Send pending rate-limited tree announces whose cooldown has expired.
pub(super) async fn send_pending_tree_announces(&mut self) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let ready: Vec<NodeAddr> = self
.peers
.iter()
.filter(|(_, peer)| peer.has_pending_tree_announce() && peer.can_send_tree_announce(now_ms))
.map(|(addr, _)| *addr)
.collect();
for peer_addr in ready {
if let Err(e) = self.send_tree_announce_to_peer(&peer_addr).await {
debug!(
peer = %peer_addr,
error = %e,
"Failed to send pending TreeAnnounce"
);
}
}
}
/// Handle an inbound TreeAnnounce from an authenticated peer.
///
/// 1. Decode the message
/// 2. Verify the sender's declaration signature (pubkey from handshake)
/// 3. Update the peer's tree state
/// 4. Re-evaluate parent selection
/// 5. If parent changed: increment seq, sign, recompute coords, announce to all
pub(super) async fn handle_tree_announce(&mut self, from: &NodeAddr, payload: &[u8]) {
let announce = match TreeAnnounce::decode(payload) {
Ok(a) => a,
Err(e) => {
debug!(from = %from, error = %e, "Malformed TreeAnnounce");
return;
}
};
// Verify sender's declaration signature using their known pubkey
let pubkey = match self.peers.get(from) {
Some(peer) => peer.pubkey(),
None => {
debug!(from = %from, "TreeAnnounce from unknown peer");
return;
}
};
// The declaring node_addr in the announce should match the sender
if announce.declaration.node_addr() != from {
debug!(
from = %from,
declared = %announce.declaration.node_addr(),
"TreeAnnounce node_addr mismatch"
);
return;
}
if let Err(e) = announce.declaration.verify(&pubkey) {
warn!(
from = %from,
error = %e,
"TreeAnnounce signature verification failed"
);
return;
}
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
// Update peer's tree state in ActivePeer
if let Some(peer) = self.peers.get_mut(from) {
peer.update_tree_position(
announce.declaration.clone(),
announce.ancestry.clone(),
now_ms,
);
}
// Update in TreeState
let updated = self.tree_state.update_peer(
announce.declaration.clone(),
announce.ancestry.clone(),
);
if !updated {
debug!(from = %from, "TreeAnnounce not fresher than existing, ignored");
return;
}
info!(
from = %from,
seq = announce.declaration.sequence(),
depth = announce.ancestry.depth(),
root = %announce.ancestry.root_id(),
"Processed TreeAnnounce"
);
// Re-evaluate parent selection
if let Some(new_parent) = self.tree_state.evaluate_parent() {
let new_seq = self.tree_state.my_declaration().sequence() + 1;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
self.tree_state.set_parent(new_parent, new_seq, timestamp);
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
warn!(error = %e, "Failed to sign declaration after parent switch");
return;
}
self.tree_state.recompute_coords();
info!(
new_parent = %new_parent,
new_seq = new_seq,
new_root = %self.tree_state.root(),
depth = self.tree_state.my_coords().depth(),
"Parent switched, announcing to all peers"
);
self.send_tree_announce_to_all().await;
}
}
/// Periodic tree maintenance, called from the tick handler.
///
/// - Sends pending rate-limited announces
/// - Refreshes root announcement every ROOT_REFRESH_INTERVAL_SECS (if root)
pub(super) async fn check_tree_state(&mut self) {
// Send any pending rate-limited announces
self.send_pending_tree_announces().await;
// Root refresh
if self.tree_state.is_root() && !self.peers.is_empty() {
let now_secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now_secs.saturating_sub(self.last_root_refresh_secs) >= ROOT_REFRESH_INTERVAL_SECS {
let new_seq = self.tree_state.my_declaration().sequence() + 1;
self.tree_state
.set_parent(*self.identity.node_addr(), new_seq, now_secs);
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
warn!(error = %e, "Failed to sign root refresh declaration");
return;
}
self.tree_state.recompute_coords();
self.last_root_refresh_secs = now_secs;
debug!(seq = new_seq, "Root refresh: announcing to all peers");
self.send_tree_announce_to_all().await;
}
}
}
/// Handle tree state cleanup when a peer is removed.
///
/// Called from `remove_active_peer`. If the removed peer was our parent,
/// attempts to find an alternative or becomes root.
///
/// Returns `true` if our tree state changed (caller should announce).
pub(super) fn handle_peer_removal_tree_cleanup(&mut self, node_addr: &NodeAddr) -> bool {
let was_parent = !self.tree_state.is_root()
&& self.tree_state.my_declaration().parent_id() == node_addr;
self.tree_state.remove_peer(node_addr);
if was_parent {
let changed = self.tree_state.handle_parent_lost();
if changed {
// Re-sign the new declaration
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
warn!(error = %e, "Failed to sign declaration after parent loss");
}
info!(
new_root = %self.tree_state.root(),
is_root = self.tree_state.is_root(),
"Tree state updated after parent loss"
);
}
changed
} else {
false
}
}
}
+37 -1
View File
@@ -94,6 +94,12 @@ pub struct ActivePeer {
/// Their path to root.
ancestry: Option<TreeCoordinate>,
// === Tree Announce Rate Limiting ===
/// Last time we sent a TreeAnnounce to this peer (Unix milliseconds).
last_tree_announce_sent_ms: u64,
/// Whether a tree announce is pending (deferred due to rate limit).
pending_tree_announce: bool,
// === Bloom Filter ===
/// What's reachable through them (inbound filter).
inbound_filter: Option<BloomFilter>,
@@ -132,6 +138,8 @@ impl ActivePeer {
current_addr: None,
declaration: None,
ancestry: None,
last_tree_announce_sent_ms: 0,
pending_tree_announce: false,
inbound_filter: None,
filter_sequence: 0,
filter_ttl: 0,
@@ -185,6 +193,8 @@ impl ActivePeer {
current_addr: Some(current_addr),
declaration: None,
ancestry: None,
last_tree_announce_sent_ms: 0,
pending_tree_announce: false,
inbound_filter: None,
filter_sequence: 0,
filter_ttl: 0,
@@ -441,6 +451,32 @@ impl ActivePeer {
self.ancestry = None;
}
// === Tree Announce Rate Limiting ===
/// Minimum interval between TreeAnnounce messages to the same peer (milliseconds).
const TREE_ANNOUNCE_MIN_INTERVAL_MS: u64 = 500;
/// Check if we can send a TreeAnnounce now (rate limiting).
pub fn can_send_tree_announce(&self, now_ms: u64) -> bool {
now_ms.saturating_sub(self.last_tree_announce_sent_ms) >= Self::TREE_ANNOUNCE_MIN_INTERVAL_MS
}
/// Record that we sent a TreeAnnounce to this peer.
pub fn record_tree_announce_sent(&mut self, now_ms: u64) {
self.last_tree_announce_sent_ms = now_ms;
self.pending_tree_announce = false;
}
/// Mark that a tree announce is pending (deferred due to rate limit).
pub fn mark_tree_announce_pending(&mut self) {
self.pending_tree_announce = true;
}
/// Check if a deferred tree announce is waiting to be sent.
pub fn has_pending_tree_announce(&self) -> bool {
self.pending_tree_announce
}
// === Filter Updates ===
/// Update peer's inbound filter.
@@ -494,7 +530,7 @@ mod tests {
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::new(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
#[test]
+328 -2
View File
@@ -21,7 +21,7 @@
//! layer, encrypted end-to-end independently of per-hop link encryption.
use crate::bloom::BloomFilter;
use crate::tree::{ParentDeclaration, TreeCoordinate};
use crate::tree::{CoordEntry, ParentDeclaration, TreeCoordinate};
use crate::NodeAddr;
use secp256k1::schnorr::Signature;
use std::fmt;
@@ -383,6 +383,13 @@ pub struct TreeAnnounce {
}
impl TreeAnnounce {
/// TreeAnnounce wire format version 1.
pub const VERSION_1: u8 = 0x01;
/// Minimum payload size (after msg_type stripped by dispatcher):
/// version(1) + sequence(8) + timestamp(8) + parent(16) + ancestry_count(2) + signature(64) = 99
const MIN_PAYLOAD_SIZE: usize = 99;
/// Create a new TreeAnnounce message.
pub fn new(declaration: ParentDeclaration, ancestry: TreeCoordinate) -> Self {
Self {
@@ -390,6 +397,160 @@ impl TreeAnnounce {
ancestry,
}
}
/// Encode as link-layer plaintext (includes msg_type byte).
///
/// The declaration must be signed. The encoded format is:
/// ```text
/// [0x10][version:1][sequence:8 LE][timestamp:8 LE][parent:16]
/// [ancestry_count:2 LE][entries:32×n][signature:64]
/// ```
pub fn encode(&self) -> Result<Vec<u8>, ProtocolError> {
let signature = self
.declaration
.signature()
.ok_or(ProtocolError::InvalidSignature)?;
let entries = self.ancestry.entries();
let ancestry_count = entries.len() as u16;
let size = 1 + Self::MIN_PAYLOAD_SIZE + entries.len() * CoordEntry::WIRE_SIZE;
let mut buf = Vec::with_capacity(size);
// msg_type
buf.push(LinkMessageType::TreeAnnounce.to_byte());
// version
buf.push(Self::VERSION_1);
// sequence (8 LE)
buf.extend_from_slice(&self.declaration.sequence().to_le_bytes());
// timestamp (8 LE)
buf.extend_from_slice(&self.declaration.timestamp().to_le_bytes());
// parent (16)
buf.extend_from_slice(self.declaration.parent_id().as_bytes());
// ancestry_count (2 LE)
buf.extend_from_slice(&ancestry_count.to_le_bytes());
// ancestry entries (32 bytes each)
for entry in entries {
buf.extend_from_slice(entry.node_addr.as_bytes()); // 16
buf.extend_from_slice(&entry.sequence.to_le_bytes()); // 8
buf.extend_from_slice(&entry.timestamp.to_le_bytes()); // 8
}
// outer signature (64)
buf.extend_from_slice(signature.as_ref());
Ok(buf)
}
/// Decode from link-layer payload (after msg_type byte stripped by dispatcher).
///
/// The payload starts with the version byte.
pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
if payload.len() < Self::MIN_PAYLOAD_SIZE {
return Err(ProtocolError::MessageTooShort {
expected: Self::MIN_PAYLOAD_SIZE,
got: payload.len(),
});
}
let mut pos = 0;
// version
let version = payload[pos];
pos += 1;
if version != Self::VERSION_1 {
return Err(ProtocolError::UnsupportedVersion(version));
}
// sequence (8 LE)
let sequence = u64::from_le_bytes(
payload[pos..pos + 8]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad sequence".into()))?,
);
pos += 8;
// timestamp (8 LE)
let timestamp = u64::from_le_bytes(
payload[pos..pos + 8]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad timestamp".into()))?,
);
pos += 8;
// parent (16)
let parent = NodeAddr::from_bytes(
payload[pos..pos + 16]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad parent".into()))?,
);
pos += 16;
// ancestry_count (2 LE)
let ancestry_count = u16::from_le_bytes(
payload[pos..pos + 2]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad ancestry count".into()))?,
) as usize;
pos += 2;
// Validate remaining length: entries + signature
let expected_remaining = ancestry_count * CoordEntry::WIRE_SIZE + 64;
if payload.len() - pos < expected_remaining {
return Err(ProtocolError::MessageTooShort {
expected: pos + expected_remaining,
got: payload.len(),
});
}
// ancestry entries (32 bytes each)
let mut entries = Vec::with_capacity(ancestry_count);
for _ in 0..ancestry_count {
let node_addr = NodeAddr::from_bytes(
payload[pos..pos + 16]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad entry node_addr".into()))?,
);
pos += 16;
let entry_seq = u64::from_le_bytes(
payload[pos..pos + 8]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad entry sequence".into()))?,
);
pos += 8;
let entry_ts = u64::from_le_bytes(
payload[pos..pos + 8]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad entry timestamp".into()))?,
);
pos += 8;
entries.push(CoordEntry::new(node_addr, entry_seq, entry_ts));
}
// signature (64)
let sig_bytes: [u8; 64] = payload[pos..pos + 64]
.try_into()
.map_err(|_| ProtocolError::Malformed("bad signature".into()))?;
let signature = Signature::from_slice(&sig_bytes)
.map_err(|_| ProtocolError::InvalidSignature)?;
// The first entry's node_addr is the declaring node
if entries.is_empty() {
return Err(ProtocolError::Malformed(
"ancestry must have at least one entry".into(),
));
}
let node_addr = entries[0].node_addr;
let declaration =
ParentDeclaration::with_signature(node_addr, parent, sequence, timestamp, signature);
let ancestry = TreeCoordinate::new(entries)
.map_err(|e| ProtocolError::Malformed(format!("bad ancestry: {}", e)))?;
Ok(Self {
declaration,
ancestry,
})
}
}
// ============ Bloom Filter Messages ============
@@ -978,7 +1139,7 @@ mod tests {
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::new(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
// ===== HandshakeMessageType Tests =====
@@ -1389,4 +1550,169 @@ mod tests {
assert_eq!(announce.declaration.node_addr(), &node);
assert_eq!(announce.ancestry.depth(), 2);
}
#[test]
fn test_tree_announce_encode_decode_root() {
use crate::identity::Identity;
let identity = Identity::generate();
let node_addr = *identity.node_addr();
// Root declaration: parent == self
let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 5000);
decl.sign(&identity).unwrap();
// Root ancestry: just the root itself
let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 5000)]).unwrap();
let announce = TreeAnnounce::new(decl, ancestry);
let encoded = announce.encode().unwrap();
// msg_type (1) + version (1) + seq (8) + ts (8) + parent (16) + count (2) + 1 entry (32) + sig (64) = 132
assert_eq!(encoded.len(), 132);
assert_eq!(encoded[0], 0x10); // LinkMessageType::TreeAnnounce
// Decode strips msg_type byte (as dispatcher does)
let decoded = TreeAnnounce::decode(&encoded[1..]).unwrap();
assert_eq!(decoded.declaration.node_addr(), &node_addr);
assert_eq!(decoded.declaration.parent_id(), &node_addr);
assert_eq!(decoded.declaration.sequence(), 1);
assert_eq!(decoded.declaration.timestamp(), 5000);
assert!(decoded.declaration.is_root());
assert!(decoded.declaration.is_signed());
assert_eq!(decoded.ancestry.depth(), 0); // root has depth 0
assert_eq!(decoded.ancestry.entries().len(), 1);
assert_eq!(decoded.ancestry.entries()[0].node_addr, node_addr);
assert_eq!(decoded.ancestry.entries()[0].sequence, 1);
assert_eq!(decoded.ancestry.entries()[0].timestamp, 5000);
}
#[test]
fn test_tree_announce_encode_decode_depth3() {
use crate::identity::Identity;
let identity = Identity::generate();
let node_addr = *identity.node_addr();
let parent = make_node_addr(2);
let grandparent = make_node_addr(3);
let root = make_node_addr(4);
let mut decl = ParentDeclaration::new(node_addr, parent, 5, 10000);
decl.sign(&identity).unwrap();
let ancestry = TreeCoordinate::new(vec![
CoordEntry::new(node_addr, 5, 10000),
CoordEntry::new(parent, 4, 9000),
CoordEntry::new(grandparent, 3, 8000),
CoordEntry::new(root, 2, 7000),
])
.unwrap();
let announce = TreeAnnounce::new(decl, ancestry);
let encoded = announce.encode().unwrap();
// 1 + 99 + 4*32 = 228
assert_eq!(encoded.len(), 228);
let decoded = TreeAnnounce::decode(&encoded[1..]).unwrap();
assert_eq!(decoded.declaration.node_addr(), &node_addr);
assert_eq!(decoded.declaration.parent_id(), &parent);
assert_eq!(decoded.declaration.sequence(), 5);
assert_eq!(decoded.declaration.timestamp(), 10000);
assert!(!decoded.declaration.is_root());
assert_eq!(decoded.ancestry.depth(), 3);
assert_eq!(decoded.ancestry.entries().len(), 4);
// Verify all entries preserved
let entries = decoded.ancestry.entries();
assert_eq!(entries[0].node_addr, node_addr);
assert_eq!(entries[0].sequence, 5);
assert_eq!(entries[1].node_addr, parent);
assert_eq!(entries[1].sequence, 4);
assert_eq!(entries[2].node_addr, grandparent);
assert_eq!(entries[2].timestamp, 8000);
assert_eq!(entries[3].node_addr, root);
assert_eq!(entries[3].timestamp, 7000);
// Root ID is last entry
assert_eq!(decoded.ancestry.root_id(), &root);
}
#[test]
fn test_tree_announce_decode_unsupported_version() {
use crate::identity::Identity;
let identity = Identity::generate();
let node_addr = *identity.node_addr();
let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 1000);
decl.sign(&identity).unwrap();
let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 1000)]).unwrap();
let announce = TreeAnnounce::new(decl, ancestry);
let mut encoded = announce.encode().unwrap();
// Corrupt version byte (byte index 1, after msg_type)
encoded[1] = 0xFF;
let result = TreeAnnounce::decode(&encoded[1..]);
assert!(matches!(result, Err(ProtocolError::UnsupportedVersion(0xFF))));
}
#[test]
fn test_tree_announce_decode_truncated() {
// Way too short
let result = TreeAnnounce::decode(&[0x01]);
assert!(matches!(
result,
Err(ProtocolError::MessageTooShort { expected: 99, .. })
));
// Just under minimum (98 bytes)
let short = vec![0u8; 98];
let result = TreeAnnounce::decode(&short);
assert!(matches!(
result,
Err(ProtocolError::MessageTooShort { expected: 99, .. })
));
}
#[test]
fn test_tree_announce_decode_ancestry_count_mismatch() {
use crate::identity::Identity;
let identity = Identity::generate();
let node_addr = *identity.node_addr();
let mut decl = ParentDeclaration::new(node_addr, node_addr, 1, 1000);
decl.sign(&identity).unwrap();
let ancestry = TreeCoordinate::new(vec![CoordEntry::new(node_addr, 1, 1000)]).unwrap();
let announce = TreeAnnounce::new(decl, ancestry);
let mut encoded = announce.encode().unwrap();
// The ancestry_count is at offset: 1 (msg_type) + 1 (version) + 8 (seq) + 8 (ts) + 16 (parent) = 34
// Set ancestry_count to 5 but we only have 1 entry's worth of data
encoded[34] = 5;
encoded[35] = 0;
let result = TreeAnnounce::decode(&encoded[1..]);
assert!(matches!(
result,
Err(ProtocolError::MessageTooShort { .. })
));
}
#[test]
fn test_tree_announce_encode_unsigned_fails() {
let node = make_node_addr(1);
let decl = ParentDeclaration::new(node, node, 1, 1000);
let ancestry = make_coords(&[1, 0]);
let announce = TreeAnnounce::new(decl, ancestry);
let result = announce.encode();
assert!(matches!(result, Err(ProtocolError::InvalidSignature)));
}
}
+476 -78
View File
@@ -207,46 +207,115 @@ impl PartialEq for ParentDeclaration {
impl Eq for ParentDeclaration {}
/// Metadata for a single node in a tree coordinate path.
///
/// Carries the node address and its declaration metadata (sequence number
/// and timestamp). Used in TreeCoordinate entries and TreeAnnounce wire
/// format.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CoordEntry {
/// The node's routing address.
pub node_addr: NodeAddr,
/// The node's declaration sequence number.
pub sequence: u64,
/// The node's declaration timestamp (Unix seconds).
pub timestamp: u64,
}
impl CoordEntry {
/// Wire size of a serialized entry: node_addr(16) + sequence(8) + timestamp(8).
pub const WIRE_SIZE: usize = 32;
/// Create a new coordinate entry.
pub fn new(node_addr: NodeAddr, sequence: u64, timestamp: u64) -> Self {
Self {
node_addr,
sequence,
timestamp,
}
}
/// Create an entry with default metadata (sequence=0, timestamp=0).
///
/// Useful for constructing coordinates when only routing (not wire format)
/// is needed, e.g., in tests or distance calculations.
pub fn addr_only(node_addr: NodeAddr) -> Self {
Self {
node_addr,
sequence: 0,
timestamp: 0,
}
}
}
/// A node's coordinates in the spanning tree.
///
/// Coordinates are the path from the node to the root:
/// `[self, parent, grandparent, ..., root]`
///
/// Each entry carries the node address plus declaration metadata (sequence
/// and timestamp) for the wire protocol. Routing operations (distance,
/// LCA) use only the node addresses.
///
/// The coordinate enables greedy routing via tree distance calculation.
/// Two nodes can compute the hops between them by finding their lowest
/// common ancestor (LCA) in the tree.
#[derive(Clone, PartialEq, Eq)]
pub struct TreeCoordinate(Vec<NodeAddr>);
pub struct TreeCoordinate(Vec<CoordEntry>);
impl TreeCoordinate {
/// Create a coordinate from a path (self to root).
/// Create a coordinate from a path of entries (self to root).
///
/// The path must be non-empty and ordered from the node to the root.
pub fn new(path: Vec<NodeAddr>) -> Result<Self, TreeError> {
pub fn new(path: Vec<CoordEntry>) -> Result<Self, TreeError> {
if path.is_empty() {
return Err(TreeError::EmptyCoordinate);
}
Ok(Self(path))
}
/// Create a coordinate from node addresses only (no metadata).
///
/// Convenience constructor for cases where only routing is needed.
/// Each entry gets sequence=0, timestamp=0.
pub fn from_addrs(addrs: Vec<NodeAddr>) -> Result<Self, TreeError> {
if addrs.is_empty() {
return Err(TreeError::EmptyCoordinate);
}
Ok(Self(
addrs
.into_iter()
.map(CoordEntry::addr_only)
.collect(),
))
}
/// Create a coordinate for a root node.
pub fn root(node_addr: NodeAddr) -> Self {
Self(vec![node_addr])
Self(vec![CoordEntry::addr_only(node_addr)])
}
/// Create a root coordinate with metadata.
pub fn root_with_meta(node_addr: NodeAddr, sequence: u64, timestamp: u64) -> Self {
Self(vec![CoordEntry::new(node_addr, sequence, timestamp)])
}
/// The node this coordinate belongs to (first element).
pub fn node_addr(&self) -> &NodeAddr {
&self.0[0]
&self.0[0].node_addr
}
/// The root of the tree (last element).
pub fn root_id(&self) -> &NodeAddr {
self.0.last().expect("coordinate never empty")
&self.0.last().expect("coordinate never empty").node_addr
}
/// The immediate parent (second element, or self if root).
pub fn parent_id(&self) -> &NodeAddr {
self.0.get(1).unwrap_or(&self.0[0])
self.0
.get(1)
.map(|e| &e.node_addr)
.unwrap_or(&self.0[0].node_addr)
}
/// Depth in the tree (0 = root).
@@ -254,11 +323,19 @@ impl TreeCoordinate {
self.0.len() - 1
}
/// The full ancestry path.
pub fn path(&self) -> &[NodeAddr] {
/// The full path of entries with metadata.
pub fn entries(&self) -> &[CoordEntry] {
&self.0
}
/// Iterator over node addresses in the path (self to root).
///
/// Use this for routing operations (distance, LCA, ancestor checks)
/// that only need the address path.
pub fn node_addrs(&self) -> impl Iterator<Item = &NodeAddr> + DoubleEndedIterator {
self.0.iter().map(|e| &e.node_addr)
}
/// Check if this coordinate is a root (length 1).
pub fn is_root(&self) -> bool {
self.0.len() == 1
@@ -286,8 +363,8 @@ impl TreeCoordinate {
/// Returns the depth (from root) of the LCA.
pub fn lca_depth(&self, other: &TreeCoordinate) -> usize {
let mut common: usize = 0;
let self_rev = self.0.iter().rev();
let other_rev = other.0.iter().rev();
let self_rev = self.node_addrs().rev();
let other_rev = other.node_addrs().rev();
for (a, b) in self_rev.zip(other_rev) {
if a == b {
@@ -303,8 +380,8 @@ impl TreeCoordinate {
/// Get the lowest common ancestor node ID.
pub fn lca(&self, other: &TreeCoordinate) -> Option<&NodeAddr> {
let self_rev: Vec<_> = self.0.iter().rev().collect();
let other_rev: Vec<_> = other.0.iter().rev().collect();
let self_rev: Vec<_> = self.node_addrs().rev().collect();
let other_rev: Vec<_> = other.node_addrs().rev().collect();
let mut lca = None;
for (a, b) in self_rev.iter().zip(other_rev.iter()) {
@@ -319,42 +396,41 @@ impl TreeCoordinate {
/// Check if `other` is an ancestor (appears in our path after self).
pub fn has_ancestor(&self, other: &NodeAddr) -> bool {
self.0.iter().skip(1).any(|id| id == other)
self.node_addrs().skip(1).any(|id| id == other)
}
/// Check if `other` is in our ancestry (including self).
pub fn contains(&self, other: &NodeAddr) -> bool {
self.0.iter().any(|id| id == other)
self.node_addrs().any(|id| id == other)
}
/// Get the ancestor at a specific depth from self.
///
/// `ancestor_at(0)` returns self, `ancestor_at(1)` returns parent, etc.
pub fn ancestor_at(&self, depth: usize) -> Option<&NodeAddr> {
self.0.get(depth)
self.0.get(depth).map(|e| &e.node_addr)
}
}
impl fmt::Debug for TreeCoordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TreeCoordinate(depth={}, path=[", self.depth())?;
for (i, id) in self.0.iter().enumerate() {
for (i, entry) in self.0.iter().enumerate() {
if i > 0 {
write!(f, "")?;
}
// Show first 4 bytes of each node ID
write!(f, "{:02x}{:02x}", id.as_bytes()[0], id.as_bytes()[1])?;
write!(
f,
"{:02x}{:02x}",
entry.node_addr.as_bytes()[0],
entry.node_addr.as_bytes()[1]
)?;
}
write!(f, "])")
}
}
impl AsRef<[NodeAddr]> for TreeCoordinate {
fn as_ref(&self) -> &[NodeAddr] {
&self.0
}
}
/// Local spanning tree state for a node.
///
/// Contains this node's declaration, coordinates, and view of peers'
@@ -386,7 +462,7 @@ impl TreeState {
.map(|d| d.as_secs())
.unwrap_or(0);
let my_declaration = ParentDeclaration::self_root(my_node_addr, 1, timestamp);
let my_coords = TreeCoordinate::root(my_node_addr);
let my_coords = TreeCoordinate::root_with_meta(my_node_addr, 1, timestamp);
Self {
my_node_addr,
@@ -482,17 +558,26 @@ impl TreeState {
/// Update this node's coordinates based on current parent's ancestry.
pub fn recompute_coords(&mut self) {
if self.my_declaration.is_root() {
self.my_coords = TreeCoordinate::root(self.my_node_addr);
self.my_coords = TreeCoordinate::root_with_meta(
self.my_node_addr,
self.my_declaration.sequence(),
self.my_declaration.timestamp(),
);
self.root = self.my_node_addr;
return;
}
let parent_id = self.my_declaration.parent_id();
if let Some(parent_coords) = self.peer_ancestry.get(parent_id) {
// Our coords = [self] ++ parent_coords
let mut path = vec![self.my_node_addr];
path.extend_from_slice(parent_coords.path());
self.my_coords = TreeCoordinate::new(path).expect("non-empty path");
// Our coords = [self_entry] ++ parent_coords entries
let self_entry = CoordEntry::new(
self.my_node_addr,
self.my_declaration.sequence(),
self.my_declaration.timestamp(),
);
let mut entries = vec![self_entry];
entries.extend_from_slice(parent_coords.entries());
self.my_coords = TreeCoordinate::new(entries).expect("non-empty path");
self.root = *self.my_coords.root_id();
}
}
@@ -513,12 +598,131 @@ impl TreeState {
None
}
/// Check if a parent switch to `candidate` would be beneficial.
/// Minimum depth improvement required to switch parents (same root).
/// Prevents thrashing on equivalent-depth paths.
const PARENT_SWITCH_THRESHOLD: usize = 1;
/// Evaluate whether to switch parents based on current peer tree state.
///
/// This is a stub - full implementation requires policy decisions.
pub fn should_switch_parent(&self, _candidate: &NodeAddr) -> bool {
// Stub: would evaluate parent switch criteria
false
/// v1 algorithm: depth-based, no latency/loss metrics.
///
/// Returns `Some(peer_node_addr)` if a parent switch is recommended,
/// or `None` if the current parent is adequate.
pub fn evaluate_parent(&self) -> Option<NodeAddr> {
if self.peer_ancestry.is_empty() {
return None;
}
// Find the smallest root visible across all peers
let mut smallest_root: Option<NodeAddr> = None;
for coords in self.peer_ancestry.values() {
let peer_root = coords.root_id();
smallest_root = Some(match smallest_root {
None => *peer_root,
Some(current) => {
if *peer_root < current {
*peer_root
} else {
current
}
}
});
}
let smallest_root = match smallest_root {
Some(r) => r,
None => return None,
};
// If we are the smallest node in the network, stay root
if self.my_node_addr <= smallest_root && self.is_root() {
return None;
}
// Among peers that reach the smallest root, find the shallowest
let mut best_peer: Option<(NodeAddr, usize)> = None; // (peer_addr, depth)
for (peer_id, coords) in &self.peer_ancestry {
if *coords.root_id() != smallest_root {
continue;
}
let depth = coords.depth();
match &best_peer {
None => best_peer = Some((*peer_id, depth)),
Some((_, best_depth)) => {
if depth < *best_depth {
best_peer = Some((*peer_id, depth));
}
}
}
}
let (best_peer_id, best_depth) = match best_peer {
Some(bp) => bp,
None => return None,
};
// If already using this peer as parent, no switch needed
if *self.my_declaration.parent_id() == best_peer_id && !self.is_root() {
return None;
}
// If our current parent is gone from peer_ancestry, our path is broken — always switch
if !self.is_root() && !self.peer_ancestry.contains_key(self.my_declaration.parent_id()) {
return Some(best_peer_id);
}
// Switching roots (smaller root found) → always switch
if smallest_root < self.root || (self.is_root() && smallest_root < self.my_node_addr) {
return Some(best_peer_id);
}
// Same root: require depth improvement ≥ threshold
if self.is_root() {
// We're root but shouldn't be (peers have a smaller root) — always switch
return Some(best_peer_id);
}
// Compare depth: our current depth vs what we'd get through best_peer
// Our new depth would be best_depth + 1
let current_depth = self.my_coords.depth();
let proposed_depth = best_depth + 1;
if current_depth >= proposed_depth + Self::PARENT_SWITCH_THRESHOLD {
return Some(best_peer_id);
}
None
}
/// Handle loss of current parent.
///
/// Tries to find an alternative parent among remaining peers.
/// If none available, becomes its own root (increments sequence).
///
/// Returns `true` if the tree state changed (caller should re-announce).
pub fn handle_parent_lost(&mut self) -> bool {
// Try to find an alternative parent
if let Some(new_parent) = self.evaluate_parent() {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let new_seq = self.my_declaration.sequence() + 1;
self.set_parent(new_parent, new_seq, timestamp);
self.recompute_coords();
return true;
}
// No alternative: become own root
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let new_seq = self.my_declaration.sequence() + 1;
self.my_declaration =
ParentDeclaration::self_root(self.my_node_addr, new_seq, timestamp);
self.recompute_coords();
true
}
/// Sign this node's declaration with the given identity.
@@ -556,6 +760,10 @@ mod tests {
NodeAddr::from_bytes(bytes)
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
// ===== TreeCoordinate Tests =====
#[test]
@@ -576,7 +784,7 @@ mod tests {
let parent = make_node_addr(2);
let root = make_node_addr(3);
let coord = TreeCoordinate::new(vec![node, parent, root]).unwrap();
let coord = make_coords(&[1, 2, 3]);
assert!(!coord.is_root());
assert_eq!(coord.depth(), 2);
@@ -587,10 +795,27 @@ mod tests {
#[test]
fn test_tree_coordinate_empty_fails() {
let result = TreeCoordinate::new(vec![]);
let result = TreeCoordinate::from_addrs(vec![]);
assert!(matches!(result, Err(TreeError::EmptyCoordinate)));
}
#[test]
fn test_tree_coordinate_entries_metadata() {
let node = make_node_addr(1);
let root = make_node_addr(0);
let coord = TreeCoordinate::new(vec![
CoordEntry::new(node, 5, 1000),
CoordEntry::new(root, 1, 500),
])
.unwrap();
assert_eq!(coord.entries()[0].sequence, 5);
assert_eq!(coord.entries()[0].timestamp, 1000);
assert_eq!(coord.entries()[1].sequence, 1);
assert_eq!(coord.entries()[1].timestamp, 500);
}
#[test]
fn test_tree_distance_same_node() {
let node = make_node_addr(1);
@@ -601,12 +826,8 @@ mod tests {
#[test]
fn test_tree_distance_siblings() {
let root = make_node_addr(0);
let a = make_node_addr(1);
let b = make_node_addr(2);
let coord_a = TreeCoordinate::new(vec![a, root]).unwrap();
let coord_b = TreeCoordinate::new(vec![b, root]).unwrap();
let coord_a = make_coords(&[1, 0]);
let coord_b = make_coords(&[2, 0]);
// a -> root -> b = 2 hops
assert_eq!(coord_a.distance_to(&coord_b), 2);
@@ -614,12 +835,8 @@ mod tests {
#[test]
fn test_tree_distance_ancestor() {
let root = make_node_addr(0);
let parent = make_node_addr(1);
let child = make_node_addr(2);
let coord_parent = TreeCoordinate::new(vec![parent, root]).unwrap();
let coord_child = TreeCoordinate::new(vec![child, parent, root]).unwrap();
let coord_parent = make_coords(&[1, 0]);
let coord_child = make_coords(&[2, 1, 0]);
// child -> parent = 1 hop
assert_eq!(coord_child.distance_to(&coord_parent), 1);
@@ -628,19 +845,13 @@ mod tests {
#[test]
fn test_tree_distance_cousins() {
// Tree structure:
// root
// root(0)
// / \
// a b
// a(1) b(2)
// / \
// c d
let root = make_node_addr(0);
let a = make_node_addr(1);
let b = make_node_addr(2);
let c = make_node_addr(3);
let d = make_node_addr(4);
let coord_c = TreeCoordinate::new(vec![c, a, root]).unwrap();
let coord_d = TreeCoordinate::new(vec![d, b, root]).unwrap();
// c(3) d(4)
let coord_c = make_coords(&[3, 1, 0]);
let coord_d = make_coords(&[4, 2, 0]);
// c -> a -> root -> b -> d = 4 hops
assert_eq!(coord_c.distance_to(&coord_d), 4);
@@ -648,11 +859,8 @@ mod tests {
#[test]
fn test_tree_distance_different_roots() {
let root1 = make_node_addr(1);
let root2 = make_node_addr(2);
let coord1 = TreeCoordinate::root(root1);
let coord2 = TreeCoordinate::root(root2);
let coord1 = TreeCoordinate::root(make_node_addr(1));
let coord2 = TreeCoordinate::root(make_node_addr(2));
assert_eq!(coord1.distance_to(&coord2), usize::MAX);
}
@@ -663,7 +871,7 @@ mod tests {
let parent = make_node_addr(1);
let child = make_node_addr(2);
let coord = TreeCoordinate::new(vec![child, parent, root]).unwrap();
let coord = make_coords(&[2, 1, 0]);
assert!(coord.has_ancestor(&parent));
assert!(coord.has_ancestor(&root));
@@ -677,7 +885,7 @@ mod tests {
let child = make_node_addr(2);
let other = make_node_addr(99);
let coord = TreeCoordinate::new(vec![child, parent, root]).unwrap();
let coord = make_coords(&[2, 1, 0]);
assert!(coord.contains(&child));
assert!(coord.contains(&parent));
@@ -691,7 +899,7 @@ mod tests {
let parent = make_node_addr(1);
let child = make_node_addr(2);
let coord = TreeCoordinate::new(vec![child, parent, root]).unwrap();
let coord = make_coords(&[2, 1, 0]);
assert_eq!(coord.ancestor_at(0), Some(&child));
assert_eq!(coord.ancestor_at(1), Some(&parent));
@@ -703,18 +911,15 @@ mod tests {
fn test_lca() {
let root = make_node_addr(0);
let a = make_node_addr(1);
let b = make_node_addr(2);
let c = make_node_addr(3);
let d = make_node_addr(4);
// c under a, d under b, both under root
let coord_c = TreeCoordinate::new(vec![c, a, root]).unwrap();
let coord_d = TreeCoordinate::new(vec![d, b, root]).unwrap();
let coord_c = make_coords(&[3, 1, 0]);
let coord_d = make_coords(&[4, 2, 0]);
assert_eq!(coord_c.lca(&coord_d), Some(&root));
// c and a share ancestry through a and root
let coord_a = TreeCoordinate::new(vec![a, root]).unwrap();
let coord_a = make_coords(&[1, 0]);
assert_eq!(coord_c.lca(&coord_a), Some(&a));
}
@@ -812,7 +1017,7 @@ mod tests {
let root = make_node_addr(2);
let decl = ParentDeclaration::new(peer, root, 1, 1000);
let coords = TreeCoordinate::new(vec![peer, root]).unwrap();
let coords = make_coords(&[1, 2]);
assert!(state.update_peer(decl.clone(), coords.clone()));
assert_eq!(state.peer_count(), 1);
@@ -837,7 +1042,7 @@ mod tests {
let root = make_node_addr(2);
let decl = ParentDeclaration::new(peer, root, 1, 1000);
let coords = TreeCoordinate::new(vec![peer, root]).unwrap();
let coords = make_coords(&[1, 2]);
state.update_peer(decl, coords);
assert_eq!(state.peer_count(), 1);
@@ -867,13 +1072,13 @@ mod tests {
// Update my state to have shared root
state.set_parent(shared_root, 1, 1000);
let my_new_coords = TreeCoordinate::new(vec![my_node, shared_root]).unwrap();
let my_new_coords = make_coords(&[0, 99]);
// Manually set coords for test (normally done by recompute_coords)
state.my_coords = my_new_coords;
state.root = shared_root;
// Update peer to have same root
let peer_coords = TreeCoordinate::new(vec![peer, shared_root]).unwrap();
let peer_coords = make_coords(&[1, 99]);
let decl = ParentDeclaration::new(peer, shared_root, 2, 2000);
state.update_peer(decl, peer_coords);
@@ -903,4 +1108,197 @@ mod tests {
assert!(ids.contains(&&peer1));
assert!(ids.contains(&&peer2));
}
// ===== Parent Selection Tests =====
#[test]
fn test_evaluate_parent_picks_smallest_root() {
// Node 5 starts as root. Peers 3 and 7 each claim different roots.
// Peer 3's path: [3, 1] (root=1)
// Peer 7's path: [7, 2] (root=2)
// Should pick peer 3 because root 1 < root 2.
let my_node = make_node_addr(5);
let mut state = TreeState::new(my_node);
let peer3 = make_node_addr(3);
let peer7 = make_node_addr(7);
state.update_peer(
ParentDeclaration::new(peer3, make_node_addr(1), 1, 1000),
make_coords(&[3, 1]),
);
state.update_peer(
ParentDeclaration::new(peer7, make_node_addr(2), 1, 1000),
make_coords(&[7, 2]),
);
let result = state.evaluate_parent();
assert_eq!(result, Some(peer3));
}
#[test]
fn test_evaluate_parent_prefers_shallowest_depth() {
// Node 5, root=0 (shared). Peer 1 at depth 1, peer 2 at depth 3.
// Both reach root 0. Should pick peer 1 (shallowest).
let my_node = make_node_addr(5);
let mut state = TreeState::new(my_node);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
let root = make_node_addr(0);
// Peer 1: depth 1 (path = [1, 0])
state.update_peer(
ParentDeclaration::new(peer1, root, 1, 1000),
make_coords(&[1, 0]),
);
// Peer 2: depth 3 (path = [2, 3, 4, 0])
state.update_peer(
ParentDeclaration::new(peer2, make_node_addr(3), 1, 1000),
make_coords(&[2, 3, 4, 0]),
);
let result = state.evaluate_parent();
assert_eq!(result, Some(peer1));
}
#[test]
fn test_evaluate_parent_stays_root_when_smallest() {
// Node 0 (smallest possible) should stay root even if peers exist.
let my_node = make_node_addr(0);
let mut state = TreeState::new(my_node);
let peer1 = make_node_addr(1);
// Peer 1 has root 0 (us) — shouldn't trigger switch
state.update_peer(
ParentDeclaration::new(peer1, my_node, 1, 1000),
make_coords(&[1, 0]),
);
assert_eq!(state.evaluate_parent(), None);
}
#[test]
fn test_evaluate_parent_no_switch_when_already_best() {
// Node 5, already using peer 1 as parent. No better option.
let my_node = make_node_addr(5);
let mut state = TreeState::new(my_node);
let peer1 = make_node_addr(1);
let root = make_node_addr(0);
state.update_peer(
ParentDeclaration::new(peer1, root, 1, 1000),
make_coords(&[1, 0]),
);
// Switch to peer1 as parent first
state.set_parent(peer1, 1, 1000);
state.recompute_coords();
// Now evaluate — should return None since peer1 is already our parent
assert_eq!(state.evaluate_parent(), None);
}
#[test]
fn test_evaluate_parent_no_peers() {
let my_node = make_node_addr(5);
let state = TreeState::new(my_node);
assert_eq!(state.evaluate_parent(), None);
}
#[test]
fn test_evaluate_parent_depth_threshold() {
// Node 5, currently at depth 4 through peer 2.
// Peer 1 offers depth 3 (improvement of 1, which equals threshold).
// Peer 3 offers depth 1 (improvement of 3, exceeds threshold).
// Should switch to peer 3.
let my_node = make_node_addr(5);
let mut state = TreeState::new(my_node);
let peer2 = make_node_addr(2);
let peer3 = make_node_addr(3);
let root = make_node_addr(0);
// Peer 2: depth 3 (we'd be depth 4 through them)
state.update_peer(
ParentDeclaration::new(peer2, make_node_addr(6), 1, 1000),
make_coords(&[2, 6, 7, 0]),
);
// Set peer2 as our parent, making us depth 4
state.set_parent(peer2, 1, 1000);
state.recompute_coords();
assert_eq!(state.my_coords().depth(), 4);
// Peer 3: depth 1 (we'd be depth 2 through them) — improvement of 2
state.update_peer(
ParentDeclaration::new(peer3, root, 1, 1000),
make_coords(&[3, 0]),
);
let result = state.evaluate_parent();
assert_eq!(result, Some(peer3));
}
#[test]
fn test_handle_parent_lost_finds_alternative() {
let my_node = make_node_addr(5);
let mut state = TreeState::new(my_node);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
let root = make_node_addr(0);
state.update_peer(
ParentDeclaration::new(peer1, root, 1, 1000),
make_coords(&[1, 0]),
);
state.update_peer(
ParentDeclaration::new(peer2, root, 1, 1000),
make_coords(&[2, 0]),
);
// Set peer1 as parent
state.set_parent(peer1, 1, 1000);
state.recompute_coords();
// Remove peer1 (parent lost)
state.remove_peer(&peer1);
let changed = state.handle_parent_lost();
assert!(changed);
// Should have switched to peer2
assert_eq!(state.my_declaration().parent_id(), &peer2);
assert!(!state.is_root());
}
#[test]
fn test_handle_parent_lost_becomes_root() {
let my_node = make_node_addr(5);
let mut state = TreeState::new(my_node);
let peer1 = make_node_addr(1);
let root = make_node_addr(0);
state.update_peer(
ParentDeclaration::new(peer1, root, 1, 1000),
make_coords(&[1, 0]),
);
// Set peer1 as parent
state.set_parent(peer1, 1, 1000);
state.recompute_coords();
let seq_before = state.my_declaration().sequence();
// Remove peer1 (only parent)
state.remove_peer(&peer1);
let changed = state.handle_parent_lost();
assert!(changed);
assert!(state.is_root());
assert!(state.my_declaration().sequence() > seq_before);
assert_eq!(state.root(), &my_node);
}
}