Skip to main content

fips_core/tree/
state.rs

1//! Local spanning tree state for a node.
2
3use std::collections::HashMap;
4use std::fmt;
5use std::time::Duration;
6
7use super::{CoordEntry, ParentDeclaration, TreeCoordinate, TreeError};
8use crate::time::{Instant, instant_now};
9use crate::{Identity, NodeAddr};
10
11/// Local spanning tree state for a node.
12///
13/// Contains this node's declaration, coordinates, and view of peers'
14/// tree positions. State is bounded by O(P × D) where P is peer count
15/// and D is tree depth.
16pub struct TreeState {
17    /// This node's NodeAddr.
18    my_node_addr: NodeAddr,
19    /// This node's current parent declaration.
20    my_declaration: ParentDeclaration,
21    /// This node's current coordinates (computed from declaration chain).
22    pub(super) my_coords: TreeCoordinate,
23    /// The current elected root (smallest reachable node_addr).
24    pub(super) root: NodeAddr,
25    /// Each peer's most recent parent declaration.
26    peer_declarations: HashMap<NodeAddr, ParentDeclaration>,
27    /// Each peer's full ancestry to root.
28    peer_ancestry: HashMap<NodeAddr, TreeCoordinate>,
29    /// Hysteresis factor for cost-based parent re-selection (0.0-1.0).
30    parent_hysteresis: f64,
31    /// Hold-down period after parent switch (0 = disabled).
32    hold_down: Duration,
33    /// Timestamp of last parent switch (for hold-down enforcement).
34    last_parent_switch: Option<Instant>,
35    /// Number of parent switches in current flap window.
36    flap_count: u32,
37    /// Start of the current flap counting window.
38    flap_window_start: Option<Instant>,
39    /// If dampened, suppressed until this instant.
40    flap_dampening_until: Option<Instant>,
41    /// Flap threshold: max switches before dampening engages.
42    flap_threshold: u32,
43    /// Flap window duration.
44    flap_window: Duration,
45    /// Dampening duration when threshold exceeded.
46    flap_dampening_duration: Duration,
47}
48
49impl TreeState {
50    /// Create initial tree state for a node (as root candidate).
51    ///
52    /// The node starts as its own root until it learns of a smaller node_addr.
53    /// Initial sequence is 1 per protocol spec; timestamp is current Unix time.
54    pub fn new(my_node_addr: NodeAddr) -> Self {
55        let timestamp = crate::time::now_secs();
56        let my_declaration = ParentDeclaration::self_root(my_node_addr, 1, timestamp);
57        let my_coords = TreeCoordinate::root_with_meta(my_node_addr, 1, timestamp);
58
59        Self {
60            my_node_addr,
61            my_declaration,
62            my_coords,
63            root: my_node_addr,
64            peer_declarations: HashMap::new(),
65            peer_ancestry: HashMap::new(),
66            parent_hysteresis: 0.0,
67            hold_down: Duration::ZERO,
68            last_parent_switch: None,
69            flap_count: 0,
70            flap_window_start: None,
71            flap_dampening_until: None,
72            flap_threshold: 4,
73            flap_window: Duration::from_secs(60),
74            flap_dampening_duration: Duration::from_secs(120),
75        }
76    }
77
78    /// Get this node's NodeAddr.
79    pub fn my_node_addr(&self) -> &NodeAddr {
80        &self.my_node_addr
81    }
82
83    /// Get this node's current declaration.
84    pub fn my_declaration(&self) -> &ParentDeclaration {
85        &self.my_declaration
86    }
87
88    /// Get this node's current coordinates.
89    pub fn my_coords(&self) -> &TreeCoordinate {
90        &self.my_coords
91    }
92
93    /// Get the current root.
94    pub fn root(&self) -> &NodeAddr {
95        &self.root
96    }
97
98    /// Check if this node is currently the root.
99    pub fn is_root(&self) -> bool {
100        self.root == self.my_node_addr
101    }
102
103    /// Get coordinates for a peer, if known.
104    pub fn peer_coords(&self, peer_id: &NodeAddr) -> Option<&TreeCoordinate> {
105        self.peer_ancestry.get(peer_id)
106    }
107
108    /// Get declaration for a peer, if known.
109    pub fn peer_declaration(&self, peer_id: &NodeAddr) -> Option<&ParentDeclaration> {
110        self.peer_declarations.get(peer_id)
111    }
112
113    /// Number of known peers.
114    pub fn peer_count(&self) -> usize {
115        self.peer_declarations.len()
116    }
117
118    /// Iterate over all peer node IDs.
119    pub fn peer_ids(&self) -> impl Iterator<Item = &NodeAddr> {
120        self.peer_declarations.keys()
121    }
122
123    /// Add or update a peer's tree state.
124    ///
125    /// Returns true if the state was updated (new or fresher declaration).
126    pub fn update_peer(
127        &mut self,
128        declaration: ParentDeclaration,
129        ancestry: TreeCoordinate,
130    ) -> bool {
131        let peer_id = *declaration.node_addr();
132
133        // Check if this is a fresh update
134        if let Some(existing) = self.peer_declarations.get(&peer_id)
135            && !declaration.is_fresher_than(existing)
136        {
137            return false;
138        }
139
140        self.peer_declarations.insert(peer_id, declaration);
141        self.peer_ancestry.insert(peer_id, ancestry);
142        true
143    }
144
145    /// Remove a peer from the tree state.
146    pub fn remove_peer(&mut self, peer_id: &NodeAddr) {
147        self.peer_declarations.remove(peer_id);
148        self.peer_ancestry.remove(peer_id);
149    }
150
151    /// Update this node's parent selection.
152    ///
153    /// Call this when switching parents. Updates the declaration and coordinates.
154    /// Returns true if flap dampening was just engaged due to this switch.
155    /// Only records a flap when the parent actually changes.
156    pub fn set_parent(&mut self, parent_id: NodeAddr, sequence: u64, timestamp: u64) -> bool {
157        let parent_changed = self.is_root() || *self.my_declaration.parent_id() != parent_id;
158        self.my_declaration =
159            ParentDeclaration::new(self.my_node_addr, parent_id, sequence, timestamp);
160        self.last_parent_switch = Some(instant_now());
161        // Record switch for flap detection only when parent actually changes;
162        // coordinates will be recomputed when ancestry is available
163        if parent_changed {
164            self.record_parent_switch()
165        } else {
166            false
167        }
168    }
169
170    /// Update this node's coordinates based on current parent's ancestry.
171    ///
172    /// Defensive: if extending the parent's ancestry would put `self` at the
173    /// minimum (because `self` is smaller than the parent's root), the
174    /// declaration is demoted to self-root in place. The caller is responsible
175    /// for re-signing the declaration after this call (do `set_parent → recompute_coords → sign_declaration`,
176    /// not `set_parent → sign_declaration → recompute_coords`).
177    pub fn recompute_coords(&mut self) {
178        if self.my_declaration.is_root() {
179            self.my_coords = TreeCoordinate::root_with_meta(
180                self.my_node_addr,
181                self.my_declaration.sequence(),
182                self.my_declaration.timestamp(),
183            );
184            self.root = self.my_node_addr;
185            return;
186        }
187
188        let parent_id = self.my_declaration.parent_id();
189        if let Some(parent_coords) = self.peer_ancestry.get(parent_id) {
190            let parent_root = *parent_coords.root_id();
191            if self.my_node_addr <= parent_root {
192                // Prepending self would put a smaller-or-equal node at depth 0,
193                // breaking the "advertised root = min path entry" invariant.
194                // Demote to self-root rather than emit a path peers will reject.
195                let seq = self.my_declaration.sequence();
196                let ts = self.my_declaration.timestamp();
197                self.my_declaration = ParentDeclaration::self_root(self.my_node_addr, seq, ts);
198                self.my_coords = TreeCoordinate::root_with_meta(self.my_node_addr, seq, ts);
199                self.root = self.my_node_addr;
200                return;
201            }
202            // Our coords = [self_entry] ++ parent_coords entries
203            let self_entry = CoordEntry::new(
204                self.my_node_addr,
205                self.my_declaration.sequence(),
206                self.my_declaration.timestamp(),
207            );
208            let mut entries = vec![self_entry];
209            entries.extend_from_slice(parent_coords.entries());
210            self.my_coords = TreeCoordinate::new(entries).expect("non-empty path");
211            self.root = *self.my_coords.root_id();
212        }
213    }
214
215    /// Smallest root_id visible across known peers.
216    pub fn smallest_visible_root(&self) -> Option<NodeAddr> {
217        self.peer_ancestry.values().map(|c| *c.root_id()).min()
218    }
219
220    /// Whether this node should be the tree root: either there are no peers,
221    /// or our NodeAddr is `<=` every visible root.
222    pub fn should_be_root(&self) -> bool {
223        match self.smallest_visible_root() {
224            Some(sr) => self.my_node_addr <= sr,
225            None => true,
226        }
227    }
228
229    /// Promote self to root with an incremented sequence number.
230    ///
231    /// Caller must `sign_declaration` afterwards before sending the result.
232    pub fn become_root(&mut self) {
233        let new_seq = self.my_declaration.sequence() + 1;
234        let timestamp = crate::time::now_secs();
235        self.my_declaration = ParentDeclaration::self_root(self.my_node_addr, new_seq, timestamp);
236        self.recompute_coords();
237    }
238
239    /// Calculate tree distance to a peer.
240    pub fn distance_to_peer(&self, peer_id: &NodeAddr) -> Option<usize> {
241        self.peer_ancestry
242            .get(peer_id)
243            .map(|coords| self.my_coords.distance_to(coords))
244    }
245
246    /// Find the best next hop toward a destination using greedy tree routing.
247    ///
248    /// Returns the peer that minimizes tree distance to the destination,
249    /// but only if that peer is strictly closer than we are (prevents
250    /// routing loops at local minima). Tie-breaks equal distance by
251    /// smallest node_addr.
252    ///
253    /// Returns `None` if:
254    /// - No peers have coordinates
255    /// - Destination is in a different tree (different root)
256    /// - No peer is closer to the destination than we are
257    pub fn find_next_hop(&self, dest_coords: &TreeCoordinate) -> Option<NodeAddr> {
258        if self.my_coords.root_id() != dest_coords.root_id() {
259            return None;
260        }
261
262        let my_distance = self.my_coords.distance_to(dest_coords);
263
264        let mut best: Option<(NodeAddr, usize)> = None;
265
266        for (peer_id, peer_coords) in &self.peer_ancestry {
267            let distance = peer_coords.distance_to(dest_coords);
268
269            let dominated = match &best {
270                None => true,
271                Some((best_id, best_dist)) => {
272                    distance < *best_dist || (distance == *best_dist && peer_id < best_id)
273                }
274            };
275
276            if dominated {
277                best = Some((*peer_id, distance));
278            }
279        }
280
281        match best {
282            Some((peer_id, distance)) if distance < my_distance => Some(peer_id),
283            _ => None,
284        }
285    }
286
287    /// Set the parent hysteresis factor (0.0-1.0).
288    pub fn set_parent_hysteresis(&mut self, hysteresis: f64) {
289        self.parent_hysteresis = hysteresis.clamp(0.0, 1.0);
290    }
291
292    /// Set the hold-down duration after parent switches.
293    pub fn set_hold_down(&mut self, secs: u64) {
294        self.hold_down = Duration::from_secs(secs);
295    }
296
297    /// Configure flap dampening parameters.
298    pub fn set_flap_dampening(&mut self, threshold: u32, window_secs: u64, dampening_secs: u64) {
299        self.flap_threshold = threshold;
300        self.flap_window = Duration::from_secs(window_secs);
301        self.flap_dampening_duration = Duration::from_secs(dampening_secs);
302    }
303
304    /// Record a parent switch for flap detection.
305    /// Returns true if dampening was just engaged.
306    pub fn record_parent_switch(&mut self) -> bool {
307        let now = instant_now();
308
309        // Reset window if expired or not started
310        match self.flap_window_start {
311            Some(start) if now.duration_since(start) < self.flap_window => {
312                self.flap_count += 1;
313            }
314            _ => {
315                self.flap_window_start = Some(now);
316                self.flap_count = 1;
317            }
318        }
319
320        // Check threshold
321        if self.flap_count >= self.flap_threshold && self.flap_dampening_until.is_none() {
322            self.flap_dampening_until = Some(now + self.flap_dampening_duration);
323            return true;
324        }
325        false
326    }
327
328    /// Check if flap dampening is currently active.
329    pub fn is_flap_dampened(&self) -> bool {
330        match self.flap_dampening_until {
331            Some(until) => instant_now() < until,
332            None => false,
333        }
334    }
335
336    /// Evaluate whether to switch parents based on current peer tree state.
337    ///
338    /// Uses effective_depth (depth + link_cost) for parent comparison.
339    /// `peer_costs` maps each peer's NodeAddr to its link cost (from local
340    /// MMP measurements). Missing entries default to 1.0 (optimistic).
341    ///
342    /// Returns `Some(peer_node_addr)` if a parent switch is recommended,
343    /// or `None` if the current parent is adequate.
344    pub fn evaluate_parent(&self, peer_costs: &HashMap<NodeAddr, f64>) -> Option<NodeAddr> {
345        if self.peer_ancestry.is_empty() {
346            return None;
347        }
348
349        // Find the smallest root visible across all peers
350        let mut smallest_root: Option<NodeAddr> = None;
351        for coords in self.peer_ancestry.values() {
352            let peer_root = coords.root_id();
353            smallest_root = Some(match smallest_root {
354                None => *peer_root,
355                Some(current) => {
356                    if *peer_root < current {
357                        *peer_root
358                    } else {
359                        current
360                    }
361                }
362            });
363        }
364
365        let smallest_root = smallest_root?;
366
367        // If our own NodeAddr is smaller than (or equal to) the smallest visible
368        // root, we are the network's smallest node and must be root. Returning
369        // `None` lets the caller promote us via `become_root` / `should_be_root`.
370        // Picking any peer here would produce an invalid path, since prepending
371        // `self` to that peer's ancestry would put `self` at depth 0 and the
372        // peer's larger root at the tail — violating "advertised root = min path
373        // entry" and getting rejected by recipients' `validate_semantics`.
374        if self.my_node_addr <= smallest_root {
375            return None;
376        }
377
378        // Among peers that reach the smallest root, find the lowest effective_depth.
379        // effective_depth(peer) = peer.depth + link_cost_to_peer
380        let mut best_peer: Option<(NodeAddr, f64)> = None; // (peer_addr, effective_depth)
381        for (peer_id, coords) in &self.peer_ancestry {
382            if *coords.root_id() != smallest_root {
383                continue;
384            }
385            // Reject candidates whose ancestry contains us (would create a loop)
386            if coords.contains(&self.my_node_addr) {
387                continue;
388            }
389            // If any peer has MMP cost data, only consider measured peers.
390            // This prevents freshly connected peers (no SRTT, default cost 1.0)
391            // from appearing artificially cheap. During cold start (no peer has
392            // MMP data, peer_costs is empty), fall back to default cost 1.0.
393            let cost = match peer_costs.get(peer_id) {
394                Some(&c) => c,
395                None if peer_costs.is_empty() => 1.0,
396                None => continue,
397            };
398            let eff_depth = coords.depth() as f64 + cost;
399            match &best_peer {
400                None => best_peer = Some((*peer_id, eff_depth)),
401                Some((best_id, best_eff)) => {
402                    if eff_depth < *best_eff || (eff_depth == *best_eff && peer_id < best_id) {
403                        best_peer = Some((*peer_id, eff_depth));
404                    }
405                }
406            }
407        }
408
409        let (best_peer_id, best_eff_depth) = best_peer?;
410
411        // If already using this peer as parent, no switch needed
412        if *self.my_declaration.parent_id() == best_peer_id && !self.is_root() {
413            return None;
414        }
415
416        // --- Mandatory switches (bypass hold-down and hysteresis) ---
417
418        // If our current parent is gone from peer_ancestry, our path is broken — always switch
419        if !self.is_root()
420            && !self
421                .peer_ancestry
422                .contains_key(self.my_declaration.parent_id())
423        {
424            return Some(best_peer_id);
425        }
426
427        // Switching roots (smaller root found) → always switch
428        if smallest_root < self.root || (self.is_root() && smallest_root < self.my_node_addr) {
429            return Some(best_peer_id);
430        }
431
432        // We're root but shouldn't be (peers have a smaller root) — always switch
433        if self.is_root() {
434            return Some(best_peer_id);
435        }
436
437        // --- Hold-down: suppress non-mandatory re-evaluation after recent switch ---
438
439        if !self.hold_down.is_zero()
440            && self
441                .last_parent_switch
442                .is_some_and(|last| last.elapsed() < self.hold_down)
443        {
444            return None;
445        }
446
447        // --- Flap dampening: suppress after excessive parent switches ---
448
449        if self.is_flap_dampened() {
450            return None;
451        }
452
453        // --- Same root, cost-aware comparison with hysteresis ---
454
455        // Current parent's effective_depth.
456        // If peer_costs is non-empty but current parent has no entry,
457        // treat as maximally expensive so any measured candidate can win.
458        // If peer_costs is empty (cold start), use default cost 1.0.
459        let current_parent_cost = peer_costs
460            .get(self.my_declaration.parent_id())
461            .copied()
462            .unwrap_or(if peer_costs.is_empty() {
463                1.0
464            } else {
465                f64::INFINITY
466            });
467        let current_parent_coords = self.peer_ancestry.get(self.my_declaration.parent_id());
468        let current_parent_eff = match current_parent_coords {
469            Some(coords) => coords.depth() as f64 + current_parent_cost,
470            None => return Some(best_peer_id), // Parent has no coords — treat as lost
471        };
472
473        // Apply hysteresis: only switch if candidate is significantly better
474        if best_eff_depth < current_parent_eff * (1.0 - self.parent_hysteresis) {
475            return Some(best_peer_id);
476        }
477
478        None
479    }
480
481    /// Handle loss of current parent.
482    ///
483    /// Tries to find an alternative parent among remaining peers.
484    /// If none available, becomes its own root (increments sequence).
485    ///
486    /// Returns `true` if the tree state changed (caller should re-announce).
487    pub fn handle_parent_lost(&mut self, peer_costs: &HashMap<NodeAddr, f64>) -> bool {
488        // Try to find an alternative parent
489        if let Some(new_parent) = self.evaluate_parent(peer_costs) {
490            let timestamp = crate::time::now_secs();
491            let new_seq = self.my_declaration.sequence() + 1;
492            self.set_parent(new_parent, new_seq, timestamp);
493            self.recompute_coords();
494            return true;
495        }
496
497        // No alternative: become own root
498        let timestamp = crate::time::now_secs();
499        let new_seq = self.my_declaration.sequence() + 1;
500        self.my_declaration = ParentDeclaration::self_root(self.my_node_addr, new_seq, timestamp);
501        self.recompute_coords();
502        true
503    }
504
505    /// Sign this node's declaration with the given identity.
506    ///
507    /// The identity's node_addr must match this TreeState's node_addr.
508    pub fn sign_declaration(&mut self, identity: &Identity) -> Result<(), TreeError> {
509        self.my_declaration.sign(identity)
510    }
511
512    /// Check if this node's declaration is signed.
513    pub fn is_declaration_signed(&self) -> bool {
514        self.my_declaration.is_signed()
515    }
516}
517
518impl fmt::Debug for TreeState {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        f.debug_struct("TreeState")
521            .field("my_node_addr", &self.my_node_addr)
522            .field("root", &self.root)
523            .field("is_root", &self.is_root())
524            .field("depth", &self.my_coords.depth())
525            .field("peers", &self.peer_count())
526            .finish()
527    }
528}