Files
fips/src/node/stats.rs
T
Johnathan Corgan bde5d797ac Retire an FSP rekey this node started when its setup or ack is lost
A session rekey this node initiated could end only on a SessionAck, a
lost tie-break, cutover or session removal. The only expiry for an armed
rekey handshake covered one the peer armed, timed from the peer's setup
message, and no rekey SessionSetup is ever resent. One lost SessionSetup
or SessionAck therefore left the rekey in flight for good, and a rekey in
flight vetoes the trigger that would start another: the session kept
running on its current keys and stopped rotating them. Where this node
has the smaller address it also drops the peer's own setup on the
tie-break, leaving the peer stuck the same way.

The handshake now carries its own deadline, taken when this node sends
its setup, and expires on the handshake timeout as one the peer armed
does. Only the handshake is dropped, so the next tick starts a fresh
rekey. Putting the handshake back after an unreadable SessionAck does not
restart the deadline, so forged acks cannot hold the rekey open. The
expiry for a handshake the peer armed is unchanged, and neither expiry
reads the other side's clock. The retirement is logged and counted in a
new rekey_unanswered session counter.

The unit test that expected an expired rekey of our own to be left alone
is replaced. Its reasoning, that the msg3 retransmission budget bounds
the cycle, holds only once msg2 has been read. No wire format change.
2026-09-22 21:43:24 +00:00

679 lines
26 KiB
Rust

