mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-04 05:46:14 +00:00
parent_switched was incremented on the line immediately before parent_switches at every site and never independently, so the two counters were always identical. Drop parent_switched from TreeMetrics, its snapshot, TreeStatsSnapshot, the show_tree fixture, and the fipstop render, keeping parent_switches as the sole counter.
641 lines
28 KiB
Rust
641 lines
28 KiB
Rust
//! Spanning Tree Announce send/receive logic.
|
|
//!
|
|
//! Handles building, sending, and receiving TreeAnnounce messages,
|
|
//! including periodic root refresh and rate-limited propagation.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::NodeAddr;
|
|
use crate::protocol::TreeAnnounce;
|
|
|
|
use super::reject::TreeReject;
|
|
use super::{Node, NodeError};
|
|
use tracing::{debug, info, trace, warn};
|
|
|
|
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();
|
|
self.metrics().tree.rate_limited.inc();
|
|
debug!(
|
|
peer = %self.peer_display_name(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
|
|
if let Err(e) = self.send_encrypted_link_message(peer_addr, &encoded).await {
|
|
self.metrics().tree.send_failed.inc();
|
|
return Err(e);
|
|
}
|
|
|
|
self.metrics().tree.sent.inc();
|
|
|
|
// Record send time
|
|
if let Some(peer) = self.peers.get_mut(peer_addr) {
|
|
peer.record_tree_announce_sent(now_ms);
|
|
}
|
|
|
|
trace!(peer = %self.peer_display_name(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 = %self.peer_display_name(&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 = %self.peer_display_name(&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]) {
|
|
self.metrics().tree.received.inc();
|
|
|
|
let announce = match TreeAnnounce::decode(payload) {
|
|
Ok(a) => a,
|
|
Err(e) => {
|
|
self.metrics().tree.decode_error.inc();
|
|
debug!(from = %self.peer_display_name(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 => {
|
|
self.metrics().tree.unknown_peer.inc();
|
|
debug!(from = %self.peer_display_name(from), "TreeAnnounce from unknown peer");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// The declaring node_addr in the announce should match the sender
|
|
if announce.declaration.node_addr() != from {
|
|
self.metrics().tree.addr_mismatch.inc();
|
|
debug!(
|
|
from = %self.peer_display_name(from),
|
|
declared = %announce.declaration.node_addr(),
|
|
"TreeAnnounce node_addr mismatch"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if let Err(e) = announce.declaration.verify(&pubkey) {
|
|
self.metrics().tree.sig_failed.inc();
|
|
warn!(
|
|
from = %self.peer_display_name(from),
|
|
error = %e,
|
|
"TreeAnnounce signature verification failed"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if let Err(e) = announce.validate_semantics() {
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::AncestryInvalid);
|
|
warn!(
|
|
from = %self.peer_display_name(from),
|
|
error = %e,
|
|
"Rejected TreeAnnounce with invalid ancestry"
|
|
);
|
|
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 {
|
|
self.metrics().tree.stale.inc();
|
|
debug!(from = %self.peer_display_name(from), "TreeAnnounce not fresher than existing, ignored");
|
|
return;
|
|
}
|
|
|
|
self.metrics().tree.accepted.inc();
|
|
|
|
debug!(
|
|
from = %self.peer_display_name(from),
|
|
seq = announce.declaration.sequence(),
|
|
depth = announce.ancestry.depth(),
|
|
root = %announce.ancestry.root_id(),
|
|
"Processed TreeAnnounce"
|
|
);
|
|
|
|
// Re-push our current position when the announcing peer advertises a
|
|
// strictly worse (higher NodeAddr) root than ours. Root election is
|
|
// smallest-NodeAddr-wins, so a peer on a higher root has a stale or
|
|
// pre-attachment view and can attach to (or re-attach through) us;
|
|
// reply with our current declaration so it does so without waiting
|
|
// for the next periodic re-broadcast cadence.
|
|
//
|
|
// Only the better-rooted side echoes. If the peer's root is lower
|
|
// (better) than ours, WE are the stale side: the peer would ignore
|
|
// our worse root anyway, and we converge via the parent re-evaluation
|
|
// below, so echoing back is pure waste — and during a root change or
|
|
// partition merge it would double announce traffic in the learning
|
|
// direction. Equal roots are already converged. Restricting to `>`
|
|
// keeps the echo to the one direction that helps.
|
|
//
|
|
// This closes a convergence wedge on a single-uplink node: its only
|
|
// peer pushes the attaching announce once at promotion time, and if
|
|
// that datagram is lost the single-uplink node cannot self-correct
|
|
// (its own periodic parent re-evaluation is disabled below two peers)
|
|
// and is stranded as a self-root until the parent's next periodic
|
|
// re-broadcast (~reeval_interval_secs later). Echoing on root
|
|
// disagreement makes tree-position exchange self-healing on the
|
|
// receive path and is naturally bounded by the per-peer 500 ms
|
|
// tree-announce rate limiter, so it does not storm during normal
|
|
// convergence (it stops as soon as the peer adopts our root).
|
|
if *announce.ancestry.root_id() > *self.tree_state.root()
|
|
&& let Err(e) = self.send_tree_announce_to_peer(from).await
|
|
{
|
|
debug!(
|
|
peer = %self.peer_display_name(from),
|
|
error = %e,
|
|
"Failed to re-push TreeAnnounce on root disagreement"
|
|
);
|
|
}
|
|
|
|
// Bloom filter exchange initiation is handled at handshake completion
|
|
// ([handshake.rs] mark_update_needed on the new peer) and on actual
|
|
// content changes via [bloom.rs::handle_filter_announce]'s
|
|
// `mark_changed_peers`. Marking the peer on every received TreeAnnounce
|
|
// is redundant — and under high TreeAnnounce churn (rapid mid-chain
|
|
// swap propagation) it amplifies bloom traffic proportionally with
|
|
// the tree announce rate, even when the local outgoing filter
|
|
// content has not changed.
|
|
|
|
// Re-evaluate parent selection with current link costs.
|
|
// Exclude peers without MMP RTT data — they are not yet eligible
|
|
// as parent candidates (prevents oscillation from optimistic defaults).
|
|
let peer_costs: HashMap<NodeAddr, f64> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(_, peer)| peer.has_srtt())
|
|
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
|
.collect();
|
|
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
|
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);
|
|
|
|
// Clone identity up front to avoid a split borrow against the
|
|
// &mut self.tree_state / &mut self.coord_cache calls below (cold path).
|
|
let our_identity = self.identity().clone();
|
|
let flap_dampened = self.tree_state.set_parent(new_parent, new_seq, timestamp);
|
|
// recompute_coords may demote to self_root if the new path would be
|
|
// invalid; sign AFTER recompute so the signature covers the final
|
|
// declaration.
|
|
self.tree_state.recompute_coords();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign declaration after parent switch");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
return;
|
|
}
|
|
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
|
|
self.coord_cache
|
|
.invalidate_via_node(our_identity.node_addr());
|
|
self.reset_discovery_backoff();
|
|
|
|
self.metrics().tree.parent_switches.inc();
|
|
|
|
info!(
|
|
new_parent = %self.peer_display_name(&new_parent),
|
|
new_seq = new_seq,
|
|
new_root = %self.tree_state.root(),
|
|
depth = self.tree_state.my_coords().depth(),
|
|
"Parent switched, invalidated downstream coord cache entries, announcing to all peers"
|
|
);
|
|
if flap_dampened {
|
|
self.metrics().tree.flap_dampened.inc();
|
|
warn!("Flap dampening engaged: excessive parent switches detected");
|
|
}
|
|
|
|
self.send_tree_announce_to_all().await;
|
|
|
|
// Tree structure changed — trigger bloom filter exchange with all peers
|
|
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
|
self.bloom_state.mark_all_updates_needed(all_peers);
|
|
} else if !self.tree_state.is_root() && self.tree_state.should_be_root() {
|
|
// Self is the smallest visible NodeAddr — promote to root rather
|
|
// than continuing to advertise a stale ancestry rooted elsewhere.
|
|
// Clone identity up front to avoid a split borrow against the
|
|
// &mut self.tree_state / &mut self.coord_cache calls below (cold path).
|
|
let our_identity = self.identity().clone();
|
|
self.tree_state.become_root();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign self-root declaration");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
return;
|
|
}
|
|
// Surgical invalidation — see CoordCache::invalidate_other_roots doc.
|
|
self.coord_cache
|
|
.invalidate_other_roots(our_identity.node_addr());
|
|
self.reset_discovery_backoff();
|
|
self.metrics().tree.parent_switches.inc();
|
|
info!(
|
|
new_root = %self.tree_state.root(),
|
|
"Self-promoted to root: smallest visible NodeAddr"
|
|
);
|
|
self.send_tree_announce_to_all().await;
|
|
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
|
self.bloom_state.mark_all_updates_needed(all_peers);
|
|
} else if !self.tree_state.is_root()
|
|
&& *self.tree_state.my_declaration().parent_id() == *from
|
|
{
|
|
// Check for loop: if parent's ancestry now contains us, drop parent
|
|
if let Some(parent_coords) = self.tree_state.peer_coords(from)
|
|
&& parent_coords.contains(self.identity().node_addr())
|
|
{
|
|
self.metrics().tree.loop_detected.inc();
|
|
warn!(
|
|
parent = %self.peer_display_name(from),
|
|
"Parent ancestry contains us — loop detected, dropping parent"
|
|
);
|
|
let peer_costs: HashMap<NodeAddr, f64> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(_, peer)| peer.has_srtt())
|
|
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
|
.collect();
|
|
if self.tree_state.handle_parent_lost(&peer_costs) {
|
|
// Clone identity up front to avoid a split borrow against the
|
|
// &mut self.tree_state / &mut self.coord_cache calls below (cold path).
|
|
let our_identity = self.identity().clone();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign declaration after loop detection");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
return;
|
|
}
|
|
// handle_parent_lost may promote to root OR find new parent;
|
|
// cover both invalidation classes.
|
|
self.coord_cache
|
|
.invalidate_via_node(our_identity.node_addr());
|
|
self.coord_cache
|
|
.invalidate_other_roots(self.tree_state.root());
|
|
self.reset_discovery_backoff();
|
|
self.send_tree_announce_to_all().await;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Our parent's ancestry changed but we're keeping the same parent.
|
|
// Recompute our own coordinates (which derive from parent's ancestry)
|
|
// and re-announce so downstream nodes stay current.
|
|
//
|
|
// Compare the full address path (not just root + depth) so that a
|
|
// mid-chain ancestor swap also triggers re-announce. A reroute that
|
|
// replaces an interior ancestor without changing the root or the
|
|
// path length leaves both `root` and `depth` unchanged but still
|
|
// alters our coords; downstream peers must learn the new path or
|
|
// they will route into a phantom intermediate that no longer
|
|
// exists on our parent's tree.
|
|
let old_root = *self.tree_state.root();
|
|
let old_depth = self.tree_state.my_coords().depth();
|
|
let old_addrs: Vec<NodeAddr> =
|
|
self.tree_state.my_coords().node_addrs().copied().collect();
|
|
|
|
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);
|
|
|
|
// Clone identity up front to avoid a split borrow against the
|
|
// &mut self.tree_state / &mut self.coord_cache calls below (cold path).
|
|
let our_identity = self.identity().clone();
|
|
self.tree_state.set_parent(*from, new_seq, timestamp);
|
|
self.tree_state.recompute_coords();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign declaration after parent update");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
return;
|
|
}
|
|
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
|
|
self.coord_cache
|
|
.invalidate_via_node(our_identity.node_addr());
|
|
self.reset_discovery_backoff();
|
|
|
|
let new_addrs: Vec<NodeAddr> =
|
|
self.tree_state.my_coords().node_addrs().copied().collect();
|
|
|
|
if old_addrs != new_addrs {
|
|
self.metrics().tree.ancestry_changed.inc();
|
|
info!(
|
|
parent = %self.peer_display_name(from),
|
|
old_root = %old_root,
|
|
new_root = %self.tree_state.root(),
|
|
old_depth = old_depth,
|
|
new_depth = self.tree_state.my_coords().depth(),
|
|
"Parent ancestry changed, re-announcing"
|
|
);
|
|
self.send_tree_announce_to_all().await;
|
|
|
|
// Bloom contents do not depend on path structure, only on
|
|
// identity sets. Our parent_id is unchanged in this branch,
|
|
// so our tree-peer set is unchanged and our outgoing filter
|
|
// content is unchanged. Use mark_changed_peers, which
|
|
// checks for actual content delta against last_sent_filters,
|
|
// instead of mark_all_updates_needed, which marks
|
|
// unconditionally regardless of whether content changed.
|
|
let peer_addrs: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
|
let peer_filters = self.peer_inbound_filters();
|
|
self.bloom_state
|
|
.mark_changed_peers(from, &peer_addrs, &peer_filters);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Periodic tree maintenance, called from the tick handler.
|
|
///
|
|
/// Sends pending rate-limited announces and checks for periodic
|
|
/// parent re-evaluation based on current MMP link costs.
|
|
pub(super) async fn check_tree_state(&mut self) {
|
|
self.send_pending_tree_announces().await;
|
|
self.check_periodic_parent_reeval().await;
|
|
}
|
|
|
|
/// Periodic parent re-evaluation based on current MMP link costs.
|
|
///
|
|
/// Self-paces using `last_parent_reeval` and the configured
|
|
/// `reeval_interval_secs`. When a better parent is found, follows
|
|
/// the same switch flow as TreeAnnounce-triggered switches.
|
|
async fn check_periodic_parent_reeval(&mut self) {
|
|
let interval_secs = self.config().node.tree.reeval_interval_secs;
|
|
if interval_secs == 0 {
|
|
return;
|
|
}
|
|
|
|
// Need at least 2 peers for a meaningful comparison
|
|
if self.peers.len() < 2 {
|
|
return;
|
|
}
|
|
|
|
let now = std::time::Instant::now();
|
|
let interval = std::time::Duration::from_secs(interval_secs);
|
|
|
|
if let Some(last) = self.last_parent_reeval
|
|
&& now.duration_since(last) < interval
|
|
{
|
|
return;
|
|
}
|
|
|
|
self.last_parent_reeval = Some(now);
|
|
|
|
let peer_costs: HashMap<NodeAddr, f64> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(_, peer)| peer.has_srtt())
|
|
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
|
.collect();
|
|
|
|
if let Some(new_parent) = self.tree_state.evaluate_parent(&peer_costs) {
|
|
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);
|
|
|
|
// Clone identity up front to avoid a split borrow against the
|
|
// &mut self.tree_state / &mut self.coord_cache calls below (cold path).
|
|
let our_identity = self.identity().clone();
|
|
let flap_dampened = self.tree_state.set_parent(new_parent, new_seq, timestamp);
|
|
self.tree_state.recompute_coords();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign declaration after periodic parent re-eval");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
return;
|
|
}
|
|
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
|
|
self.coord_cache
|
|
.invalidate_via_node(our_identity.node_addr());
|
|
self.reset_discovery_backoff();
|
|
|
|
self.metrics().tree.parent_switches.inc();
|
|
|
|
info!(
|
|
new_parent = %self.peer_display_name(&new_parent),
|
|
new_seq = new_seq,
|
|
new_root = %self.tree_state.root(),
|
|
depth = self.tree_state.my_coords().depth(),
|
|
trigger = "periodic",
|
|
"Parent switched via periodic cost re-evaluation"
|
|
);
|
|
if flap_dampened {
|
|
self.metrics().tree.flap_dampened.inc();
|
|
warn!("Flap dampening engaged: excessive parent switches detected");
|
|
}
|
|
|
|
self.send_tree_announce_to_all().await;
|
|
|
|
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
|
self.bloom_state.mark_all_updates_needed(all_peers);
|
|
} else if !self.tree_state.is_root() && self.tree_state.should_be_root() {
|
|
// Clone identity up front to avoid a split borrow against the
|
|
// &mut self.tree_state / &mut self.coord_cache calls below (cold path).
|
|
let our_identity = self.identity().clone();
|
|
self.tree_state.become_root();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign self-root declaration in periodic reeval");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
return;
|
|
}
|
|
// Surgical invalidation — see CoordCache::invalidate_other_roots doc.
|
|
self.coord_cache
|
|
.invalidate_other_roots(our_identity.node_addr());
|
|
self.reset_discovery_backoff();
|
|
self.metrics().tree.parent_switches.inc();
|
|
info!(
|
|
new_root = %self.tree_state.root(),
|
|
trigger = "periodic",
|
|
"Self-promoted to root in periodic reeval: smallest visible NodeAddr"
|
|
);
|
|
self.send_tree_announce_to_all().await;
|
|
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
|
|
self.bloom_state.mark_all_updates_needed(all_peers);
|
|
} else {
|
|
// Periodic re-broadcast on no-change: makes TreeAnnounce
|
|
// distribution eventually-consistent. Receivers coalesce
|
|
// by sequence via ParentDeclaration::is_fresher_than and
|
|
// short-circuit at the `if !updated` gate in
|
|
// handle_tree_announce; the per-peer 500 ms rate-limiter
|
|
// never blocks at this 60 s cadence. Closes the cross-init
|
|
// in-flight loss recovery gap where the swap window can
|
|
// strand one side's announce on a session-index the other
|
|
// side cannot decrypt.
|
|
trace!(
|
|
seq = self.tree_state.my_declaration().sequence(),
|
|
root = %self.tree_state.root(),
|
|
"Periodic TreeAnnounce re-broadcast (no state change)"
|
|
);
|
|
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 {
|
|
self.metrics().tree.parent_losses.inc();
|
|
let peer_costs: HashMap<NodeAddr, f64> = self
|
|
.peers
|
|
.iter()
|
|
.filter(|(_, peer)| peer.has_srtt())
|
|
.map(|(addr, peer)| (*addr, peer.link_cost()))
|
|
.collect();
|
|
let changed = self.tree_state.handle_parent_lost(&peer_costs);
|
|
if changed {
|
|
// Re-sign the new declaration. Clone identity to avoid a split
|
|
// borrow against the &mut self.tree_state receiver (cold path).
|
|
let our_identity = self.identity().clone();
|
|
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
|
|
warn!(error = %e, "Failed to sign declaration after parent loss");
|
|
self.metrics()
|
|
.tree
|
|
.record_reject(TreeReject::OutboundSignFailed);
|
|
}
|
|
// handle_parent_lost may promote to root OR find new parent;
|
|
// cover both invalidation classes (same as the loop-detection
|
|
// branch above). Without this, cached downstream entries keep
|
|
// our now-stale coordinate prefix until TTL — and get_and_touch
|
|
// refreshes the TTL on every routing access, so an actively
|
|
// routed stale entry never self-expires.
|
|
self.coord_cache
|
|
.invalidate_via_node(our_identity.node_addr());
|
|
self.coord_cache
|
|
.invalidate_other_roots(self.tree_state.root());
|
|
info!(
|
|
new_root = %self.tree_state.root(),
|
|
is_root = self.tree_state.is_root(),
|
|
"Tree state updated after parent loss"
|
|
);
|
|
}
|
|
changed
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|