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
+93 -42
View File
@@ -35,24 +35,30 @@ coordinates and distances.
### 2.1 Wire Format
```text
TreeAnnounce {
TreeAnnounce (v1) {
version: u8, // Protocol version (0x01 for v1)
sequence: u64, // Monotonic, increments on parent change
timestamp: u64, // Unix timestamp (seconds)
parent: NodeAddr, // 16 bytes, truncated SHA-256(pubkey) of selected parent
ancestry: Vec<AncestryEntry>, // Path from self to root
signature: Signature, // 64 bytes, signs (sequence || timestamp || parent || ancestry)
parent: NodeAddr, // 16 bytes, truncated SHA-256(pubkey) of selected parent
ancestry_count: u16, // Number of ancestry entries
ancestry: [AncestryEntry], // Path from self to root
signature: Signature, // 64 bytes, outer signature over entire message
}
AncestryEntry {
node_addr: NodeAddr, // 16 bytes
AncestryEntry (v1) {
node_addr: NodeAddr, // 16 bytes
sequence: u64, // That node's sequence number
timestamp: u64, // That node's timestamp
signature: Signature, // That node's signature over its declaration
}
```
Note: v1 ancestry entries are 32 bytes each (no per-entry signature). See §2.7 Trust Model.
### 2.2 Field Semantics
**version**: Protocol version number. v1 = 0x01. Receivers MUST reject messages
with unrecognized version numbers to ensure forward compatibility.
**sequence**: Incremented each time the node changes its parent declaration.
Higher sequence numbers supersede lower ones for conflict resolution.
@@ -63,20 +69,33 @@ stale if `now - timestamp > ROOT_TIMEOUT` (default 60 minutes for root).
the node is declaring itself as root.
**ancestry**: The chain from this node up to the root. The first entry is this
node's own declaration, followed by parent, grandparent, etc. Each entry is
signed by the declaring node, allowing verification of the entire chain.
node's own declaration, followed by parent, grandparent, etc. In v1, entries
carry only routing metadata (node_addr, sequence, timestamp) without per-entry
signatures. See §2.7 for the trust model.
### 2.3 Size Estimate
| Component | Size |
|-----------|------|
| version | 1 byte |
| sequence | 8 bytes |
| timestamp | 8 bytes |
| parent | 16 bytes |
| ancestry_count | 2 bytes |
| signature | 64 bytes |
| Per ancestry entry | 16 + 8 + 8 + 64 = 96 bytes |
| Per ancestry entry (v1) | 16 + 8 + 8 = 32 bytes |
For tree depth D: `96 + D * 96` bytes. At depth 10: ~1.1 KB.
For tree depth D (ancestry_count = D + 1): `100 + (D + 1) × 32` bytes payload.
| Tree Depth | Payload Size | With Link Overhead |
|------------|--------------|--------------------|
| 0 (root) | 132 bytes | 161 bytes |
| 3 | 228 bytes | 257 bytes |
| 5 | 292 bytes | 321 bytes |
| 10 | 452 bytes | 481 bytes |
Note: v1 ancestry entries omit per-entry signatures (32 bytes vs 96 bytes in
the original design). See §2.7 for the rationale.
### 2.4 Exchange Rules
@@ -104,18 +123,22 @@ For tree depth D: `96 + D * 96` bytes. At depth 10: ~1.1 KB.
When receiving TreeAnnounce from peer P:
```text
1. Verify signature on sender's declaration
2. Verify signatures on all ancestry entries
3. For each entry in ancestry:
- If entry.sequence > stored.sequence: update stored entry
- If entry.sequence == stored.sequence && entry.timestamp > stored.timestamp: update
- Otherwise: keep existing entry
4. Update peer P's record with new ancestry
5. Re-evaluate parent selection:
- If better path to root available: change parent, announce to all peers
- Apply stability threshold to prevent flapping
1. Decode message; reject if version != 0x01
2. Verify P's declaration signature using P's known public key (from Noise IK)
3. Verify that declaration node_addr matches sender's identity
4. Check sequence freshness:
- If sequence <= stored sequence for P: discard (stale)
5. Update peer P's tree state (declaration + ancestry)
6. Re-evaluate parent selection:
- Find smallest root visible across all peers
- Among peers reaching smallest root, prefer shallowest depth
- Apply stability threshold to prevent flapping (depth improvement ≥ 1)
- If parent changed: increment own sequence, sign, recompute coords, announce to all
```
Note: In v1, only the sender's declaration signature is verified (step 2).
Ancestry entries beyond the direct peer are accepted on trust. See §2.7.
### 2.6 Rate Limiting
To prevent announcement storms during reconvergence:
@@ -124,6 +147,34 @@ To prevent announcement storms during reconvergence:
- If change occurs during cooldown: mark pending, send after cooldown
- Coalesce multiple pending changes into single announcement
### 2.7 Trust Model (v1)
**v1 uses transitive trust**: each node verifies only its direct peer's
declaration signature. The peer's public key is known from the Noise IK
handshake, so verification is straightforward. Ancestry entries from nodes
beyond the direct peer are accepted on trust from the authenticated sender.
**Why transitive trust?** NodeAddr values are truncated SHA-256 hashes of
public keys — this mapping is intentionally one-way. To verify an ancestry
entry's signature, a node would need the entry's public key, but FIPS does not
distribute node_addr→pubkey mappings by design. Exposing these mappings would
enable traffic analysis, undermining a core privacy property.
**Limitation**: An adversarial interior node could fabricate ancestry chains,
potentially attracting traffic to itself (sinkhole attack) or manipulating tree
topology. This risk is mitigated by:
- **Authenticated peers have reputation cost**: Misbehaving nodes can be
disconnected and blocked by their direct peers.
- **Multi-path observation**: Nodes receiving conflicting tree state from
multiple peers can detect inconsistencies (future enhancement).
**Versioning**: The wire format includes a version byte (v1 = 0x01) to enable
future protocol evolution. A future version could introduce stronger ancestry
verification (e.g., zero-knowledge proofs of key ownership) without breaking
backward compatibility. Nodes MUST reject TreeAnnounce messages with
unrecognized version numbers.
---
## 3. FilterAnnounce
@@ -433,79 +484,79 @@ Propagates spanning tree state between directly connected peers.
│ │ Decrypt │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ TREE ANNOUNCE (plaintext) │ │
│ │ TREE ANNOUNCE v1 (plaintext) │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x10 │ │
│ │ 1 │ sequence8 bytesu64 LE, monotonic counter │ │
│ │ 9timestamp │ 8 bytes │ u64 LE, Unix seconds │ │
│ │ 17parent 16 bytes │ NodeAddr of selected parent │ │
│ │ 33ancestry_count │ 2 bytes u16 LE, number of entries │ │
│ │ 35 │ ancestry[0..n] │ 96 × n │ AncestryEntry array │ │
│ │ 1 │ version 1 byte 0x01 (v1) │ │
│ │ 2sequence │ 8 bytes │ u64 LE, monotonic counter │ │
│ │ 10timestamp8 bytes u64 LE, Unix seconds │ │
│ │ 18parent │ 16 bytes │ NodeAddr of selected parent │ │
│ │ 34 │ ancestry_count │ 2 bytes │ u16 LE, number of entries │ │
│ │ 36 │ ancestry[0..n] │ 32 × n │ AncestryEntry array │ │
│ │ ... │ signature │ 64 bytes │ Schnorr sig over all above │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ ANCESTRY ENTRY (96 bytes each) │ │
│ │ ANCESTRY ENTRY v1 (32 bytes each) │ │
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ node_addr │ 16 bytes │ Truncated SHA-256(pubkey) │ │
│ │ 0 │ node_addr │ 16 bytes │ Truncated SHA-256(pubkey) │ │
│ │ 16 │ sequence │ 8 bytes │ u64 LE, node's seq number │ │
│ │ 24 │ timestamp │ 8 bytes │ u64 LE, node's timestamp │ │
│ │ 32 │ signature │ 64 bytes │ Node's sig over its decl │ │
│ └────────┴──────────────────┴───────────┴───────────────────────────────┘ │
│ │
│ Note: v1 entries omit per-entry signatures. Only the sender's outer │
│ signature is verified (transitive trust model, see §2.7). │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
**Size calculation**: `1 + 8 + 8 + 16 + 2 + (depth × 96) + 64` bytes
**Size calculation**: `1 + 1 + 8 + 8 + 16 + 2 + (depth × 32) + 64 = 100 + (depth × 32)` bytes
| Tree Depth | Payload Size | With Link Overhead |
|------------|--------------|-------------------|
| 1 (root) | 195 bytes | 224 bytes |
| 3 | 387 bytes | 416 bytes |
| 5 | 579 bytes | 608 bytes |
| 10 | 1059 bytes | 1088 bytes |
|------------|--------------|--------------------|
| 0 (root) | 132 bytes | 161 bytes |
| 3 | 228 bytes | 257 bytes |
| 5 | 292 bytes | 321 bytes |
| 10 | 452 bytes | 481 bytes |
**Concrete example** (node D at depth 3, ancestry = [D, P1, P2, Root]):
```text
PLAINTEXT BYTES (hex layout):
10 ← msg_type = TreeAnnounce
01 ← version = 1
05 00 00 00 00 00 00 00 ← sequence = 5
C3 B2 A1 67 00 00 00 00 ← timestamp (Unix seconds)
[16 bytes P1's node_addr] ← parent
[16 bytes P1's node_addr] ← parent
04 00 ← ancestry_count = 4
ANCESTRY[0] - Self (D):
[16 bytes D's node_addr]
05 00 00 00 00 00 00 00 ← D's sequence
C3 B2 A1 67 00 00 00 00 ← D's timestamp
[64 bytes D's signature]
ANCESTRY[1] - Parent (P1):
[16 bytes P1's node_addr]
0A 00 00 00 00 00 00 00 ← P1's sequence
00 B0 A1 67 00 00 00 00 ← P1's timestamp
[64 bytes P1's signature]
ANCESTRY[2] - Grandparent (P2):
[16 bytes P2's node_addr]
03 00 00 00 00 00 00 00 ← P2's sequence
00 A0 A1 67 00 00 00 00 ← P2's timestamp
[64 bytes P2's signature]
ANCESTRY[3] - Root:
[16 bytes Root's node_addr]
01 00 00 00 00 00 00 00 ← Root's sequence
00 90 A1 67 00 00 00 00 ← Root's timestamp
[64 bytes Root's signature]
[64 bytes D's outer signature] ← signs entire message
Total payload: 1 + 8 + 8 + 16 + 2 + (4 × 96) + 64 = 483 bytes
Total payload: 1 + 1 + 8 + 8 + 16 + 2 + (4 × 32) + 64 = 228 bytes
```
### A.2 FilterAnnounce (0x11)
+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);
}
}