//! 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
//! on a single `&mut self` context. A `snapshot()` method produces a
//! copyable struct for control socket queries.
use serde::Serialize;
use crate::node::reject::{
HandshakeReject, MmpReject, RejectReason, SessionReject, TransportReject,
};
/// FSP session statistics — receive-path silent-rejection counters.
///
/// Covers the unknown-session and state-machine-mismatch rejection
/// sites in `handlers/session.rs`. Each counter increments once per
/// dropped inbound message; the WARN/DEBUG log line at the site is
/// preserved alongside the counter bump for operator visibility.
#[derive(Default)]
pub struct SessionStats {
/// Inbound session-layer message arrived for a peer address with no
/// matching `SessionEntry`. Aggregates across encrypted data,
/// SessionAck, SessionMsg3, SessionReceiverReport, and
/// PathMtuNotification.
pub unknown_session: u64,
/// Inbound session-layer message arrived for a `SessionEntry` whose
/// state is incompatible with the message type (encrypted data
/// before Established; SessionAck outside Initiating; SessionMsg3
/// outside AwaitingMsg3).
pub bad_state: u64,
/// Inbound XK msg3 whose initiator static key does not derive the
/// source address the datagram claimed. The half-open session is
/// dropped and no identity is registered.
pub addr_mismatch: u64,
/// Inbound rekey XK msg3 whose initiator static key differs from
/// the key the session was established with. The rekey is
/// abandoned and the existing session is left intact.
pub rekey_key_mismatch: u64,
/// A setup message naming an already-established peer armed a
/// responder-side handshake alongside the running session and a
/// SessionAck was sent. The message carries no authenticator, so this
/// counts genuine peer restarts and forged setups alike; its rate is
/// the signal that something is spraying setup messages, which the
/// per-message DEBUG line cannot carry safely at line rate.
pub rekey_armed: u64,
/// A setup message named an established peer while our own rekey of
/// that session was in flight and our address sorted smaller, so the
/// tie-break dropped their msg1 and kept us as initiator.
pub rekey_tiebreak: u64,
/// A setup message named an established peer while our own rekey of
/// that session was in flight and our address sorted larger, so we
/// abandoned our own rekey and answered as responder. A sustained
/// rate here means local key rotation is being suppressed.
pub rekey_yielded: u64,
/// A setup message named an established peer that already holds a
/// completed rekey awaiting cut-over, so the message was dropped
/// rather than arming a second handshake.
pub rekey_pending: u64,
/// A responder-side handshake armed by a peer's setup message passed
/// the handshake timeout without a msg3 and was discarded. The
/// established session is retained.
pub rekey_expired: u64,
/// A rekey this node initiated got no readable SessionAck within the
/// handshake timeout, and its handshake was discarded so the trigger
/// can retry. The established session is retained. A sustained rate
/// means setups or acks to that peer are being lost, or the peer holds
/// a stuck handshake of its own and wins the tie-break.
pub rekey_unanswered: u64,
/// A completed rekey session still waiting for the peer's cut-over was
/// replaced by a newer one, completed from a msg3 carrying the same
/// authenticated peer key. This drops key material the peer may
/// already have adopted, so a sustained rate means one side keeps
/// rekeying while the other never appears on the new epoch.
pub pending_replaced: u64,
/// An inbound SessionAck failed the XK msg2 read against a session we
/// are initiating. The entry is kept and the handshake rolled back,
/// since the message authenticates nothing; a sustained rate is either
/// a broken path to the responder or forged acks holding establishment
/// down.
pub ack_handshake_failed: u64,
/// A setup message was refused by the per-link-peer setup limiter,
/// before any handshake state was created or any ack sent.
pub setup_rate_limited: u64,
/// A session would have been created but the table is at
/// `node.limits.max_sessions`. A sustained rate means either the cap
/// is sized below what this node legitimately carries, or something is
/// holding the table full.
pub table_full: u64,
/// A session would have been created but unauthenticated half-open
/// entries already hold their share of the table.
pub half_open_full: u64,
}
impl SessionStats {
pub fn snapshot(&self) -> SessionStatsSnapshot {
SessionStatsSnapshot {
unknown_session: self.unknown_session,
bad_state: self.bad_state,
addr_mismatch: self.addr_mismatch,
rekey_key_mismatch: self.rekey_key_mismatch,
rekey_armed: self.rekey_armed,
rekey_tiebreak: self.rekey_tiebreak,
rekey_yielded: self.rekey_yielded,
rekey_pending: self.rekey_pending,
rekey_expired: self.rekey_expired,
rekey_unanswered: self.rekey_unanswered,
pending_replaced: self.pending_replaced,
ack_handshake_failed: self.ack_handshake_failed,
setup_rate_limited: self.setup_rate_limited,
table_full: self.table_full,
half_open_full: self.half_open_full,
}
}
pub(super) fn record_reject(&mut self, reason: SessionReject) {
match reason {
SessionReject::UnknownSession => self.unknown_session += 1,
SessionReject::BadState => self.bad_state += 1,
SessionReject::AddrMismatch => self.addr_mismatch += 1,
SessionReject::RekeyKeyMismatch => self.rekey_key_mismatch += 1,
SessionReject::RekeyTiebreak => self.rekey_tiebreak += 1,
SessionReject::RekeyYielded => self.rekey_yielded += 1,
SessionReject::RekeyPending => self.rekey_pending += 1,
SessionReject::AckHandshakeFailed => self.ack_handshake_failed += 1,
SessionReject::SetupRateLimited => self.setup_rate_limited += 1,
SessionReject::TableFull => self.table_full += 1,
SessionReject::HalfOpenFull => self.half_open_full += 1,
}
}
}
/// Noise-handshake statistics — receive-path silent-rejection counters.
///
/// Covers the state-machine and lookup-miss rejection sites in
/// `handlers/handshake.rs` across msg1, msg2, and (on the XX side) msg3.
/// Each counter increments once per dropped inbound message; the
/// WARN/DEBUG log line at the site is preserved alongside the counter
/// bump for operator visibility.
#[derive(Default)]
pub struct HandshakeStats {
/// Handshake state-machine rejection: header parse failed, Noise
/// crypto step failed, identity could not be learned, index allocator
/// returned an error, msg2/msg3 send failed, promote_connection
/// returned an error, ACL gate rejected the peer, or the admission
/// gate fired (max_peers / accept_connections).
pub bad_state: u64,
/// Inbound handshake message arrived but no matching connection was
/// found by the receiver_idx (or addr) lookup: msg2 for an unknown
/// pending-outbound index, duplicate msg1 with no stored msg2 to
/// resend, msg3 for an unknown pending-inbound index without a
/// matching rekey-responder slot.
pub unknown_connection: u64,
}
impl HandshakeStats {
pub fn snapshot(&self) -> HandshakeStatsSnapshot {
HandshakeStatsSnapshot {
bad_state: self.bad_state,
unknown_connection: self.unknown_connection,
}
}
pub(super) fn record_reject(&mut self, reason: HandshakeReject) {
match reason {
HandshakeReject::BadState => self.bad_state += 1,
HandshakeReject::UnknownConnection => self.unknown_connection += 1,
}
}
}
/// MMP link-layer rejection statistics.
///
/// Covers the receive-path silent-rejection sites in
/// `src/node/handlers/mmp.rs::handle_sender_report` and
/// `handle_receiver_report`. Each counter increments once per
/// dropped inbound report; the WARN/DEBUG log line at the site is
/// preserved alongside the counter bump.
#[derive(Default)]
pub struct MmpStats {
/// `SenderReport::decode` or `ReceiverReport::decode` returned
/// an error. Aggregated across the two report types.
pub decode_error: u64,
/// SenderReport or ReceiverReport arrived from a peer with no
/// `ActivePeer` record on this node.
pub unknown_peer: u64,
}
impl MmpStats {
pub fn snapshot(&self) -> MmpStatsSnapshot {
MmpStatsSnapshot {
decode_error: self.decode_error,
unknown_peer: self.unknown_peer,
}
}
pub(super) fn record_reject(&mut self, reason: MmpReject) {
match reason {
MmpReject::DecodeError => self.decode_error += 1,
MmpReject::UnknownPeer => self.unknown_peer += 1,
}
}
}
/// Transport-layer rejection statistics aggregated at the node level.
///
/// Per-transport modules (`transport/tcp/stats.rs`, `transport/tor/stats.rs`)
/// keep their own `connections_accepted` / `connections_rejected` /
/// `pool_inbound` / `pool_outbound` counters at the transport layer.
/// `TransportStats` here collects node-level visibility for any future
/// admission-rejection paths that the node code itself decides to
/// register via `record_reject(RejectReason::Transport(...))`.
///
/// The `inbound_cap_exceeded` counter is the typed-dispatch parity
/// counterpart of the per-transport `connections_rejected` counter,
/// which lives in the accept-loop task with no `NodeStats` access.
/// Currently this node-side counter stays at zero; it exists so the
/// typed-rejection enum stays the canonical entry point and so a
/// future transport-to-node bridge (event or sampling) has a
/// well-known destination.
///
/// `payload_len_mismatch` is different in kind: it counts a framing
/// drop the node itself performs at the receive dispatch point, so it
/// has a live writer and can be non-zero on a running node.
#[derive(Default)]
pub struct TransportStats {
/// Reserved for node-side inbound-cap-exceeded admission rejection
/// dispatch. Per-transport accept-loop cap rejections are tracked
/// on the transport-level stats (`TcpStats::connections_rejected`,
/// `TorStats::connections_rejected`) directly.
pub inbound_cap_exceeded: u64,
/// Inbound FMP frames dropped because the payload length declared
/// in the common prefix did not match the frame the transport
/// delivered.
pub payload_len_mismatch: u64,
}
impl TransportStats {
pub fn snapshot(&self) -> TransportStatsSnapshot {
TransportStatsSnapshot {
inbound_cap_exceeded: self.inbound_cap_exceeded,
payload_len_mismatch: self.payload_len_mismatch,
}
}
pub(super) fn record_reject(&mut self, reason: TransportReject) {
match reason {
TransportReject::InboundCapExceeded => self.inbound_cap_exceeded += 1,
TransportReject::PayloadLenMismatch => self.payload_len_mismatch += 1,
}
}
}
/// 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 session: SessionStats,
pub handshake: HandshakeStats,
pub mmp: MmpStats,
pub transport: TransportStats,
}
impl NodeStats {
pub fn new() -> Self {
Self::default()
}
/// Record a typed rejection from a silent-rejection site.
///
/// Dispatches to the appropriate sub-stats `record_reject` based on
/// 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::Session(r) => self.session.record_reject(r),
RejectReason::Handshake(r) => self.handshake.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");
}
}
}
}
// --- Snapshot types (copyable, serializable) ---
#[derive(Clone, Debug, Default, Serialize)]
pub struct ForwardingStatsSnapshot {
pub received_packets: u64,
pub received_bytes: u64,
pub decode_error_packets: u64,
pub decode_error_bytes: u64,
pub warm_malformed_packets: u64,
pub warm_malformed_bytes: u64,
pub coord_warm_foreign_root: u64,
pub coord_warm_key_mismatch: u64,
pub coord_hint_changed: u64,
pub coord_hint_rejected: 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,
pub route_tree_up: u64,
pub route_tree_down: u64,
pub route_tree_down_cross: u64,
pub route_crosslink_descend: u64,
pub route_crosslink_ascend: u64,
pub route_direct_peer: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct LookupStatsSnapshot {
pub req_received: u64,
pub req_decode_error: u64,
pub req_duplicate: u64,
pub req_own_loopback: u64,
pub req_dedup_cache_full: u64,
pub req_dedup_evicted: u64,
pub req_sign_rate_limited: 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,
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_unsolicited: u64,
pub resp_no_route: u64,
pub resp_accepted: u64,
pub resp_timed_out: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TreeStatsSnapshot {
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 loop_detected: u64,
pub ancestry_changed: u64,
pub sent: u64,
pub rate_limited: u64,
pub send_failed: u64,
pub outbound_sign_failed: u64,
pub parent_switches: u64,
pub parent_losses: u64,
pub flap_dampened: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct BloomStatsSnapshot {
pub received: u64,
pub decode_error: u64,
pub invalid: u64,
pub non_v1: u64,
pub unknown_peer: u64,
pub stale: u64,
pub fill_exceeded: u64,
pub accepted: u64,
pub sent: u64,
pub debounce_suppressed: u64,
pub send_failed: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct SessionStatsSnapshot {
pub unknown_session: u64,
pub bad_state: u64,
pub addr_mismatch: u64,
pub rekey_key_mismatch: u64,
pub rekey_armed: u64,
pub rekey_tiebreak: u64,
pub rekey_yielded: u64,
pub rekey_pending: u64,
pub rekey_expired: u64,
pub rekey_unanswered: u64,
pub pending_replaced: u64,
pub ack_handshake_failed: u64,
pub setup_rate_limited: u64,
pub table_full: u64,
pub half_open_full: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct HandshakeStatsSnapshot {
pub bad_state: u64,
pub unknown_connection: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct MmpStatsSnapshot {
pub decode_error: u64,
pub unknown_peer: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct TransportStatsSnapshot {
pub inbound_cap_exceeded: u64,
pub payload_len_mismatch: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct ErrorSignalStatsSnapshot {
pub coords_required: u64,
pub path_broken: u64,
pub mtu_exceeded: u64,
pub path_mtu_notif_below_floor: u64,
pub mtu_exceeded_below_floor: u64,
pub lookup_resp_mtu_below_floor: u64,
pub unbound_coords: u64,
pub unbound_broken: u64,
pub unbound_mtu: u64,
pub unbound_forged: u64,
pub emit_over_peer_budget: u64,
pub emit_over_dest_interval: u64,
pub emit_limiter_at_capacity: u64,
pub mtu_exceeded_uncorroborated: u64,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct CongestionStatsSnapshot {
pub ce_forwarded: u64,
pub ce_received: u64,
pub congestion_detected: u64,
pub kernel_drop_events: u64,
}
/// Native datagram API counters.
///
/// Eight of the `drop_*` fields are one per
/// [`DropReason`](crate::native::link::DropReason) variant, so a variant added
/// later has nowhere to hide. `drop_oversize` is the ninth and has no reason
/// because it is refused on the send side, before the registry sees the
/// datagram.
///
/// Present on every platform even though the API builds only on Linux and
/// FreeBSD, so `show_metrics` carries one schema everywhere and a consumer does
/// not branch on the host.
#[derive(Clone, Debug, Default, Serialize)]
pub struct NativeStatsSnapshot {
pub flows_opened: u64,
pub flows_accepted: u64,
pub flows_closed: u64,
pub flows_expired: u64,
pub sent_datagrams: u64,
pub sent_bytes: u64,
pub received_datagrams: u64,
pub received_bytes: u64,
pub drop_no_port: u64,
pub drop_backlog_full: u64,
pub drop_too_many_flows: u64,
pub drop_pending_queue_full: u64,
pub drop_flow_queue_full: u64,
pub drop_arrival_queue_full: u64,
pub drop_listener_not_reading: u64,
pub drop_listener_gone: u64,
pub drop_oversize: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_stats_record_reject_unknown_session() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::UnknownSession);
stats.record_reject(SessionReject::UnknownSession);
assert_eq!(stats.unknown_session, 2);
assert_eq!(stats.bad_state, 0);
}
#[test]
fn session_stats_record_reject_bad_state() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::BadState);
stats.record_reject(SessionReject::BadState);
assert_eq!(stats.bad_state, 2);
assert_eq!(stats.unknown_session, 0);
}
#[test]
fn session_stats_record_reject_addr_mismatch() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::AddrMismatch);
stats.record_reject(SessionReject::AddrMismatch);
assert_eq!(stats.addr_mismatch, 2);
assert_eq!(stats.rekey_key_mismatch, 0);
assert_eq!(stats.unknown_session, 0);
}
#[test]
fn session_stats_record_reject_rekey_key_mismatch() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::RekeyKeyMismatch);
assert_eq!(stats.rekey_key_mismatch, 1);
assert_eq!(stats.addr_mismatch, 0);
}
#[test]
fn session_stats_record_reject_separates_the_three_rekey_arming_refusals() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::RekeyTiebreak);
stats.record_reject(SessionReject::RekeyYielded);
stats.record_reject(SessionReject::RekeyYielded);
stats.record_reject(SessionReject::RekeyPending);
assert_eq!(stats.rekey_tiebreak, 1);
assert_eq!(stats.rekey_yielded, 2);
assert_eq!(stats.rekey_pending, 1);
assert_eq!(stats.rekey_armed, 0);
}
#[test]
fn session_stats_snapshot_carries_the_rekey_arming_counters() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::RekeyTiebreak);
stats.rekey_armed = 7;
stats.rekey_expired = 3;
stats.pending_replaced = 2;
let snap = stats.snapshot();
assert_eq!(snap.rekey_tiebreak, 1);
assert_eq!(snap.rekey_armed, 7);
assert_eq!(snap.rekey_expired, 3);
assert_eq!(snap.pending_replaced, 2);
}
#[test]
fn session_stats_snapshot_carries_identity_binding_counters() {
let mut stats = SessionStats::default();
stats.record_reject(SessionReject::AddrMismatch);
stats.record_reject(SessionReject::RekeyKeyMismatch);
stats.record_reject(SessionReject::RekeyKeyMismatch);
let snap = stats.snapshot();
assert_eq!(snap.addr_mismatch, 1);
assert_eq!(snap.rekey_key_mismatch, 2);
}
#[test]
fn node_stats_record_reject_dispatches_to_session() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Session(SessionReject::UnknownSession));
stats.record_reject(RejectReason::Session(SessionReject::BadState));
assert_eq!(stats.session.unknown_session, 1);
assert_eq!(stats.session.bad_state, 1);
}
#[test]
fn handshake_stats_record_reject_bad_state() {
let mut stats = HandshakeStats::default();
stats.record_reject(HandshakeReject::BadState);
stats.record_reject(HandshakeReject::BadState);
stats.record_reject(HandshakeReject::BadState);
assert_eq!(stats.bad_state, 3);
assert_eq!(stats.unknown_connection, 0);
}
#[test]
fn handshake_stats_record_reject_unknown_connection() {
let mut stats = HandshakeStats::default();
stats.record_reject(HandshakeReject::UnknownConnection);
stats.record_reject(HandshakeReject::UnknownConnection);
assert_eq!(stats.unknown_connection, 2);
assert_eq!(stats.bad_state, 0);
}
#[test]
fn node_stats_record_reject_dispatches_to_handshake() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
stats.record_reject(RejectReason::Handshake(HandshakeReject::UnknownConnection));
stats.record_reject(RejectReason::Handshake(HandshakeReject::BadState));
assert_eq!(stats.handshake.bad_state, 2);
assert_eq!(stats.handshake.unknown_connection, 1);
assert_eq!(stats.session.unknown_session, 0);
}
#[test]
fn mmp_stats_record_reject_decode_error() {
let mut s = MmpStats::default();
s.record_reject(MmpReject::DecodeError);
s.record_reject(MmpReject::DecodeError);
assert_eq!(s.decode_error, 2);
assert_eq!(s.unknown_peer, 0);
}
#[test]
fn mmp_stats_record_reject_unknown_peer() {
let mut s = MmpStats::default();
s.record_reject(MmpReject::UnknownPeer);
assert_eq!(s.unknown_peer, 1);
assert_eq!(s.decode_error, 0);
}
#[test]
fn node_stats_record_reject_dispatches_to_mmp() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Mmp(MmpReject::DecodeError));
stats.record_reject(RejectReason::Mmp(MmpReject::UnknownPeer));
assert_eq!(stats.mmp.decode_error, 1);
assert_eq!(stats.mmp.unknown_peer, 1);
}
#[test]
fn transport_stats_record_reject_inbound_cap_exceeded() {
let mut s = TransportStats::default();
s.record_reject(TransportReject::InboundCapExceeded);
s.record_reject(TransportReject::InboundCapExceeded);
assert_eq!(s.inbound_cap_exceeded, 2);
}
#[test]
fn node_stats_record_reject_dispatches_to_transport() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Transport(TransportReject::InboundCapExceeded));
assert_eq!(stats.transport.inbound_cap_exceeded, 1);
}
/// Records both transport reasons so a swapped or shared arm shows up
/// as a mis-attributed counter rather than as a plausible total.
#[test]
fn transport_stats_record_reject_keeps_the_two_reasons_on_separate_counters() {
let mut s = TransportStats::default();
s.record_reject(TransportReject::PayloadLenMismatch);
s.record_reject(TransportReject::PayloadLenMismatch);
s.record_reject(TransportReject::InboundCapExceeded);
assert_eq!(s.payload_len_mismatch, 2);
assert_eq!(s.inbound_cap_exceeded, 1);
}
#[test]
fn node_stats_record_reject_dispatches_payload_len_mismatch_to_transport() {
let mut stats = NodeStats::new();
stats.record_reject(RejectReason::Transport(TransportReject::PayloadLenMismatch));
assert_eq!(stats.transport.payload_len_mismatch, 1);
assert_eq!(stats.transport.inbound_cap_exceeded, 0);
assert_eq!(stats.handshake.bad_state, 0);
}
}