Implement cost-based parent selection with periodic re-evaluation

Cost-based parent selection:
- Replace depth-only parent selection with effective_depth = depth + link_cost
- link_cost computed from locally measured MMP metrics: etx * (1.0 + srtt_ms / 100.0)
- Prevents bottleneck subtrees in heterogeneous networks where a LoRa link
  at depth 1 would otherwise always beat fiber at depth 2
- Configurable hysteresis (default 0.2) prevents marginal parent switches
- Configurable hold-down timer (default 30s) suppresses re-evaluation
  after parent switch
- Mandatory switches (parent lost, root change) bypass both safeguards
- Link costs passed as HashMap parameter to keep TreeState pure

Periodic re-evaluation:
- evaluate_parent() was only called on TreeAnnounce receipt or parent loss;
  after tree stabilization, link degradation went undetected
- Added timer-based re-evaluation (reeval_interval_secs, default 60s) that
  calls evaluate_parent() from the tick handler with current MMP link costs
- Respects existing hold-down and hysteresis safeguards
- Short-circuits when disabled or <2 peers

Design documentation:
- Update 7 design docs to reflect cost-based parent selection
- Replace depth-only algorithm descriptions with effective_depth model
- Replace rejected cumulative path cost spec with local-only design rationale
- Rewrite Example 2 (heterogeneous links) for local-only cost model
- Update config docs: parent_switch_threshold replaced by parent_hysteresis,
  hold_down_secs, reeval_interval_secs

Chaos simulation enhancements:
- fips_overrides with deep merge for per-scenario FIPS config customization
- Explicit topology algorithm for deterministic test graphs
- Control socket querying via fipsctl for tree/MMP snapshot collection
- Edge existence validation in netem manager
- Per-link netem policy overrides
- 9 new chaos scenarios covering cost avoidance, depth-vs-cost tradeoffs,
  stability, mixed topologies, periodic re-evaluation, and bottleneck parent

12 new unit tests, 667 total passing, clippy clean.
This commit is contained in:
Johnathan Corgan
2026-02-23 17:15:20 +00:00
parent 717be3d960
commit 0d93a19e07
26 changed files with 1675 additions and 200 deletions
+10 -2
View File
@@ -337,6 +337,10 @@ pub struct Node {
/// are exhausted.
retry_pending: HashMap<NodeAddr, retry::RetryState>,
// === Periodic Parent Re-evaluation ===
/// Timestamp of last periodic parent re-evaluation (for pacing).
last_parent_reeval: Option<std::time::Instant>,
// === Display Names ===
/// Human-readable names for configured peers (alias or short npub).
/// Populated at startup from peer config.
@@ -368,7 +372,8 @@ impl Node {
// Initialize tree state with signed self-declaration
let mut tree_state = TreeState::new(node_addr);
tree_state.set_parent_switch_threshold(config.node.tree.parent_switch_threshold);
tree_state.set_parent_hysteresis(config.node.tree.parent_hysteresis);
tree_state.set_hold_down(config.node.tree.hold_down_secs);
tree_state
.sign_declaration(&identity)
.expect("signing own declaration should never fail");
@@ -432,6 +437,7 @@ impl Node {
std::time::Duration::from_millis(coords_response_interval_ms),
),
retry_pending: HashMap::new(),
last_parent_reeval: None,
peer_aliases: HashMap::new(),
})
}
@@ -451,7 +457,8 @@ impl Node {
// Initialize tree state with signed self-declaration
let mut tree_state = TreeState::new(node_addr);
tree_state.set_parent_switch_threshold(config.node.tree.parent_switch_threshold);
tree_state.set_parent_hysteresis(config.node.tree.parent_hysteresis);
tree_state.set_hold_down(config.node.tree.hold_down_secs);
tree_state
.sign_declaration(&identity)
.expect("signing own declaration should never fail");
@@ -518,6 +525,7 @@ impl Node {
std::time::Duration::from_millis(coords_response_interval_ms),
),
retry_pending: HashMap::new(),
last_parent_reeval: None,
peer_aliases: HashMap::new(),
}
}