Skip to main content

macula_rust/frame/
wire_counters.rs

1//! The wire 1.0 counters (WIRE_1_0.md section 8), one set per process: what a
2//! receiver read past instead of refusing. This crate logs nothing, so these
3//! are where a peer speaking a newer wire 1.x shows.
4
5use std::collections::HashMap;
6use std::sync::{LazyLock, Mutex, MutexGuard};
7
8/// A snapshot of the wire counters.
9#[derive(Debug, Clone, Default, PartialEq, Eq)]
10pub struct WireCounts {
11    /// `wire_unknown_frame_type`: frames of a type this node does not know,
12    /// dropped with the link left up.
13    pub unknown_frame_type: u64,
14    /// `wire_ext_ignored`, by extension name: non-critical extensions this
15    /// node does not know.
16    pub ext_ignored: HashMap<String, u64>,
17    /// `wire_ext_refused`, by extension name: unknown critical extensions
18    /// that refused the frame or signed object carrying them.
19    pub ext_refused: HashMap<String, u64>,
20    /// `wire_enum_fallback`, by field name: per-hop enum values this node does
21    /// not know, read as the field's fallback.
22    pub enum_fallback: HashMap<String, u64>,
23}
24
25static COUNTS: LazyLock<Mutex<WireCounts>> = LazyLock::new(Mutex::default);
26
27fn counts() -> MutexGuard<'static, WireCounts> {
28    COUNTS
29        .lock()
30        .unwrap_or_else(|poisoned| poisoned.into_inner())
31}
32
33/// This process's wire counters.
34pub fn wire_counters() -> WireCounts {
35    counts().clone()
36}
37
38/// Counts one dropped frame of a type this node does not know.
39pub(crate) fn count_unknown_frame_type() {
40    counts().unknown_frame_type += 1;
41}
42
43/// How many extension names a counter keeps apart; any further name is
44/// counted under [`OTHER`] (WIRE_1_0.md section 8).
45pub const MAX_COUNTED_NAMES: usize = 64;
46
47/// The label of the names past [`MAX_COUNTED_NAMES`].
48pub const OTHER: &str = "other";
49
50/// Counts one ignored extension under its name, which the envelope rule
51/// already bounds to 64 bytes.
52pub(crate) fn count_ext_ignored(name: &str) {
53    count_named(&mut counts().ext_ignored, name);
54}
55
56/// Counts one unknown critical extension that refused what carried it.
57pub(crate) fn count_ext_refused(name: &str) {
58    count_named(&mut counts().ext_refused, name);
59}
60
61/// Counts `name` in `by_name`, or under [`OTHER`] once it holds
62/// [`MAX_COUNTED_NAMES`] names, so a peer cannot grow it without bound.
63fn count_named(by_name: &mut HashMap<String, u64>, name: &str) {
64    let label = match by_name.contains_key(name) || by_name.len() < MAX_COUNTED_NAMES {
65        true => name,
66        false => OTHER,
67    };
68    *by_name.entry(label.to_owned()).or_default() += 1;
69}
70
71/// Counts one per-hop enum value read as its field's fallback.
72pub(crate) fn count_enum_fallback(field: &str) {
73    *counts().enum_fallback.entry(field.to_owned()).or_default() += 1;
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn a_counter_keeps_64_names_apart_and_the_rest_under_other() {
82        let mut by_name = HashMap::new();
83        for n in 0..MAX_COUNTED_NAMES + 10 {
84            count_named(&mut by_name, &format!("example.e{n}"));
85        }
86        count_named(&mut by_name, "example.e0");
87        assert_eq!(by_name.len(), MAX_COUNTED_NAMES + 1);
88        assert_eq!(by_name[OTHER], 10);
89        assert_eq!(
90            by_name["example.e0"], 2,
91            "a name already kept is counted as itself"
92        );
93    }
94}