node: extract immutable state into a shared context and atomic metric registry

Store node counters in an atomic metric registry read through &self, and
introduce a shared NodeContext bundle holding the effectively-immutable
fields (config, identity, startup epoch, node profile, capability limits).
Source the immutable config and identity reads across the receive hot
path, the XX handshake/session/rekey state machines, and the discovery,
tree, bloom, retry, and lifecycle modules through the context accessors.
Includes the bloom delta/full/NACK/resize and byte-total counters in the
registry. The Node fields and the context are rebuilt in lockstep at every
mutation site.
This commit is contained in:
Johnathan Corgan
2026-06-02 13:05:03 +00:00
parent 88e48fdc68
commit e181df0ac4
30 changed files with 1301 additions and 1003 deletions
+29 -4
View File
@@ -115,10 +115,18 @@ with v0.3.x peers.
addition to the shared clusters. Several handshake, MMP, tree, and
discovery rejection paths that had no counter at all are now counted,
including the `send_lookup_response` no-route drop
(`DiscoveryStats::resp_no_route`). Existing direct counters at the
bloom / discovery / forwarding sites are retained alongside the new
dispatch while the rollout is in progress; a later change collapses
the duplicate increment.
(`DiscoveryStats::resp_no_route`). Existing
direct counters at the bloom / discovery / forwarding sites are
retained alongside the new dispatch while the rollout is in progress;
a later change collapses the duplicate increment.
- Internal atomic metric registry (`Arc<MetricsRegistry>`) that shadows
the plain-`u64` `NodeStats` counters, written alongside them and
validated by a whole-struct debug-build parity check. Covers the
forwarding receive counters, the full discovery counter family, and the
tree, bloom, congestion, and error-signal counter families so far, with
the hottest counters cache-line padded. Behavior-neutral:
`NodeStats` remains the serving path. Groundwork for sampling metrics
without contending the receive loop.
- `Node::update_peers` for runtime peer-list refresh, returning an
`UpdatePeersOutcome` summarizing added, removed, and retained peers.
Re-derives active peer connections from a new peer configuration
@@ -284,6 +292,23 @@ with v0.3.x peers.
### Fixed
- Six discovery counters (`req_decode_error`, `req_duplicate`,
`req_ttl_exhausted`, `resp_decode_error`, `resp_identity_miss`,
`resp_proof_failed`) no longer double-count. Each was incremented both
by a direct bump and again through the typed reject dispatch; the
redundant direct increment is removed, so each counts once per event.
- Six bloom counters (`decode_error`, `invalid`, `non_v1`,
`unknown_peer`, `stale`, `fill_exceeded`) no longer double-count. Each
was incremented both by a direct bump and again through the typed
reject dispatch; the redundant direct increment is removed, so each
counts once per event.
- Five forwarding reject packet counters (`decode_error_packets`,
`ttl_exhausted_packets`, `drop_no_route_packets`,
`drop_mtu_exceeded_packets`, `drop_send_error_packets`) no longer
double-count. Each was incremented both by the byte-aware outcome
recorder and again through the typed reject dispatch; the two calls are
collapsed into a single byte-aware reject entry point, so packets and
bytes each count once per event.
- A stale FSP (session-layer) session is now cleared when a peer
restart is detected during FMP rekey or cross-connection promotion.
Previously the old session could linger after the peer came back
+8 -8
View File
@@ -47,7 +47,7 @@ pub fn show_status(node: &Node) -> Value {
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "-".into());
let uptime_secs = node.uptime().as_secs();
let fwd = node.stats().snapshot().forwarding;
let fwd = node.metrics().forwarding.snapshot();
// Inline last-N-second sparklines for dashboard rendering. Kept
// short so the status payload stays compact; longer windows use
@@ -336,7 +336,7 @@ pub fn show_tree(node: &Node) -> Value {
let parent_hex = hex::encode(parent_addr.as_bytes());
let parent_display = node.peer_display_name(parent_addr);
let tree_stats = node.stats().snapshot().tree;
let tree_stats = node.metrics().tree.snapshot();
json!({
"my_node_addr": hex::encode(tree.my_node_addr().as_bytes()),
@@ -469,7 +469,7 @@ pub fn show_bloom(node: &Node) -> Value {
})
.collect();
let bloom_stats = node.stats().snapshot().bloom;
let bloom_stats = node.metrics().bloom.snapshot();
json!({
"own_node_addr": hex::encode(node.node_addr().as_bytes()),
@@ -706,7 +706,7 @@ pub fn show_routing(node: &Node) -> Value {
let cache = node.coord_cache();
let now = now_ms();
let cache_stats = cache.stats(now);
let node_stats = node.stats().snapshot();
let metrics = node.metrics();
// Pending discovery lookups (individual targets)
let lookups: Vec<Value> = node
@@ -745,10 +745,10 @@ pub fn show_routing(node: &Node) -> Value {
"pending_tun_packets": node.pending_tun_total_packets(),
"recent_requests": node.recent_request_count(),
"retries": retries,
"forwarding": serde_json::to_value(&node_stats.forwarding).unwrap_or_default(),
"discovery": serde_json::to_value(&node_stats.discovery).unwrap_or_default(),
"error_signals": serde_json::to_value(&node_stats.errors).unwrap_or_default(),
"congestion": serde_json::to_value(&node_stats.congestion).unwrap_or_default(),
"forwarding": serde_json::to_value(metrics.forwarding.snapshot()).unwrap_or_default(),
"discovery": serde_json::to_value(metrics.discovery.snapshot()).unwrap_or_default(),
"error_signals": serde_json::to_value(metrics.errors.snapshot()).unwrap_or_default(),
"congestion": serde_json::to_value(metrics.congestion.snapshot()).unwrap_or_default(),
})
}
+1
View File
@@ -11,6 +11,7 @@ use super::{FipsAddress, IdentityError, NodeAddr, sha256};
///
/// The identity holds the secp256k1 keypair and provides methods for signing
/// and verifying protocol messages.
#[derive(Clone)]
pub struct Identity {
keypair: Keypair,
node_addr: NodeAddr,
+30 -32
View File
@@ -8,7 +8,7 @@ use crate::NodeAddr;
use crate::bloom::BloomFilter;
use crate::protocol::{FilterAnnounce, FilterNack};
use super::reject::{BloomReject, RejectReason};
use super::reject::BloomReject;
use super::{Node, NodeError};
use std::collections::HashMap;
use tracing::{debug, trace, warn};
@@ -75,7 +75,7 @@ impl Node {
// Check debounce
if !self.bloom_state.should_send_update(peer_addr, now_ms) {
self.stats_mut().bloom.debounce_suppressed += 1;
self.metrics().bloom.debounce_suppressed.inc();
// Either not pending or rate-limited; will retry on tick
return Ok(());
}
@@ -104,18 +104,24 @@ impl Node {
// Send
if let Err(e) = self.send_encrypted_link_message(peer_addr, &encoded).await {
self.stats_mut().bloom.send_failed += 1;
self.metrics().bloom.send_failed.inc();
return Err(e);
}
self.stats_mut().bloom.sent += 1;
self.metrics().bloom.sent.inc();
if is_delta {
self.stats_mut().bloom.deltas_sent += 1;
self.metrics().bloom.deltas_sent.inc();
} else {
self.stats_mut().bloom.full_sends += 1;
self.metrics().bloom.full_sends.inc();
}
self.stats_mut().bloom.total_compressed_bytes += stats.compressed_bytes as u64;
self.stats_mut().bloom.total_raw_bytes += (stats.raw_words * 8) as u64;
self.metrics()
.bloom
.total_compressed_bytes
.add(stats.compressed_bytes as u64);
self.metrics()
.bloom
.total_raw_bytes
.add((stats.raw_words * 8) as u64);
// Self-plausibility check: WARN if our own outgoing filter is
// above the antipoison cap. Independent detection signal if
@@ -123,7 +129,7 @@ impl Node {
// despite M1. Rate-limited to once per 60s globally — outgoing
// cadence can be per-tick during churn, and we want the
// operator to see one clear message, not spam.
let max_fpr = self.config.node.bloom.max_inbound_fpr;
let max_fpr = self.config().node.bloom.max_inbound_fpr;
let out_fill = sent_filter.fill_ratio();
let out_fpr = out_fill.powi(sent_filter.hash_count() as i32);
if out_fpr > max_fpr {
@@ -205,14 +211,12 @@ impl Node {
/// Supports both full sends and delta (XOR diff) updates.
/// On out-of-sequence delta, sends a NACK to request full retransmission.
pub(super) async fn handle_filter_announce(&mut self, from: &NodeAddr, payload: &[u8]) {
self.stats_mut().bloom.received += 1;
self.metrics().bloom.received.inc();
let announce = match FilterAnnounce::decode(payload) {
Ok(a) => a,
Err(e) => {
self.stats_mut().bloom.decode_error += 1;
self.stats_mut()
.record_reject(RejectReason::Bloom(BloomReject::DecodeError));
self.metrics().bloom.record_reject(BloomReject::DecodeError);
debug!(from = %self.peer_display_name(from), error = %e, "Malformed FilterAnnounce");
return;
}
@@ -220,9 +224,7 @@ impl Node {
// Validate
if !announce.is_valid() {
self.stats_mut().bloom.invalid += 1;
self.stats_mut()
.record_reject(RejectReason::Bloom(BloomReject::Invalid));
self.metrics().bloom.record_reject(BloomReject::Invalid);
debug!(from = %self.peer_display_name(from), "FilterAnnounce filter/size_class mismatch");
return;
}
@@ -231,9 +233,7 @@ impl Node {
let peer = match self.peers.get(from) {
Some(p) => p,
None => {
self.stats_mut().bloom.unknown_peer += 1;
self.stats_mut()
.record_reject(RejectReason::Bloom(BloomReject::UnknownPeer));
self.metrics().bloom.record_reject(BloomReject::UnknownPeer);
debug!(from = %self.peer_display_name(from), "FilterAnnounce from unknown peer");
return;
}
@@ -242,9 +242,7 @@ impl Node {
// Reject stale/replay
if announce.sequence <= current_seq {
self.stats_mut().bloom.stale += 1;
self.stats_mut()
.record_reject(RejectReason::Bloom(BloomReject::Stale));
self.metrics().bloom.record_reject(BloomReject::Stale);
trace!(
from = %self.peer_display_name(from),
received_seq = announce.sequence,
@@ -271,7 +269,7 @@ impl Node {
};
let nack_encoded = nack.encode();
let _ = self.send_encrypted_link_message(from, &nack_encoded).await;
self.stats_mut().bloom.nacks_sent += 1;
self.metrics().bloom.nacks_sent.inc();
return;
}
@@ -290,7 +288,7 @@ impl Node {
expected_seq: current_seq,
};
let _ = self.send_encrypted_link_message(from, &nack.encode()).await;
self.stats_mut().bloom.nacks_sent += 1;
self.metrics().bloom.nacks_sent.inc();
return;
}
result
@@ -303,7 +301,7 @@ impl Node {
);
let nack = FilterNack { expected_seq: 0 };
let _ = self.send_encrypted_link_message(from, &nack.encode()).await;
self.stats_mut().bloom.nacks_sent += 1;
self.metrics().bloom.nacks_sent.inc();
return;
}
}
@@ -320,13 +318,13 @@ impl Node {
// contribution to aggregation. Applied to the resolved filter so
// deltas that reconstruct to an over-cap state are caught as well
// as full sends.
let max_fpr = self.config.node.bloom.max_inbound_fpr;
let max_fpr = self.config().node.bloom.max_inbound_fpr;
let fill = resolved_filter.fill_ratio();
let fpr = fill.powi(resolved_filter.hash_count() as i32);
if fpr > max_fpr {
self.stats_mut().bloom.fill_exceeded += 1;
self.stats_mut()
.record_reject(RejectReason::Bloom(BloomReject::FillExceeded));
self.metrics()
.bloom
.record_reject(BloomReject::FillExceeded);
warn!(
from = %self.peer_display_name(from),
seq = announce.sequence,
@@ -338,7 +336,7 @@ impl Node {
return;
}
self.stats_mut().bloom.accepted += 1;
self.metrics().bloom.accepted.inc();
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -390,7 +388,7 @@ impl Node {
"Received FilterNack, scheduling full re-send"
);
self.stats_mut().bloom.nacks_received += 1;
self.metrics().bloom.nacks_received.inc();
// Clear sent state for this peer → next send will be full
self.bloom_state.clear_sent_filter(from);
self.bloom_state.mark_update_needed(*from);
@@ -430,7 +428,7 @@ impl Node {
);
self.bloom_state.set_size_class(new_class);
self.bloom_state.clear_all_sent_filters();
self.stats_mut().bloom.size_changes += 1;
self.metrics().bloom.size_changes.inc();
let all_peers: Vec<NodeAddr> = self.peers.keys().copied().collect();
self.bloom_state.mark_all_updates_needed(all_peers);
}
+86
View File
@@ -0,0 +1,86 @@
//! Shared immutable context bundle.
//!
//! [`NodeContext`] groups the [`Node`](super::Node)'s effectively-immutable
//! fields behind a single `Arc` so that handlers can eventually borrow a
//! cheap `&NodeContext` clone instead of `&self`.
//!
//! During the migration it is a *parallel, authoritative* copy of the
//! corresponding `Node` fields: both are kept in lockstep at the only three
//! mutation points — the constructor, [`update_peers`](super::Node::update_peers),
//! and the test-only `set_max_*` setters — via
//! [`Node::rebuild_context`](super::Node::rebuild_context). Readers migrate
//! onto the bundle incrementally; the duplicated `Node` fields are removed
//! once the last reader has moved over.
use std::sync::Arc;
use crate::protocol::NodeProfile;
use crate::{Config, Identity};
/// Effectively-immutable `Node` state, shared via `Arc<NodeContext>`.
#[derive(Clone)]
pub(crate) struct NodeContext {
/// Loaded configuration. A static snapshot: replaced wholesale (never
/// interior-mutated) when `update_peers` rebuilds the runtime peer list.
pub config: Arc<Config>,
/// This node's cryptographic identity.
pub identity: Identity,
/// Random epoch generated at startup for peer restart detection.
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub startup_epoch: [u8; 8],
/// Instant when the node was created, for uptime reporting.
pub started_at: std::time::Instant,
/// Whether this is a leaf-only node.
pub is_leaf_only: bool,
/// This node's routing profile (Full, NonRouting, Leaf).
pub node_profile: NodeProfile,
/// Maximum connections (0 = unlimited).
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub max_connections: usize,
/// Maximum peers (0 = unlimited).
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub max_peers: usize,
/// Maximum links (0 = unlimited).
// Consumed by readers migrating in a later sub-PR.
#[allow(dead_code)]
pub max_links: usize,
}
impl NodeContext {
/// Build a context bundle from the individual values.
#[allow(clippy::too_many_arguments)]
pub fn new(
config: Arc<Config>,
identity: Identity,
startup_epoch: [u8; 8],
started_at: std::time::Instant,
is_leaf_only: bool,
node_profile: NodeProfile,
max_connections: usize,
max_peers: usize,
max_links: usize,
) -> Self {
Self {
config,
identity,
startup_epoch,
started_at,
is_leaf_only,
node_profile,
max_connections,
max_peers,
max_links,
}
}
}
+45 -43
View File
@@ -5,7 +5,7 @@
//! bloom filter contains the target. TTL and request_id dedup provide
//! safety bounds.
use crate::node::reject::{DiscoveryReject, RejectReason};
use crate::node::reject::DiscoveryReject;
use crate::node::{Node, RecentRequest};
use crate::protocol::{LookupRequest, LookupResponse};
use crate::transport::{TransportAddr, TransportId};
@@ -25,14 +25,14 @@ impl Node {
/// 5. If we're the target, generate and send response
/// 6. If TTL > 0, forward to tree peers whose bloom filter matches
pub(in crate::node) async fn handle_lookup_request(&mut self, from: &NodeAddr, payload: &[u8]) {
self.stats_mut().discovery.req_received += 1;
self.metrics().discovery.req_received.inc();
let request = match LookupRequest::decode(payload) {
Ok(req) => req,
Err(e) => {
self.stats_mut().discovery.req_decode_error += 1;
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDecodeError));
self.metrics()
.discovery
.record_reject(DiscoveryReject::ReqDecodeError);
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupRequest");
return;
}
@@ -45,9 +45,9 @@ impl Node {
// Also serves as loop protection — tree routing is loop-free,
// but request_id dedup catches edge cases during tree restructuring.
if self.recent_requests.contains_key(&request.request_id) {
self.stats_mut().discovery.req_duplicate += 1;
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDuplicate));
self.metrics()
.discovery
.record_reject(DiscoveryReject::ReqDuplicate);
debug!(
request_id = request.request_id,
from = %self.peer_display_name(from),
@@ -57,8 +57,9 @@ impl Node {
}
if self.recent_requests.len() >= MAX_RECENT_DISCOVERY_REQUESTS {
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDedupCacheFull));
self.metrics()
.discovery
.record_reject(DiscoveryReject::ReqDedupCacheFull);
debug!(
request_id = request.request_id,
from = %self.peer_display_name(from),
@@ -75,7 +76,7 @@ impl Node {
// Are we the target?
if request.target == *self.node_addr() {
self.stats_mut().discovery.req_target_is_us += 1;
self.metrics().discovery.req_target_is_us.inc();
debug!(
request_id = request.request_id,
origin = %self.peer_display_name(&request.origin),
@@ -93,7 +94,7 @@ impl Node {
.discovery_forward_limiter
.should_forward(&request.target)
{
self.stats_mut().discovery.req_forward_rate_limited += 1;
self.metrics().discovery.req_forward_rate_limited.inc();
debug!(
request_id = request.request_id,
target = %self.peer_display_name(&request.target),
@@ -101,12 +102,12 @@ impl Node {
);
return;
}
self.stats_mut().discovery.req_forwarded += 1;
self.metrics().discovery.req_forwarded.inc();
self.forward_lookup_request(request).await;
} else {
self.stats_mut().discovery.req_ttl_exhausted += 1;
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::ReqTtlExhausted));
self.metrics()
.discovery
.record_reject(DiscoveryReject::ReqTtlExhausted);
debug!(
request_id = request.request_id,
target = %self.peer_display_name(&request.target),
@@ -127,14 +128,14 @@ impl Node {
from: &NodeAddr,
payload: &[u8],
) {
self.stats_mut().discovery.resp_received += 1;
self.metrics().discovery.resp_received.inc();
let mut response = match LookupResponse::decode(payload) {
Ok(resp) => resp,
Err(e) => {
self.stats_mut().discovery.resp_decode_error += 1;
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::RespDecodeError));
self.metrics()
.discovery
.record_reject(DiscoveryReject::RespDecodeError);
debug!(from = %self.peer_display_name(from), error = %e, "Malformed LookupResponse");
return;
}
@@ -158,7 +159,7 @@ impl Node {
// Transit node: reverse-path forward
let from_peer = recent.from_peer;
self.stats_mut().discovery.resp_forwarded += 1;
self.metrics().discovery.resp_forwarded.inc();
// Apply path_mtu min() from the outgoing link's transport MTU
self.apply_outgoing_link_mtu_to_response(&mut response, &from_peer);
@@ -190,9 +191,9 @@ impl Node {
let target_pubkey = match self.lookup_by_fips_prefix(&prefix) {
Some((_addr, pubkey)) => pubkey,
None => {
self.stats_mut().discovery.resp_identity_miss += 1;
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::RespIdentityMiss));
self.metrics()
.discovery
.record_reject(DiscoveryReject::RespIdentityMiss);
warn!(
request_id = response.request_id,
target = %self.peer_display_name(&target),
@@ -208,9 +209,9 @@ impl Node {
let proof_data =
LookupResponse::proof_bytes(response.request_id, &target, &response.target_coords);
if !peer_id.verify(&proof_data, &response.proof) {
self.stats_mut().discovery.resp_proof_failed += 1;
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::RespProofFailed));
self.metrics()
.discovery
.record_reject(DiscoveryReject::RespProofFailed);
warn!(
request_id = response.request_id,
target = %self.peer_display_name(&target),
@@ -219,7 +220,7 @@ impl Node {
return;
}
self.stats_mut().discovery.resp_accepted += 1;
self.metrics().discovery.resp_accepted.inc();
// Clear backoff on success — target is reachable
self.discovery_backoff.record_success(&target);
@@ -265,10 +266,10 @@ impl Node {
self.pending_lookups.remove(&target);
// If an established session exists, reset the warmup counter.
let n = self.config().node.session.coords_warmup_packets;
if let Some(entry) = self.sessions.get_mut(&target)
&& entry.is_established()
{
let n = self.config.node.session.coords_warmup_packets;
entry.set_coords_warmup_remaining(n);
debug!(
dest = %self.peer_display_name(&target),
@@ -315,8 +316,9 @@ impl Node {
origin = %self.peer_display_name(&request.origin),
"Cannot route LookupResponse: no reverse path or tree route to origin"
);
self.stats_mut()
.record_reject(RejectReason::Discovery(DiscoveryReject::RespNoRoute));
self.metrics()
.discovery
.record_reject(DiscoveryReject::RespNoRoute);
return;
}
}
@@ -396,7 +398,7 @@ impl Node {
.map(|(addr, _)| *addr)
.collect();
if fallback.is_empty() {
self.stats_mut().discovery.req_no_tree_peer += 1;
self.metrics().discovery.req_no_tree_peer.inc();
trace!(
request_id = request.request_id,
"No eligible peers to forward LookupRequest"
@@ -409,7 +411,7 @@ impl Node {
};
if used_fallback {
self.stats_mut().discovery.req_fallback_forwarded += 1;
self.metrics().discovery.req_fallback_forwarded.inc();
debug!(
request_id = request.request_id,
target = %self.peer_display_name(&request.target),
@@ -447,10 +449,10 @@ impl Node {
/// The originator does NOT record the request_id in recent_requests,
/// so when the response arrives, it's recognized as "our request".
pub(in crate::node) async fn initiate_lookup(&mut self, target: &NodeAddr, ttl: u8) -> usize {
self.stats_mut().discovery.req_initiated += 1;
self.metrics().discovery.req_initiated.inc();
let origin = *self.node_addr();
let min_mtu = self.config.tun.mtu();
let min_mtu = self.config().tun.mtu();
let request = LookupRequest::generate(*target, origin, ttl, min_mtu);
// Send only to full tree peers whose bloom filter contains the target
@@ -508,7 +510,7 @@ impl Node {
// Dedup: any pending lookup means we are already trying.
if self.pending_lookups.contains_key(dest) {
self.stats_mut().discovery.req_deduplicated += 1;
self.metrics().discovery.req_deduplicated.inc();
debug!(
target_node = %self.peer_display_name(dest),
"Discovery lookup deduplicated, already pending"
@@ -519,7 +521,7 @@ impl Node {
// Optional post-failure suppression. Defaults are 0/0 (inert);
// operators can opt in by setting `node.discovery.backoff_*_secs`.
if self.discovery_backoff.is_suppressed(dest) {
self.stats_mut().discovery.req_backoff_suppressed += 1;
self.metrics().discovery.req_backoff_suppressed.inc();
debug!(
target_node = %self.peer_display_name(dest),
failures = self.discovery_backoff.failure_count(dest),
@@ -532,7 +534,7 @@ impl Node {
// it's not in the mesh — skip the lookup and record as failure.
let reachable = self.peers.values().any(|peer| peer.may_reach(dest));
if !reachable {
self.stats_mut().discovery.req_bloom_miss += 1;
self.metrics().discovery.req_bloom_miss.inc();
self.discovery_backoff.record_failure(dest);
debug!(
target_node = %self.peer_display_name(dest),
@@ -543,7 +545,7 @@ impl Node {
self.pending_lookups
.insert(*dest, PendingLookup::new(now_ms));
let ttl = self.config.node.discovery.ttl;
let ttl = self.config().node.discovery.ttl;
let sent = self.initiate_lookup(dest, ttl).await;
// If no tree peers had the target, fail immediately
@@ -568,7 +570,7 @@ impl Node {
/// - Otherwise: declare the destination unreachable, drop queued packets,
/// and emit ICMPv6 destination-unreachable for each.
pub(in crate::node) async fn check_pending_lookups(&mut self, now_ms: u64) {
let timeouts = self.config.node.discovery.attempt_timeouts_secs.clone();
let timeouts = self.config().node.discovery.attempt_timeouts_secs.clone();
let max_attempts = timeouts.len() as u8;
// Collect targets needing action
@@ -594,7 +596,7 @@ impl Node {
entry.last_sent_ms = now_ms;
let attempt = entry.attempt;
let ttl = self.config.node.discovery.ttl;
let ttl = self.config().node.discovery.ttl;
let sent = self.initiate_lookup(&target, ttl).await;
if sent > 0 {
debug!(
@@ -608,7 +610,7 @@ impl Node {
// Process timeouts
for addr in to_timeout {
self.stats_mut().discovery.resp_timed_out += 1;
self.metrics().discovery.resp_timed_out.inc();
self.pending_lookups.remove(&addr);
// Record failure for optional backoff
@@ -666,7 +668,7 @@ impl Node {
/// Remove expired entries from the recent_requests cache.
fn purge_expired_requests(&mut self, current_time_ms: u64) {
let expiry_ms = self.config.node.discovery.recent_expiry_secs * 1000;
let expiry_ms = self.config().node.discovery.recent_expiry_secs * 1000;
self.recent_requests
.retain(|_, entry| !entry.is_expired(current_time_ms, expiry_ms));
}
+1 -1
View File
@@ -64,7 +64,7 @@ impl Node {
let pending_their = peer.pending_their_index();
info!(
peer = %display_name,
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
their_addr = %node_addr,
pending_our_index = ?pending_our,
pending_their_index = ?pending_their,
+22 -32
View File
@@ -6,7 +6,7 @@
//! locally, and generates error signals on routing failure.
use crate::NodeAddr;
use crate::node::reject::{ForwardingReject, RejectReason};
use crate::node::reject::ForwardingReject;
use crate::node::session_wire::{
FSP_COMMON_PREFIX_SIZE, FSP_HEADER_SIZE, FSP_PHASE_ESTABLISHED, FSP_PHASE_MSG1, FSP_PHASE_MSG2,
FspCommonPrefix, parse_encrypted_coords,
@@ -30,16 +30,14 @@ impl Node {
payload: &[u8],
incoming_ce: bool,
) {
self.stats_mut().forwarding.record_received(payload.len());
self.metrics().forwarding.record_received(payload.len());
let datagram_ref = match SessionDatagramRef::decode(payload) {
Ok(dg) => dg,
Err(e) => {
self.stats_mut()
self.metrics()
.forwarding
.record_decode_error(payload.len());
self.stats_mut()
.record_reject(RejectReason::Forwarding(ForwardingReject::DecodeError));
.record_reject_bytes(ForwardingReject::DecodeError, payload.len());
debug!(error = %e, "Malformed SessionDatagram");
return;
}
@@ -48,11 +46,9 @@ impl Node {
// TTL enforcement: decrement for forwarding and drop only if the
// received datagram was already exhausted.
if datagram_ref.ttl == 0 {
self.stats_mut()
self.metrics()
.forwarding
.record_ttl_exhausted(payload.len());
self.stats_mut()
.record_reject(RejectReason::Forwarding(ForwardingReject::TtlExhausted));
.record_reject_bytes(ForwardingReject::TtlExhausted, payload.len());
debug!(
src = %datagram_ref.src_addr,
dest = %datagram_ref.dest_addr,
@@ -68,7 +64,7 @@ impl Node {
// Local delivery: dispatch to session layer handlers without
// materializing an owned SessionDatagram payload Vec.
if datagram_ref.dest_addr == *self.node_addr() {
self.stats_mut().forwarding.record_delivered(payload.len());
self.metrics().forwarding.record_delivered(payload.len());
self.handle_session_payload(
&datagram_ref.src_addr,
datagram_ref.payload,
@@ -86,11 +82,9 @@ impl Node {
let next_hop_addr = match self.find_next_hop(&datagram.dest_addr) {
Some(peer) => *peer.node_addr(),
None => {
self.stats_mut()
self.metrics()
.forwarding
.record_drop_no_route(payload.len());
self.stats_mut()
.record_reject(RejectReason::Forwarding(ForwardingReject::NoRoute));
.record_reject_bytes(ForwardingReject::NoRoute, payload.len());
debug!(
src = %self.peer_display_name(&datagram.src_addr),
dest = %self.peer_display_name(&datagram.dest_addr),
@@ -118,7 +112,7 @@ impl Node {
let local_congestion = self.detect_congestion(&next_hop_addr);
let outgoing_ce = incoming_ce || local_congestion;
if local_congestion {
self.stats_mut().congestion.record_congestion_detected();
self.metrics().congestion.congestion_detected.inc();
let now = Instant::now();
let should_log = self
.last_congestion_log
@@ -138,19 +132,15 @@ impl Node {
{
match e {
NodeError::MtuExceeded { mtu, .. } => {
self.stats_mut()
self.metrics()
.forwarding
.record_drop_mtu_exceeded(payload.len());
self.stats_mut()
.record_reject(RejectReason::Forwarding(ForwardingReject::MtuExceeded));
.record_reject_bytes(ForwardingReject::MtuExceeded, payload.len());
self.send_mtu_exceeded_error(&datagram, mtu).await;
}
_ => {
self.stats_mut()
self.metrics()
.forwarding
.record_drop_send_error(payload.len());
self.stats_mut()
.record_reject(RejectReason::Forwarding(ForwardingReject::SendError));
.record_reject_bytes(ForwardingReject::SendError, payload.len());
debug!(
next_hop = %next_hop_addr,
dest = %datagram.dest_addr,
@@ -160,9 +150,9 @@ impl Node {
}
}
} else {
self.stats_mut().forwarding.record_forwarded(encoded.len());
self.metrics().forwarding.record_forwarded(encoded.len());
if outgoing_ce {
self.stats_mut().congestion.record_ce_forwarded();
self.metrics().congestion.ce_forwarded.inc();
}
}
}
@@ -291,7 +281,7 @@ impl Node {
};
let error_dg = SessionDatagram::new(my_addr, original.src_addr, error_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
let next_hop_addr = match self.find_next_hop(&original.src_addr) {
Some(peer) => *peer.node_addr(),
@@ -343,7 +333,7 @@ impl Node {
let error_payload = MtuExceeded::new(original.dest_addr, my_addr, bottleneck_mtu).encode();
let error_dg = SessionDatagram::new(my_addr, original.src_addr, error_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
let next_hop_addr = match self.find_next_hop(&original.src_addr) {
Some(peer) => *peer.node_addr(),
@@ -385,7 +375,7 @@ impl Node {
///
/// Returns `true` if any signal indicates congestion.
pub(in crate::node) fn detect_congestion(&self, next_hop: &NodeAddr) -> bool {
if !self.config.node.ecn.enabled {
if !self.config().node.ecn.enabled {
return false;
}
// Outgoing link MMP metrics
@@ -393,8 +383,8 @@ impl Node {
&& let Some(mmp) = peer.mmp()
{
let metrics = &mmp.metrics;
if metrics.loss_rate() >= self.config.node.ecn.loss_threshold
|| metrics.etx >= self.config.node.ecn.etx_threshold
if metrics.loss_rate() >= self.config().node.ecn.loss_threshold
|| metrics.etx >= self.config().node.ecn.etx_threshold
{
return true;
}
@@ -423,7 +413,7 @@ impl Node {
}
}
for tid in new_drop_events {
self.stats_mut().congestion.record_kernel_drop_event();
self.metrics().congestion.kernel_drop_events.inc();
warn!(
transport_id = tid.as_u32(),
"Kernel recv drops first observed on transport"
+22 -17
View File
@@ -181,7 +181,7 @@ impl Node {
// Create FMP negotiation payload for msg2 (includes profile, MMP bits, bloom TLV)
let neg_payload = NegotiationPayload::fmp(1, 1, self.node_profile).encode();
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let noise_msg1 = &packet.data[header.noise_msg1_offset..];
let msg2_response = match conn.receive_handshake_init(
our_keypair,
@@ -234,7 +234,7 @@ impl Node {
packet.transport_id,
packet.remote_addr.clone(),
LinkDirection::Inbound,
Duration::from_millis(self.config.node.base_rtt_ms),
Duration::from_millis(self.config().node.base_rtt_ms),
);
self.links.insert(link_id, link);
@@ -442,7 +442,7 @@ impl Node {
debug!(
peer = %display_name,
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
new_our_index = %our_index,
new_their_index = %header.sender_idx,
"rekey-msg2 initiator: pending session set, awaiting K-bit cutover"
@@ -580,7 +580,7 @@ impl Node {
return;
}
if peer_node_addr == *self.identity.node_addr() {
if peer_node_addr == *self.identity().node_addr() {
debug!(link_id = %link_id, "Discovered self via shared-media beacon, dropping");
self.connections.remove(&link_id);
self.stats_mut()
@@ -638,7 +638,7 @@ impl Node {
// outbound = the loser's inbound).
if self.peers.contains_key(&peer_node_addr) {
let our_outbound_wins = cross_connection_winner(
self.identity.node_addr(),
self.identity().node_addr(),
&peer_node_addr,
true, // this IS our outbound
);
@@ -1000,7 +1000,7 @@ impl Node {
return;
}
if peer_node_addr == *self.identity.node_addr() {
if peer_node_addr == *self.identity().node_addr() {
debug!(link_id = %link_id, "Received msg3 from self, dropping");
self.connections.remove(&link_id);
self.remove_link(&link_id);
@@ -1100,7 +1100,7 @@ impl Node {
// both sides converge on a single Noise session pair.
if existing_peer.link_id() != link_id && session_age_secs < 30 {
let our_inbound_wins = cross_connection_winner(
self.identity.node_addr(),
self.identity().node_addr(),
&peer_node_addr,
false, // this connection is inbound
);
@@ -1168,7 +1168,7 @@ impl Node {
}
// Check for rekey: session must be at least 30s old.
if self.config.node.rekey.enabled
if self.config().node.rekey.enabled
&& existing_peer.has_session()
&& existing_peer.is_healthy()
&& session_age_secs >= 30
@@ -1188,7 +1188,7 @@ impl Node {
if existing_peer.rekey_in_progress()
|| existing_peer.pending_new_session().is_some()
{
let our_addr = self.identity.node_addr();
let our_addr = self.identity().node_addr();
if our_addr < &peer_node_addr {
// We win — keep our session, drop their msg3.
info!(
@@ -1280,7 +1280,7 @@ impl Node {
debug!(
peer = %self.peer_display_name(&peer_node_addr),
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
new_our_index = %our_new_index,
new_their_index = %header.sender_idx,
"rekey-msg3 responder: pending session set, awaiting K-bit cutover"
@@ -1465,7 +1465,7 @@ impl Node {
debug!(
peer = %display_name,
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
new_our_index = %our_index,
new_their_index = %header.sender_idx,
"rekey-msg3 responder (existing link): pending session set, awaiting K-bit cutover"
@@ -1577,7 +1577,11 @@ impl Node {
// authenticated connection must replace them regardless of the
// tie-breaker direction.
let this_wins = remote_epoch_changed
|| cross_connection_winner(self.identity.node_addr(), &peer_node_addr, is_outbound);
|| cross_connection_winner(
self.identity().node_addr(),
&peer_node_addr,
is_outbound,
);
if this_wins {
// This connection wins, replace the existing peer
@@ -1633,13 +1637,13 @@ impl Node {
current_addr,
link_stats,
is_outbound,
&self.config.node.mmp,
&self.config().node.mmp,
remote_epoch,
self.node_profile,
peer_profile,
);
new_peer.set_tree_announce_min_interval_ms(
self.config.node.tree.announce_min_interval_ms,
self.config().node.tree.announce_min_interval_ms,
);
self.peers.insert(peer_node_addr, new_peer);
@@ -1744,13 +1748,14 @@ impl Node {
current_addr,
link_stats,
is_outbound,
&self.config.node.mmp,
&self.config().node.mmp,
remote_epoch,
self.node_profile,
peer_profile,
);
new_peer
.set_tree_announce_min_interval_ms(self.config.node.tree.announce_min_interval_ms);
new_peer.set_tree_announce_min_interval_ms(
self.config().node.tree.announce_min_interval_ms,
);
if let Some(ts) = old_announce_ts {
new_peer.set_last_tree_announce_sent_ms(ts);
}
+24 -16
View File
@@ -160,18 +160,23 @@ impl Node {
.unwrap_or(0);
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(&self.identity) {
// Clone identity once: sign_declaration borrows &mut tree_state while
// the identity() accessor borrows all of &self, so an owned copy avoids
// the split-borrow conflict on this infrequent parent-switch 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 first-RTT parent eval");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
self.coord_cache
.invalidate_via_node(self.identity.node_addr());
.invalidate_via_node(our_identity.node_addr());
self.reset_discovery_backoff();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
self.metrics().tree.parent_switched.inc();
self.metrics().tree.parent_switches.inc();
info!(
new_parent = %self.peer_display_name(&new_parent),
new_seq = new_seq,
@@ -181,7 +186,7 @@ impl Node {
"Parent switched after first RTT measurement"
);
if flap_dampened {
self.stats_mut().tree.flap_dampened += 1;
self.metrics().tree.flap_dampened.inc();
warn!("Flap dampening engaged: excessive parent switches detected");
}
self.send_tree_announce_to_all().await;
@@ -189,18 +194,21 @@ impl Node {
self.bloom_state.mark_all_updates_needed(all_peers);
} else if !self.tree_state.is_root() && self.tree_state.should_be_root() {
self.tree_state.become_root();
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
// Clone identity once (see the parent-switch branch above for why).
let our_identity = self.identity().clone();
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
warn!(error = %e, "Failed to sign self-root declaration after first-RTT");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_other_roots doc.
self.coord_cache
.invalidate_other_roots(self.identity.node_addr());
.invalidate_other_roots(our_identity.node_addr());
self.reset_discovery_backoff();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
self.metrics().tree.parent_switched.inc();
self.metrics().tree.parent_switches.inc();
info!(
new_root = %self.tree_state.root(),
trigger = "first-rtt",
@@ -537,8 +545,8 @@ impl Node {
/// hasn't sent us a frame within the link dead timeout.
pub(in crate::node) async fn check_link_heartbeats(&mut self) {
let now = Instant::now();
let heartbeat_interval = Duration::from_secs(self.config.node.heartbeat_interval_secs);
let dead_timeout = Duration::from_secs(self.config.node.link_dead_timeout_secs);
let heartbeat_interval = Duration::from_secs(self.config().node.heartbeat_interval_secs);
let dead_timeout = Duration::from_secs(self.config().node.link_dead_timeout_secs);
let heartbeat_msg = [LinkMessageType::Heartbeat.to_byte()];
// Collect heartbeats to send and dead peers to remove
@@ -581,7 +589,7 @@ impl Node {
for addr in &dead_peers {
warn!(
peer = %self.peer_display_name(addr),
timeout_secs = self.config.node.link_dead_timeout_secs,
timeout_secs = self.config().node.link_dead_timeout_secs,
"Removing peer: link dead timeout"
);
self.remove_active_peer(addr);
+18 -18
View File
@@ -38,12 +38,12 @@ impl Node {
/// - If the drain window has expired, clean up the previous session
/// - If the rekey timer/counter fires, initiate a new handshake
pub(in crate::node) async fn check_rekey(&mut self) {
if !self.config.node.rekey.enabled {
if !self.config().node.rekey.enabled {
return;
}
let rekey_after_secs = self.config.node.rekey.after_secs;
let rekey_after_messages = self.config.node.rekey.after_messages;
let rekey_after_secs = self.config().node.rekey.after_secs;
let rekey_after_messages = self.config().node.rekey.after_messages;
// Collect peers that need action (to avoid borrow conflicts)
let mut peers_to_cutover: Vec<NodeAddr> = Vec::new();
@@ -109,7 +109,7 @@ impl Node {
let their_index = peer.their_index();
info!(
peer = %self.peer_display_name(&node_addr),
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
their_addr = %node_addr,
our_index = ?our_index,
their_index = ?their_index,
@@ -202,7 +202,7 @@ impl Node {
};
// Create XX initiator handshake directly (no PeerConnection)
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let mut hs = HandshakeState::new_initiator(our_keypair);
hs.set_local_epoch(self.startup_epoch);
@@ -248,7 +248,7 @@ impl Node {
}
// Store handshake state on the ActivePeer (not a separate PeerConnection)
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
let now_ms = Self::now_ms();
if let Some(peer) = self.peers.get_mut(node_addr) {
peer.set_rekey_state(hs, our_index, wire_msg1, now_ms + resend_interval);
@@ -264,11 +264,11 @@ impl Node {
/// Called from the tick loop. Uses the same resend interval and max
/// resend count as initial handshakes.
pub(in crate::node) async fn resend_pending_rekeys(&mut self, now_ms: u64) {
if !self.config.node.rekey.enabled {
if !self.config().node.rekey.enabled {
return;
}
let interval_ms = self.config.node.rate_limit.handshake_resend_interval_ms;
let interval_ms = self.config().node.rate_limit.handshake_resend_interval_ms;
// Collect peers needing action
let mut to_resend: Vec<(NodeAddr, Vec<u8>)> = Vec::new();
@@ -337,14 +337,14 @@ impl Node {
/// is safe — an abandoned cycle never leaves a divergent unsafe
/// state.
pub(in crate::node) async fn resend_pending_session_msg3(&mut self, now_ms: u64) {
if !self.config.node.rekey.enabled || self.sessions.is_empty() {
if !self.config().node.rekey.enabled || self.sessions.is_empty() {
return;
}
let interval_ms = self.config.node.rate_limit.handshake_resend_interval_ms;
let backoff = self.config.node.rate_limit.handshake_resend_backoff;
let max_resends = self.config.node.rate_limit.handshake_max_resends;
let ttl = self.config.node.session.default_ttl;
let interval_ms = self.config().node.rate_limit.handshake_resend_interval_ms;
let backoff = self.config().node.rate_limit.handshake_resend_backoff;
let max_resends = self.config().node.rate_limit.handshake_max_resends;
let ttl = self.config().node.session.default_ttl;
let my_addr = *self.node_addr();
// Collect rekey initiators whose msg3 retransmission is due.
@@ -420,12 +420,12 @@ impl Node {
/// `resend_pending_session_msg3`; its lifetime is tied to the
/// responder receiving msg3, not to this initiator's cutover.
pub(in crate::node) async fn check_session_rekey(&mut self) {
if !self.config.node.rekey.enabled {
if !self.config().node.rekey.enabled {
return;
}
let rekey_after_secs = self.config.node.rekey.after_secs;
let rekey_after_messages = self.config.node.rekey.after_messages;
let rekey_after_secs = self.config().node.rekey.after_secs;
let rekey_after_messages = self.config().node.rekey.after_messages;
let now_ms = Self::now_ms();
let drain_ms = DRAIN_WINDOW_SECS * 1000;
let dampening_ms = REKEY_DAMPENING_SECS * 1000;
@@ -541,7 +541,7 @@ impl Node {
let _dest_pubkey = *entry.remote_pubkey();
// Create Noise XX initiator handshake (rekey: no negotiation payload)
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_initiator(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
@@ -568,7 +568,7 @@ impl Node {
// Send through the mesh
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, setup_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
if let Err(e) = self.send_session_datagram(&mut datagram).await {
debug!(
+3 -3
View File
@@ -69,14 +69,14 @@ impl Node {
};
let mut tick =
tokio::time::interval(Duration::from_secs(self.config.node.tick_interval_secs));
tokio::time::interval(Duration::from_secs(self.config().node.tick_interval_secs));
// Set up control socket channel
let (control_tx, mut control_rx) =
tokio::sync::mpsc::channel::<crate::control::ControlMessage>(32);
if self.config.node.control.enabled {
let config = self.config.node.control.clone();
if self.config().node.control.enabled {
let config = self.config().node.control.clone();
let tx = control_tx.clone();
tokio::spawn(async move {
match ControlSocket::bind(&config) {
+39 -38
View File
@@ -344,7 +344,7 @@ impl Node {
Some(mut packet) => {
if ce_flag {
mark_ipv6_ecn_ce(&mut packet);
self.stats_mut().congestion.record_ce_received();
self.metrics().congestion.ce_received.inc();
}
if let Some(tun_tx) = &self.tun_tx {
if let Err(e) = tun_tx.send(packet) {
@@ -435,7 +435,7 @@ impl Node {
if let Some(existing) = self.sessions.get(src_addr) {
if existing.is_initiating() {
// Simultaneous initiation: smaller NodeAddr wins as initiator
if self.identity.node_addr() < src_addr {
if self.identity().node_addr() < src_addr {
// We win — drop their setup, they'll process ours
debug!(
src = %self.peer_display_name(src_addr),
@@ -454,7 +454,7 @@ impl Node {
debug!(src = %self.peer_display_name(src_addr), "Duplicate SessionSetup, resending SessionAck");
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *src_addr, payload.to_vec())
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
if let Err(e) = self.send_session_datagram(&mut datagram).await {
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to resend SessionAck");
}
@@ -465,7 +465,7 @@ impl Node {
} else if existing.is_established() {
// Rekey: if rekey enabled, treat as rekey for key rotation.
// The existing established session remains active for traffic.
if self.config.node.rekey.enabled {
if self.config().node.rekey.enabled {
let rekey_in_progress = existing.has_rekey_in_progress();
let has_pending = existing.pending_new_session().is_some();
@@ -480,11 +480,11 @@ impl Node {
// Apply the smaller-NodeAddr tie-breaker uniformly so
// both sides converge on a single Noise session.
if rekey_in_progress || has_pending {
if self.identity.node_addr() < src_addr {
if self.identity().node_addr() < src_addr {
// We win — keep our session, drop their msg1.
info!(
src = %self.peer_display_name(src_addr),
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
their_addr = %src_addr,
rekey_in_progress = rekey_in_progress,
pending_new_session = has_pending,
@@ -495,7 +495,7 @@ impl Node {
// We lose — abandon our rekey/pending, fall through as responder.
info!(
src = %self.peer_display_name(src_addr),
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
their_addr = %src_addr,
rekey_in_progress = rekey_in_progress,
pending_new_session = has_pending,
@@ -505,7 +505,7 @@ impl Node {
entry.abandon_rekey();
}
}
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_responder(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
@@ -529,7 +529,7 @@ impl Node {
let ack_payload = ack.encode();
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *src_addr, ack_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
if let Err(e) = self.send_session_datagram(&mut datagram).await {
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send rekey SessionAck");
@@ -556,7 +556,7 @@ impl Node {
}
// Create XX responder handshake and process msg1
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_responder(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
@@ -594,7 +594,7 @@ impl Node {
let ack_payload = ack.encode();
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *src_addr, ack_payload.clone())
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
// Route the ack back to the initiator
if let Err(e) = self.send_session_datagram(&mut datagram).await {
@@ -605,9 +605,9 @@ impl Node {
// Store session entry in AwaitingMsg3 state with ack payload for potential resend.
// Use a dummy pubkey since we don't know the initiator's identity yet.
// We use our own pubkey as placeholder; it will be replaced in handle_session_msg3.
let placeholder_pubkey = self.identity.keypair().public_key();
let placeholder_pubkey = self.identity().keypair().public_key();
let now_ms = Self::now_ms();
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
let mut entry = SessionEntry::new(
*src_addr,
placeholder_pubkey,
@@ -687,7 +687,7 @@ impl Node {
let msg3_payload = msg3_wire.encode();
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *src_addr, msg3_payload.clone())
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
if let Err(e) = self.send_session_datagram(&mut datagram).await {
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send rekey SessionMsg3");
@@ -716,7 +716,7 @@ impl Node {
// before the responder receives msg3; overlapping-epoch
// decrypt keeps both directions safe meanwhile.
let now_ms = Self::now_ms();
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
entry.set_pending_session(session);
entry.set_rekey_completed_ms(now_ms);
entry.set_rekey_msg3_payload(msg3_payload, now_ms + resend_interval);
@@ -724,7 +724,7 @@ impl Node {
debug!(
src = %self.peer_display_name(src_addr),
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
their_addr = %src_addr,
"FSP rekey: completed XX as initiator, msg3 sent, pending cutover"
);
@@ -811,7 +811,7 @@ impl Node {
let msg3_payload = msg3_wire.encode();
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *src_addr, msg3_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
if let Err(e) = self.send_session_datagram(&mut datagram).await {
debug!(error = %e, dest = %self.peer_display_name(src_addr), "Failed to send SessionMsg3");
@@ -829,9 +829,9 @@ impl Node {
let now_ms = Self::now_ms();
entry.set_state(EndToEndState::Established(session));
entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
entry.set_coords_warmup_remaining(self.config().node.session.coords_warmup_packets);
entry.mark_established(now_ms);
entry.init_mmp(&self.config.node.session_mmp);
entry.init_mmp(&self.config().node.session_mmp);
entry.clear_handshake_payload();
entry.touch(now_ms);
self.sessions.insert(*src_addr, entry);
@@ -911,7 +911,7 @@ impl Node {
debug!(
src = %self.peer_display_name(src_addr),
our_addr = %self.identity.node_addr(),
our_addr = %self.identity().node_addr(),
their_addr = %src_addr,
"FSP rekey: completed XX as responder, pending cutover"
);
@@ -990,9 +990,9 @@ impl Node {
now_ms,
false,
);
new_entry.set_coords_warmup_remaining(self.config.node.session.coords_warmup_packets);
new_entry.set_coords_warmup_remaining(self.config().node.session.coords_warmup_packets);
new_entry.mark_established(now_ms);
new_entry.init_mmp(&self.config.node.session_mmp);
new_entry.init_mmp(&self.config().node.session_mmp);
new_entry.touch(now_ms);
self.sessions.insert(*src_addr, new_entry);
@@ -1192,7 +1192,7 @@ impl Node {
/// immediately (rate-limited), trigger discovery, and reset the
/// warmup counter for subsequent data packets.
async fn handle_coords_required(&mut self, inner: &[u8]) {
self.stats_mut().errors.coords_required += 1;
self.metrics().errors.coords_required.inc();
let msg = match CoordsRequired::decode(inner) {
Ok(m) => m,
@@ -1236,8 +1236,8 @@ impl Node {
// Reset coords warmup counter so the next N packets also include
// COORDS_PRESENT, re-warming transit caches along the path.
let n = self.config().node.session.coords_warmup_packets;
if let Some(entry) = self.sessions.get_mut(&msg.dest_addr) {
let n = self.config.node.session.coords_warmup_packets;
entry.set_coords_warmup_remaining(n);
debug!(
dest = %msg.dest_addr,
@@ -1253,7 +1253,7 @@ impl Node {
/// Send a standalone CoordsWarmup immediately (rate-limited), invalidate
/// cached coordinates, trigger re-discovery, and reset the warmup counter.
async fn handle_path_broken(&mut self, inner: &[u8]) {
self.stats_mut().errors.path_broken += 1;
self.metrics().errors.path_broken.inc();
let msg = match PathBroken::decode(inner) {
Ok(m) => m,
@@ -1302,8 +1302,8 @@ impl Node {
// Reset coords warmup counter so the next N packets include
// COORDS_PRESENT, re-warming transit caches along the new path.
let n = self.config().node.session.coords_warmup_packets;
if let Some(entry) = self.sessions.get_mut(&msg.dest_addr) {
let n = self.config.node.session.coords_warmup_packets;
entry.set_coords_warmup_remaining(n);
debug!(
dest = %msg.dest_addr,
@@ -1319,7 +1319,7 @@ impl Node {
/// next-hop transport MTU. Apply the reported bottleneck MTU to our
/// PathMtuState for the affected session, causing an immediate decrease.
pub(in crate::node) async fn handle_mtu_exceeded(&mut self, inner: &[u8]) {
self.stats_mut().errors.mtu_exceeded += 1;
self.metrics().errors.mtu_exceeded.inc();
let msg = match MtuExceeded::decode(inner) {
Ok(m) => m,
@@ -1417,7 +1417,7 @@ impl Node {
}
// Create Noise XX initiator handshake
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let mut handshake = HandshakeState::new_initiator(our_keypair);
handshake.set_local_epoch(self.startup_epoch);
let msg1 = handshake
@@ -1436,7 +1436,7 @@ impl Node {
// Wrap in SessionDatagram
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, dest_addr, setup_payload.clone())
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
// Route toward destination
self.send_session_datagram(&mut datagram).await?;
@@ -1446,7 +1446,7 @@ impl Node {
// Store session entry with handshake payload for potential resend
let now_ms = Self::now_ms();
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
let mut entry = SessionEntry::new(
dest_addr,
dest_pubkey,
@@ -1608,7 +1608,7 @@ impl Node {
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
self.send_session_datagram(&mut datagram).await?;
@@ -1836,7 +1836,7 @@ impl Node {
wire_buf.extend_from_slice(&timestamp_ms.to_le_bytes());
// SessionDatagram-encoded layout (matches `SessionDatagram::encode`):
wire_buf.push(LinkMessageType::SessionDatagram.to_byte());
wire_buf.push(self.config.node.session.default_ttl);
wire_buf.push(self.config().node.session.default_ttl);
wire_buf.extend_from_slice(&path_mtu.to_le_bytes());
wire_buf.extend_from_slice(self.node_addr().as_bytes());
wire_buf.extend_from_slice(dest_addr.as_bytes());
@@ -1860,7 +1860,7 @@ impl Node {
.record_sent(fmp_counter, timestamp_ms, predicted_bytes);
}
}
self.stats_mut()
self.metrics()
.forwarding
.record_originated(link_plaintext_len + crate::noise::TAG_SIZE);
@@ -1996,7 +1996,7 @@ impl Node {
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
self.send_session_datagram(&mut datagram).await?;
@@ -2080,7 +2080,7 @@ impl Node {
let my_addr = *self.node_addr();
let mut datagram = SessionDatagram::new(my_addr, *dest_addr, fsp_payload)
.with_ttl(self.config.node.session.default_ttl);
.with_ttl(self.config().node.session.default_ttl);
self.send_session_datagram(&mut datagram).await?;
@@ -2137,7 +2137,7 @@ impl Node {
let encoded = datagram.encode();
self.send_encrypted_link_message(&next_hop_addr, &encoded)
.await?;
self.stats_mut().forwarding.record_originated(encoded.len());
self.metrics().forwarding.record_originated(encoded.len());
Ok(())
}
@@ -2308,15 +2308,16 @@ impl Node {
/// Queue a packet while waiting for session establishment.
fn queue_pending_packet(&mut self, dest_addr: NodeAddr, packet: Vec<u8>) {
// Reject if we already have too many pending destinations
let max_dests = self.config.node.session.pending_max_destinations;
let max_dests = self.config().node.session.pending_max_destinations;
if !self.pending_tun_packets.contains_key(&dest_addr)
&& self.pending_tun_packets.len() >= max_dests
{
return;
}
let per_dest = self.config().node.session.pending_packets_per_dest;
let queue = self.pending_tun_packets.entry(dest_addr).or_default();
if queue.len() >= self.config.node.session.pending_packets_per_dest {
if queue.len() >= per_dest {
queue.pop_front(); // Drop oldest
}
queue.push_back(packet);
+10 -10
View File
@@ -17,7 +17,7 @@ impl Node {
}
let now_ms = Self::now_ms();
let timeout_ms = self.config.node.rate_limit.handshake_timeout_secs * 1000;
let timeout_ms = self.config().node.rate_limit.handshake_timeout_secs * 1000;
let stale: Vec<LinkId> = self
.connections
@@ -94,9 +94,9 @@ impl Node {
return;
}
let max_resends = self.config.node.rate_limit.handshake_max_resends;
let interval_ms = self.config.node.rate_limit.handshake_resend_interval_ms;
let backoff = self.config.node.rate_limit.handshake_resend_backoff;
let max_resends = self.config().node.rate_limit.handshake_max_resends;
let interval_ms = self.config().node.rate_limit.handshake_resend_interval_ms;
let backoff = self.config().node.rate_limit.handshake_resend_backoff;
// Collect resend candidates: outbound, in SentMsg1, with stored msg1,
// under max resends, and past the scheduled time.
@@ -173,11 +173,11 @@ impl Node {
return;
}
let timeout_ms = self.config.node.rate_limit.handshake_timeout_secs * 1000;
let max_resends = self.config.node.rate_limit.handshake_max_resends;
let interval_ms = self.config.node.rate_limit.handshake_resend_interval_ms;
let backoff = self.config.node.rate_limit.handshake_resend_backoff;
let ttl = self.config.node.session.default_ttl;
let timeout_ms = self.config().node.rate_limit.handshake_timeout_secs * 1000;
let max_resends = self.config().node.rate_limit.handshake_max_resends;
let interval_ms = self.config().node.rate_limit.handshake_resend_interval_ms;
let backoff = self.config().node.rate_limit.handshake_resend_backoff;
let ttl = self.config().node.session.default_ttl;
// First pass: find timed-out sessions to remove
let timed_out: Vec<crate::NodeAddr> = self
@@ -245,7 +245,7 @@ impl Node {
/// Only targets sessions in the Established state. Initiating/AwaitingMsg3
/// sessions are handled by the handshake timeout.
pub(in crate::node) fn purge_idle_sessions(&mut self, now_ms: u64) {
let timeout_ms = self.config.node.session.idle_timeout_secs * 1000;
let timeout_ms = self.config().node.session.idle_timeout_secs * 1000;
if timeout_ms == 0 {
return; // disabled
}
+63 -49
View File
@@ -55,6 +55,10 @@ impl Node {
new_by_addr.insert(*identity.node_addr(), peer);
}
// Read the current peer set directly from the field: update_peers is the
// config-source mutation owner (it writes self.config.peers below and then
// rebuilds the context), so it manages config.peers directly rather than
// through the context accessor — same rationale as the write at line ~124.
let current_by_addr: HashMap<NodeAddr, PeerConfig> = self
.config
.peers()
@@ -121,6 +125,7 @@ impl Node {
.collect();
self.config.peers = new_by_addr.into_values().collect();
self.rebuild_context();
for peer_config in added_configs {
outcome.added += 1;
@@ -171,11 +176,12 @@ impl Node {
} else {
match self.initiate_peer_connection(&peer_config).await {
Ok(()) => {
let handshake_timeout_secs =
self.config().node.rate_limit.handshake_timeout_secs;
if let Some(state) = self.retry_pending.get_mut(&node_addr) {
state.peer_config = peer_config;
state.retry_after_ms = Self::now_ms().saturating_add(
self.config.node.rate_limit.handshake_timeout_secs * 1000,
);
state.retry_after_ms =
Self::now_ms().saturating_add(handshake_timeout_secs * 1000);
}
}
Err(err) => {
@@ -204,7 +210,7 @@ impl Node {
// initiation) immediately on startup — without waiting for the link-layer
// handshake to complete first.
let peer_identities: Vec<(PeerIdentity, Option<String>)> = self
.config
.config()
.peers()
.iter()
.filter_map(|pc| {
@@ -224,7 +230,7 @@ impl Node {
}
// Collect peer configs to avoid borrow conflicts
let peer_configs: Vec<_> = self.config.auto_connect_peers().cloned().collect();
let peer_configs: Vec<_> = self.config().auto_connect_peers().cloned().collect();
if peer_configs.is_empty() {
debug!("No static peers configured");
@@ -400,7 +406,7 @@ impl Node {
transport_id,
remote_addr.clone(),
LinkDirection::Outbound,
Duration::from_millis(self.config.node.base_rtt_ms),
Duration::from_millis(self.config().node.base_rtt_ms),
)
} else {
Link::connectionless(
@@ -408,7 +414,7 @@ impl Node {
transport_id,
remote_addr.clone(),
LinkDirection::Outbound,
Duration::from_millis(self.config.node.base_rtt_ms),
Duration::from_millis(self.config().node.base_rtt_ms),
)
};
@@ -494,7 +500,7 @@ impl Node {
};
// Start the Noise handshake and get message 1
let our_keypair = self.identity.keypair();
let our_keypair = self.identity().keypair();
let noise_msg1 =
match connection.start_handshake(our_keypair, self.startup_epoch, current_time_ms) {
Ok(msg) => msg,
@@ -535,7 +541,7 @@ impl Node {
}
// Store msg1 for resend and schedule first resend
let resend_interval = self.config.node.rate_limit.handshake_resend_interval_ms;
let resend_interval = self.config().node.rate_limit.handshake_resend_interval_ms;
connection.set_handshake_msg1(wire_msg1.clone(), current_time_ms + resend_interval);
// Track in pending_outbound for msg2 dispatch
@@ -608,7 +614,7 @@ impl Node {
let node_addr = *identity.node_addr();
// Skip self
if node_addr == *self.identity.node_addr() {
if node_addr == *self.identity().node_addr() {
continue;
}
@@ -777,7 +783,7 @@ impl Node {
// semantics, and its existing cross-connection
// logic in handle_msg1 reconciles when the winner's
// fresh msg1 arrives over the adopted socket.
let our_addr = self.identity.node_addr();
let our_addr = self.identity().node_addr();
if our_addr >= &peer_addr {
debug!(
peer_npub = %peer_npub,
@@ -925,14 +931,14 @@ impl Node {
/// extracts a scope from the Nostr app tag used by default scoped
/// discovery.
pub(super) fn lan_discovery_scope(&self) -> Option<String> {
if let Some(scope) = self.config.node.discovery.lan.scope.as_deref() {
if let Some(scope) = self.config().node.discovery.lan.scope.as_deref() {
let scope = scope.trim();
if !scope.is_empty() {
return Some(scope.to_string());
}
}
let app = self.config.node.discovery.nostr.app.trim();
let app = self.config().node.discovery.nostr.app.trim();
if app.is_empty() {
return None;
}
@@ -1110,7 +1116,7 @@ impl Node {
self.state = NodeState::Starting;
// Create packet channel for transport -> Node communication
let packet_buffer_size = self.config.node.buffers.packet_channel;
let packet_buffer_size = self.config().node.buffers.packet_channel;
let (packet_tx, packet_rx) = packet_channel(packet_buffer_size);
self.packet_tx = Some(packet_tx.clone());
self.packet_rx = Some(packet_rx);
@@ -1187,8 +1193,8 @@ impl Node {
}
}
if self.config.node.discovery.nostr.enabled {
match NostrDiscovery::start(&self.identity, self.config.node.discovery.nostr.clone())
if self.config().node.discovery.nostr.enabled {
match NostrDiscovery::start(self.identity(), self.config().node.discovery.nostr.clone())
.await
{
Ok(runtime) => {
@@ -1208,7 +1214,7 @@ impl Node {
// mDNS / DNS-SD LAN discovery. Independent of Nostr — runs even
// when Nostr is disabled, since it gives us sub-second pairing
// on the same link without any relay or NAT-traversal roundtrip.
if self.config.node.discovery.lan.enabled {
if self.config().node.discovery.lan.enabled {
// Advertise the port of a non-bootstrap operational UDP transport.
// Bootstrap transports must be excluded (they are not the node's
// listening data-plane socket), and a stable selector (lowest
@@ -1229,10 +1235,10 @@ impl Node {
.unwrap_or(0);
let scope = self.lan_discovery_scope();
match crate::discovery::lan::LanDiscovery::start(
&self.identity,
self.identity(),
scope,
advertised_udp_port,
self.config.node.discovery.lan.clone(),
self.config().node.discovery.lan.clone(),
)
.await
{
@@ -1251,9 +1257,9 @@ impl Node {
self.initiate_peer_connections().await;
// Initialize TUN interface last, after transports and peers are ready
if self.config.tun.enabled {
let address = *self.identity.address();
match TunDevice::create(&self.config.tun, address).await {
if self.config().tun.enabled {
let address = *self.identity().address();
match TunDevice::create(&self.config().tun, address).await {
Ok(device) => {
let mtu = device.mtu();
let name = device.name().to_string();
@@ -1300,7 +1306,7 @@ impl Node {
let reader_tun_tx = tun_tx.clone();
// Create outbound channel for TUN reader → Node
let tun_channel_size = self.config.node.buffers.tun_channel;
let tun_channel_size = self.config().node.buffers.tun_channel;
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(tun_channel_size);
// Spawn reader thread
@@ -1366,19 +1372,19 @@ impl Node {
// `IPV6_V6ONLY=0` is set explicitly so IPv4 clients on
// 127.0.0.1 still reach us regardless of kernel sysctl
// defaults — but only when bind is on a wildcard / IPv6 path.
if self.config.dns.enabled {
let addr_str = self.config.dns.bind_addr();
if self.config().dns.enabled {
let addr_str = self.config().dns.bind_addr();
match addr_str.parse::<std::net::IpAddr>() {
Ok(ip) => {
let bind = std::net::SocketAddr::new(ip, self.config.dns.port());
let bind = std::net::SocketAddr::new(ip, self.config().dns.port());
match Self::bind_dns_socket(bind) {
Ok(socket) => {
let dns_channel_size = self.config.node.buffers.dns_channel;
let dns_channel_size = self.config().node.buffers.dns_channel;
let (identity_tx, identity_rx) =
tokio::sync::mpsc::channel(dns_channel_size);
let dns_ttl = self.config.dns.ttl();
let dns_ttl = self.config().dns.ttl();
let base_hosts = crate::upper::hosts::HostMap::from_peer_configs(
self.config.peers(),
self.config().peers(),
);
let hosts_path =
std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
@@ -1391,7 +1397,7 @@ impl Node {
// When TUN isn't enabled or the name can't be
// resolved, `None` disables the filter (there
// is no mesh surface to defend anyway).
let mesh_ifindex = Self::lookup_mesh_ifindex(self.config.tun.name());
let mesh_ifindex = Self::lookup_mesh_ifindex(self.config().tun.name());
info!(
bind = %bind,
hosts = reloader.hosts().len(),
@@ -1658,9 +1664,9 @@ impl Node {
peer_config: &PeerConfig,
existing: &[PeerAddress],
) -> Vec<PeerAddress> {
if !self.config.node.discovery.nostr.enabled
if !self.config().node.discovery.nostr.enabled
|| !peer_config.via_nostr
|| self.config.node.discovery.nostr.policy
|| self.config().node.discovery.nostr.policy
== crate::config::NostrDiscoveryPolicy::Disabled
{
return Vec::new();
@@ -1884,14 +1890,15 @@ impl Node {
max_age_secs: Option<u64>,
caller: &'static str,
) {
if !self.config.node.discovery.nostr.enabled
|| self.config.node.discovery.nostr.policy != crate::config::NostrDiscoveryPolicy::Open
if !self.config().node.discovery.nostr.enabled
|| self.config().node.discovery.nostr.policy
!= crate::config::NostrDiscoveryPolicy::Open
{
return;
}
let configured_npubs = self
.config
.config()
.peers()
.iter()
.map(|peer| peer.npub.clone())
@@ -1960,7 +1967,7 @@ impl Node {
}
};
let node_addr = *peer_identity.node_addr();
if node_addr == *self.identity.node_addr() {
if node_addr == *self.identity().node_addr() {
skipped_self = skipped_self.saturating_add(1);
continue;
}
@@ -2082,8 +2089,9 @@ impl Node {
if self.startup_open_discovery_sweep_done {
return;
}
if !self.config.node.discovery.nostr.enabled
|| self.config.node.discovery.nostr.policy != crate::config::NostrDiscoveryPolicy::Open
if !self.config().node.discovery.nostr.enabled
|| self.config().node.discovery.nostr.policy
!= crate::config::NostrDiscoveryPolicy::Open
{
// Mark done so we don't keep re-checking on every tick.
self.startup_open_discovery_sweep_done = true;
@@ -2094,7 +2102,7 @@ impl Node {
};
let now_ms = Self::now_ms();
let delay_ms = self
.config
.config()
.node
.discovery
.nostr
@@ -2104,7 +2112,12 @@ impl Node {
return;
}
let max_age_secs = self.config.node.discovery.nostr.startup_sweep_max_age_secs;
let max_age_secs = self
.config()
.node
.discovery
.nostr
.startup_sweep_max_age_secs;
self.run_open_discovery_sweep(bootstrap, Some(max_age_secs), "startup")
.await;
self.startup_open_discovery_sweep_done = true;
@@ -2198,7 +2211,7 @@ impl Node {
.count();
let cap_remaining = self
.config
.config()
.node
.discovery
.nostr
@@ -2210,7 +2223,7 @@ impl Node {
fn open_discovery_retry_expires_at_ms(&self, now_ms: u64) -> u64 {
now_ms.saturating_add(
self.config
self.config()
.node
.discovery
.nostr
@@ -2224,7 +2237,7 @@ impl Node {
&self,
bootstrap: &std::sync::Arc<NostrDiscovery>,
) -> Option<OverlayAdvert> {
if !self.config.node.discovery.nostr.enabled {
if !self.config().node.discovery.nostr.enabled {
return None;
}
@@ -2364,9 +2377,10 @@ impl Node {
identifier: ADVERT_IDENTIFIER.to_string(),
version: ADVERT_VERSION,
endpoints,
signal_relays: has_udp_nat.then(|| self.config.node.discovery.nostr.dm_relays.clone()),
signal_relays: has_udp_nat
.then(|| self.config().node.discovery.nostr.dm_relays.clone()),
stun_servers: has_udp_nat
.then(|| self.config.node.discovery.nostr.stun_servers.clone()),
.then(|| self.config().node.discovery.nostr.stun_servers.clone()),
})
}
@@ -2379,7 +2393,7 @@ impl Node {
}
fn lookup_udp_config(&self, transport_name: Option<&str>) -> Option<&crate::config::UdpConfig> {
match (&self.config.transports.udp, transport_name) {
match (&self.config().transports.udp, transport_name) {
(crate::config::TransportInstances::Single(cfg), None) => Some(cfg),
(crate::config::TransportInstances::Named(configs), Some(name)) => configs.get(name),
_ => None,
@@ -2387,7 +2401,7 @@ impl Node {
}
fn lookup_tcp_config(&self, transport_name: Option<&str>) -> Option<&crate::config::TcpConfig> {
match (&self.config.transports.tcp, transport_name) {
match (&self.config().transports.tcp, transport_name) {
(crate::config::TransportInstances::Single(cfg), None) => Some(cfg),
(crate::config::TransportInstances::Named(configs), Some(name)) => configs.get(name),
_ => None,
@@ -2395,7 +2409,7 @@ impl Node {
}
fn lookup_tor_config(&self, transport_name: Option<&str>) -> Option<&crate::config::TorConfig> {
match (&self.config.transports.tor, transport_name) {
match (&self.config().transports.tor, transport_name) {
(crate::config::TransportInstances::Single(cfg), None) => Some(cfg),
(crate::config::TransportInstances::Named(configs), Some(name)) => configs.get(name),
_ => None,
@@ -2529,7 +2543,7 @@ impl Node {
return false;
};
let stale_after_ms = self
.config
.config()
.node
.heartbeat_interval_secs
.saturating_mul(1000)
+493
View File
@@ -0,0 +1,493 @@
//! Lock-free metric counters backed by atomics.
//!
//! Mirrors the `NodeStats` counter surface (`stats.rs`) but stores each
//! counter in an `AtomicU64`, so it can be bumped through `&self` and, in
//! a later step, sampled without dispatching through the rx_loop task. The
//! hottest counters are cache-line padded to avoid false sharing once
//! reads move off-thread.
//!
//! The forwarding, discovery, tree, bloom, congestion, and error families
//! live here exclusively and are both written and served from the registry.
//! The remaining families (session, handshake, mmp, transport) stay on
//! `NodeStats`.
use std::sync::atomic::{AtomicU64, Ordering};
use crate::node::reject::{BloomReject, DiscoveryReject, ForwardingReject, TreeReject};
use crate::node::stats::{
BloomStatsSnapshot, CongestionStatsSnapshot, DiscoveryStatsSnapshot, ErrorSignalStatsSnapshot,
ForwardingStatsSnapshot, TreeStatsSnapshot,
};
/// An atomic counter.
#[derive(Default)]
pub struct Counter(AtomicU64);
impl Counter {
#[inline]
pub fn inc(&self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn add(&self, n: u64) {
self.0.fetch_add(n, Ordering::Relaxed);
}
#[inline]
pub fn get(&self) -> u64 {
self.0.load(Ordering::Relaxed)
}
}
/// Cache-line padding wrapper for the hottest counters.
///
/// Padding keeps a hot counter off shared cache lines so that concurrent
/// reads (introduced when metric sampling moves off the rx_loop task) do
/// not false-share with the writer. With a single writer today the padding
/// is forward-looking insurance. Derefs to the inner counter so the call
/// sites are identical to an unpadded one.
#[repr(align(64))]
#[derive(Default)]
pub struct Padded<T>(pub T);
impl<T> std::ops::Deref for Padded<T> {
type Target = T;
#[inline]
fn deref(&self) -> &T {
&self.0
}
}
/// Forwarding metric counters.
#[derive(Default)]
pub struct ForwardingMetrics {
pub received_packets: Padded<Counter>,
pub received_bytes: Counter,
pub decode_error_packets: Counter,
pub decode_error_bytes: Counter,
pub ttl_exhausted_packets: Counter,
pub ttl_exhausted_bytes: Counter,
pub delivered_packets: Counter,
pub delivered_bytes: Counter,
pub forwarded_packets: Counter,
pub forwarded_bytes: Counter,
pub drop_no_route_packets: Counter,
pub drop_no_route_bytes: Counter,
pub drop_mtu_exceeded_packets: Counter,
pub drop_mtu_exceeded_bytes: Counter,
pub drop_send_error_packets: Counter,
pub drop_send_error_bytes: Counter,
pub originated_packets: Counter,
pub originated_bytes: Counter,
}
impl ForwardingMetrics {
/// Record a received packet of `bytes` payload (packets and bytes).
#[inline]
pub fn record_received(&self, bytes: usize) {
self.received_packets.inc();
self.received_bytes.add(bytes as u64);
}
/// Record a locally-delivered packet of `bytes` payload.
#[inline]
pub fn record_delivered(&self, bytes: usize) {
self.delivered_packets.inc();
self.delivered_bytes.add(bytes as u64);
}
/// Record a forwarded (transit) packet of `bytes` payload.
#[inline]
pub fn record_forwarded(&self, bytes: usize) {
self.forwarded_packets.inc();
self.forwarded_bytes.add(bytes as u64);
}
/// Record a locally-originated packet of `bytes` payload.
#[inline]
pub fn record_originated(&self, bytes: usize) {
self.originated_packets.inc();
self.originated_bytes.add(bytes as u64);
}
/// Mirror of `ForwardingStats::record_reject_bytes`: route a typed
/// forwarding rejection of `bytes` payload to its packet and byte
/// counters.
#[inline]
pub fn record_reject_bytes(&self, reason: ForwardingReject, bytes: usize) {
let bytes = bytes as u64;
match reason {
ForwardingReject::DecodeError => {
self.decode_error_packets.inc();
self.decode_error_bytes.add(bytes);
}
ForwardingReject::TtlExhausted => {
self.ttl_exhausted_packets.inc();
self.ttl_exhausted_bytes.add(bytes);
}
ForwardingReject::NoRoute => {
self.drop_no_route_packets.inc();
self.drop_no_route_bytes.add(bytes);
}
ForwardingReject::MtuExceeded => {
self.drop_mtu_exceeded_packets.inc();
self.drop_mtu_exceeded_bytes.add(bytes);
}
ForwardingReject::SendError => {
self.drop_send_error_packets.inc();
self.drop_send_error_bytes.add(bytes);
}
}
}
/// Sample every counter into a serializable snapshot.
pub fn snapshot(&self) -> ForwardingStatsSnapshot {
ForwardingStatsSnapshot {
received_packets: self.received_packets.get(),
received_bytes: self.received_bytes.get(),
decode_error_packets: self.decode_error_packets.get(),
decode_error_bytes: self.decode_error_bytes.get(),
ttl_exhausted_packets: self.ttl_exhausted_packets.get(),
ttl_exhausted_bytes: self.ttl_exhausted_bytes.get(),
delivered_packets: self.delivered_packets.get(),
delivered_bytes: self.delivered_bytes.get(),
forwarded_packets: self.forwarded_packets.get(),
forwarded_bytes: self.forwarded_bytes.get(),
drop_no_route_packets: self.drop_no_route_packets.get(),
drop_no_route_bytes: self.drop_no_route_bytes.get(),
drop_mtu_exceeded_packets: self.drop_mtu_exceeded_packets.get(),
drop_mtu_exceeded_bytes: self.drop_mtu_exceeded_bytes.get(),
drop_send_error_packets: self.drop_send_error_packets.get(),
drop_send_error_bytes: self.drop_send_error_bytes.get(),
originated_packets: self.originated_packets.get(),
originated_bytes: self.originated_bytes.get(),
}
}
}
/// Discovery metric counters.
#[derive(Default)]
pub struct DiscoveryMetrics {
pub req_received: Padded<Counter>,
pub req_decode_error: Counter,
pub req_duplicate: Counter,
pub req_dedup_cache_full: Counter,
pub req_target_is_us: Counter,
pub req_forwarded: Counter,
pub req_ttl_exhausted: Counter,
pub req_initiated: Counter,
pub req_deduplicated: Counter,
pub req_backoff_suppressed: Counter,
pub req_forward_rate_limited: Counter,
pub req_bloom_miss: Counter,
pub req_no_tree_peer: Counter,
pub req_fallback_forwarded: Counter,
pub resp_received: Counter,
pub resp_decode_error: Counter,
pub resp_forwarded: Counter,
pub resp_identity_miss: Counter,
pub resp_proof_failed: Counter,
pub resp_no_route: Counter,
pub resp_accepted: Counter,
pub resp_timed_out: Counter,
}
impl DiscoveryMetrics {
/// Mirror of `DiscoveryStats::record_reject`: route a typed discovery
/// rejection to its counter.
#[inline]
pub fn record_reject(&self, reason: DiscoveryReject) {
match reason {
DiscoveryReject::ReqDecodeError => self.req_decode_error.inc(),
DiscoveryReject::ReqDuplicate => self.req_duplicate.inc(),
DiscoveryReject::ReqDedupCacheFull => self.req_dedup_cache_full.inc(),
DiscoveryReject::ReqTtlExhausted => self.req_ttl_exhausted.inc(),
DiscoveryReject::RespDecodeError => self.resp_decode_error.inc(),
DiscoveryReject::RespIdentityMiss => self.resp_identity_miss.inc(),
DiscoveryReject::RespProofFailed => self.resp_proof_failed.inc(),
DiscoveryReject::RespNoRoute => self.resp_no_route.inc(),
}
}
/// Sample every counter into a serializable snapshot.
pub fn snapshot(&self) -> DiscoveryStatsSnapshot {
DiscoveryStatsSnapshot {
req_received: self.req_received.get(),
req_decode_error: self.req_decode_error.get(),
req_duplicate: self.req_duplicate.get(),
req_dedup_cache_full: self.req_dedup_cache_full.get(),
req_target_is_us: self.req_target_is_us.get(),
req_forwarded: self.req_forwarded.get(),
req_ttl_exhausted: self.req_ttl_exhausted.get(),
req_initiated: self.req_initiated.get(),
req_deduplicated: self.req_deduplicated.get(),
req_backoff_suppressed: self.req_backoff_suppressed.get(),
req_forward_rate_limited: self.req_forward_rate_limited.get(),
req_bloom_miss: self.req_bloom_miss.get(),
req_no_tree_peer: self.req_no_tree_peer.get(),
req_fallback_forwarded: self.req_fallback_forwarded.get(),
resp_received: self.resp_received.get(),
resp_decode_error: self.resp_decode_error.get(),
resp_forwarded: self.resp_forwarded.get(),
resp_identity_miss: self.resp_identity_miss.get(),
resp_proof_failed: self.resp_proof_failed.get(),
resp_no_route: self.resp_no_route.get(),
resp_accepted: self.resp_accepted.get(),
resp_timed_out: self.resp_timed_out.get(),
}
}
}
/// Spanning-tree metric counters.
#[derive(Default)]
pub struct TreeMetrics {
pub received: Counter,
pub decode_error: Counter,
pub unknown_peer: Counter,
pub addr_mismatch: Counter,
pub sig_failed: Counter,
pub stale: Counter,
pub ancestry_invalid: Counter,
pub accepted: Counter,
pub parent_switched: Counter,
pub loop_detected: Counter,
pub ancestry_changed: Counter,
pub sent: Counter,
pub rate_limited: Counter,
pub send_failed: Counter,
pub outbound_sign_failed: Counter,
pub parent_switches: Counter,
pub parent_losses: Counter,
pub flap_dampened: Counter,
}
impl TreeMetrics {
/// Mirror of `TreeStats::record_reject`: route a typed tree
/// rejection to its counter.
#[inline]
pub fn record_reject(&self, reason: TreeReject) {
match reason {
TreeReject::AncestryInvalid => self.ancestry_invalid.inc(),
TreeReject::OutboundSignFailed => self.outbound_sign_failed.inc(),
}
}
/// Sample every counter into a serializable snapshot.
pub fn snapshot(&self) -> TreeStatsSnapshot {
TreeStatsSnapshot {
received: self.received.get(),
decode_error: self.decode_error.get(),
unknown_peer: self.unknown_peer.get(),
addr_mismatch: self.addr_mismatch.get(),
sig_failed: self.sig_failed.get(),
stale: self.stale.get(),
ancestry_invalid: self.ancestry_invalid.get(),
accepted: self.accepted.get(),
parent_switched: self.parent_switched.get(),
loop_detected: self.loop_detected.get(),
ancestry_changed: self.ancestry_changed.get(),
sent: self.sent.get(),
rate_limited: self.rate_limited.get(),
send_failed: self.send_failed.get(),
outbound_sign_failed: self.outbound_sign_failed.get(),
parent_switches: self.parent_switches.get(),
parent_losses: self.parent_losses.get(),
flap_dampened: self.flap_dampened.get(),
}
}
}
/// Bloom-filter metric counters.
#[derive(Default)]
pub struct BloomMetrics {
pub received: Counter,
pub decode_error: Counter,
pub invalid: Counter,
pub non_v1: Counter,
pub unknown_peer: Counter,
pub stale: Counter,
pub fill_exceeded: Counter,
pub accepted: Counter,
pub sent: Counter,
pub debounce_suppressed: Counter,
pub send_failed: Counter,
// Delta compression
pub deltas_sent: Counter,
pub full_sends: Counter,
pub nacks_sent: Counter,
pub nacks_received: Counter,
// Adaptive sizing
pub size_changes: Counter,
// Compression tracking
pub total_compressed_bytes: Counter,
pub total_raw_bytes: Counter,
}
impl BloomMetrics {
/// Mirror of `BloomStats::record_reject`: route a typed bloom
/// rejection to its counter.
#[inline]
pub fn record_reject(&self, reason: BloomReject) {
match reason {
BloomReject::DecodeError => self.decode_error.inc(),
BloomReject::Invalid => self.invalid.inc(),
BloomReject::NonV1 => self.non_v1.inc(),
BloomReject::UnknownPeer => self.unknown_peer.inc(),
BloomReject::Stale => self.stale.inc(),
BloomReject::FillExceeded => self.fill_exceeded.inc(),
}
}
/// Sample every counter into a serializable snapshot.
pub fn snapshot(&self) -> BloomStatsSnapshot {
BloomStatsSnapshot {
received: self.received.get(),
decode_error: self.decode_error.get(),
invalid: self.invalid.get(),
non_v1: self.non_v1.get(),
unknown_peer: self.unknown_peer.get(),
stale: self.stale.get(),
fill_exceeded: self.fill_exceeded.get(),
accepted: self.accepted.get(),
sent: self.sent.get(),
debounce_suppressed: self.debounce_suppressed.get(),
send_failed: self.send_failed.get(),
deltas_sent: self.deltas_sent.get(),
full_sends: self.full_sends.get(),
nacks_sent: self.nacks_sent.get(),
nacks_received: self.nacks_received.get(),
size_changes: self.size_changes.get(),
total_compressed_bytes: self.total_compressed_bytes.get(),
total_raw_bytes: self.total_raw_bytes.get(),
}
}
}
/// Congestion metric counters.
#[derive(Default)]
pub struct CongestionMetrics {
pub ce_forwarded: Counter,
pub ce_received: Counter,
pub congestion_detected: Counter,
pub kernel_drop_events: Counter,
}
impl CongestionMetrics {
/// Sample every counter into a serializable snapshot.
pub fn snapshot(&self) -> CongestionStatsSnapshot {
CongestionStatsSnapshot {
ce_forwarded: self.ce_forwarded.get(),
ce_received: self.ce_received.get(),
congestion_detected: self.congestion_detected.get(),
kernel_drop_events: self.kernel_drop_events.get(),
}
}
}
/// Error-signal metric counters.
#[derive(Default)]
pub struct ErrorMetrics {
pub coords_required: Counter,
pub path_broken: Counter,
pub mtu_exceeded: Counter,
}
impl ErrorMetrics {
/// Sample every counter into a serializable snapshot.
pub fn snapshot(&self) -> ErrorSignalStatsSnapshot {
ErrorSignalStatsSnapshot {
coords_required: self.coords_required.get(),
path_broken: self.path_broken.get(),
mtu_exceeded: self.mtu_exceeded.get(),
}
}
}
/// Atomic counter registry shared across the node via `Arc`.
///
/// Sole storage for the forwarding, discovery, tree, bloom, congestion,
/// and error counter families; these were migrated off `NodeStats`, which
/// now holds only the session, handshake, mmp, and transport families.
#[derive(Default)]
pub struct MetricsRegistry {
pub forwarding: ForwardingMetrics,
pub discovery: DiscoveryMetrics,
pub tree: TreeMetrics,
pub bloom: BloomMetrics,
pub congestion: CongestionMetrics,
pub errors: ErrorMetrics,
}
impl MetricsRegistry {
pub fn new() -> Self {
Self::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forwarding_received_tracks_packets_and_bytes() {
let m = ForwardingMetrics::default();
m.record_received(100);
m.record_received(40);
assert_eq!(m.received_packets.get(), 2);
assert_eq!(m.received_bytes.get(), 140);
}
#[test]
fn discovery_record_reject_routes_to_field() {
let m = DiscoveryMetrics::default();
m.record_reject(DiscoveryReject::ReqDuplicate);
m.record_reject(DiscoveryReject::ReqDuplicate);
m.record_reject(DiscoveryReject::RespNoRoute);
assert_eq!(m.req_duplicate.get(), 2);
assert_eq!(m.resp_no_route.get(), 1);
assert_eq!(m.req_decode_error.get(), 0);
}
#[test]
fn discovery_direct_counters_increment() {
let m = DiscoveryMetrics::default();
m.req_received.inc();
m.req_forwarded.inc();
m.req_forwarded.inc();
assert_eq!(m.req_received.get(), 1);
assert_eq!(m.req_forwarded.get(), 2);
}
#[test]
fn tree_record_reject_routes_to_field() {
let m = TreeMetrics::default();
m.record_reject(TreeReject::OutboundSignFailed);
m.record_reject(TreeReject::OutboundSignFailed);
m.record_reject(TreeReject::AncestryInvalid);
assert_eq!(m.outbound_sign_failed.get(), 2);
assert_eq!(m.ancestry_invalid.get(), 1);
}
#[test]
fn bloom_record_reject_routes_to_field() {
let m = BloomMetrics::default();
m.record_reject(BloomReject::Stale);
m.record_reject(BloomReject::Stale);
m.record_reject(BloomReject::DecodeError);
assert_eq!(m.stale.get(), 2);
assert_eq!(m.decode_error.get(), 1);
assert_eq!(m.invalid.get(), 0);
}
#[test]
fn registry_subcounters_are_independent() {
let r = MetricsRegistry::new();
r.forwarding.record_received(10);
r.discovery.req_received.inc();
assert_eq!(r.forwarding.received_packets.get(), 1);
assert_eq!(r.forwarding.received_bytes.get(), 10);
assert_eq!(r.discovery.req_received.get(), 1);
}
}
+84 -13
View File
@@ -6,6 +6,7 @@
mod acl;
mod bloom;
mod context;
#[cfg(unix)]
pub(crate) mod decrypt_worker;
mod discovery_rate_limit;
@@ -13,6 +14,7 @@ mod discovery_rate_limit;
pub(crate) mod encrypt_worker;
mod handlers;
mod lifecycle;
pub(crate) mod metrics;
mod rate_limit;
pub(crate) mod reject;
mod reloadable;
@@ -319,6 +321,13 @@ pub struct Node {
/// Loaded configuration.
config: Config,
/// Shared immutable context bundle. A parallel, authoritative copy of the
/// effectively-immutable fields (config/identity/startup_epoch/started_at/
/// is_leaf_only/max_*), kept in lockstep with the `Node` fields via
/// `rebuild_context`. Readers migrate onto it in later sub-PRs; the
/// duplicated `Node` fields are removed once the last reader has moved.
context: Arc<context::NodeContext>,
// === State ===
/// Node operational state.
state: NodeState,
@@ -413,6 +422,10 @@ pub struct Node {
/// Routing, forwarding, discovery, and error signal counters.
stats: stats::NodeStats,
/// Lock-free atomic metric counters. Shadows `stats` during the
/// counter migration; bumped alongside it with a parity check.
metrics: std::sync::Arc<metrics::MetricsRegistry>,
/// Time-series history of node-level metrics (1s/1m rings).
stats_history: stats_history::StatsHistory,
@@ -649,11 +662,25 @@ impl Node {
let (decrypt_fallback_tx, decrypt_fallback_rx) =
tokio::sync::mpsc::unbounded_channel::<decrypt_worker::DecryptWorkerEvent>();
let started_at = std::time::Instant::now();
let context = Arc::new(context::NodeContext::new(
Arc::new(config.clone()),
identity.clone(),
startup_epoch,
started_at,
is_leaf_only,
node_profile,
max_connections,
max_peers,
max_links,
));
Ok(Self {
identity,
startup_epoch,
started_at: std::time::Instant::now(),
started_at,
config,
context,
state: NodeState::Created,
is_leaf_only,
node_profile,
@@ -679,6 +706,7 @@ impl Node {
next_link_id: 1,
next_transport_id: 1,
stats: stats::NodeStats::new(),
metrics: std::sync::Arc::new(metrics::MetricsRegistry::new()),
stats_history: stats_history::StatsHistory::new(),
tun_state,
tun_name: None,
@@ -797,11 +825,25 @@ impl Node {
let (decrypt_fallback_tx, decrypt_fallback_rx) =
tokio::sync::mpsc::unbounded_channel::<decrypt_worker::DecryptWorkerEvent>();
let started_at = std::time::Instant::now();
let context = Arc::new(context::NodeContext::new(
Arc::new(config.clone()),
identity.clone(),
startup_epoch,
started_at,
false,
NodeProfile::Full,
max_connections,
max_peers,
max_links,
));
Ok(Self {
identity,
startup_epoch,
started_at: std::time::Instant::now(),
started_at,
config,
context,
state: NodeState::Created,
is_leaf_only: false,
node_profile: NodeProfile::Full,
@@ -827,6 +869,7 @@ impl Node {
next_link_id: 1,
next_transport_id: 1,
stats: stats::NodeStats::new(),
metrics: std::sync::Arc::new(metrics::MetricsRegistry::new()),
stats_history: stats_history::StatsHistory::new(),
tun_state,
tun_name: None,
@@ -886,6 +929,7 @@ impl Node {
node.is_leaf_only = true;
node.node_profile = NodeProfile::Leaf;
node.bloom_state = BloomState::leaf_only(*node.identity.node_addr());
node.rebuild_context();
Ok(node)
}
@@ -1100,7 +1144,7 @@ impl Node {
/// Get this node's identity.
pub fn identity(&self) -> &Identity {
&self.identity
&self.context.identity
}
/// Get this node's NodeAddr.
@@ -1150,7 +1194,26 @@ impl Node {
/// Get the configuration.
pub fn config(&self) -> &Config {
&self.config
self.context.config.as_ref()
}
/// Rebuild the shared [`context::NodeContext`] from the current `Node`
/// fields. Called after any mutation of a bundled field (`update_peers`,
/// the `set_max_*` setters) so `self.context` stays equal to the `Node`
/// fields it mirrors. Cheap — the only deep copy is the (rare) `Config`
/// clone.
fn rebuild_context(&mut self) {
self.context = Arc::new(context::NodeContext::new(
Arc::new(self.config.clone()),
self.identity.clone(),
self.startup_epoch,
self.started_at,
self.is_leaf_only,
self.node_profile,
self.max_connections,
self.max_peers,
self.max_links,
));
}
/// Calculate the effective IPv6 MTU that can be sent over FIPS.
@@ -1204,7 +1267,7 @@ impl Node {
/// Get the node uptime.
pub fn uptime(&self) -> std::time::Duration {
self.started_at.elapsed()
self.context.started_at.elapsed()
}
/// Check if node is operational.
@@ -1214,12 +1277,12 @@ impl Node {
/// Check if this is a leaf-only node.
pub fn is_leaf_only(&self) -> bool {
self.is_leaf_only
self.context.is_leaf_only
}
/// Get the node's profile (Full, NonRouting, Leaf).
pub fn node_profile(&self) -> NodeProfile {
self.node_profile
self.context.node_profile
}
/// Collect the set of peers that are not full nodes (non-routing/leaf).
@@ -1376,6 +1439,11 @@ impl Node {
&mut self.stats
}
/// Get the atomic metric registry.
pub(crate) fn metrics(&self) -> &metrics::MetricsRegistry {
&self.metrics
}
/// Get the stats history collector.
pub fn stats_history(&self) -> &stats_history::StatsHistory {
&self.stats_history
@@ -1384,7 +1452,7 @@ impl Node {
/// Sample the current node state into the stats history ring.
/// Called once per tick from the RX loop.
pub(crate) fn record_stats_history(&mut self) {
let fwd = &self.stats.forwarding;
let fwd = &self.metrics.forwarding;
let peers_with_mmp: Vec<f64> = self
.peers
.values()
@@ -1400,11 +1468,11 @@ impl Node {
mesh_size: self.estimated_mesh_size,
tree_depth: self.tree_state.my_coords().depth() as u32,
peer_count: self.peers.len() as u64,
parent_switches_total: self.stats.tree.parent_switches,
bytes_in_total: fwd.received_bytes,
bytes_out_total: fwd.forwarded_bytes + fwd.originated_bytes,
packets_in_total: fwd.received_packets,
packets_out_total: fwd.forwarded_packets + fwd.originated_packets,
parent_switches_total: self.metrics.tree.parent_switches.get(),
bytes_in_total: fwd.received_bytes.get(),
bytes_out_total: fwd.forwarded_bytes.get() + fwd.originated_bytes.get(),
packets_in_total: fwd.received_packets.get(),
packets_out_total: fwd.forwarded_packets.get() + fwd.originated_packets.get(),
loss_rate,
active_sessions: self.sessions.len() as u64,
};
@@ -1457,11 +1525,13 @@ impl Node {
/// Set the maximum number of connections (handshake phase).
pub fn set_max_connections(&mut self, max: usize) {
self.max_connections = max;
self.rebuild_context();
}
/// Set the maximum number of peers (authenticated).
pub fn set_max_peers(&mut self, max: usize) {
self.max_peers = max;
self.rebuild_context();
}
/// Returns false when we are at or above the configured `max_peers`
@@ -1479,6 +1549,7 @@ impl Node {
/// Set the maximum number of links.
pub fn set_max_links(&mut self, max: usize) {
self.max_links = max;
self.rebuild_context();
}
// === Counts ===
+7 -7
View File
@@ -66,7 +66,7 @@ impl Node {
/// indefinitely). Does nothing if the peer is already connected or has
/// a connection in progress.
pub(super) fn schedule_retry(&mut self, node_addr: NodeAddr, now_ms: u64) {
let retry_cfg = &self.config.node.retry;
let retry_cfg = &self.config().node.retry;
let max_retries = retry_cfg.max_retries;
if max_retries == 0 {
return;
@@ -105,7 +105,7 @@ impl Node {
} else {
// First failure — find the matching PeerConfig
let peer_config = self
.config
.config()
.auto_connect_peers()
.find(|pc| {
PeerIdentity::from_npub(&pc.npub)
@@ -144,7 +144,7 @@ impl Node {
pub(super) fn schedule_reconnect(&mut self, node_addr: NodeAddr, now_ms: u64) {
// Find peer in auto-connect config
let peer_config = self
.config
.config()
.auto_connect_peers()
.find(|pc| {
PeerIdentity::from_npub(&pc.npub)
@@ -165,8 +165,8 @@ impl Node {
return;
}
let base_interval_ms = self.config.node.retry.base_interval_secs * 1000;
let max_backoff_ms = self.config.node.retry.max_backoff_secs * 1000;
let base_interval_ms = self.config().node.retry.base_interval_secs * 1000;
let max_backoff_ms = self.config().node.retry.max_backoff_secs * 1000;
let peer_name = self.peer_display_name(&node_addr);
// If we already have accumulated backoff from previous failed attempts,
@@ -305,14 +305,14 @@ impl Node {
// succeeds, promote_connection() clears retry_pending. If
// it times out, check_timeouts() calls schedule_retry()
// which bumps the counter and applies proper backoff.
let hs_timeout_ms = self.config.node.rate_limit.handshake_timeout_secs * 1000;
let hs_timeout_ms = self.config().node.rate_limit.handshake_timeout_secs * 1000;
if let Some(state) = self.retry_pending.get_mut(&node_addr) {
state.retry_after_ms = now_ms + hs_timeout_ms;
}
debug!(
peer = %self.peer_display_name(&node_addr),
"Retry connection initiated, suppressing re-fire for {}s",
self.config.node.rate_limit.handshake_timeout_secs,
self.config().node.rate_limit.handshake_timeout_secs,
);
}
Err(e) => {
+24 -600
View File
@@ -1,4 +1,7 @@
//! Node-level statistics for routing, forwarding, and discovery operations.
//! Node-level statistics for the session, handshake, mmp, and transport
//! families. The forwarding, discovery, tree, bloom, congestion, and error
//! families have migrated to the atomic
//! [`MetricsRegistry`](crate::node::metrics::MetricsRegistry).
//!
//! Unlike `EthernetStats` (which uses `AtomicU64` + `Arc` for cross-task
//! sharing), these counters use plain `u64` because `Node` handlers run
@@ -8,319 +11,9 @@
use serde::Serialize;
use crate::node::reject::{
BloomReject, DiscoveryReject, ForwardingReject, HandshakeReject, MmpReject, RejectReason,
SessionReject, TransportReject, TreeReject,
HandshakeReject, MmpReject, RejectReason, SessionReject, TransportReject,
};
/// Forwarding statistics — packets and bytes for each outcome.
#[derive(Default)]
pub struct ForwardingStats {
pub received_packets: u64,
pub received_bytes: u64,
pub decode_error_packets: u64,
pub decode_error_bytes: u64,
pub ttl_exhausted_packets: u64,
pub ttl_exhausted_bytes: u64,
pub delivered_packets: u64,
pub delivered_bytes: u64,
pub forwarded_packets: u64,
pub forwarded_bytes: u64,
pub drop_no_route_packets: u64,
pub drop_no_route_bytes: u64,
pub drop_mtu_exceeded_packets: u64,
pub drop_mtu_exceeded_bytes: u64,
pub drop_send_error_packets: u64,
pub drop_send_error_bytes: u64,
pub originated_packets: u64,
pub originated_bytes: u64,
}
impl ForwardingStats {
pub fn record_received(&mut self, bytes: usize) {
self.received_packets += 1;
self.received_bytes += bytes as u64;
}
pub fn record_decode_error(&mut self, bytes: usize) {
self.decode_error_packets += 1;
self.decode_error_bytes += bytes as u64;
}
pub fn record_ttl_exhausted(&mut self, bytes: usize) {
self.ttl_exhausted_packets += 1;
self.ttl_exhausted_bytes += bytes as u64;
}
pub fn record_delivered(&mut self, bytes: usize) {
self.delivered_packets += 1;
self.delivered_bytes += bytes as u64;
}
pub fn record_forwarded(&mut self, bytes: usize) {
self.forwarded_packets += 1;
self.forwarded_bytes += bytes as u64;
}
pub fn record_drop_no_route(&mut self, bytes: usize) {
self.drop_no_route_packets += 1;
self.drop_no_route_bytes += bytes as u64;
}
pub fn record_drop_mtu_exceeded(&mut self, bytes: usize) {
self.drop_mtu_exceeded_packets += 1;
self.drop_mtu_exceeded_bytes += bytes as u64;
}
pub fn record_drop_send_error(&mut self, bytes: usize) {
self.drop_send_error_packets += 1;
self.drop_send_error_bytes += bytes as u64;
}
pub fn record_originated(&mut self, bytes: usize) {
self.originated_packets += 1;
self.originated_bytes += bytes as u64;
}
/// Dispatch a typed forwarding rejection to its packet counter.
///
/// The byte-counted side of each outcome is recorded by the
/// existing `record_*` methods at the call site (which know the
/// payload size); `record_reject` only bumps the packet count and
/// is paired with the byte-aware call at the call site for the
/// duration of the typed-rejection rollout. A later change may collapse the
/// two calls into a single typed entry point.
pub(super) fn record_reject(&mut self, reason: ForwardingReject) {
match reason {
ForwardingReject::DecodeError => self.decode_error_packets += 1,
ForwardingReject::TtlExhausted => self.ttl_exhausted_packets += 1,
ForwardingReject::NoRoute => self.drop_no_route_packets += 1,
ForwardingReject::MtuExceeded => self.drop_mtu_exceeded_packets += 1,
ForwardingReject::SendError => self.drop_send_error_packets += 1,
}
}
pub fn snapshot(&self) -> ForwardingStatsSnapshot {
ForwardingStatsSnapshot {
received_packets: self.received_packets,
received_bytes: self.received_bytes,
decode_error_packets: self.decode_error_packets,
decode_error_bytes: self.decode_error_bytes,
ttl_exhausted_packets: self.ttl_exhausted_packets,
ttl_exhausted_bytes: self.ttl_exhausted_bytes,
delivered_packets: self.delivered_packets,
delivered_bytes: self.delivered_bytes,
forwarded_packets: self.forwarded_packets,
forwarded_bytes: self.forwarded_bytes,
drop_no_route_packets: self.drop_no_route_packets,
drop_no_route_bytes: self.drop_no_route_bytes,
drop_mtu_exceeded_packets: self.drop_mtu_exceeded_packets,
drop_mtu_exceeded_bytes: self.drop_mtu_exceeded_bytes,
drop_send_error_packets: self.drop_send_error_packets,
drop_send_error_bytes: self.drop_send_error_bytes,
originated_packets: self.originated_packets,
originated_bytes: self.originated_bytes,
}
}
}
/// Discovery statistics — packet counts for request and response handling.
#[derive(Default)]
pub struct DiscoveryStats {
// Request counters
pub req_received: u64,
pub req_decode_error: u64,
pub req_duplicate: u64,
pub req_dedup_cache_full: u64,
pub req_target_is_us: u64,
pub req_forwarded: u64,
pub req_ttl_exhausted: u64,
pub req_initiated: u64,
pub req_deduplicated: u64,
pub req_backoff_suppressed: u64,
pub req_forward_rate_limited: u64,
pub req_bloom_miss: u64,
pub req_no_tree_peer: u64,
pub req_fallback_forwarded: u64,
// Response counters
pub resp_received: u64,
pub resp_decode_error: u64,
pub resp_forwarded: u64,
pub resp_identity_miss: u64,
pub resp_proof_failed: u64,
pub resp_no_route: u64,
pub resp_accepted: u64,
pub resp_timed_out: u64,
}
impl DiscoveryStats {
pub(super) fn record_reject(&mut self, reason: DiscoveryReject) {
match reason {
DiscoveryReject::ReqDecodeError => self.req_decode_error += 1,
DiscoveryReject::ReqDuplicate => self.req_duplicate += 1,
DiscoveryReject::ReqDedupCacheFull => self.req_dedup_cache_full += 1,
DiscoveryReject::ReqTtlExhausted => self.req_ttl_exhausted += 1,
DiscoveryReject::RespDecodeError => self.resp_decode_error += 1,
DiscoveryReject::RespIdentityMiss => self.resp_identity_miss += 1,
DiscoveryReject::RespProofFailed => self.resp_proof_failed += 1,
DiscoveryReject::RespNoRoute => self.resp_no_route += 1,
}
}
pub fn snapshot(&self) -> DiscoveryStatsSnapshot {
DiscoveryStatsSnapshot {
req_received: self.req_received,
req_decode_error: self.req_decode_error,
req_duplicate: self.req_duplicate,
req_dedup_cache_full: self.req_dedup_cache_full,
req_target_is_us: self.req_target_is_us,
req_forwarded: self.req_forwarded,
req_ttl_exhausted: self.req_ttl_exhausted,
req_initiated: self.req_initiated,
req_deduplicated: self.req_deduplicated,
req_backoff_suppressed: self.req_backoff_suppressed,
req_forward_rate_limited: self.req_forward_rate_limited,
req_bloom_miss: self.req_bloom_miss,
req_no_tree_peer: self.req_no_tree_peer,
req_fallback_forwarded: self.req_fallback_forwarded,
resp_received: self.resp_received,
resp_decode_error: self.resp_decode_error,
resp_forwarded: self.resp_forwarded,
resp_identity_miss: self.resp_identity_miss,
resp_proof_failed: self.resp_proof_failed,
resp_no_route: self.resp_no_route,
resp_accepted: self.resp_accepted,
resp_timed_out: self.resp_timed_out,
}
}
}
/// Spanning tree statistics — announce handling and parent tracking.
#[derive(Default)]
pub struct TreeStats {
// Inbound announce handling
pub received: u64,
pub decode_error: u64,
pub unknown_peer: u64,
pub addr_mismatch: u64,
pub sig_failed: u64,
pub stale: u64,
pub ancestry_invalid: u64,
pub accepted: u64,
pub parent_switched: u64,
pub loop_detected: u64,
pub ancestry_changed: u64,
// Outbound announce sending
pub sent: u64,
pub rate_limited: u64,
pub send_failed: u64,
pub outbound_sign_failed: u64,
// Cumulative events
pub parent_switches: u64,
pub parent_losses: u64,
pub flap_dampened: u64,
}
impl TreeStats {
pub fn snapshot(&self) -> TreeStatsSnapshot {
TreeStatsSnapshot {
received: self.received,
decode_error: self.decode_error,
unknown_peer: self.unknown_peer,
addr_mismatch: self.addr_mismatch,
sig_failed: self.sig_failed,
stale: self.stale,
ancestry_invalid: self.ancestry_invalid,
accepted: self.accepted,
parent_switched: self.parent_switched,
loop_detected: self.loop_detected,
ancestry_changed: self.ancestry_changed,
sent: self.sent,
rate_limited: self.rate_limited,
send_failed: self.send_failed,
outbound_sign_failed: self.outbound_sign_failed,
parent_switches: self.parent_switches,
parent_losses: self.parent_losses,
flap_dampened: self.flap_dampened,
}
}
pub(super) fn record_reject(&mut self, reason: TreeReject) {
match reason {
TreeReject::AncestryInvalid => self.ancestry_invalid += 1,
TreeReject::OutboundSignFailed => self.outbound_sign_failed += 1,
}
}
}
/// Bloom filter statistics — filter announce handling.
#[derive(Default)]
pub struct BloomStats {
// Inbound announce handling
pub received: u64,
pub decode_error: u64,
pub invalid: u64,
/// Non-v1-compliant size class. Reserved for symmetry with the
/// master-side BloomStats shape; the next-side handler does not
/// presently check `is_v1_compliant()` so the counter stays at
/// zero. Kept as a field so `BloomReject::NonV1` dispatches to a
/// real target and the cross-line enum shape is identical.
pub non_v1: u64,
pub unknown_peer: u64,
pub stale: u64,
pub fill_exceeded: u64,
pub accepted: u64,
// Outbound announce sending
pub sent: u64,
pub debounce_suppressed: u64,
pub send_failed: u64,
// Delta compression
pub deltas_sent: u64,
pub full_sends: u64,
pub nacks_sent: u64,
pub nacks_received: u64,
// Adaptive sizing
pub size_changes: u64,
// Compression tracking
pub total_compressed_bytes: u64,
pub total_raw_bytes: u64,
}
impl BloomStats {
pub(super) fn record_reject(&mut self, reason: BloomReject) {
match reason {
BloomReject::DecodeError => self.decode_error += 1,
BloomReject::Invalid => self.invalid += 1,
BloomReject::NonV1 => self.non_v1 += 1,
BloomReject::UnknownPeer => self.unknown_peer += 1,
BloomReject::Stale => self.stale += 1,
BloomReject::FillExceeded => self.fill_exceeded += 1,
}
}
pub fn snapshot(&self) -> BloomStatsSnapshot {
BloomStatsSnapshot {
received: self.received,
decode_error: self.decode_error,
invalid: self.invalid,
non_v1: self.non_v1,
unknown_peer: self.unknown_peer,
stale: self.stale,
fill_exceeded: self.fill_exceeded,
accepted: self.accepted,
sent: self.sent,
debounce_suppressed: self.debounce_suppressed,
send_failed: self.send_failed,
deltas_sent: self.deltas_sent,
full_sends: self.full_sends,
nacks_sent: self.nacks_sent,
nacks_received: self.nacks_received,
size_changes: self.size_changes,
total_compressed_bytes: self.total_compressed_bytes,
total_raw_bytes: self.total_raw_bytes,
}
}
}
/// FSP session statistics — receive-path silent-rejection counters.
///
/// Covers the unknown-session and state-machine-mismatch rejection
@@ -468,77 +161,19 @@ impl TransportStats {
}
}
/// Error signal statistics — counts of each error signal type received.
#[derive(Default)]
pub struct ErrorSignalStats {
pub coords_required: u64,
pub path_broken: u64,
pub mtu_exceeded: u64,
}
impl ErrorSignalStats {
pub fn snapshot(&self) -> ErrorSignalStatsSnapshot {
ErrorSignalStatsSnapshot {
coords_required: self.coords_required,
path_broken: self.path_broken,
mtu_exceeded: self.mtu_exceeded,
}
}
}
/// Congestion event statistics — ECN CE tracking and detection triggers.
#[derive(Default)]
pub struct CongestionStats {
/// Packets forwarded with the CE flag set (incoming CE or locally detected).
pub ce_forwarded: u64,
/// CE-flagged packets received at this node as final destination.
pub ce_received: u64,
/// Number of times detect_congestion() returned true.
pub congestion_detected: u64,
/// Rising-edge transport kernel drop events (not-dropping → dropping).
pub kernel_drop_events: u64,
}
impl CongestionStats {
pub fn record_ce_forwarded(&mut self) {
self.ce_forwarded += 1;
}
pub fn record_ce_received(&mut self) {
self.ce_received += 1;
}
pub fn record_congestion_detected(&mut self) {
self.congestion_detected += 1;
}
pub fn record_kernel_drop_event(&mut self) {
self.kernel_drop_events += 1;
}
pub fn snapshot(&self) -> CongestionStatsSnapshot {
CongestionStatsSnapshot {
ce_forwarded: self.ce_forwarded,
ce_received: self.ce_received,
congestion_detected: self.congestion_detected,
kernel_drop_events: self.kernel_drop_events,
}
}
}
/// Aggregate node statistics.
///
/// Holds only the families that have not migrated to the atomic
/// [`MetricsRegistry`](crate::node::metrics::MetricsRegistry): session,
/// handshake, mmp, and transport. The forwarding, discovery, tree,
/// bloom, congestion, and error families are served exclusively from
/// the registry.
#[derive(Default)]
pub struct NodeStats {
pub forwarding: ForwardingStats,
pub discovery: DiscoveryStats,
pub tree: TreeStats,
pub bloom: BloomStats,
pub session: SessionStats,
pub handshake: HandshakeStats,
pub mmp: MmpStats,
pub transport: TransportStats,
pub errors: ErrorSignalStats,
pub congestion: CongestionStats,
}
impl NodeStats {
@@ -546,37 +181,28 @@ impl NodeStats {
Self::default()
}
pub fn snapshot(&self) -> NodeStatsSnapshot {
NodeStatsSnapshot {
forwarding: self.forwarding.snapshot(),
discovery: self.discovery.snapshot(),
tree: self.tree.snapshot(),
bloom: self.bloom.snapshot(),
session: self.session.snapshot(),
handshake: self.handshake.snapshot(),
mmp: self.mmp.snapshot(),
transport: self.transport.snapshot(),
errors: self.errors.snapshot(),
congestion: self.congestion.snapshot(),
}
}
/// Record a typed rejection from a silent-rejection site.
///
/// Dispatches to the appropriate sub-stats `record_reject` based on
/// the [`RejectReason`] top-level variant. Sub-enums that have not
/// yet had any variants populated still use `match r {}` to keep
/// the dispatch arm exhaustive without dead-code complaints.
/// the [`RejectReason`] top-level variant. Only the families still
/// stored on `NodeStats` (session, handshake, mmp, transport) are
/// routed here; the migrated families are recorded directly on the
/// [`MetricsRegistry`](crate::node::metrics::MetricsRegistry).
pub fn record_reject(&mut self, reason: RejectReason) {
match reason {
RejectReason::Tree(r) => self.tree.record_reject(r),
RejectReason::Bloom(r) => self.bloom.record_reject(r),
RejectReason::Discovery(r) => self.discovery.record_reject(r),
RejectReason::Session(r) => self.session.record_reject(r),
RejectReason::Handshake(r) => self.handshake.record_reject(r),
RejectReason::Forwarding(r) => self.forwarding.record_reject(r),
RejectReason::Transport(r) => self.transport.record_reject(r),
RejectReason::Mmp(r) => self.mmp.record_reject(r),
// The forwarding, discovery, tree, and bloom families are
// recorded directly on the MetricsRegistry and never reach
// this NodeStats dispatch.
RejectReason::Forwarding(_)
| RejectReason::Discovery(_)
| RejectReason::Tree(_)
| RejectReason::Bloom(_) => {
debug_assert!(false, "migrated reject family must use MetricsRegistry");
}
}
}
}
@@ -713,50 +339,10 @@ pub struct CongestionStatsSnapshot {
pub kernel_drop_events: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct NodeStatsSnapshot {
pub forwarding: ForwardingStatsSnapshot,
pub discovery: DiscoveryStatsSnapshot,
pub tree: TreeStatsSnapshot,
pub bloom: BloomStatsSnapshot,
pub session: SessionStatsSnapshot,
pub handshake: HandshakeStatsSnapshot,
pub mmp: MmpStatsSnapshot,
pub transport: TransportStatsSnapshot,
pub errors: ErrorSignalStatsSnapshot,
pub congestion: CongestionStatsSnapshot,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tree_stats_record_reject_ancestry_invalid() {
let mut stats = TreeStats::default();
stats.record_reject(TreeReject::AncestryInvalid);
stats.record_reject(TreeReject::AncestryInvalid);
assert_eq!(stats.ancestry_invalid, 2);
assert_eq!(stats.outbound_sign_failed, 0);
}
#[test]
fn tree_stats_record_reject_outbound_sign_failed() {
let mut stats = TreeStats::default();
stats.record_reject(TreeReject::OutboundSignFailed);
stats.record_reject(TreeReject::OutboundSignFailed);
assert_eq!(stats.outbound_sign_failed, 2);
assert_eq!(stats.ancestry_invalid, 0);
}
#[test]
fn node_stats_record_reject_dispatches_to_tree() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
assert_eq!(stats.tree.outbound_sign_failed, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
fn session_stats_record_reject_unknown_session() {
let mut stats = SessionStats::default();
@@ -782,7 +368,6 @@ mod tests {
stats.record_reject(RejectReason::Session(SessionReject::BadState));
assert_eq!(stats.session.unknown_session, 1);
assert_eq!(stats.session.bad_state, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
@@ -813,165 +398,6 @@ mod tests {
assert_eq!(stats.handshake.bad_state, 2);
assert_eq!(stats.handshake.unknown_connection, 1);
assert_eq!(stats.session.unknown_session, 0);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
fn bloom_stats_record_reject_decode_error() {
let mut s = BloomStats::default();
s.record_reject(BloomReject::DecodeError);
s.record_reject(BloomReject::DecodeError);
assert_eq!(s.decode_error, 2);
assert_eq!(s.invalid, 0);
}
#[test]
fn bloom_stats_record_reject_invalid() {
let mut s = BloomStats::default();
s.record_reject(BloomReject::Invalid);
assert_eq!(s.invalid, 1);
}
#[test]
fn bloom_stats_record_reject_non_v1() {
let mut s = BloomStats::default();
s.record_reject(BloomReject::NonV1);
assert_eq!(s.non_v1, 1);
}
#[test]
fn bloom_stats_record_reject_unknown_peer() {
let mut s = BloomStats::default();
s.record_reject(BloomReject::UnknownPeer);
assert_eq!(s.unknown_peer, 1);
}
#[test]
fn bloom_stats_record_reject_stale() {
let mut s = BloomStats::default();
s.record_reject(BloomReject::Stale);
assert_eq!(s.stale, 1);
}
#[test]
fn bloom_stats_record_reject_fill_exceeded() {
let mut s = BloomStats::default();
s.record_reject(BloomReject::FillExceeded);
assert_eq!(s.fill_exceeded, 1);
}
#[test]
fn node_stats_record_reject_dispatches_to_bloom() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Bloom(BloomReject::DecodeError));
stats.record_reject(RejectReason::Bloom(BloomReject::Stale));
assert_eq!(stats.bloom.decode_error, 1);
assert_eq!(stats.bloom.stale, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
fn discovery_stats_record_reject_req_decode_error() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::ReqDecodeError);
assert_eq!(s.req_decode_error, 1);
}
#[test]
fn discovery_stats_record_reject_req_duplicate() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::ReqDuplicate);
assert_eq!(s.req_duplicate, 1);
}
#[test]
fn discovery_stats_record_reject_req_dedup_cache_full() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::ReqDedupCacheFull);
assert_eq!(s.req_dedup_cache_full, 1);
}
#[test]
fn discovery_stats_record_reject_req_ttl_exhausted() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::ReqTtlExhausted);
assert_eq!(s.req_ttl_exhausted, 1);
}
#[test]
fn discovery_stats_record_reject_resp_decode_error() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::RespDecodeError);
assert_eq!(s.resp_decode_error, 1);
}
#[test]
fn discovery_stats_record_reject_resp_identity_miss() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::RespIdentityMiss);
assert_eq!(s.resp_identity_miss, 1);
}
#[test]
fn discovery_stats_record_reject_resp_proof_failed() {
let mut s = DiscoveryStats::default();
s.record_reject(DiscoveryReject::RespProofFailed);
assert_eq!(s.resp_proof_failed, 1);
}
#[test]
fn node_stats_record_reject_dispatches_to_discovery() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Discovery(DiscoveryReject::ReqDecodeError));
stats.record_reject(RejectReason::Discovery(DiscoveryReject::RespProofFailed));
assert_eq!(stats.discovery.req_decode_error, 1);
assert_eq!(stats.discovery.resp_proof_failed, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
fn forwarding_stats_record_reject_decode_error() {
let mut s = ForwardingStats::default();
s.record_reject(ForwardingReject::DecodeError);
assert_eq!(s.decode_error_packets, 1);
}
#[test]
fn forwarding_stats_record_reject_ttl_exhausted() {
let mut s = ForwardingStats::default();
s.record_reject(ForwardingReject::TtlExhausted);
assert_eq!(s.ttl_exhausted_packets, 1);
}
#[test]
fn forwarding_stats_record_reject_no_route() {
let mut s = ForwardingStats::default();
s.record_reject(ForwardingReject::NoRoute);
assert_eq!(s.drop_no_route_packets, 1);
}
#[test]
fn forwarding_stats_record_reject_mtu_exceeded() {
let mut s = ForwardingStats::default();
s.record_reject(ForwardingReject::MtuExceeded);
assert_eq!(s.drop_mtu_exceeded_packets, 1);
}
#[test]
fn forwarding_stats_record_reject_send_error() {
let mut s = ForwardingStats::default();
s.record_reject(ForwardingReject::SendError);
assert_eq!(s.drop_send_error_packets, 1);
}
#[test]
fn node_stats_record_reject_dispatches_to_forwarding() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Forwarding(ForwardingReject::NoRoute));
stats.record_reject(RejectReason::Forwarding(ForwardingReject::MtuExceeded));
assert_eq!(stats.forwarding.drop_no_route_packets, 1);
assert_eq!(stats.forwarding.drop_mtu_exceeded_packets, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
@@ -998,7 +424,6 @@ mod tests {
stats.record_reject(RejectReason::Mmp(MmpReject::UnknownPeer));
assert_eq!(stats.mmp.decode_error, 1);
assert_eq!(stats.mmp.unknown_peer, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
#[test]
@@ -1014,6 +439,5 @@ mod tests {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Transport(TransportReject::InboundCapExceeded));
assert_eq!(stats.transport.inbound_cap_exceeded, 1);
assert_eq!(stats.tree.ancestry_invalid, 0);
}
}
+102
View File
@@ -0,0 +1,102 @@
//! Registry-counter coverage tests for the bloom-v2 metric counters that
//! the mesh-lab suites do not reliably exercise.
//!
//! The send-path bloom counters (`deltas_sent`, `full_sends`,
//! `total_compressed_bytes`, `total_raw_bytes`) fire on every filter
//! announce and are covered by the steady-state suites. The three
//! condition-dependent counters (`nacks_sent`, `nacks_received`,
//! `size_changes`) only fire on out-of-sequence deltas, inbound NACKs,
//! and adaptive resizes — none of which occur in the stable, lossless
//! mesh-lab scenarios. These tests drive each of those paths directly and
//! assert the registry counter increments.
use super::*;
use crate::bloom::{BloomFilter, V1_SIZE_CLASS};
use crate::peer::ActivePeer;
use crate::protocol::{FilterAnnounce, FilterNack};
/// Inject a synthetic active peer with a known NodeAddr; returns it.
fn inject_peer(node: &mut Node) -> NodeAddr {
let peer_identity = make_peer_identity();
let peer_addr = *peer_identity.node_addr();
let peer = ActivePeer::new(peer_identity, LinkId::new(1), 0);
node.peers.insert(peer_addr, peer);
peer_addr
}
/// Encode a FilterAnnounce to the payload format handle_filter_announce
/// expects (msg_type byte stripped).
fn encode_announce(announce: &FilterAnnounce) -> Vec<u8> {
let (mut full, _stats) = announce.encode().unwrap();
full.remove(0); // strip msg_type byte
full
}
/// An out-of-sequence delta to a peer with no stored filter makes the node
/// send a NACK, bumping `nacks_sent`.
#[tokio::test]
async fn test_bloom_nacks_sent_counter() {
let mut node = make_node();
let peer_addr = inject_peer(&mut node);
// Fresh peer: filter_sequence == 0. A delta whose base_seq does not
// match the expected base (0) is out-of-sequence → NACK.
let announce = FilterAnnounce::delta(BloomFilter::new(), 2, 5, V1_SIZE_CLASS);
let payload = encode_announce(&announce);
node.handle_filter_announce(&peer_addr, &payload).await;
assert_eq!(
node.metrics().bloom.nacks_sent.get(),
1,
"registry nacks_sent must increment on out-of-sequence delta"
);
}
/// An inbound FilterNack bumps `nacks_received`.
#[tokio::test]
async fn test_bloom_nacks_received_counter() {
let mut node = make_node();
let peer_addr = inject_peer(&mut node);
let mut payload = FilterNack { expected_seq: 7 }.encode();
payload.remove(0); // strip msg_type byte (decode expects the seq only)
node.handle_filter_nack(&peer_addr, &payload).await;
assert_eq!(
node.metrics().bloom.nacks_received.get(),
1,
"registry nacks_received must increment on inbound NACK"
);
}
/// A fresh Full node starts at V1_SIZE_CLASS with a nearly empty outgoing
/// filter (just its own addr), so the first adaptive-sizing pass steps the
/// size class down, bumping `size_changes`.
#[tokio::test]
async fn test_bloom_size_changes_counter() {
let mut node = make_node();
// check_adaptive_sizing needs at least one peer for the representative
// outgoing-filter computation.
let _peer = inject_peer(&mut node);
assert_eq!(
node.bloom_state.size_class(),
V1_SIZE_CLASS,
"fresh node starts at the v1 size class"
);
node.check_bloom_state().await;
assert_eq!(
node.metrics().bloom.size_changes.get(),
1,
"registry size_changes must increment on adaptive resize"
);
assert_eq!(
node.bloom_state.size_class(),
V1_SIZE_CLASS - 1,
"near-empty outgoing filter steps the size class down"
);
}
+18 -21
View File
@@ -43,24 +43,22 @@ async fn test_m1_rejects_all_ones_filter_announce() {
let announce = FilterAnnounce::full(all_ones, 1, 1);
let payload = encode_payload(&announce);
let before_fill_exceeded = node.stats().bloom.fill_exceeded;
let before_accepted = node.stats().bloom.accepted;
let before_fill_exceeded = node.metrics().bloom.fill_exceeded.get();
let before_accepted = node.metrics().bloom.accepted.get();
node.handle_filter_announce(&peer_addr, &payload).await;
let after = &node.stats().bloom;
// While the typed-rejection rollout is in progress the call site bumps
// counter directly AND dispatches through record_reject, which
// hits the same counter. A later change will collapse this to a
// single increment by removing the legacy direct bump; for now
// the rejection-path event yields a +2 delta.
// The rejection path bumps the counter once, through the typed
// record_reject dispatch. The legacy direct bump has been removed,
// so the rejection-path event yields a +1 delta.
assert_eq!(
after.fill_exceeded,
before_fill_exceeded + 2,
node.metrics().bloom.fill_exceeded.get(),
before_fill_exceeded + 1,
"fill_exceeded counter must increment on all-ones rejection"
);
assert_eq!(
after.accepted, before_accepted,
node.metrics().bloom.accepted.get(),
before_accepted,
"accepted counter must NOT increment on rejection"
);
@@ -93,18 +91,18 @@ async fn test_m1_accepts_sub_cap_filter() {
let announce = FilterAnnounce::full(filter, 1, 1);
let payload = encode_payload(&announce);
let before_fill_exceeded = node.stats().bloom.fill_exceeded;
let before_accepted = node.stats().bloom.accepted;
let before_fill_exceeded = node.metrics().bloom.fill_exceeded.get();
let before_accepted = node.metrics().bloom.accepted.get();
node.handle_filter_announce(&peer_addr, &payload).await;
let after = &node.stats().bloom;
assert_eq!(
after.fill_exceeded, before_fill_exceeded,
node.metrics().bloom.fill_exceeded.get(),
before_fill_exceeded,
"fill_exceeded must NOT increment on legitimate sub-cap filter"
);
assert_eq!(
after.accepted,
node.metrics().bloom.accepted.get(),
before_accepted + 1,
"accepted must increment on legitimate filter"
);
@@ -164,9 +162,8 @@ async fn test_m1_sequence_not_advanced_allows_recovery() {
"compliant announce at same seq must be accepted after rejection"
);
assert_eq!(peer.filter_sequence(), 1);
// Direct bump + record_reject dispatch both increment the same
// counter while the typed-rejection rollout is in progress. A later
// change collapses these back to a single increment.
assert_eq!(node.stats().bloom.fill_exceeded, 2);
assert_eq!(node.stats().bloom.accepted, 1);
// The typed record_reject dispatch increments the counter once; the
// legacy direct bump has been removed.
assert_eq!(node.metrics().bloom.fill_exceeded.get(), 1);
assert_eq!(node.metrics().bloom.accepted.get(), 1);
}
+6 -4
View File
@@ -333,11 +333,12 @@ async fn test_third_peer_can_handshake_via_adopted_transport_socket() {
#[tokio::test]
async fn test_adopted_udp_inherits_mtu_from_single_primary_config() {
let mut node = make_node();
node.config.transports.udp = TransportInstances::Single(UdpConfig {
let mut config = Config::new();
config.transports.udp = TransportInstances::Single(UdpConfig {
mtu: Some(1500),
..Default::default()
});
let mut node = make_node_with(config);
let (packet_tx, packet_rx) = packet_channel(64);
node.packet_tx = Some(packet_tx);
@@ -370,7 +371,6 @@ async fn test_adopted_udp_inherits_mtu_from_single_primary_config() {
#[tokio::test]
async fn test_adopted_udp_inherits_mtu_from_named_primary_config() {
let mut node = make_node();
let mut named = HashMap::new();
named.insert(
"primary".to_string(),
@@ -386,7 +386,9 @@ async fn test_adopted_udp_inherits_mtu_from_named_primary_config() {
..Default::default()
},
);
node.config.transports.udp = TransportInstances::Named(named);
let mut config = Config::new();
config.transports.udp = TransportInstances::Named(named);
let mut node = make_node_with(config);
let (packet_tx, packet_rx) = packet_channel(64);
node.packet_tx = Some(packet_tx);
+9 -9
View File
@@ -1232,8 +1232,8 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
node.pending_lookups
.insert(target_addr, PendingLookup::new(0));
let baseline_initiated = node.stats().discovery.req_initiated;
let baseline_timed_out = node.stats().discovery.resp_timed_out;
let baseline_initiated = node.metrics().discovery.req_initiated.get();
let baseline_timed_out = node.metrics().discovery.resp_timed_out.get();
// --- t = 1100ms: first retry deadline (1*1000) ---
node.check_pending_lookups(1100).await;
@@ -1246,7 +1246,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
assert_eq!(entry.last_sent_ms, 1100);
}
assert_eq!(
node.stats().discovery.req_initiated,
node.metrics().discovery.req_initiated.get(),
baseline_initiated + 1,
"retry #1 must invoke initiate_lookup exactly once"
);
@@ -1262,7 +1262,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
assert_eq!(entry.last_sent_ms, 3100);
}
assert_eq!(
node.stats().discovery.req_initiated,
node.metrics().discovery.req_initiated.get(),
baseline_initiated + 2,
"retry #2 must invoke initiate_lookup exactly once more"
);
@@ -1278,7 +1278,7 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
assert_eq!(entry.last_sent_ms, 7100);
}
assert_eq!(
node.stats().discovery.req_initiated,
node.metrics().discovery.req_initiated.get(),
baseline_initiated + 3,
"retry #3 must invoke initiate_lookup exactly once more"
);
@@ -1290,12 +1290,12 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
"8s window not yet expired: pending_lookup must persist"
);
assert_eq!(
node.stats().discovery.req_initiated,
node.metrics().discovery.req_initiated.get(),
baseline_initiated + 3,
"no new attempt before final deadline"
);
assert_eq!(
node.stats().discovery.resp_timed_out,
node.metrics().discovery.resp_timed_out.get(),
baseline_timed_out,
"no timeout before final deadline"
);
@@ -1314,13 +1314,13 @@ async fn test_check_pending_lookups_default_sequence_unreachable() {
);
// resp_timed_out counter ticked.
assert_eq!(
node.stats().discovery.resp_timed_out,
node.metrics().discovery.resp_timed_out.get(),
baseline_timed_out + 1,
"final timeout must increment discovery.resp_timed_out"
);
// No additional initiate_lookup on the timeout step.
assert_eq!(
node.stats().discovery.req_initiated,
node.metrics().discovery.req_initiated.get(),
baseline_initiated + 3,
"the final-timeout step must NOT call initiate_lookup"
);
+3 -2
View File
@@ -757,8 +757,9 @@ fn test_detect_congestion_with_transport_drops() {
#[test]
fn test_detect_congestion_disabled_ecn() {
let mut node = make_node();
node.config.node.ecn.enabled = false;
let mut config = Config::new();
config.node.ecn.enabled = false;
let mut node = Node::new(config).unwrap();
// Even with transport drops, disabled ECN should return false
let tid = TransportId::new(1);
+9 -1
View File
@@ -8,6 +8,7 @@ mod acl;
#[cfg(target_os = "linux")]
mod ble;
mod bloom;
mod bloom_metrics;
mod bloom_poison;
mod bootstrap;
mod decrypt_failure;
@@ -24,7 +25,14 @@ mod tcp;
mod unit;
pub(super) fn make_node() -> Node {
let config = Config::new();
make_node_with(Config::new())
}
/// Build a test node from an explicit `Config`. Prefer this over poking
/// `node.config.*` after construction: immutable fields are mirrored into the
/// shared `NodeContext` at build time, so a post-construction field poke is
/// invisible to any reader that has migrated onto the `config()` accessor.
pub(super) fn make_node_with(config: Config) -> Node {
Node::new(config).unwrap()
}
+3 -2
View File
@@ -1468,8 +1468,9 @@ fn test_purge_idle_sessions_cleans_pending_packets() {
#[test]
fn test_purge_idle_sessions_disabled_when_zero() {
let mut node = make_node();
node.config.node.session.idle_timeout_secs = 0;
let mut config = Config::new();
config.node.session.idle_timeout_secs = 0;
let mut node = make_node_with(config);
let remote = Identity::generate();
let remote_addr = *remote.node_addr();
+2 -2
View File
@@ -832,7 +832,7 @@ async fn test_rejects_tree_announce_with_inconsistent_root() {
.coords()
.unwrap()
.clone();
let accepted_before = nodes[1].node.stats().tree.accepted;
let accepted_before = nodes[1].node.metrics().tree.accepted.get();
// Use two fixed synthetic ancestors so the forged path is explicit:
// - fake_parent = 00000000000000000000000000000000
@@ -877,7 +877,7 @@ async fn test_rejects_tree_announce_with_inconsistent_root() {
nodes[1].node.tree_state().my_coords().depth(),
current_depth
);
assert_eq!(nodes[1].node.stats().tree.accepted, accepted_before);
assert_eq!(nodes[1].node.metrics().tree.accepted.get(), accepted_before);
assert_eq!(
nodes[1].node.get_peer(&a_addr).unwrap().coords().unwrap(),
&peer_coords_before
+23 -14
View File
@@ -6,7 +6,7 @@
//! All tests use 127.0.0.1:0 (ephemeral ports) and need no privileges.
use super::*;
use crate::config::TcpConfig;
use crate::config::{Config, TcpConfig};
use crate::transport::tcp::TcpTransport;
use crate::transport::{TransportAddr, TransportHandle, TransportId, packet_channel};
use spanning_tree::{
@@ -20,7 +20,14 @@ use std::time::Duration;
/// TcpTransport instead of UDP. Binds to 127.0.0.1:0 for an
/// ephemeral port.
async fn make_test_node_tcp() -> TestNode {
let mut node = make_node();
make_test_node_tcp_with(Config::new()).await
}
/// Like `make_test_node_tcp` but builds the node from an explicit `Config`,
/// so immutable fields (e.g. heartbeat/link-dead timeouts) are set before the
/// `NodeContext` is built rather than poked afterward.
async fn make_test_node_tcp_with(config: Config) -> TestNode {
let mut node = make_node_with(config);
let transport_id = TransportId::new(1);
let config = TcpConfig {
@@ -166,13 +173,14 @@ async fn test_tcp_mixed_transport_coexistence() {
/// link-dead timeout fires.
#[tokio::test]
async fn test_tcp_connection_loss_detection() {
let mut nodes = vec![make_test_node_tcp().await, make_test_node_tcp().await];
// Short heartbeat/link-dead timeouts for faster test execution
for tn in nodes.iter_mut() {
tn.node.config.node.heartbeat_interval_secs = 1;
tn.node.config.node.link_dead_timeout_secs = 3;
}
let mut config = Config::new();
config.node.heartbeat_interval_secs = 1;
config.node.link_dead_timeout_secs = 3;
let mut nodes = vec![
make_test_node_tcp_with(config.clone()).await,
make_test_node_tcp_with(config).await,
];
// Establish peering
initiate_handshake(&mut nodes, 0, 1).await;
@@ -215,13 +223,14 @@ async fn test_tcp_connection_loss_detection() {
/// Verifies that bidirectional peering is restored.
#[tokio::test]
async fn test_tcp_reconnection_after_link_death() {
let mut nodes = vec![make_test_node_tcp().await, make_test_node_tcp().await];
// Short timeouts
for tn in nodes.iter_mut() {
tn.node.config.node.heartbeat_interval_secs = 1;
tn.node.config.node.link_dead_timeout_secs = 3;
}
let mut config = Config::new();
config.node.heartbeat_interval_secs = 1;
config.node.link_dead_timeout_secs = 3;
let mut nodes = vec![
make_test_node_tcp_with(config.clone()).await,
make_test_node_tcp_with(config).await,
];
// Establish initial peering
initiate_handshake(&mut nodes, 0, 1).await;
+32
View File
@@ -1006,6 +1006,38 @@ fn active_peer_same_path_discovery_refreshes_stale_peer() {
));
}
#[tokio::test]
async fn node_context_mirrors_config_and_immutable_facades() {
let mut node = make_node();
// The immutable facades read the shared NodeContext.
let expected_addr = *node.identity().node_addr();
assert_eq!(node.node_addr(), &expected_addr);
assert!(!node.is_leaf_only());
let _ = node.uptime();
assert_eq!(node.config().peers().len(), 0);
// update_peers must rebuild the context so config() — which now reads the
// context — reflects the new peer list. Guards the copy-on-write sync.
let peer = Identity::generate();
let new_peer = crate::config::PeerConfig {
npub: peer.npub(),
alias: None,
addresses: vec![],
connect_policy: crate::config::ConnectPolicy::OnDemand,
auto_reconnect: false,
via_nostr: false,
};
node.update_peers(vec![new_peer]).await.unwrap();
assert_eq!(
node.config().peers().len(),
1,
"config() must reflect update_peers through the rebuilt context"
);
assert_eq!(node.config().peers()[0].npub, peer.npub());
}
#[tokio::test]
async fn update_peers_races_new_alternative_without_dropping_active_peer() {
let mut node = make_node();
+85 -57
View File
@@ -8,7 +8,7 @@ use std::collections::HashMap;
use crate::NodeAddr;
use crate::protocol::TreeAnnounce;
use super::reject::{RejectReason, TreeReject};
use super::reject::TreeReject;
use super::{Node, NodeError};
use tracing::{debug, info, trace, warn};
@@ -20,7 +20,7 @@ impl Node {
if !decl.is_signed() {
return Err(NodeError::SendFailed {
node_addr: *self.identity.node_addr(),
node_addr: *self.identity().node_addr(),
reason: "declaration not signed".into(),
});
}
@@ -53,7 +53,7 @@ impl Node {
if !peer.can_send_tree_announce(now_ms) {
peer.mark_tree_announce_pending();
self.stats_mut().tree.rate_limited += 1;
self.metrics().tree.rate_limited.inc();
debug!(
peer = %self.peer_display_name(peer_addr),
"TreeAnnounce rate-limited, marking pending"
@@ -70,11 +70,11 @@ impl Node {
// Send
if let Err(e) = self.send_encrypted_link_message(peer_addr, &encoded).await {
self.stats_mut().tree.send_failed += 1;
self.metrics().tree.send_failed.inc();
return Err(e);
}
self.stats_mut().tree.sent += 1;
self.metrics().tree.sent.inc();
// Record send time
if let Some(peer) = self.peers.get_mut(peer_addr) {
@@ -135,12 +135,12 @@ impl Node {
/// 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.stats_mut().tree.received += 1;
self.metrics().tree.received.inc();
let announce = match TreeAnnounce::decode(payload) {
Ok(a) => a,
Err(e) => {
self.stats_mut().tree.decode_error += 1;
self.metrics().tree.decode_error.inc();
debug!(from = %self.peer_display_name(from), error = %e, "Malformed TreeAnnounce");
return;
}
@@ -150,7 +150,7 @@ impl Node {
let pubkey = match self.peers.get(from) {
Some(peer) => peer.pubkey(),
None => {
self.stats_mut().tree.unknown_peer += 1;
self.metrics().tree.unknown_peer.inc();
debug!(from = %self.peer_display_name(from), "TreeAnnounce from unknown peer");
return;
}
@@ -158,7 +158,7 @@ impl Node {
// The declaring node_addr in the announce should match the sender
if announce.declaration.node_addr() != from {
self.stats_mut().tree.addr_mismatch += 1;
self.metrics().tree.addr_mismatch.inc();
debug!(
from = %self.peer_display_name(from),
declared = %announce.declaration.node_addr(),
@@ -168,7 +168,7 @@ impl Node {
}
if let Err(e) = announce.declaration.verify(&pubkey) {
self.stats_mut().tree.sig_failed += 1;
self.metrics().tree.sig_failed.inc();
warn!(
from = %self.peer_display_name(from),
error = %e,
@@ -178,8 +178,9 @@ impl Node {
}
if let Err(e) = announce.validate_semantics() {
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::AncestryInvalid));
self.metrics()
.tree
.record_reject(TreeReject::AncestryInvalid);
warn!(
from = %self.peer_display_name(from),
error = %e,
@@ -208,12 +209,12 @@ impl Node {
.update_peer(announce.declaration.clone(), announce.ancestry.clone());
if !updated {
self.stats_mut().tree.stale += 1;
self.metrics().tree.stale.inc();
debug!(from = %self.peer_display_name(from), "TreeAnnounce not fresher than existing, ignored");
return;
}
self.stats_mut().tree.accepted += 1;
self.metrics().tree.accepted.inc();
debug!(
from = %self.peer_display_name(from),
@@ -249,24 +250,28 @@ impl Node {
.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(&self.identity) {
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
warn!(error = %e, "Failed to sign declaration after parent switch");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
self.coord_cache
.invalidate_via_node(self.identity.node_addr());
.invalidate_via_node(our_identity.node_addr());
self.reset_discovery_backoff();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
self.metrics().tree.parent_switched.inc();
self.metrics().tree.parent_switches.inc();
info!(
new_parent = %self.peer_display_name(&new_parent),
@@ -276,7 +281,7 @@ impl Node {
"Parent switched, invalidated downstream coord cache entries, announcing to all peers"
);
if flap_dampened {
self.stats_mut().tree.flap_dampened += 1;
self.metrics().tree.flap_dampened.inc();
warn!("Flap dampening engaged: excessive parent switches detected");
}
@@ -288,19 +293,23 @@ impl Node {
} 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(&self.identity) {
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
warn!(error = %e, "Failed to sign self-root declaration");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_other_roots doc.
self.coord_cache
.invalidate_other_roots(self.identity.node_addr());
.invalidate_other_roots(our_identity.node_addr());
self.reset_discovery_backoff();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
self.metrics().tree.parent_switched.inc();
self.metrics().tree.parent_switches.inc();
info!(
new_root = %self.tree_state.root(),
"Self-promoted to root: smallest visible NodeAddr"
@@ -313,9 +322,9 @@ impl Node {
{
// 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())
&& parent_coords.contains(self.identity().node_addr())
{
self.stats_mut().tree.loop_detected += 1;
self.metrics().tree.loop_detected.inc();
warn!(
parent = %self.peer_display_name(from),
"Parent ancestry contains us — loop detected, dropping parent"
@@ -327,16 +336,20 @@ impl Node {
.map(|(addr, peer)| (*addr, peer.link_cost()))
.collect();
if self.tree_state.handle_parent_lost(&peer_costs) {
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
// 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.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
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(self.identity.node_addr());
.invalidate_via_node(our_identity.node_addr());
self.coord_cache
.invalidate_other_roots(self.tree_state.root());
self.reset_discovery_backoff();
@@ -367,24 +380,28 @@ impl Node {
.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(&self.identity) {
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
warn!(error = %e, "Failed to sign declaration after parent update");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
self.coord_cache
.invalidate_via_node(self.identity.node_addr());
.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.stats_mut().tree.ancestry_changed += 1;
self.metrics().tree.ancestry_changed.inc();
info!(
parent = %self.peer_display_name(from),
old_root = %old_root,
@@ -425,7 +442,7 @@ impl Node {
/// `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;
let interval_secs = self.config().node.tree.reeval_interval_secs;
if interval_secs == 0 {
return;
}
@@ -461,21 +478,25 @@ impl Node {
.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(&self.identity) {
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
warn!(error = %e, "Failed to sign declaration after periodic parent re-eval");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_via_node doc.
self.coord_cache
.invalidate_via_node(self.identity.node_addr());
.invalidate_via_node(our_identity.node_addr());
self.reset_discovery_backoff();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
self.metrics().tree.parent_switched.inc();
self.metrics().tree.parent_switches.inc();
info!(
new_parent = %self.peer_display_name(&new_parent),
@@ -486,7 +507,7 @@ impl Node {
"Parent switched via periodic cost re-evaluation"
);
if flap_dampened {
self.stats_mut().tree.flap_dampened += 1;
self.metrics().tree.flap_dampened.inc();
warn!("Flap dampening engaged: excessive parent switches detected");
}
@@ -495,19 +516,23 @@ impl Node {
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(&self.identity) {
if let Err(e) = self.tree_state.sign_declaration(&our_identity) {
warn!(error = %e, "Failed to sign self-root declaration in periodic reeval");
self.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
return;
}
// Surgical invalidation — see CoordCache::invalidate_other_roots doc.
self.coord_cache
.invalidate_other_roots(self.identity.node_addr());
.invalidate_other_roots(our_identity.node_addr());
self.reset_discovery_backoff();
self.stats_mut().tree.parent_switched += 1;
self.stats_mut().tree.parent_switches += 1;
self.metrics().tree.parent_switched.inc();
self.metrics().tree.parent_switches.inc();
info!(
new_root = %self.tree_state.root(),
trigger = "periodic",
@@ -548,7 +573,7 @@ impl Node {
self.tree_state.remove_peer(node_addr);
if was_parent {
self.stats_mut().tree.parent_losses += 1;
self.metrics().tree.parent_losses.inc();
let peer_costs: HashMap<NodeAddr, f64> = self
.peers
.iter()
@@ -557,11 +582,14 @@ impl Node {
.collect();
let changed = self.tree_state.handle_parent_lost(&peer_costs);
if changed {
// Re-sign the new declaration
if let Err(e) = self.tree_state.sign_declaration(&self.identity) {
// 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.stats_mut()
.record_reject(RejectReason::Tree(TreeReject::OutboundSignFailed));
self.metrics()
.tree
.record_reject(TreeReject::OutboundSignFailed);
}
info!(
new_root = %self.tree_state.root(),