mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
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:
+1
-1
@@ -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
@@ -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
@@ -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,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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user