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
+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]