Files
fips/src/proto/stp/state.rs
T

709 lines
29 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Local spanning tree state for a node.
use alloc::collections::{BTreeMap, BTreeSet};
use core::fmt;
use super::core::ParentEval;
use super::limits::FlapDampener;
use super::{CoordEntry, ParentDeclaration, TreeCoordinate};
use crate::NodeAddr;
use crate::proto::mmp::delivery::{Acks, LinkEvidence, ResendReason};
/// What a parent-loss recovery did: whether the tree state changed, and
/// whether the recovery switch was the one that armed a dampening episode.
///
/// Crate-internal on purpose. The published entry point is
/// [`TreeState::handle_parent_lost`], whose `bool` return this type must not
/// displace.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ParentLoss {
/// Whether the tree state changed and the caller should re-announce.
pub(crate) changed: bool,
/// Whether the recovery switch armed a flap dampening episode.
pub(crate) dampened: bool,
}
/// Local spanning tree state for a node.
///
/// Contains this node's declaration, coordinates, and view of peers'
/// tree positions. State is bounded by O(P × D) where P is peer count
/// and D is tree depth.
pub struct TreeState {
/// This node's NodeAddr.
my_node_addr: NodeAddr,
/// This node's current parent declaration.
my_declaration: ParentDeclaration,
/// This node's current coordinates (computed from declaration chain).
pub(super) my_coords: TreeCoordinate,
/// The current elected root (smallest reachable node_addr).
pub(super) root: NodeAddr,
/// Each peer's most recent parent declaration.
peer_declarations: BTreeMap<NodeAddr, ParentDeclaration>,
/// Each peer's full ancestry to root.
peer_ancestry: BTreeMap<NodeAddr, TreeCoordinate>,
/// Per-peer delivery tracking for sent TreeAnnounces.
acks: Acks,
/// The declaration sequence last recorded as sent to each peer.
announced: BTreeMap<NodeAddr, u64>,
/// Hysteresis factor for cost-based parent re-selection (0.0-1.0).
parent_hysteresis: f64,
/// Flap-dampening / hold-down state machine.
flap: FlapDampener,
/// Whether this node is a Leaf-profile node.
///
/// A Leaf holds a single upstream Full peer, sends no tree announces, and
/// must never self-elect as tree root: peers refuse a non-Full node as a
/// parent (`non_full_peers()` skip), so a Leaf that self-elected would form
/// an isolated second root and partition the mesh. When `true`, the node is
/// excluded from root self-election and attaches under its upstream instead,
/// holding that subtree's coordinate for its own routing (never announced, so
/// the coordinate's `self < root` is safe). Defaults to `false`
/// (tree-participating); the shell sets it from the node profile. NonRouting
/// nodes keep `false` — they announce, so the same relaxation would emit a
/// wire-invalid coordinate; a global-min NonRouting node is a separate open
/// problem for the leaf/non-routing tree-participation model.
self_is_leaf: bool,
}
impl TreeState {
/// Create initial tree state for a node (as root candidate).
///
/// The node starts as its own root until it learns of a smaller node_addr.
/// Initial sequence is 1 per protocol spec; `now_secs` is the injected
/// wall-clock Unix time in seconds stamped onto the initial declaration.
pub fn new(my_node_addr: NodeAddr, now_secs: u64) -> Self {
let timestamp = now_secs;
let my_declaration = ParentDeclaration::self_root(my_node_addr, 1, timestamp);
let my_coords = TreeCoordinate::root_with_meta(my_node_addr, 1, timestamp);
Self {
my_node_addr,
my_declaration,
my_coords,
root: my_node_addr,
peer_declarations: BTreeMap::new(),
peer_ancestry: BTreeMap::new(),
acks: Acks::new(),
announced: BTreeMap::new(),
parent_hysteresis: 0.0,
flap: FlapDampener::new(),
self_is_leaf: false,
}
}
/// Get this node's NodeAddr.
pub fn my_node_addr(&self) -> &NodeAddr {
&self.my_node_addr
}
/// Get this node's current declaration.
pub fn my_declaration(&self) -> &ParentDeclaration {
&self.my_declaration
}
/// Get this node's current coordinates.
pub fn my_coords(&self) -> &TreeCoordinate {
&self.my_coords
}
/// Test-only override of this node's coordinates, bypassing the
/// parent/declaration state machine. Lets routing tests place the node at
/// an arbitrary tree position to exercise coordinate-based classification.
#[cfg(test)]
pub(crate) fn set_my_coords_for_test(&mut self, coords: TreeCoordinate) {
self.root = *coords.root_id();
self.my_coords = coords;
}
/// Get the current root.
pub fn root(&self) -> &NodeAddr {
&self.root
}
/// Check if this node is currently the root.
pub fn is_root(&self) -> bool {
self.root == self.my_node_addr
}
/// Get coordinates for a peer, if known.
pub fn peer_coords(&self, peer_id: &NodeAddr) -> Option<&TreeCoordinate> {
self.peer_ancestry.get(peer_id)
}
/// Get declaration for a peer, if known.
pub fn peer_declaration(&self, peer_id: &NodeAddr) -> Option<&ParentDeclaration> {
self.peer_declarations.get(peer_id)
}
/// Number of known peers.
pub fn peer_count(&self) -> usize {
self.peer_declarations.len()
}
/// Iterate over all peer node IDs.
pub fn peer_ids(&self) -> impl Iterator<Item = &NodeAddr> {
self.peer_declarations.keys()
}
/// Add or update a peer's tree state.
///
/// Returns true if the state was updated (new or fresher declaration).
pub fn update_peer(
&mut self,
declaration: ParentDeclaration,
ancestry: TreeCoordinate,
) -> bool {
let peer_id = *declaration.node_addr();
// Check if this is a fresh update
if let Some(existing) = self.peer_declarations.get(&peer_id)
&& !declaration.is_fresher_than(existing)
{
return false;
}
self.peer_declarations.insert(peer_id, declaration);
self.peer_ancestry.insert(peer_id, ancestry);
true
}
/// Remove a peer from the tree state.
pub fn remove_peer(&mut self, peer_id: &NodeAddr) {
self.peer_declarations.remove(peer_id);
self.peer_ancestry.remove(peer_id);
self.acks.remove(peer_id);
self.announced.remove(peer_id);
}
/// Record a TreeAnnounce the transport accepted for `peer`, carrying our
/// declaration sequence `seq` and sent with link counter `counter`, so it
/// stays outstanding until the peer's receiver reports show it arrived.
///
/// The transport accepting a frame is not delivery. A lost announce would
/// leave the peer on our old tree position until the next announce, which
/// on a node with one peer may never come. The announce's content changes
/// only with the declaration sequence, so a sequence other than the one
/// last recorded for `peer` starts a new lineage with fresh resend
/// budgets, and a resend or periodic re-broadcast of the same declaration
/// spends from its lineage's budget.
pub fn record_announce(
&mut self,
peer: NodeAddr,
seq: u64,
counter: u64,
link: &LinkEvidence,
now_ms: u64,
) {
let fresh = self.announced.insert(peer, seq) != Some(seq);
self.acks.record(peer, fresh, counter, link, now_ms);
}
/// Decide whether the outstanding TreeAnnounce to `peer` must be resent,
/// by the shared receiver-report rule ([`Acks::check`]).
///
/// Marks nothing: on `Some`, the caller marks the peer's announce pending
/// so the ordinary send path delivers the current declaration.
pub fn check_announce(
&mut self,
peer: &NodeAddr,
link: &LinkEvidence,
now_ms: u64,
) -> Option<ResendReason> {
self.acks.check(peer, link, now_ms)
}
/// Whether a TreeAnnounce to `peer` is still awaiting confirmation.
pub fn announce_outstanding(&self, peer: &NodeAddr) -> bool {
self.outstanding_counter(peer).is_some()
}
/// The link counter of the TreeAnnounce to `peer` awaiting confirmation.
pub fn outstanding_counter(&self, peer: &NodeAddr) -> Option<u64> {
self.acks.outstanding(peer)
}
/// The declaration sequence of the TreeAnnounce to `peer` awaiting
/// confirmation.
pub fn outstanding_seq(&self, peer: &NodeAddr) -> Option<u64> {
self.outstanding_counter(peer)?;
self.announced.get(peer).copied()
}
/// Set how long a TreeAnnounce the receiver reports cannot check waits
/// before its fallback resend. Defaults to
/// [`FALLBACK_MS`](crate::proto::mmp::delivery::FALLBACK_MS).
pub fn set_fallback(&mut self, ms: u64) {
self.acks.set_fallback(ms);
}
/// Update this node's parent selection.
///
/// Call this when switching parents. Updates the declaration and coordinates.
/// Returns true if flap dampening was just engaged due to this switch.
/// Only records a flap when the parent actually changes.
///
/// `timestamp` is the escaping wall-clock Unix seconds stamped onto the new
/// declaration; `now_ms` is the monotonic milliseconds driving the
/// flap-dampening timers (the two clock bases must not be crossed).
pub fn set_parent(
&mut self,
parent_id: NodeAddr,
sequence: u64,
timestamp: u64,
now_ms: u64,
) -> bool {
let parent_changed = self.is_root() || *self.my_declaration.parent_id() != parent_id;
self.my_declaration =
ParentDeclaration::new(self.my_node_addr, parent_id, sequence, timestamp);
self.flap.mark_switch(now_ms);
// Record switch for flap detection only when parent actually changes;
// coordinates will be recomputed when ancestry is available
if parent_changed {
self.flap.record_parent_switch(now_ms)
} else {
false
}
}
/// Update this node's coordinates based on current parent's ancestry.
///
/// Defensive: if extending the parent's ancestry would put `self` at the
/// minimum (because `self` is smaller than the parent's root), the
/// declaration is demoted to self-root in place. The caller is responsible
/// for re-signing the declaration after this call (do `set_parent → recompute_coords → sign_declaration`,
/// not `set_parent → sign_declaration → recompute_coords`).
pub fn recompute_coords(&mut self) {
if self.my_declaration.is_root() {
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) {
let parent_root = *parent_coords.root_id();
if !self.self_is_leaf && self.my_node_addr <= parent_root {
// Prepending self would put a smaller-or-equal node at depth 0,
// breaking the "advertised root = min path entry" invariant.
// Demote to self-root rather than emit a path peers will reject.
//
// A Leaf is exempt: it keeps the `[self, parent, …, root]`
// coordinate even when `self <= parent_root`, so it lives as a
// depth-N member of its upstream's tree rather than demoting to
// an isolated self-root (which would partition the mesh). Safe
// because a Leaf never announces this coordinate; it reaches
// peers only via session-carried coords, which are not subject
// to the root-min `validate_semantics`.
let seq = self.my_declaration.sequence();
let ts = self.my_declaration.timestamp();
self.my_declaration = ParentDeclaration::self_root(self.my_node_addr, seq, ts);
self.my_coords = TreeCoordinate::root_with_meta(self.my_node_addr, seq, ts);
self.root = self.my_node_addr;
return;
}
// 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();
}
}
/// Smallest root_id visible across known peers.
pub fn smallest_visible_root(&self) -> Option<NodeAddr> {
self.peer_ancestry.values().map(|c| *c.root_id()).min()
}
/// Whether this node should be the tree root: either there are no peers,
/// or our NodeAddr is `<=` every visible root.
///
/// A Leaf never self-elects as root while it has a peer to attach under: it
/// cannot forward transit, peers refuse it as a parent, and a Leaf-root
/// would partition the mesh. An isolated Leaf (no visible root) is still its
/// own root, which is harmless.
pub fn should_be_root(&self) -> bool {
match self.smallest_visible_root() {
Some(sr) => !self.self_is_leaf && self.my_node_addr <= sr,
None => true,
}
}
/// Promote self to root with an incremented sequence number.
///
/// `now_secs` is the injected wall-clock Unix time in seconds stamped onto
/// the new self-root declaration. Caller must `sign_declaration` afterwards
/// before sending the result.
pub fn become_root(&mut self, now_secs: u64) {
let new_seq = self.my_declaration.sequence() + 1;
let timestamp = now_secs;
self.my_declaration = ParentDeclaration::self_root(self.my_node_addr, new_seq, timestamp);
self.recompute_coords();
}
/// Calculate tree distance to a peer.
pub fn distance_to_peer(&self, peer_id: &NodeAddr) -> Option<usize> {
self.peer_ancestry
.get(peer_id)
.map(|coords| self.my_coords.distance_to(coords))
}
/// Find the best next hop toward a destination using greedy tree routing.
///
/// Returns the peer that minimizes tree distance to the destination,
/// but only if that peer is strictly closer than we are (prevents
/// routing loops at local minima). Tie-breaks equal distance by
/// smallest node_addr.
///
/// Returns `None` if:
/// - No peers have coordinates
/// - Destination is in a different tree (different root)
/// - No peer is closer to the destination than we are
///
/// `skip_peers` contains peers that should not be used as transit
/// (e.g., non-routing and leaf nodes).
pub fn find_next_hop(
&self,
dest_coords: &TreeCoordinate,
skip_peers: &BTreeSet<NodeAddr>,
) -> Option<NodeAddr> {
if self.my_coords.root_id() != dest_coords.root_id() {
return None;
}
let my_distance = self.my_coords.distance_to(dest_coords);
let mut best: Option<(NodeAddr, usize)> = None;
for (peer_id, peer_coords) in &self.peer_ancestry {
if skip_peers.contains(peer_id) {
continue;
}
let distance = peer_coords.distance_to(dest_coords);
let dominated = match &best {
None => true,
Some((best_id, best_dist)) => {
distance < *best_dist || (distance == *best_dist && peer_id < best_id)
}
};
if dominated {
best = Some((*peer_id, distance));
}
}
match best {
Some((peer_id, distance)) if distance < my_distance => Some(peer_id),
_ => None,
}
}
/// Mark this node as a Leaf (or not), gating it out of root self-election.
///
/// A Leaf attaches under its upstream Full peer and never becomes root; see
/// the `self_is_leaf` field docs. Set from the node profile at construction.
pub fn set_self_is_leaf(&mut self, is_leaf: bool) {
self.self_is_leaf = is_leaf;
}
/// Set the parent hysteresis factor (0.0-1.0).
pub fn set_parent_hysteresis(&mut self, hysteresis: f64) {
self.parent_hysteresis = hysteresis.clamp(0.0, 1.0);
}
/// Set the hold-down duration after parent switches.
pub fn set_hold_down(&mut self, secs: u64) {
self.flap.set_hold_down(secs);
}
/// Configure flap dampening parameters.
pub fn set_flap_dampening(&mut self, threshold: u32, window_secs: u64, dampening_secs: u64) {
self.flap
.set_flap_dampening(threshold, window_secs, dampening_secs);
}
/// How long a dampening episode suppresses discretionary parent switching,
/// after the configured value is clamped.
///
/// Crate-internal: it feeds a log field on the engagement warning.
pub(crate) fn dampening_secs(&self) -> u64 {
self.flap.dampening_secs()
}
/// Check if flap dampening is currently active. `now_ms` is the injected
/// monotonic time in milliseconds.
pub fn is_flap_dampened(&self, now_ms: u64) -> bool {
self.flap.is_flap_dampened(now_ms)
}
/// Whether a *discretionary* parent switch should currently be suppressed by the
/// flap-dampening / hold-down veto. `now_ms` is the injected monotonic time.
/// Mandatory switches ignore this. Read at the shell edge; the classify core is
/// clock-free and takes the resulting bool.
pub fn is_switch_suppressed(&self, now_ms: u64) -> bool {
self.flap.is_hold_down_active(now_ms) || self.flap.is_flap_dampened(now_ms)
}
/// Evaluate whether to switch parents based on current peer tree state.
///
/// Uses effective_depth (depth + link_cost) for parent comparison.
/// `peer_costs` maps each peer's NodeAddr to its link cost (from local
/// MMP measurements). Missing entries default to 1.0 (optimistic).
///
/// Returns a [`ParentEval`] describing whether a parent switch is warranted:
/// `Mandatory` (path-breaking / root-correcting — always taken), `Discretionary`
/// (an improvement the caller applies only if its veto is inactive), or `None`.
///
/// This core is clock-free: it no longer applies the flap-dampening / hold-down
/// veto. The caller reads the clock, computes the veto verdict via
/// [`is_switch_suppressed`](Self::is_switch_suppressed), and suppresses the
/// `Discretionary` arm at the edge; `Mandatory` switches bypass the veto.
///
/// `skip_peers` contains peers that should not be considered as parent
/// candidates (e.g., non-routing and leaf nodes that don't forward transit).
pub(crate) fn evaluate_parent(
&self,
peer_costs: &BTreeMap<NodeAddr, f64>,
skip_peers: &BTreeSet<NodeAddr>,
) -> ParentEval {
if self.peer_ancestry.is_empty() {
return ParentEval::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 ParentEval::None,
};
// If our own NodeAddr is smaller than (or equal to) the smallest visible
// root, we are the network's smallest node and must be root. Returning
// `None` lets the caller promote us via `become_root` / `should_be_root`.
// Picking any peer here would produce an invalid path, since prepending
// `self` to that peer's ancestry would put `self` at depth 0 and the
// peer's larger root at the tail — violating "advertised root = min path
// entry" and getting rejected by recipients' `validate_semantics`.
//
// A Leaf is exempt: it must not self-elect as root (peers refuse it as a
// parent, so it would partition the mesh). It falls through to select its
// upstream Full peer as parent. The resulting
// `self < root` coordinate is invalid on the wire, but a Leaf never
// announces it (`send_tree_announce_to_peer` is Leaf-gated); it is used
// only for the Leaf's own routing and propagates to peers via the
// coords carried on its session frames, which are not root-validated.
if !self.self_is_leaf && self.my_node_addr <= smallest_root {
return ParentEval::None;
}
// Among peers that reach the smallest root, find the lowest effective_depth.
// effective_depth(peer) = peer.depth + link_cost_to_peer
let mut best_peer: Option<(NodeAddr, f64)> = None; // (peer_addr, effective_depth)
for (peer_id, coords) in &self.peer_ancestry {
if *coords.root_id() != smallest_root {
continue;
}
// Skip non-routing/leaf peers (can't forward transit)
if skip_peers.contains(peer_id) {
continue;
}
// Reject candidates whose ancestry contains us (would create a loop)
if coords.contains(&self.my_node_addr) {
continue;
}
// If any peer has MMP cost data, only consider measured peers.
// This prevents freshly connected peers (no SRTT, default cost 1.0)
// from appearing artificially cheap. During cold start (no peer has
// MMP data, peer_costs is empty), fall back to default cost 1.0.
let cost = match peer_costs.get(peer_id) {
Some(&c) => c,
None if peer_costs.is_empty() => 1.0,
None => continue,
};
let eff_depth = coords.depth() as f64 + cost;
match &best_peer {
None => best_peer = Some((*peer_id, eff_depth)),
Some((best_id, best_eff)) => {
if eff_depth < *best_eff || (eff_depth == *best_eff && peer_id < best_id) {
best_peer = Some((*peer_id, eff_depth));
}
}
}
}
let (best_peer_id, best_eff_depth) = match best_peer {
Some(b) => b,
None => return ParentEval::None,
};
// If already using this peer as parent, no switch needed
if *self.my_declaration.parent_id() == best_peer_id && !self.is_root() {
return ParentEval::None;
}
// --- Mandatory switches (bypass the shell's hold-down / flap veto) ---
// 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 ParentEval::Mandatory(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 ParentEval::Mandatory(best_peer_id);
}
// We're root but shouldn't be (peers have a smaller root) — always switch
if self.is_root() {
return ParentEval::Mandatory(best_peer_id);
}
// --- Discretionary switches (the caller applies the veto before taking) ---
//
// Same root, cost-aware comparison with hysteresis. Everything below is
// veto-gated at the edge: the shell suppresses these `Discretionary` results
// while hold-down / flap-dampening is active.
// Current parent's effective_depth.
// If peer_costs is non-empty but current parent has no entry,
// treat as maximally expensive so any measured candidate can win.
// If peer_costs is empty (cold start), use default cost 1.0.
let current_parent_cost = peer_costs
.get(self.my_declaration.parent_id())
.copied()
.unwrap_or(if peer_costs.is_empty() {
1.0
} else {
f64::INFINITY
});
let current_parent_coords = self.peer_ancestry.get(self.my_declaration.parent_id());
let current_parent_eff = match current_parent_coords {
Some(coords) => coords.depth() as f64 + current_parent_cost,
// Parent has no coords — treat as lost. This sat BELOW the veto, so it
// is veto-gated: Discretionary, not Mandatory.
None => return ParentEval::Discretionary(best_peer_id),
};
// Apply hysteresis: only switch if candidate is significantly better
if best_eff_depth < current_parent_eff * (1.0 - self.parent_hysteresis) {
return ParentEval::Discretionary(best_peer_id);
}
ParentEval::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).
///
/// `now_secs` is the injected wall-clock Unix seconds stamped onto the new
/// declaration; `now_ms` is the monotonic milliseconds driving the parent
/// re-evaluation's flap timers (the two clock bases must not be crossed).
pub fn handle_parent_lost(
&mut self,
peer_costs: &BTreeMap<NodeAddr, f64>,
now_secs: u64,
now_ms: u64,
) -> bool {
self.recover(peer_costs, now_secs, now_ms).changed
}
/// Handle loss of current parent, reporting whether the recovery switch
/// itself armed a flap dampening episode.
///
/// Same recovery as [`TreeState::handle_parent_lost`], which delegates
/// here. A caller holding a metrics handle uses this one so the
/// engagement can be counted and logged; the published signature stays
/// `bool`.
pub(crate) fn recover(
&mut self,
peer_costs: &BTreeMap<NodeAddr, f64>,
now_secs: u64,
now_ms: u64,
) -> ParentLoss {
// Try to find an alternative parent. The veto is computed at the edge and
// applied only to a discretionary result; a mandatory switch bypasses it.
let suppressed = self.is_switch_suppressed(now_ms);
let alt = match self.evaluate_parent(peer_costs, &BTreeSet::new()) {
ParentEval::Mandatory(p) => Some(p),
ParentEval::Discretionary(p) if !suppressed => Some(p),
ParentEval::Discretionary(_) | ParentEval::None => None,
};
if let Some(new_parent) = alt {
let new_seq = self.my_declaration.sequence() + 1;
let dampened = self.set_parent(new_parent, new_seq, now_secs, now_ms);
self.recompute_coords();
return ParentLoss {
changed: true,
dampened,
};
}
// No alternative: become own root. This branch never calls
// `set_parent`, so it cannot arm a dampening episode.
let new_seq = self.my_declaration.sequence() + 1;
self.my_declaration = ParentDeclaration::self_root(self.my_node_addr, new_seq, now_secs);
self.recompute_coords();
ParentLoss {
changed: true,
dampened: false,
}
}
/// Mutable access to this node's declaration.
///
/// Exposed so the shell can write back a signature after signing: the
/// declaration data + `signing_bytes()` live in-core, but the key-crypto
/// (the schnorr sign) is a shell-driven boundary (§6), mirroring discovery.
pub(crate) fn my_declaration_mut(&mut self) -> &mut ParentDeclaration {
&mut self.my_declaration
}
/// Check if this node's declaration is signed.
pub fn is_declaration_signed(&self) -> bool {
self.my_declaration.is_signed()
}
}
impl fmt::Debug for TreeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TreeState")
.field("my_node_addr", &self.my_node_addr)
.field("root", &self.root)
.field("is_root", &self.is_root())
.field("depth", &self.my_coords.depth())
.field("peers", &self.peer_count())
.finish()
}
}