Skip to main content

offline_protocol/protocol/
mesh_relay.rs

1//! Admission control and pacing for mesh forwarding.
2//!
3//! Forwarding is what turns a set of nearby devices into a network: a frame
4//! addressed to someone out of radio range is carried by the devices in
5//! between. The hazard is that the same property which gives coverage —
6//! every node repeating what it hears — also multiplies traffic, and in a
7//! dense room the multiplication can outrun the radio. This module is the
8//! governor on that: it decides whether a frame is forwarded at all, when,
9//! and how fast frames may leave.
10//!
11//! Four mechanisms, each covering something the others cannot:
12//!
13//! 1. **Suppression** ([`RelaySeenCache`]) — an id is forwarded once. Without
14//!    it, copies circulate until their hop limit runs out, and every node
15//!    repeats every copy.
16//! 2. **Jitter with cancellation** — a forward waits a short randomized delay
17//!    before going out, and is dropped if the same id arrives again while it
18//!    waits. In a dense cluster the first neighbor to fire covers everyone who
19//!    can hear it, and the rest stand down. This is what makes cost scale with
20//!    *coverage* rather than with the number of links, and it adapts on its own
21//!    as the room fills up — no threshold to tune.
22//! 3. **Hop limits** — an arriving frame's remaining hop budget is clamped
23//!    before use, so a frame that claims an implausible budget (nothing
24//!    authenticates it at this layer) cannot circulate longer than one our own
25//!    policy would have issued.
26//! 4. **Budgets** — a hard ceiling on frames per second, in total and per
27//!    neighbor. The first three assume frames are what they appear to be;
28//!    budgets hold even when they are not. Whatever arrives, and however it is
29//!    shaped, a node cannot be made to transmit faster than its budget: that
30//!    invariant is what bounds the worst case, so the failure mode under attack
31//!    or overload is added delay rather than a radio that never goes quiet.
32//!
33//! Ordering matters. Suppression runs before anything expensive, admission
34//! before queueing (so a flood cannot grow the queue), and pacing at the moment
35//! of transmission (so a forward cancelled while waiting costs no budget).
36//!
37//! On top of those four sits **capability bias**: how willing this particular
38//! device is, right now, to spend itself on other people's traffic. A charging
39//! laptop and a phone at 20% are both allowed to forward, and the mechanisms
40//! above treat them identically — so the phone pays the same share as the
41//! laptop. Bias tilts three of the existing dials by battery and charging
42//! state, continuously: a weaker device waits longer before transmitting (so a
43//! stronger neighbor holding the same frame wins the cancellation race and it
44//! stands down having spent nothing), forwards to fewer neighbors, and refills
45//! its send budget more slowly.
46//!
47//! This is deliberately a *bias* and not a role. A threshold that switches
48//! forwarding on and off makes the network's shape depend on a state machine,
49//! and the failure mode of getting that wrong is a partition — devices that
50//! could carry a frame declining to. Scaling instead means every device
51//! forwards, capable ones simply forward first and more, and the worst a
52//! misjudged scale costs is some redundancy. The relay *role* reported to apps
53//! is derived the other way round, from what this device has actually carried
54//! ([`MeshRelayGovernor::observe_activity`]), so it describes behaviour rather
55//! than predicting it.
56
57use offline_protocol_core::{Message, MessagePriority};
58use offline_protocol_reliability::{RelaySeenCache, RelaySeenConfig};
59use std::collections::hash_map::DefaultHasher;
60use std::collections::HashMap;
61use std::hash::{Hash, Hasher};
62use std::time::{Duration, Instant};
63
64/// Hop budget a forwarded frame is clamped to under normal density.
65pub const DEFAULT_RELAY_MAX_TTL: u8 = 8;
66
67/// Hop budget used once the local neighborhood is dense.
68///
69/// With many neighbors, coverage is reached in fewer hops — the same message
70/// travels further per hop — so a lower ceiling reaches the same devices for
71/// less traffic.
72pub const DEFAULT_RELAY_DENSE_MAX_TTL: u8 = 5;
73
74/// Neighbor count at or above which the dense hop budget applies.
75pub const DEFAULT_RELAY_DENSE_DEGREE: usize = 6;
76
77/// Absolute ceiling on how many times a frame may be forwarded, regardless of
78/// the hop budget it carries. A backstop against a frame whose hop fields have
79/// been shaped to evade the clamp.
80pub const RELAY_MAX_HOP_COUNT: u8 = 16;
81
82/// Neighbors a single frame is forwarded to.
83pub const DEFAULT_RELAY_FANOUT: usize = 3;
84
85/// Shortest delay before a queued forward is transmitted.
86pub const DEFAULT_RELAY_JITTER_MIN_MS: u64 = 20;
87
88/// Longest delay before a queued forward is transmitted, at low density.
89pub const DEFAULT_RELAY_JITTER_MAX_MS: u64 = 200;
90
91/// Sustained ceiling on forwarded frames leaving this node, per second.
92pub const DEFAULT_RELAY_RATE_PER_SEC: f32 = 10.0;
93
94/// Burst allowance on top of the sustained forwarding rate.
95pub const DEFAULT_RELAY_BURST: f32 = 30.0;
96
97/// Transmissions held back from carrying other people's traffic so this device
98/// can always get its own frames out.
99///
100/// Own traffic and forwarded traffic share one budget, because they share one
101/// radio. Without a reserve they also share one queue discipline, and a device
102/// forwarding at its ceiling would starve its *own* sends behind strangers'
103/// — including the delivery acknowledgement for a message it just received,
104/// whose absence makes a delivered message look lost. Carving the reserve out
105/// of the burst rather than adding a second bucket keeps the ceiling exactly
106/// where it was: this changes who may spend the last few tokens, not how many
107/// there are.
108///
109/// It costs forwarding no throughput at saturation. Tokens refill continuously,
110/// so a forward simply waits for the level to climb back above the reserve —
111/// which at the sustained rate is a fraction of a second.
112pub const RELAY_OWN_TRAFFIC_RESERVE: f32 = 5.0;
113
114/// Sustained ceiling on frames accepted for forwarding from any one neighbor.
115///
116/// Keeps a single noisy or hostile link from consuming the whole node's
117/// forwarding budget: its frames are refused at the door, leaving the budget
118/// for everyone else's traffic.
119pub const DEFAULT_RELAY_PEER_RATE_PER_SEC: f32 = 5.0;
120
121/// Burst allowance for a single neighbor.
122pub const DEFAULT_RELAY_PEER_BURST: f32 = 15.0;
123
124/// Maximum forwards waiting for their delay to elapse.
125pub const DEFAULT_RELAY_QUEUE_CAPACITY: usize = 256;
126
127/// Maximum neighbors tracked for per-neighbor rate limiting.
128pub const MAX_RELAY_RATE_PEERS: usize = 256;
129
130/// How long a queued forward may sit past its due time before being abandoned.
131///
132/// If a node is so far over budget that a frame waits this long, the frame's
133/// usefulness has passed — other paths have carried it or the sender has
134/// retransmitted — and holding it only displaces newer traffic.
135///
136/// Public because it bounds what the delay tunables may be set to: a jitter
137/// window plus a bias handicap that could exceed it would turn every biased
138/// forward into an abandoned one, which `ProtocolConfig::validate` refuses.
139pub const RELAY_QUEUE_MAX_OVERDUE: Duration = Duration::from_secs(5);
140
141/// Smallest share of the full forwarding effort a device is scaled down to by
142/// capability bias.
143///
144/// A floor rather than zero, because bias must never become an off switch:
145/// a device at 1% battery that is still allowed to forward at all (the
146/// [`RelayConfig::min_battery_for_relay`] gate is what decides that, not this)
147/// keeps a usable share, so a network of uniformly low devices still carries
148/// traffic instead of quietly ceasing to be a network.
149///
150/// [`RelayConfig::min_battery_for_relay`]: offline_protocol_router::RelayConfig::min_battery_for_relay
151pub const DEFAULT_RELAY_BIAS_MIN_SCALE: f32 = 0.25;
152
153/// Longest extra delay capability bias adds before a weaker device transmits.
154///
155/// This is the whole mechanism by which a stronger neighbor gets to cover a
156/// frame first: the weaker device's delay window opens later, so in the common
157/// case it is still holding the frame when the neighbor's copy arrives and it
158/// stands down having spent no airtime at all.
159///
160/// A fixed ceiling rather than a multiple of the jitter span, and that matters:
161/// the span already widens with density, and compounding the two would push a
162/// weak device in a crowded room past [`RELAY_QUEUE_MAX_OVERDUE`], where its
163/// forwards are abandoned rather than merely late. Bias must cost redundancy,
164/// never delivery.
165pub const DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS: u64 = 400;
166
167/// How long a stretch of forwarding activity is measured over.
168pub const DEFAULT_RELAY_ACTIVITY_WINDOW_SECS: u64 = 60;
169
170/// Frames carried for other people within one window at or above which this
171/// device reads as an active relay.
172pub const DEFAULT_RELAY_ACTIVITY_MIN_FORWARDS: u64 = 3;
173
174/// Consecutive quiet windows before an active relay reads as inactive again.
175///
176/// Asymmetric with promotion on purpose: becoming a relay takes one busy
177/// window, ceasing to be one takes several quiet ones. A relay in a mesh that
178/// simply has nothing to say for a minute has not stopped being a relay, and
179/// reporting that it has would produce churn no app can act on.
180pub const DEFAULT_RELAY_ACTIVITY_IDLE_WINDOWS: u32 = 2;
181
182/// Tunables for mesh forwarding.
183#[derive(Debug, Clone)]
184pub struct MeshRelayConfig {
185    /// Hop budget a forwarded frame is clamped to.
186    pub max_ttl: u8,
187    /// Hop budget applied once the neighborhood is dense.
188    pub dense_max_ttl: u8,
189    /// Neighbor count at which the dense budget applies.
190    pub dense_degree: usize,
191    /// Neighbors a frame is forwarded to.
192    pub fanout: usize,
193    /// Shortest pre-transmit delay.
194    pub jitter_min: Duration,
195    /// Longest pre-transmit delay at low density.
196    pub jitter_max: Duration,
197    /// Sustained forwarding rate, frames per second.
198    pub rate_per_sec: f32,
199    /// Burst allowance above the sustained rate.
200    pub burst: f32,
201    /// Sustained per-neighbor acceptance rate.
202    pub peer_rate_per_sec: f32,
203    /// Per-neighbor burst allowance.
204    pub peer_burst: f32,
205    /// Maximum forwards awaiting transmission.
206    pub queue_capacity: usize,
207    /// Suppression cache sizing.
208    pub seen: RelaySeenConfig,
209    /// Smallest share of the full forwarding effort capability bias scales a
210    /// device down to. `1.0` disables bias: every device forwards as eagerly
211    /// as every other, whatever its battery.
212    pub bias_min_scale: f32,
213    /// Longest extra pre-transmit delay bias adds to a weaker device.
214    pub bias_max_handicap: Duration,
215    /// How long a stretch of forwarding activity is measured over.
216    pub activity_window: Duration,
217    /// Frames carried within one window at or above which this device reads as
218    /// an active relay.
219    pub activity_min_forwards: u64,
220    /// Consecutive quiet windows before an active relay reads as inactive.
221    pub activity_idle_windows: u32,
222}
223
224impl Default for MeshRelayConfig {
225    fn default() -> Self {
226        Self {
227            max_ttl: DEFAULT_RELAY_MAX_TTL,
228            dense_max_ttl: DEFAULT_RELAY_DENSE_MAX_TTL,
229            dense_degree: DEFAULT_RELAY_DENSE_DEGREE,
230            fanout: DEFAULT_RELAY_FANOUT,
231            jitter_min: Duration::from_millis(DEFAULT_RELAY_JITTER_MIN_MS),
232            jitter_max: Duration::from_millis(DEFAULT_RELAY_JITTER_MAX_MS),
233            rate_per_sec: DEFAULT_RELAY_RATE_PER_SEC,
234            burst: DEFAULT_RELAY_BURST,
235            peer_rate_per_sec: DEFAULT_RELAY_PEER_RATE_PER_SEC,
236            peer_burst: DEFAULT_RELAY_PEER_BURST,
237            queue_capacity: DEFAULT_RELAY_QUEUE_CAPACITY,
238            seen: RelaySeenConfig::default(),
239            bias_min_scale: DEFAULT_RELAY_BIAS_MIN_SCALE,
240            bias_max_handicap: Duration::from_millis(DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS),
241            activity_window: Duration::from_secs(DEFAULT_RELAY_ACTIVITY_WINDOW_SECS),
242            activity_min_forwards: DEFAULT_RELAY_ACTIVITY_MIN_FORWARDS,
243            activity_idle_windows: DEFAULT_RELAY_ACTIVITY_IDLE_WINDOWS,
244        }
245    }
246}
247
248/// This device's own condition, as far as forwarding is concerned.
249///
250/// Pushed in from the engine each tick rather than read here, because the
251/// battery reading belongs to the host and reaches the SDK through
252/// `set_battery_state` — the governor holds no transports and asks nothing.
253#[derive(Debug, Clone, Copy, Default)]
254struct RelayConditions {
255    /// Host-reported battery percentage, or `None` while nothing has reported
256    /// one.
257    battery: Option<u8>,
258    /// Whether the device is plugged in.
259    is_charging: bool,
260    /// Whether the configuration asks this device to relay eagerly
261    /// (`RelayPriority::Always`).
262    eager: bool,
263    /// The soft battery floor forwarding is gated on, which is also where the
264    /// bias ramp starts.
265    soft_floor: u8,
266}
267
268/// A change in whether this device is carrying traffic for other people.
269///
270/// Returned by [`MeshRelayGovernor::observe_activity`] only at the tick the
271/// answer changes, so a caller can announce it exactly once.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum RelayActivity {
274    /// This device started carrying third-party traffic.
275    Began {
276        /// Frames carried in the window that triggered this.
277        forwarded: u64,
278    },
279    /// This device stopped carrying third-party traffic.
280    Ceased,
281}
282
283/// Why a frame was not queued for forwarding.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum RelayRejection {
286    /// Already handled; this is another copy of a frame we have dealt with.
287    AlreadySeen,
288    /// Another copy arrived while this one was waiting, so the pending forward
289    /// was cancelled — a neighbor has covered it.
290    SupersededByDuplicate,
291    /// The frame has no hop budget left.
292    HopLimitReached,
293    /// The neighbor that sent it is over its acceptance rate.
294    PeerRateLimited,
295    /// No room in the pending queue.
296    QueueFull,
297}
298
299/// The outcome of offering an inbound frame to the governor.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum RelayAdmission {
302    /// Queued; it will be transmitted once its delay elapses.
303    Queued,
304    /// Not queued.
305    Rejected(RelayRejection),
306}
307
308/// A forward waiting for its delay to elapse.
309#[derive(Debug, Clone)]
310pub struct PendingRelay {
311    /// The frame to forward, with hop fields already adjusted.
312    pub message: Message,
313    /// The neighbor it arrived from, which must not receive it back.
314    pub arrival_peer: Option<String>,
315    /// When it becomes eligible to transmit.
316    pub due_at: Instant,
317}
318
319/// Running totals, for telemetry and for tests that assert a flood stayed
320/// bounded.
321#[derive(Debug, Clone, Default, PartialEq, Eq)]
322pub struct MeshRelayCounters {
323    /// Frames queued for forwarding.
324    pub queued: u64,
325    /// Times a frame was put on a link, counting each link separately.
326    ///
327    /// This is the airtime meter — what the send budget actually bounds — so it
328    /// counts this device handing over its *own* messages too. For "how much
329    /// have I carried for other people", see [`Self::forwarded`].
330    pub transmissions: u64,
331    /// Third-party frames that crossed at least one link on someone's behalf.
332    ///
333    /// Counted once per frame carried, not once per link, and never for this
334    /// device's own traffic.
335    pub forwarded: u64,
336    /// Frames put back on the queue because they reached no neighbor at all —
337    /// the budget ran out, the links chosen for them went away, or there was
338    /// no usable link to choose.
339    pub requeued: u64,
340    /// Queued forwards displaced to make room for a higher-priority frame.
341    pub queue_evicted: u64,
342    /// Arrivals suppressed as already handled.
343    pub duplicates_suppressed: u64,
344    /// Pending forwards cancelled because a neighbor covered them.
345    pub cancelled_by_duplicate: u64,
346    /// Frames refused because their sender was over its rate.
347    pub peer_rate_limited: u64,
348    /// Frames refused because the pending queue was full.
349    pub queue_full: u64,
350    /// Frames dropped for having no hop budget left.
351    pub hop_limit_reached: u64,
352    /// Transmissions deferred because the node was at its forwarding rate.
353    pub rate_deferred: u64,
354    /// Queued forwards abandoned after waiting too long past their due time.
355    pub abandoned_overdue: u64,
356    /// Hop budgets reduced because the frame claimed more than policy allows.
357    pub ttl_clamped: u64,
358}
359
360/// What this device has carried for other people.
361///
362/// A snapshot, safe to poll. Every field is cumulative since start-up except
363/// [`Self::awaiting_transmission`], which is a gauge: it is the queue depth
364/// right now, and it goes down as well as up.
365#[derive(Debug, Clone, Default, PartialEq, Eq)]
366pub struct MeshRelayStats {
367    /// Frames carried onward on someone else's behalf, counted once each.
368    pub forwarded: u64,
369    /// Times this device put a frame on a link, counting each link separately
370    /// and including hand-offs of its own messages.
371    ///
372    /// This is what the per-second forwarding budget bounds, so it is the
373    /// number to compare against that ceiling — [`Self::forwarded`] is the
374    /// contribution figure to show a user.
375    pub transmissions: u64,
376    /// Frames accepted for forwarding.
377    pub queued: u64,
378    /// Frames waiting to go out right now.
379    pub awaiting_transmission: usize,
380    /// Arrivals ignored because the frame had already been handled.
381    pub duplicates_suppressed: u64,
382    /// Forwards dropped because a neighbor transmitted the same frame first —
383    /// the saving that keeps a crowded room from repeating itself.
384    pub covered_by_a_neighbor: u64,
385    /// Frames refused because the neighbor sending them was over its share.
386    pub peer_rate_limited: u64,
387    /// Frames refused admission, or refused room on their way back to the
388    /// queue, because the pending queue was full.
389    ///
390    /// This is a real loss: the frame reached nobody, and only a copy behind
391    /// it or the sender's own retransmission will carry it now. A rising value
392    /// means forwards are arriving faster than this device can transmit them,
393    /// so the queue is the thing to raise, or the rate ceiling that is filling
394    /// it — see [`Self::rate_deferred`].
395    pub refused_queue_full: u64,
396    /// Transmissions held back because this device was at its forwarding rate.
397    ///
398    /// A frame counted here is delayed, not dropped: it goes back on the queue
399    /// and is tried again on the next tick, until it is either sent or has
400    /// waited so long past its turn that carrying it no longer helps.
401    pub rate_deferred: u64,
402    /// Queued forwards given up on after waiting too long past their due time.
403    ///
404    /// The other end of [`Self::rate_deferred`]: this is where a frame that
405    /// kept being delayed finally stops being carried. Deferral is free to
406    /// look healthy while this climbs, so it is the pair that says whether
407    /// back-pressure is costing anything.
408    pub abandoned_overdue: u64,
409    /// Frames that had travelled as far as they were allowed to.
410    pub hop_limit_reached: u64,
411    /// Frames whose claimed reach was cut down to local policy.
412    pub reach_clamped: u64,
413    /// Ids forgotten for lack of room rather than age.
414    ///
415    /// Expected to stay at zero. A non-zero value means this device is seeing
416    /// more traffic than it can remember having handled, and a frame it forgets
417    /// can be forwarded a second time.
418    pub dropped_for_capacity: u64,
419}
420
421/// A refilling allowance, in frames.
422#[derive(Debug, Clone)]
423struct TokenBucket {
424    tokens: f32,
425    capacity: f32,
426    refill_per_sec: f32,
427    last_refill: Instant,
428}
429
430impl TokenBucket {
431    fn new(capacity: f32, refill_per_sec: f32, now: Instant) -> Self {
432        Self {
433            tokens: capacity.max(0.0),
434            capacity: capacity.max(0.0),
435            refill_per_sec: refill_per_sec.max(0.0),
436            last_refill: now,
437        }
438    }
439
440    fn refill(&mut self, now: Instant) {
441        let elapsed = now
442            .saturating_duration_since(self.last_refill)
443            .as_secs_f32();
444        if elapsed > 0.0 {
445            self.tokens = (self.tokens + elapsed * self.refill_per_sec).min(self.capacity);
446            self.last_refill = now;
447        }
448    }
449
450    /// Re-sizes the allowance in place, keeping what has been spent spent.
451    ///
452    /// Refills against the *old* rate first, so the time already elapsed is
453    /// credited at the terms that applied to it, then clamps the level to the
454    /// new capacity. Clamping is what makes this safe to call every tick: a
455    /// device whose bias improves gets a larger bucket to refill into, and one
456    /// whose bias worsens gets a smaller one immediately, but neither is handed
457    /// tokens it did not earn — otherwise a value oscillating around a
458    /// threshold would mint a fresh burst on every crossing.
459    fn resize(&mut self, capacity: f32, refill_per_sec: f32, now: Instant) {
460        self.refill(now);
461        self.capacity = capacity.max(0.0);
462        self.refill_per_sec = refill_per_sec.max(0.0);
463        self.tokens = self.tokens.min(self.capacity);
464    }
465
466    /// Spends a token, but only while more than `floor` would remain
467    /// untouched. A `floor` of zero spends down to empty.
468    fn try_take_above(&mut self, floor: f32, now: Instant) -> bool {
469        self.refill(now);
470        if self.tokens >= 1.0 + floor {
471            self.tokens -= 1.0;
472            true
473        } else {
474            false
475        }
476    }
477
478    /// Whether a token could be spent right now leaving `floor` untouched,
479    /// without spending it.
480    ///
481    /// For a caller that must satisfy two meters at once and may spend neither
482    /// unless both allow it.
483    fn has_above(&mut self, floor: f32, now: Instant) -> bool {
484        self.refill(now);
485        self.tokens >= 1.0 + floor
486    }
487
488    /// Spends a token already confirmed available by [`Self::has_above`].
489    fn spend(&mut self) {
490        self.tokens = (self.tokens - 1.0).max(0.0);
491    }
492}
493
494/// Decides what gets forwarded, when, and how fast.
495///
496/// Holds no transports and sends nothing itself: it answers questions and hands
497/// back due work, so its behavior is testable without a radio and its policy
498/// stays in one place rather than spread through the receive path.
499#[derive(Debug)]
500pub struct MeshRelayGovernor {
501    config: MeshRelayConfig,
502    seen: RelaySeenCache,
503    /// Forwards awaiting their delay, in the order they were queued.
504    pending: Vec<PendingRelay>,
505    /// This device's whole airtime ceiling — every frame it transmits, its own
506    /// included. Never scaled by capability bias: it is what the device is
507    /// allowed to say, and bias is about how much of that it spends on other
508    /// people.
509    send_budget: TokenBucket,
510    /// The share of [`Self::send_budget`] capability bias leaves for forwarding.
511    ///
512    /// A second meter rather than a smaller shared bucket, and the distinction
513    /// is the whole point: scaling the shared bucket would throttle this
514    /// device's *own* messages and acknowledgements in step with its battery,
515    /// which is delivery rather than redundancy. Forwarding must satisfy both
516    /// meters; own traffic answers only to the shared one. At full capability
517    /// this one matches the shared bucket and never binds first, so a capable
518    /// device behaves exactly as it did before bias existed.
519    forward_budget: TokenBucket,
520    /// Tokens of [`Self::send_budget`] that only this device's own frames may
521    /// spend. Clamped to half the burst so a small configured burst cannot
522    /// reserve the whole bucket and stop forwarding outright.
523    own_reserve: f32,
524    peer_budgets: HashMap<String, TokenBucket>,
525    counters: MeshRelayCounters,
526    /// Seeds the per-frame delay so two nodes holding the same frame pick
527    /// different delays.
528    local_id: String,
529    /// This device's condition, as last reported by the host.
530    conditions: RelayConditions,
531    /// Whether this device currently reads as carrying traffic for others.
532    active_relay: bool,
533    /// Start of the activity window in progress.
534    window_started_at: Instant,
535    /// Frames carried for other people within the window in progress.
536    window_forwards: u64,
537    /// Consecutive completed windows in which nothing was carried.
538    idle_windows: u32,
539}
540
541impl MeshRelayGovernor {
542    /// Creates a governor with explicit tunables.
543    pub fn with_config(local_id: impl Into<String>, config: MeshRelayConfig) -> Self {
544        let now = Instant::now();
545        let seen = RelaySeenCache::with_config(config.seen.clone());
546        Self {
547            send_budget: TokenBucket::new(config.burst, config.rate_per_sec, now),
548            forward_budget: TokenBucket::new(config.burst, config.rate_per_sec, now),
549            own_reserve: RELAY_OWN_TRAFFIC_RESERVE.min(config.burst.max(0.0) / 2.0),
550            seen,
551            pending: Vec::new(),
552            peer_budgets: HashMap::new(),
553            counters: MeshRelayCounters::default(),
554            local_id: local_id.into(),
555            conditions: RelayConditions::default(),
556            active_relay: false,
557            window_started_at: now,
558            window_forwards: 0,
559            idle_windows: 0,
560            config,
561        }
562    }
563
564    /// Re-points the governor at this device's real identity once it is known.
565    ///
566    /// The governor is built before `initialize_mls` derives the address, so it
567    /// starts out seeded with the profile. Both uses are hashes — fan-out
568    /// spread and send jitter — so the change is only a reseed: it shifts which
569    /// frames this node prefers, never whether a frame is relayed.
570    pub fn set_local_id(&mut self, local_id: impl Into<String>) {
571        self.local_id = local_id.into();
572    }
573
574    /// The tunables actually in force.
575    ///
576    /// This is the governor's own snapshot, taken at construction, and it is
577    /// what every forwarding decision reads. Reporting it rather than
578    /// `ProtocolConfig::mesh_relay` keeps the answer honest: the two agree
579    /// today only because nothing can update the section after construction,
580    /// and a reader that trusted the config copy would start lying the moment
581    /// that changed.
582    pub fn config(&self) -> &MeshRelayConfig {
583        &self.config
584    }
585
586    /// Reports this device's condition, which scales how eagerly it forwards.
587    ///
588    /// Called from the process tick with the host's battery feed. Everything it
589    /// changes is continuous — delay, fan-out, refill rate — so there is no
590    /// value of any argument that stops this device forwarding. Whether it may
591    /// forward at all is the caller's gate (`RelayConfig::allow_relay` and the
592    /// battery floor), deliberately kept out of here so the two decisions
593    /// cannot half-apply.
594    ///
595    /// `battery` of `None` means the host has reported nothing, which is
596    /// treated as fully capable for the same reason the forwarding gate treats
597    /// it as willing: most platforms report a level, and penalising the ones
598    /// that do not would quietly thin the network on those devices.
599    ///
600    /// Applied to the forwarding budget immediately rather than on next use, so
601    /// a device that has just been unplugged stops refilling at the plugged-in
602    /// rate this tick.
603    ///
604    /// Only the *forwarding* share is scaled. This device's own airtime ceiling
605    /// is left alone, so its messages and acknowledgements go out at the same
606    /// rate on a dying phone as on a charging laptop — bias decides how much of
607    /// the room's traffic it carries, never how well it can speak for itself.
608    pub fn set_conditions(
609        &mut self,
610        battery: Option<u8>,
611        is_charging: bool,
612        eager: bool,
613        soft_floor: u8,
614    ) {
615        self.conditions = RelayConditions {
616            battery,
617            is_charging,
618            eager,
619            soft_floor,
620        };
621        let scale = self.bias_scale();
622        self.forward_budget.resize(
623            self.config.burst * scale,
624            self.config.rate_per_sec * scale,
625            Instant::now(),
626        );
627    }
628
629    /// How much of the full forwarding effort this device is currently willing
630    /// to spend, in `[bias_min_scale, 1.0]`.
631    ///
632    /// Fully capable when charging, when configured to relay eagerly, or when
633    /// no battery reading exists. Otherwise it ramps linearly from the soft
634    /// floor — the level at which forwarding would stop altogether — up to a
635    /// full battery, so the scale is a measure of headroom above the floor
636    /// rather than of the raw percentage.
637    fn bias_scale(&self) -> f32 {
638        let min_scale = self.config.bias_min_scale.clamp(0.0, 1.0);
639        if self.conditions.is_charging || self.conditions.eager {
640            return 1.0;
641        }
642        let Some(level) = self.conditions.battery else {
643            return 1.0;
644        };
645        let floor = self.conditions.soft_floor.min(100);
646        if level >= 100 {
647            return 1.0;
648        }
649        if level <= floor {
650            return min_scale;
651        }
652        let headroom = (level - floor) as f32 / (100 - floor).max(1) as f32;
653        min_scale + headroom * (1.0 - min_scale)
654    }
655
656    /// Whether this device currently reads as carrying traffic for other
657    /// people.
658    ///
659    /// Derived from what it has actually forwarded, not from what its battery
660    /// and neighbor count suggest it could — see [`Self::observe_activity`].
661    pub fn is_active_relay(&self) -> bool {
662        self.active_relay
663    }
664
665    /// Rolls the activity window and reports a change in relay activity.
666    ///
667    /// Answers "is this device carrying other people's traffic" from the only
668    /// evidence that cannot be wrong about it: whether it has. A device becomes
669    /// a relay by having carried [`MeshRelayConfig::activity_min_forwards`]
670    /// frames within one window, and stops being one after
671    /// [`MeshRelayConfig::activity_idle_windows`] consecutive windows carrying
672    /// none.
673    ///
674    /// Returns `Some` only at the tick the answer changes, so the caller emits
675    /// one event per transition. Cheap enough for every tick: it does nothing
676    /// at all until a window has elapsed.
677    pub fn observe_activity(&mut self, now: Instant) -> Option<RelayActivity> {
678        if now.saturating_duration_since(self.window_started_at) < self.config.activity_window {
679            return None;
680        }
681
682        let forwarded = std::mem::take(&mut self.window_forwards);
683        self.window_started_at = now;
684
685        if forwarded >= self.config.activity_min_forwards.max(1) {
686            self.idle_windows = 0;
687            if !self.active_relay {
688                self.active_relay = true;
689                return Some(RelayActivity::Began { forwarded });
690            }
691            return None;
692        }
693
694        if !self.active_relay {
695            return None;
696        }
697
698        self.idle_windows = self.idle_windows.saturating_add(1);
699        if self.idle_windows >= self.config.activity_idle_windows.max(1) {
700            self.active_relay = false;
701            return Some(RelayActivity::Ceased);
702        }
703        None
704    }
705
706    /// Drops the relay standing immediately, reporting whether it was held.
707    ///
708    /// For the caller's own gates closing — relaying switched off in
709    /// configuration, or the battery falling through the forwarding floor.
710    /// Those stop forwarding outright, so waiting out the idle windows would
711    /// leave this device reported as a relay for a minute after it had stopped
712    /// being one; the activity window is a measure of quiet, not of refusal.
713    pub fn force_inactive(&mut self) -> bool {
714        self.window_forwards = 0;
715        self.idle_windows = 0;
716        std::mem::replace(&mut self.active_relay, false)
717    }
718
719    /// Running totals.
720    pub fn counters(&self) -> &MeshRelayCounters {
721        &self.counters
722    }
723
724    /// Forwards awaiting transmission.
725    pub fn pending_len(&self) -> usize {
726        self.pending.len()
727    }
728
729    /// Ids dropped from the suppression cache for capacity rather than age.
730    /// Expected to stay zero; see [`RelaySeenCache::capacity_evictions`].
731    pub fn seen_capacity_evictions(&self) -> u64 {
732        self.seen.capacity_evictions()
733    }
734
735    /// Offers an inbound third-party frame for forwarding.
736    ///
737    /// Runs the full admission sequence and, when the frame is accepted,
738    /// queues an adjusted copy for later transmission. `degree` is the current
739    /// neighbor count, which sets both the hop budget and the delay spread.
740    /// `is_last_hop` says whether the frame's recipient is one of our own
741    /// neighbors, which changes what a duplicate means — see below.
742    pub fn admit(
743        &mut self,
744        message: &Message,
745        arrival_peer: Option<&str>,
746        degree: usize,
747        is_last_hop: bool,
748    ) -> RelayAdmission {
749        let now = Instant::now();
750        let message_id = message.id.as_str();
751
752        // Has this id been dealt with? Checked without recording, because a
753        // frame that is about to be *refused* must not be remembered as
754        // handled: another copy of it arriving over a healthy link a moment
755        // later is the alternate path that should still carry it. Recording on
756        // rejection would leave the id suppressed for the whole retention
757        // window, so a single link running hot — or one frame with its hop
758        // budget rewritten to nothing, which nothing here can authenticate —
759        // would blank every path through this device.
760        if self.seen.contains(&message_id) {
761            self.counters.duplicates_suppressed =
762                self.counters.duplicates_suppressed.saturating_add(1);
763
764            // Our own copy is still waiting, so a neighbor has transmitted
765            // this frame already.
766            if let Some(index) = self
767                .pending
768                .iter()
769                .position(|p| p.message.id.as_str() == message_id)
770            {
771                // Standing down is right in the middle of a network, where a
772                // neighbor's transmission reaches the same region ours would.
773                // It is wrong at the last hop: our pending copy is addressed
774                // to the recipient themselves, and no other device can be
775                // assumed to hold that link. Dropping it there loses the
776                // delivery outright — and leaves the id suppressed, so the
777                // sender's retries cannot rescue it either.
778                if is_last_hop {
779                    return RelayAdmission::Rejected(RelayRejection::AlreadySeen);
780                }
781
782                self.pending.remove(index);
783                self.counters.cancelled_by_duplicate =
784                    self.counters.cancelled_by_duplicate.saturating_add(1);
785                return RelayAdmission::Rejected(RelayRejection::SupersededByDuplicate);
786            }
787
788            return RelayAdmission::Rejected(RelayRejection::AlreadySeen);
789        }
790
791        // Per-neighbor admission, before the frame can occupy queue space.
792        //
793        // A frame whose arrival link the carrier did not identify skips this
794        // step: there is no link to charge it to, and inventing a shared bucket
795        // for "unknown" would let one such carrier throttle every other. Those
796        // frames are still bounded by the queue capacity and the per-second
797        // send budget below, which is what makes skipping it acceptable — the
798        // per-neighbor limit shapes *fairness between links*, and a frame with
799        // no known link has no share to exceed.
800        if let Some(peer) = arrival_peer {
801            if !self.take_peer_token(peer, now) {
802                self.counters.peer_rate_limited = self.counters.peer_rate_limited.saturating_add(1);
803                return RelayAdmission::Rejected(RelayRejection::PeerRateLimited);
804            }
805        }
806
807        // Hop accounting. The arriving budget is clamped to what our own policy
808        // would have issued before it is spent, so an inflated claim buys at
809        // most this one hop.
810        let Some(forwarded) = self.prepare_hop(message, degree) else {
811            self.counters.hop_limit_reached = self.counters.hop_limit_reached.saturating_add(1);
812            return RelayAdmission::Rejected(RelayRejection::HopLimitReached);
813        };
814
815        if self.pending.len() >= self.config.queue_capacity && !self.evict_for(&forwarded, now) {
816            self.counters.queue_full = self.counters.queue_full.saturating_add(1);
817            return RelayAdmission::Rejected(RelayRejection::QueueFull);
818        }
819
820        // Accepted: now it is genuinely handled, and further copies can be
821        // suppressed. Recording here rather than at the top also means the
822        // cache fills at the rate frames are *taken on*, which the per-neighbor
823        // and per-device limits bound — not at the rate they arrive, which
824        // nothing bounds.
825        self.seen.observe(&message_id);
826
827        let due_at = now + self.jitter_for(&message_id, degree);
828        self.pending.push(PendingRelay {
829            message: forwarded,
830            arrival_peer: arrival_peer.map(str::to_string),
831            due_at,
832        });
833        self.counters.queued = self.counters.queued.saturating_add(1);
834
835        RelayAdmission::Queued
836    }
837
838    /// Returns the forwards whose delay has elapsed, removing them from the
839    /// queue.
840    ///
841    /// Frames whose delay has not elapsed stay queued, as do frames the device
842    /// has no budget left to transmit. A frame that has waited far past its
843    /// turn is abandoned instead: by then other paths have carried it or the
844    /// sender has retransmitted, and holding it only displaces newer traffic.
845    ///
846    /// Budget is *checked* here and *spent* in [`Self::take_send_token`], once
847    /// per link a frame actually crosses. Spending it here instead would
848    /// undercount by the fan-out — one release from this queue can put the
849    /// frame on several links — and the whole point of the ceiling is that it
850    /// bounds what reaches the radio.
851    ///
852    /// The check is therefore not a reservation, and cannot be: a released
853    /// frame may still find the budget gone by the time it reaches the radio,
854    /// because the frames released alongside it spend from the same bucket. A
855    /// caller that gets nothing onto a link for that reason must hand the frame
856    /// back with [`Self::requeue`] rather than drop it — the id is already
857    /// recorded as handled here, so dropping it would lose this copy *and*
858    /// refuse the copies and retransmissions that follow.
859    pub fn take_due(&mut self, now: Instant) -> Vec<PendingRelay> {
860        self.seen.expire(now);
861
862        let mut due = Vec::new();
863        let mut keep: Vec<PendingRelay> = Vec::with_capacity(self.pending.len());
864
865        for relay in std::mem::take(&mut self.pending) {
866            if relay.due_at > now {
867                keep.push(relay);
868                continue;
869            }
870
871            if now.saturating_duration_since(relay.due_at) > RELAY_QUEUE_MAX_OVERDUE {
872                // Never made it onto a link, so its id must not stay
873                // suppressed: the sender's retransmissions carry the same id,
874                // and refusing them would close this route for the whole
875                // retention window over a few seconds of congestion.
876                self.seen.forget(&relay.message.id.as_str());
877                self.counters.abandoned_overdue = self.counters.abandoned_overdue.saturating_add(1);
878                continue;
879            }
880
881            if self.has_send_budget(now) {
882                due.push(relay);
883            } else {
884                self.counters.rate_deferred = self.counters.rate_deferred.saturating_add(1);
885                keep.push(relay);
886            }
887        }
888
889        self.pending = keep;
890        due
891    }
892
893    /// Whether any budget remains to forward right now.
894    ///
895    /// Measured against both meters [`Self::take_send_token`] must satisfy, so
896    /// `take_due` does not release a batch the flush is about to refuse.
897    fn has_send_budget(&mut self, now: Instant) -> bool {
898        self.send_budget.has_above(self.own_reserve, now) && self.forward_budget.has_above(0.0, now)
899    }
900
901    /// Claims budget for putting one **forwarded** frame on one link.
902    ///
903    /// Satisfies two meters. The shared ceiling, stopping short of the
904    /// own-traffic reserve so this device can still send its own frames while
905    /// forwarding at its limit (see [`RELAY_OWN_TRAFFIC_RESERVE`] and
906    /// [`Self::take_own_send_token`]); and the capability-scaled forwarding
907    /// share, which is how bias reduces what a weak device carries without
908    /// touching what it can say for itself.
909    ///
910    /// Neither is spent unless both allow it, so a refusal by one cannot leak
911    /// a token out of the other.
912    pub fn take_send_token(&mut self) -> bool {
913        let now = Instant::now();
914        if !self.has_send_budget(now) {
915            self.counters.rate_deferred = self.counters.rate_deferred.saturating_add(1);
916            return false;
917        }
918        self.send_budget.spend();
919        self.forward_budget.spend();
920        self.counters.transmissions = self.counters.transmissions.saturating_add(1);
921        true
922    }
923
924    /// Claims budget for putting one of **this device's own** frames on one
925    /// link.
926    ///
927    /// Metered against the same ceiling — it is the same radio — but allowed
928    /// into the reserve, so a device forwarding at its limit can still get its
929    /// own messages and acknowledgements out.
930    ///
931    /// Untouched by capability bias, which lives on the forwarding meter alone:
932    /// a device at 10% battery hands its own messages to the mesh exactly as
933    /// fast as a charging one, and only its share of everyone else's traffic
934    /// shrinks.
935    pub fn take_own_send_token(&mut self) -> bool {
936        self.claim_transmission(0.0)
937    }
938
939    fn claim_transmission(&mut self, floor: f32) -> bool {
940        if self.send_budget.try_take_above(floor, Instant::now()) {
941            self.counters.transmissions = self.counters.transmissions.saturating_add(1);
942            true
943        } else {
944            self.counters.rate_deferred = self.counters.rate_deferred.saturating_add(1);
945            false
946        }
947    }
948
949    /// Records that a third-party frame was carried onward.
950    ///
951    /// Separate from [`Self::take_send_token`], which meters airtime for every
952    /// frame this device transmits including its own. Counted once per frame
953    /// carried rather than once per link, so it reads as "messages I moved for
954    /// other people".
955    pub fn record_forwarded(&mut self) {
956        self.counters.forwarded = self.counters.forwarded.saturating_add(1);
957        self.window_forwards = self.window_forwards.saturating_add(1);
958    }
959
960    /// Puts a released forward back on the queue, keeping its original due
961    /// time.
962    ///
963    /// For the frame that reached no neighbor at all between [`Self::take_due`]
964    /// releasing it and the radio being asked — because the budget ran out,
965    /// because every link picked for it had gone away, or because there was no
966    /// usable link to pick. All three end the same way: the frame has travelled
967    /// nowhere, its id is already recorded as handled, and dropping it would
968    /// refuse the copies and retransmissions behind it for the whole retention
969    /// window.
970    ///
971    /// It keeps its due time deliberately: the overdue cut-off in `take_due` is
972    /// measured from that, so a frame that stays stuck is eventually abandoned
973    /// instead of being retried forever.
974    ///
975    /// Refused only if the queue has filled meanwhile, which is the same
976    /// bound every other queued frame is subject to.
977    pub fn requeue(&mut self, relay: PendingRelay) {
978        if self.pending.len() >= self.config.queue_capacity {
979            // Refused room on the way back, so this frame is being dropped
980            // having reached nobody. Release its id for the same reason the
981            // overdue cut-off does — a copy behind it, or the sender's own
982            // retransmission, is now the only way it travels.
983            self.seen.forget(&relay.message.id.as_str());
984            self.counters.queue_full = self.counters.queue_full.saturating_add(1);
985            return;
986        }
987        self.counters.requeued = self.counters.requeued.saturating_add(1);
988        self.pending.push(relay);
989    }
990
991    /// Records an id as handled without queueing a forward for it.
992    ///
993    /// Used for frames this device **originates** and hands to its neighbors:
994    /// a copy circling back to us through the mesh is then recognized rather
995    /// than carried onward as though it were someone else's.
996    ///
997    /// Deliberately not called for frames addressed to us. A later copy of one
998    /// of those is still addressed to us, so it never reaches the forwarding
999    /// path at all — recording it would only fill the cache at delivery rate,
1000    /// which the capacity is not sized for and which would make
1001    /// [`Self::seen_capacity_evictions`] climb for reasons unrelated to relay
1002    /// load.
1003    pub fn mark_handled(&mut self, message_id: &str) {
1004        self.seen.observe(message_id);
1005    }
1006
1007    /// Absorbs an arrival that is a copy of a frame already dealt with and
1008    /// needs no further decision, reporting whether it did.
1009    ///
1010    /// This is [`Self::admit`]'s suppression check, split out so a caller can
1011    /// reach it without first assembling the arguments `admit` needs. In a
1012    /// dense neighborhood most third-party arrivals are copies, and the caller's
1013    /// `degree` and `is_last_hop` cost a status snapshot across every transport
1014    /// and an enumeration of every link — work the suppression check would only
1015    /// throw away.
1016    ///
1017    /// Deliberately answers `false` while we still hold a pending copy of the
1018    /// id, even though that is also a duplicate: standing down for a neighbor
1019    /// is the one duplicate outcome that depends on the neighbor set, so it has
1020    /// to go through the full path. Those are rare — the pending window is one
1021    /// jitter delay wide.
1022    pub fn absorb_settled_duplicate(&mut self, message_id: &str) -> bool {
1023        if !self.seen.contains(message_id) {
1024            return false;
1025        }
1026        if self
1027            .pending
1028            .iter()
1029            .any(|p| p.message.id.as_str() == message_id)
1030        {
1031            return false;
1032        }
1033        self.counters.duplicates_suppressed = self.counters.duplicates_suppressed.saturating_add(1);
1034        true
1035    }
1036
1037    /// Neighbors a **forwarded** frame should be carried to, given the current
1038    /// neighbor set and the peers it must not go back to.
1039    ///
1040    /// Excludes the neighbor it arrived from and the peer that wrote it —
1041    /// both already have it, and returning it is how a frame ends up bouncing
1042    /// between two nodes.
1043    ///
1044    /// Capability-biased: a weaker device spends fewer of its neighbors on
1045    /// other people's traffic. That is safe here precisely because the frame is
1046    /// someone else's — neighbors hold copies of it and its sender is still
1047    /// retrying, so a narrower fan-out costs redundancy. Frames this device
1048    /// originated have neither, and go through
1049    /// [`Self::select_origination_targets`] instead.
1050    pub fn select_targets<'a>(
1051        &self,
1052        neighbors: impl IntoIterator<Item = (&'a str, u8)>,
1053        exclude: &[&str],
1054        message_id: &str,
1055    ) -> Vec<String> {
1056        self.select_within_fanout(neighbors, exclude, message_id, self.biased_fanout())
1057    }
1058
1059    /// Neighbors one of **this device's own** frames should be handed to when
1060    /// no transport reaches the recipient directly.
1061    ///
1062    /// Deliberately *not* capability-biased, for the same reason the jitter
1063    /// handicap skips this path (see [`Self::jitter_for`]): nobody else is
1064    /// holding this frame. For a forward, a narrower fan-out drops paths that
1065    /// neighbors' copies duplicate; here the fan-out *is* the delivery attempt,
1066    /// so narrowing it would cost delivery rather than redundancy — and because
1067    /// target choice is stable per message id, every retransmission would funnel
1068    /// into the same reduced set rather than eventually trying another.
1069    ///
1070    /// Bias still reaches this device's own traffic through the send budget's
1071    /// forwarding share, which is the one dial that cannot mistake the two: see
1072    /// [`Self::take_own_send_token`].
1073    pub fn select_origination_targets<'a>(
1074        &self,
1075        neighbors: impl IntoIterator<Item = (&'a str, u8)>,
1076        exclude: &[&str],
1077        message_id: &str,
1078    ) -> Vec<String> {
1079        self.select_within_fanout(neighbors, exclude, message_id, self.config.fanout.max(1))
1080    }
1081
1082    /// The fan-out width for other people's traffic.
1083    ///
1084    /// Never fewer than one: a fan-out of zero is not a cheaper forward, it is
1085    /// a silent drop, and this device has already taken the frame on.
1086    fn biased_fanout(&self) -> usize {
1087        ((self.config.fanout as f32 * self.bias_scale()).round() as usize).max(1)
1088    }
1089
1090    fn select_within_fanout<'a>(
1091        &self,
1092        neighbors: impl IntoIterator<Item = (&'a str, u8)>,
1093        exclude: &[&str],
1094        message_id: &str,
1095        fanout: usize,
1096    ) -> Vec<String> {
1097        let mut candidates: Vec<(&str, u8)> = neighbors
1098            .into_iter()
1099            .filter(|(peer, _)| !exclude.contains(peer))
1100            .collect();
1101
1102        if candidates.len() <= fanout {
1103            return candidates
1104                .into_iter()
1105                .map(|(peer, _)| peer.to_string())
1106                .collect();
1107        }
1108
1109        // Strongest links first: a fan-out slot spent on a barely-connected
1110        // neighbor is one the frame probably does not survive, and there are
1111        // only a few slots.
1112        //
1113        // Ties break on a hash of (id, peer, self), which does two things.
1114        // Within a group of equally good links it spreads different messages
1115        // across different neighbors rather than pinning every frame to the
1116        // same one. And it is stable per frame: a second copy of the same
1117        // message picks the same neighbors instead of a fresh set, so a
1118        // retransmission does not widen its own footprint.
1119        candidates.sort_by(|(a_peer, a_quality), (b_peer, b_quality)| {
1120            b_quality.cmp(a_quality).then_with(|| {
1121                Self::hash_of(&[message_id, a_peer, &self.local_id]).cmp(&Self::hash_of(&[
1122                    message_id,
1123                    b_peer,
1124                    &self.local_id,
1125                ]))
1126            })
1127        });
1128        candidates
1129            .into_iter()
1130            .take(fanout)
1131            .map(|(peer, _)| peer.to_string())
1132            .collect()
1133    }
1134
1135    /// Drops per-neighbor rate state for a peer that has gone away.
1136    pub fn forget_peer(&mut self, peer_id: &str) {
1137        self.peer_budgets.remove(peer_id);
1138    }
1139
1140    /// Releases expired suppression entries. Safe to call from a periodic tick.
1141    pub fn maintain(&mut self, now: Instant) {
1142        self.seen.expire(now);
1143    }
1144
1145    /// Builds the copy to forward, or `None` when the frame has no hop budget
1146    /// left.
1147    fn prepare_hop(&mut self, message: &Message, degree: usize) -> Option<Message> {
1148        if message.hop_count.value() >= RELAY_MAX_HOP_COUNT {
1149            return None;
1150        }
1151
1152        let ceiling = if degree >= self.config.dense_degree {
1153            self.config.dense_max_ttl
1154        } else {
1155            self.config.max_ttl
1156        };
1157
1158        let claimed = message.ttl.value();
1159        let effective = claimed.min(ceiling);
1160        if effective < claimed {
1161            self.counters.ttl_clamped = self.counters.ttl_clamped.saturating_add(1);
1162        }
1163
1164        // A budget of 1 or less is spent: this node is the last that may hold
1165        // the frame, matching `TTL::is_exhausted`.
1166        let remaining = offline_protocol_core::TTL::from_value(effective).decrement()?;
1167
1168        let mut forwarded = message.clone();
1169        // Rewrite rather than decrement in place: the frame leaves with a
1170        // budget this node vouches for, so an inflated claim cannot survive
1171        // past this hop.
1172        forwarded.ttl = remaining;
1173        let _ = forwarded.increment_hop();
1174        Some(forwarded)
1175    }
1176
1177    /// Makes room for a higher-priority forward by displacing the
1178    /// lowest-priority one that is not yet due.
1179    ///
1180    /// Frames already past their due time are left alone: they are on their way
1181    /// to the radio this tick, so taking one costs a transmission that was
1182    /// about to happen while the frame replacing it still has to wait out its
1183    /// hold. Only when every lower-priority frame is already due does it
1184    /// displace one of those instead of refusing outright.
1185    fn evict_for(&mut self, candidate: &Message, now: Instant) -> bool {
1186        let candidate_rank = priority_rank(candidate.priority);
1187        let lower_priority = |p: &PendingRelay| priority_rank(p.message.priority) < candidate_rank;
1188
1189        let victim = self
1190            .pending
1191            .iter()
1192            .enumerate()
1193            .filter(|(_, p)| lower_priority(p) && p.due_at > now)
1194            .min_by_key(|(_, p)| priority_rank(p.message.priority))
1195            .map(|(index, _)| index)
1196            .or_else(|| {
1197                self.pending
1198                    .iter()
1199                    .enumerate()
1200                    .filter(|(_, p)| lower_priority(p))
1201                    .min_by_key(|(_, p)| priority_rank(p.message.priority))
1202                    .map(|(index, _)| index)
1203            });
1204
1205        match victim {
1206            Some(index) => {
1207                let displaced = self.pending.remove(index);
1208                // Displaced before it ever reached a link. Its id goes back to
1209                // being unknown, so the copy behind it — or the sender's
1210                // retransmission — can still be carried by this device once
1211                // there is room again.
1212                self.seen.forget(&displaced.message.id.as_str());
1213                self.counters.queue_evicted = self.counters.queue_evicted.saturating_add(1);
1214                true
1215            }
1216            None => false,
1217        }
1218    }
1219
1220    fn take_peer_token(&mut self, peer_id: &str, now: Instant) -> bool {
1221        if !self.peer_budgets.contains_key(peer_id) {
1222            // Bound the map: an attacker cannot grow it with invented peer ids
1223            // because entries are only created for peers we hold a link to,
1224            // but churn over a long session can still accumulate.
1225            if self.peer_budgets.len() >= MAX_RELAY_RATE_PEERS {
1226                // Make room by forgetting the link that is spending least,
1227                // rather than by clearing the table. A wholesale reset hands
1228                // every neighbor a fresh burst — including the noisy one the
1229                // per-link limit exists to hold back, which is the entry it can
1230                // least afford to lose. The bucket that has recovered the most
1231                // tokens is the one whose absence changes the least: at full
1232                // capacity it is indistinguishable from a link never tracked.
1233                //
1234                // Costs a scan of the table, but only once the ceiling is
1235                // reached and only when a genuinely new link appears.
1236                for bucket in self.peer_budgets.values_mut() {
1237                    bucket.refill(now);
1238                }
1239                let most_recovered = self
1240                    .peer_budgets
1241                    .iter()
1242                    .max_by(|(_, a), (_, b)| a.tokens.total_cmp(&b.tokens))
1243                    .map(|(peer, _)| peer.clone());
1244                if let Some(peer) = most_recovered {
1245                    self.peer_budgets.remove(&peer);
1246                }
1247            }
1248            self.peer_budgets.insert(
1249                peer_id.to_string(),
1250                TokenBucket::new(self.config.peer_burst, self.config.peer_rate_per_sec, now),
1251            );
1252        }
1253
1254        self.peer_budgets
1255            .get_mut(peer_id)
1256            .map(|bucket| bucket.try_take_above(0.0, now))
1257            .unwrap_or(true)
1258    }
1259
1260    /// The delay before a queued forward is transmitted.
1261    ///
1262    /// Derived from the frame and this node's identity, so two nodes holding
1263    /// the same frame wait different amounts and one of them gets to cancel.
1264    /// The spread widens with density, because that is where the contention is.
1265    ///
1266    /// Capability shifts the whole window later rather than widening it, so
1267    /// between two neighbors holding the same frame the capable one usually
1268    /// transmits and the weaker one stands down having spent nothing. That is
1269    /// the point — the saving is the forward that never happens, not a cheaper
1270    /// one.
1271    ///
1272    /// How reliably it wins depends on density, and the handicap is a fixed
1273    /// constant precisely so it cannot chase it. Below [`dense_degree`] the
1274    /// span is narrower than the handicap, so the weak device's earliest delay
1275    /// is later than the capable one's latest and the ordering is absolute.
1276    /// Past that the span grows and the two windows overlap, leaving a
1277    /// probabilistic bias — the capable device still wins most frames, and the
1278    /// weak one's wait stays bounded by one handicap rather than growing with
1279    /// the room. Widening the handicap to restore the absolute ordering is
1280    /// exactly the shape that would push a weak device in a crowded room past
1281    /// [`RELAY_QUEUE_MAX_OVERDUE`], where forwards are abandoned rather than
1282    /// merely late.
1283    ///
1284    /// Frames this device originated are not delayed at all — see
1285    /// [`Self::select_origination_targets`] and the caller in `send.rs`.
1286    ///
1287    /// [`dense_degree`]: MeshRelayConfig::dense_degree
1288    fn jitter_for(&self, message_id: &str, degree: usize) -> Duration {
1289        let min_ms = self.config.jitter_min.as_millis() as u64;
1290        let max_ms = (self.config.jitter_max.as_millis() as u64).max(min_ms);
1291
1292        // More neighbors means more nodes racing to forward the same frame, so
1293        // give them more room to separate.
1294        let density_scale = 1 + (degree / self.config.dense_degree.max(1)) as u64;
1295        let span = (max_ms - min_ms).saturating_mul(density_scale).max(1);
1296
1297        // Bounded by a constant rather than by the span, which already grows
1298        // with density: compounding the two would push a weak device in a
1299        // crowded room past the overdue cut-off, turning late forwards into
1300        // abandoned ones.
1301        let handicap_ms = ((1.0 - self.bias_scale()).clamp(0.0, 1.0)
1302            * self.config.bias_max_handicap.as_millis() as f32) as u64;
1303
1304        let hash = Self::hash_of(&[message_id, &self.local_id]);
1305        Duration::from_millis(min_ms + handicap_ms + (hash % span))
1306    }
1307
1308    /// Stable hash used to spread delays and fan-out choices.
1309    ///
1310    /// `DefaultHasher` is unseeded, so this is predictable to anyone who knows
1311    /// the inputs — both of which (a message id and a user id) are visible on
1312    /// the wire. That is accepted: knowing it buys only the ability to guess
1313    /// which neighbor fires first or which three links a frame takes, and every
1314    /// path it could steer a frame down is already bounded by the fan-out cap
1315    /// and the send budget. It must stay *stable*, which a randomly-seeded
1316    /// hasher would not be — a second copy of a frame has to pick the same
1317    /// neighbors, or a retransmission widens its own footprint.
1318    fn hash_of(parts: &[&str]) -> u64 {
1319        let mut hasher = DefaultHasher::new();
1320        for part in parts {
1321            part.hash(&mut hasher);
1322        }
1323        hasher.finish()
1324    }
1325}
1326
1327fn priority_rank(priority: MessagePriority) -> u8 {
1328    match priority {
1329        MessagePriority::Low => 0,
1330        MessagePriority::Medium => 1,
1331        MessagePriority::High => 2,
1332        MessagePriority::Critical => 3,
1333    }
1334}
1335
1336#[cfg(test)]
1337mod tests {
1338    use super::*;
1339    use offline_protocol_core::{AppId, UserId, TTL};
1340
1341    /// Zero jitter so `admit` queues a forward that is immediately due, which
1342    /// keeps the tests deterministic without a clock abstraction.
1343    fn immediate_config() -> MeshRelayConfig {
1344        MeshRelayConfig {
1345            jitter_min: Duration::from_millis(0),
1346            jitter_max: Duration::from_millis(0),
1347            ..Default::default()
1348        }
1349    }
1350
1351    fn governor() -> MeshRelayGovernor {
1352        MeshRelayGovernor::with_config("relay-node", immediate_config())
1353    }
1354
1355    fn frame_with(ttl: u8, priority: MessagePriority) -> Message {
1356        Message::builder(
1357            UserId::new("alice").unwrap(),
1358            UserId::new("bob").unwrap(),
1359            AppId::new("app").unwrap(),
1360        )
1361        .content("hello")
1362        .priority(priority)
1363        .ttl(TTL::new(ttl).unwrap())
1364        .build()
1365    }
1366
1367    fn frame() -> Message {
1368        frame_with(8, MessagePriority::Medium)
1369    }
1370
1371    #[test]
1372    fn a_frame_is_forwarded_once_however_many_copies_arrive() {
1373        let mut gov = governor();
1374        let msg = frame();
1375
1376        assert_eq!(gov.admit(&msg, Some("b"), 3, false), RelayAdmission::Queued);
1377
1378        // Copies arriving after ours has gone out are suppressed, not forwarded.
1379        let due = gov.take_due(Instant::now());
1380        assert_eq!(due.len(), 1);
1381
1382        for _ in 0..5 {
1383            assert_eq!(
1384                gov.admit(&msg, Some("c"), 3, false),
1385                RelayAdmission::Rejected(RelayRejection::AlreadySeen)
1386            );
1387        }
1388        assert_eq!(gov.counters().queued, 1);
1389        assert_eq!(gov.counters().duplicates_suppressed, 5);
1390    }
1391
1392    #[test]
1393    fn a_neighbor_beating_us_to_it_cancels_our_pending_forward() {
1394        // The density control: in a cluster where everyone hears everyone, the
1395        // first node to transmit covers the rest, and they stand down instead
1396        // of each sending their own copy.
1397        let mut gov = MeshRelayGovernor::with_config(
1398            "relay-node",
1399            MeshRelayConfig {
1400                jitter_min: Duration::from_millis(50),
1401                jitter_max: Duration::from_millis(200),
1402                ..Default::default()
1403            },
1404        );
1405        let msg = frame();
1406
1407        assert_eq!(gov.admit(&msg, Some("b"), 6, false), RelayAdmission::Queued);
1408        assert_eq!(gov.pending_len(), 1);
1409
1410        // The same frame arrives again while ours is still waiting.
1411        assert_eq!(
1412            gov.admit(&msg, Some("c"), 6, false),
1413            RelayAdmission::Rejected(RelayRejection::SupersededByDuplicate)
1414        );
1415
1416        assert_eq!(gov.pending_len(), 0);
1417        assert_eq!(gov.counters().cancelled_by_duplicate, 1);
1418        assert!(gov
1419            .take_due(Instant::now() + Duration::from_secs(1))
1420            .is_empty());
1421        assert_eq!(gov.counters().transmissions, 0);
1422    }
1423
1424    #[test]
1425    fn an_inflated_hop_budget_is_cut_to_local_policy() {
1426        // Nothing authenticates the hop budget at this layer, so a frame can
1427        // claim any value. It must not buy more than one hop past our own
1428        // ceiling.
1429        let mut gov = governor();
1430        let msg = frame_with(255, MessagePriority::Medium);
1431
1432        assert_eq!(gov.admit(&msg, Some("b"), 2, false), RelayAdmission::Queued);
1433        let due = gov.take_due(Instant::now());
1434
1435        assert_eq!(due.len(), 1);
1436        assert_eq!(due[0].message.ttl.value(), DEFAULT_RELAY_MAX_TTL - 1);
1437        assert_eq!(gov.counters().ttl_clamped, 1);
1438    }
1439
1440    #[test]
1441    fn a_dense_neighborhood_uses_the_lower_hop_ceiling() {
1442        let mut gov = governor();
1443        let msg = frame_with(255, MessagePriority::Medium);
1444
1445        assert_eq!(
1446            gov.admit(&msg, Some("b"), DEFAULT_RELAY_DENSE_DEGREE, false),
1447            RelayAdmission::Queued
1448        );
1449        let due = gov.take_due(Instant::now());
1450        assert_eq!(due[0].message.ttl.value(), DEFAULT_RELAY_DENSE_MAX_TTL - 1);
1451    }
1452
1453    #[test]
1454    fn a_frame_out_of_hops_is_not_forwarded() {
1455        let mut gov = governor();
1456        // A budget of 1 is spent: this node is the last that may hold it.
1457        let msg = frame_with(1, MessagePriority::Medium);
1458
1459        assert_eq!(
1460            gov.admit(&msg, Some("b"), 3, false),
1461            RelayAdmission::Rejected(RelayRejection::HopLimitReached)
1462        );
1463        assert_eq!(gov.counters().hop_limit_reached, 1);
1464    }
1465
1466    #[test]
1467    fn the_last_hop_is_forwarded_and_the_one_after_it_is_not() {
1468        let mut gov = governor();
1469
1470        // Two hops left: goes out with one, which the next node will refuse to
1471        // forward. Verifies the budget lands exactly on `TTL::is_exhausted`
1472        // rather than a hop early or late.
1473        assert_eq!(
1474            gov.admit(&frame_with(2, MessagePriority::Medium), Some("b"), 3, false),
1475            RelayAdmission::Queued
1476        );
1477        let due = gov.take_due(Instant::now());
1478        assert_eq!(due[0].message.ttl.value(), 1);
1479        assert!(due[0].message.is_ttl_exhausted());
1480    }
1481
1482    #[test]
1483    fn the_hop_ceiling_stops_a_frame_that_evaded_the_budget() {
1484        let mut gov = governor();
1485        let mut msg = frame_with(255, MessagePriority::Medium);
1486        for _ in 0..RELAY_MAX_HOP_COUNT {
1487            let _ = msg.increment_hop();
1488        }
1489
1490        assert_eq!(
1491            gov.admit(&msg, Some("b"), 3, false),
1492            RelayAdmission::Rejected(RelayRejection::HopLimitReached)
1493        );
1494    }
1495
1496    #[test]
1497    fn one_noisy_neighbor_cannot_consume_the_whole_node() {
1498        let mut gov = governor();
1499
1500        // Spend the noisy neighbor's allowance on unique frames — the shape
1501        // suppression cannot help with, since every id is new.
1502        let mut accepted_from_noisy = 0;
1503        for _ in 0..200 {
1504            if gov.admit(&frame(), Some("noisy"), 3, false) == RelayAdmission::Queued {
1505                accepted_from_noisy += 1;
1506            }
1507        }
1508
1509        assert!(
1510            accepted_from_noisy <= DEFAULT_RELAY_PEER_BURST as usize + 2,
1511            "accepted {accepted_from_noisy} frames from one neighbor"
1512        );
1513        assert!(gov.counters().peer_rate_limited > 0);
1514
1515        // A different neighbor still gets through: the limit is per link, so
1516        // one bad actor degrades only itself.
1517        assert_eq!(
1518            gov.admit(&frame(), Some("quiet"), 3, false),
1519            RelayAdmission::Queued
1520        );
1521    }
1522
1523    #[test]
1524    fn transmission_never_exceeds_the_node_budget() {
1525        // The invariant the whole design rests on: whatever arrives, what the
1526        // radio is asked to send is capped. Asserted on transmissions rather
1527        // than on queue releases, because one release can put a frame on
1528        // several links and it is the links that cost airtime.
1529        let mut gov = MeshRelayGovernor::with_config(
1530            "relay-node",
1531            MeshRelayConfig {
1532                queue_capacity: 512,
1533                // Admission is not what is under test here.
1534                peer_rate_per_sec: 10_000.0,
1535                peer_burst: 10_000.0,
1536                ..immediate_config()
1537            },
1538        );
1539
1540        for _ in 0..200 {
1541            gov.admit(&frame(), Some("b"), 3, false);
1542        }
1543        let due = gov.take_due(Instant::now());
1544        assert!(!due.is_empty());
1545
1546        // Ask for far more transmissions than the budget allows.
1547        let granted = (0..1000).filter(|_| gov.take_send_token()).count();
1548
1549        assert!(
1550            granted <= DEFAULT_RELAY_BURST as usize + 1,
1551            "granted {granted} transmissions against a burst of {DEFAULT_RELAY_BURST}"
1552        );
1553        assert_eq!(gov.counters().transmissions as usize, granted);
1554        assert!(gov.counters().rate_deferred > 0);
1555    }
1556
1557    #[test]
1558    fn a_frame_that_reached_no_neighbor_for_budget_goes_back_on_the_queue() {
1559        // `take_due` checks the budget without reserving it, so a batch of due
1560        // frames can be released against a single token: the first spends it
1561        // and the rest reach the radio with nothing left. Those frames have
1562        // been forwarded nowhere, and their ids are already recorded as
1563        // handled — dropping one would lose this copy and refuse both the
1564        // copies behind it and the sender's retransmissions for the whole
1565        // retention window. It has to go back on the queue.
1566        let mut gov = MeshRelayGovernor::with_config(
1567            "relay-node",
1568            MeshRelayConfig {
1569                peer_rate_per_sec: 10_000.0,
1570                peer_burst: 10_000.0,
1571                ..immediate_config()
1572            },
1573        );
1574
1575        for _ in 0..40 {
1576            gov.admit(&frame(), Some("b"), 3, false);
1577        }
1578        let due = gov.take_due(Instant::now());
1579        assert!(
1580            due.len() > DEFAULT_RELAY_BURST as usize,
1581            "the batch must outrun the budget for this to be the case under test"
1582        );
1583
1584        // Drain the budget the way a fan-out does, then hand back everything
1585        // that got nowhere.
1586        let mut requeued = 0;
1587        for relay in due {
1588            if gov.take_send_token() {
1589                continue;
1590            }
1591            gov.requeue(relay);
1592            requeued += 1;
1593        }
1594
1595        assert!(requeued > 0, "the budget must have run out mid-batch");
1596        assert_eq!(gov.pending_len(), requeued, "every starved frame is kept");
1597        assert_eq!(gov.counters().requeued as usize, requeued);
1598    }
1599
1600    #[test]
1601    fn a_requeued_frame_is_still_abandoned_once_it_is_far_past_its_turn() {
1602        // Re-queueing must not become an unbounded retry: a frame kept for
1603        // budget keeps its original due time, so the overdue cut-off still
1604        // reaches it.
1605        let mut gov = governor();
1606        gov.admit(&frame(), Some("b"), 3, false);
1607        let mut due = gov.take_due(Instant::now());
1608        assert_eq!(due.len(), 1);
1609
1610        gov.requeue(due.remove(0));
1611        assert_eq!(gov.pending_len(), 1);
1612
1613        let later = gov.take_due(Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_secs(1));
1614        assert!(later.is_empty());
1615        assert_eq!(gov.counters().abandoned_overdue, 1);
1616        assert_eq!(gov.pending_len(), 0);
1617    }
1618
1619    #[test]
1620    fn a_frame_abandoned_for_waiting_too_long_releases_its_id() {
1621        // A frame dropped for waiting past its turn never reached a link, so
1622        // nothing is circulating that a second forward could duplicate. Its id
1623        // must go back to being unknown, or the sender's retransmissions —
1624        // which carry the same id — are refused for the whole retention window
1625        // because of a few seconds of congestion.
1626        let mut gov = governor();
1627        let msg = frame();
1628        gov.admit(&msg, Some("b"), 3, false);
1629
1630        let due = gov.take_due(Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_secs(1));
1631        assert!(due.is_empty());
1632        assert_eq!(gov.counters().abandoned_overdue, 1);
1633
1634        assert_eq!(
1635            gov.admit(&msg, Some("c"), 3, false),
1636            RelayAdmission::Queued,
1637            "the sender's retransmission must still be carried"
1638        );
1639    }
1640
1641    #[test]
1642    fn a_frame_displaced_from_the_queue_releases_its_id() {
1643        // Same rule for a frame evicted to make room for an urgent one: it was
1644        // never transmitted, so the route through this device must stay open.
1645        let mut gov = MeshRelayGovernor::with_config(
1646            "relay-node",
1647            MeshRelayConfig {
1648                queue_capacity: 1,
1649                peer_rate_per_sec: 10_000.0,
1650                peer_burst: 10_000.0,
1651                ..immediate_config()
1652            },
1653        );
1654
1655        let routine = frame_with(8, MessagePriority::Low);
1656        gov.admit(&routine, Some("b"), 3, false);
1657        gov.admit(
1658            &frame_with(8, MessagePriority::Critical),
1659            Some("b"),
1660            3,
1661            false,
1662        );
1663        assert_eq!(gov.counters().queue_evicted, 1);
1664
1665        // The urgent frame goes out, leaving room again. A later copy of the
1666        // displaced one — or the sender's retransmission — must be carryable.
1667        gov.take_due(Instant::now());
1668        assert_eq!(
1669            gov.admit(&routine, Some("c"), 3, false),
1670            RelayAdmission::Queued,
1671            "the displaced frame's id must not be blackholed"
1672        );
1673    }
1674
1675    #[test]
1676    fn a_frame_refused_room_on_the_way_back_releases_its_id() {
1677        // The third drop that happens after the id was recorded: a frame that
1678        // reached no neighbor and finds the queue full when handed back.
1679        let mut gov = MeshRelayGovernor::with_config(
1680            "relay-node",
1681            MeshRelayConfig {
1682                queue_capacity: 1,
1683                peer_rate_per_sec: 10_000.0,
1684                peer_burst: 10_000.0,
1685                ..immediate_config()
1686            },
1687        );
1688
1689        let starved = frame();
1690        gov.admit(&starved, Some("b"), 3, false);
1691        let mut due = gov.take_due(Instant::now());
1692        assert_eq!(due.len(), 1);
1693
1694        // The queue fills while the frame is out being transmitted, so there is
1695        // no room for it on the way back.
1696        gov.admit(&frame(), Some("b"), 3, false);
1697        gov.requeue(due.remove(0));
1698        assert_eq!(gov.counters().queue_full, 1);
1699        assert_eq!(gov.pending_len(), 1, "the dropped frame is not held");
1700
1701        // Once the queue drains, the copy behind it must still be carryable.
1702        gov.take_due(Instant::now());
1703        assert_eq!(
1704            gov.admit(&starved, Some("c"), 3, false),
1705            RelayAdmission::Queued,
1706            "a frame that got nowhere and could not be kept must stay carryable"
1707        );
1708    }
1709
1710    #[test]
1711    fn forwarding_leaves_room_for_this_devices_own_traffic() {
1712        // One radio, one budget — but a device saturated with other people's
1713        // traffic must still be able to send its own, above all the delivery
1714        // acknowledgement whose absence makes a delivered message look lost.
1715        let mut gov = MeshRelayGovernor::with_config(
1716            "relay-node",
1717            MeshRelayConfig {
1718                queue_capacity: 512,
1719                peer_rate_per_sec: 10_000.0,
1720                peer_burst: 10_000.0,
1721                ..immediate_config()
1722            },
1723        );
1724
1725        for _ in 0..200 {
1726            gov.admit(&frame(), Some("b"), 3, false);
1727        }
1728        gov.take_due(Instant::now());
1729
1730        // Drain the budget the way a saturated forwarder does.
1731        let forwarded = (0..1000).filter(|_| gov.take_send_token()).count();
1732        assert!(forwarded > 0, "forwarding must get its share first");
1733        assert!(
1734            !gov.take_send_token(),
1735            "forwarding must stop at the reserve, not at empty"
1736        );
1737
1738        // The reserve is still there for us.
1739        let own = (0..1000).filter(|_| gov.take_own_send_token()).count();
1740        assert!(
1741            own >= RELAY_OWN_TRAFFIC_RESERVE as usize,
1742            "own traffic got {own} transmissions against a reserve of {RELAY_OWN_TRAFFIC_RESERVE}"
1743        );
1744
1745        // And the ceiling still holds across both: the reserve is carved out of
1746        // the burst, not added to it.
1747        assert!(
1748            (forwarded + own) <= DEFAULT_RELAY_BURST as usize + 1,
1749            "{forwarded} forwarded + {own} own exceeds the burst of {DEFAULT_RELAY_BURST}"
1750        );
1751    }
1752
1753    #[test]
1754    fn a_small_configured_burst_still_forwards() {
1755        // The reserve is clamped to half the burst, so a device configured with
1756        // a burst smaller than the reserve does not silently stop forwarding
1757        // altogether.
1758        let mut gov = MeshRelayGovernor::with_config(
1759            "relay-node",
1760            MeshRelayConfig {
1761                burst: 2.0,
1762                rate_per_sec: 1.0,
1763                peer_rate_per_sec: 10_000.0,
1764                peer_burst: 10_000.0,
1765                ..immediate_config()
1766            },
1767        );
1768
1769        gov.admit(&frame(), Some("b"), 3, false);
1770        assert!(
1771            !gov.take_due(Instant::now()).is_empty(),
1772            "a small burst must still release forwards"
1773        );
1774        assert!(gov.take_send_token(), "and still allow transmitting them");
1775    }
1776
1777    #[test]
1778    fn a_settled_duplicate_is_absorbed_without_the_full_admission_path() {
1779        let mut gov = governor();
1780        let msg = frame();
1781
1782        assert!(
1783            !gov.absorb_settled_duplicate(&msg.id.as_str()),
1784            "an id never seen is not a duplicate"
1785        );
1786
1787        gov.admit(&msg, Some("b"), 3, false);
1788        // While our copy is still pending, standing down is a decision that
1789        // needs the neighbor set, so the cheap path must decline to make it.
1790        assert!(!gov.absorb_settled_duplicate(&msg.id.as_str()));
1791
1792        // Once it has gone out, further copies are settled duplicates.
1793        gov.take_due(Instant::now());
1794        assert!(gov.absorb_settled_duplicate(&msg.id.as_str()));
1795        assert_eq!(gov.counters().duplicates_suppressed, 1);
1796    }
1797
1798    #[test]
1799    fn eviction_prefers_a_frame_that_has_not_yet_gone_out() {
1800        // A due frame is on its way to the radio this tick; taking it costs a
1801        // transmission that was about to happen, while the frame replacing it
1802        // still has to wait out its hold.
1803        let mut gov = MeshRelayGovernor::with_config(
1804            "relay-node",
1805            MeshRelayConfig {
1806                queue_capacity: 2,
1807                jitter_min: Duration::from_millis(0),
1808                jitter_max: Duration::from_millis(0),
1809                peer_rate_per_sec: 10_000.0,
1810                peer_burst: 10_000.0,
1811                ..Default::default()
1812            },
1813        );
1814
1815        // One low-priority frame due immediately.
1816        let due_now = frame_with(8, MessagePriority::Low);
1817        gov.admit(&due_now, Some("b"), 3, false);
1818
1819        // A second low-priority frame that is not due for a while.
1820        gov.config.jitter_min = Duration::from_millis(500);
1821        gov.config.jitter_max = Duration::from_millis(500);
1822        let waiting = frame_with(8, MessagePriority::Low);
1823        gov.admit(&waiting, Some("b"), 3, false);
1824
1825        // An urgent frame needs the room.
1826        gov.admit(
1827            &frame_with(8, MessagePriority::Critical),
1828            Some("b"),
1829            3,
1830            false,
1831        );
1832
1833        assert_eq!(gov.counters().queue_evicted, 1);
1834        assert_eq!(
1835            gov.counters().queue_full,
1836            0,
1837            "making room is not the same as refusing"
1838        );
1839        assert!(
1840            gov.pending
1841                .iter()
1842                .any(|p| p.message.id.as_str() == due_now.id.as_str()),
1843            "the frame already due must survive"
1844        );
1845        assert!(
1846            !gov.pending
1847                .iter()
1848                .any(|p| p.message.id.as_str() == waiting.id.as_str()),
1849            "the frame still waiting is the one displaced"
1850        );
1851    }
1852
1853    #[test]
1854    fn a_full_peer_table_keeps_the_links_that_are_spending() {
1855        // Clearing the table wholesale hands every neighbor a fresh burst,
1856        // including the noisy one the per-link limit exists to hold back.
1857        let mut gov = MeshRelayGovernor::with_config("relay-node", immediate_config());
1858
1859        // One neighbor spends most of its allowance.
1860        for _ in 0..(DEFAULT_RELAY_PEER_BURST as usize - 1) {
1861            gov.admit(&frame(), Some("noisy"), 3, false);
1862        }
1863
1864        // Fill the table with links that arrive once and go quiet.
1865        for i in 0..MAX_RELAY_RATE_PEERS {
1866            gov.admit(&frame(), Some(&format!("idle-{i}")), 3, false);
1867        }
1868
1869        assert!(gov.peer_budgets.len() <= MAX_RELAY_RATE_PEERS);
1870        // Still tracked at all: the entry the limit exists to hold back is the
1871        // one it can least afford to forget, and it never sends again here, so
1872        // an eviction would leave it absent rather than rebuilt.
1873        let noisy = gov
1874            .peer_budgets
1875            .get("noisy")
1876            .expect("a link mid-spend must not be forgotten to make room for idle ones");
1877        assert!(
1878            noisy.tokens < noisy.capacity,
1879            "the noisy link kept its spent allowance rather than being handed a fresh burst"
1880        );
1881    }
1882
1883    #[test]
1884    fn a_refused_frame_leaves_the_way_open_for_another_copy() {
1885        // A frame turned away must not be remembered as handled: the copy
1886        // arriving over a healthy link a moment later is the alternate path,
1887        // and suppressing it would blank every route through this device for
1888        // the whole retention window.
1889        let mut gov = governor();
1890
1891        // Refused for having no hops left — the cheapest thing an attacker can
1892        // forge, since nothing authenticates the claim.
1893        let msg = frame_with(1, MessagePriority::Medium);
1894        assert_eq!(
1895            gov.admit(&msg, Some("hostile"), 3, false),
1896            RelayAdmission::Rejected(RelayRejection::HopLimitReached)
1897        );
1898
1899        // The same message, arriving intact by another route.
1900        let mut healthy = msg.clone();
1901        healthy.ttl = TTL::new(8).unwrap();
1902        assert_eq!(
1903            gov.admit(&healthy, Some("good"), 3, false),
1904            RelayAdmission::Queued,
1905            "a refused frame must not blackhole its id"
1906        );
1907    }
1908
1909    #[test]
1910    fn a_rate_limited_neighbor_does_not_blackhole_the_ids_it_sent() {
1911        let mut gov = governor();
1912
1913        // Exhaust one neighbor's allowance with distinct frames.
1914        let mut refused = Vec::new();
1915        for _ in 0..200 {
1916            let msg = frame();
1917            if gov.admit(&msg, Some("noisy"), 3, false)
1918                == RelayAdmission::Rejected(RelayRejection::PeerRateLimited)
1919            {
1920                refused.push(msg);
1921            }
1922        }
1923        assert!(!refused.is_empty());
1924
1925        // Those same messages, reaching us through someone else, must still be
1926        // carried.
1927        let victim = &refused[0];
1928        assert_eq!(
1929            gov.admit(victim, Some("quiet"), 3, false),
1930            RelayAdmission::Queued
1931        );
1932    }
1933
1934    #[test]
1935    fn the_last_hop_does_not_stand_down_for_a_neighbor() {
1936        // Standing down assumes a neighbor's transmission covers the same
1937        // ground ours would. At the last hop it does not: our copy is going to
1938        // the recipient over a link only we are known to hold, so dropping it
1939        // loses the delivery.
1940        let mut gov = MeshRelayGovernor::with_config(
1941            "relay-node",
1942            MeshRelayConfig {
1943                jitter_min: Duration::from_millis(50),
1944                jitter_max: Duration::from_millis(200),
1945                ..Default::default()
1946            },
1947        );
1948        let msg = frame();
1949
1950        assert_eq!(gov.admit(&msg, Some("b"), 3, true), RelayAdmission::Queued);
1951        // Another forwarder hands us the same frame.
1952        assert_eq!(
1953            gov.admit(&msg, Some("c"), 3, true),
1954            RelayAdmission::Rejected(RelayRejection::AlreadySeen)
1955        );
1956
1957        assert_eq!(gov.pending_len(), 1, "the delivering copy must survive");
1958        assert_eq!(gov.counters().cancelled_by_duplicate, 0);
1959    }
1960
1961    #[test]
1962    fn a_forward_waiting_far_past_its_turn_is_abandoned() {
1963        let mut gov = governor();
1964        gov.admit(&frame(), Some("b"), 3, false);
1965
1966        // Long enough that other paths have carried it or the sender has
1967        // retransmitted; holding it would only displace newer traffic.
1968        let due = gov.take_due(Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_secs(1));
1969        assert!(due.is_empty());
1970        assert_eq!(gov.counters().abandoned_overdue, 1);
1971        assert_eq!(gov.pending_len(), 0);
1972    }
1973
1974    #[test]
1975    fn a_full_queue_refuses_rather_than_growing() {
1976        let mut gov = MeshRelayGovernor::with_config(
1977            "relay-node",
1978            MeshRelayConfig {
1979                queue_capacity: 4,
1980                jitter_min: Duration::from_millis(500),
1981                jitter_max: Duration::from_millis(500),
1982                peer_rate_per_sec: 10_000.0,
1983                peer_burst: 10_000.0,
1984                ..Default::default()
1985            },
1986        );
1987
1988        for _ in 0..4 {
1989            assert_eq!(
1990                gov.admit(&frame(), Some("b"), 3, false),
1991                RelayAdmission::Queued
1992            );
1993        }
1994        assert_eq!(
1995            gov.admit(&frame(), Some("b"), 3, false),
1996            RelayAdmission::Rejected(RelayRejection::QueueFull)
1997        );
1998        assert_eq!(gov.pending_len(), 4);
1999    }
2000
2001    #[test]
2002    fn a_full_queue_still_takes_an_urgent_frame_over_a_routine_one() {
2003        let mut gov = MeshRelayGovernor::with_config(
2004            "relay-node",
2005            MeshRelayConfig {
2006                queue_capacity: 2,
2007                jitter_min: Duration::from_millis(500),
2008                jitter_max: Duration::from_millis(500),
2009                peer_rate_per_sec: 10_000.0,
2010                peer_burst: 10_000.0,
2011                ..Default::default()
2012            },
2013        );
2014
2015        gov.admit(&frame_with(8, MessagePriority::Low), Some("b"), 3, false);
2016        gov.admit(&frame_with(8, MessagePriority::Low), Some("b"), 3, false);
2017
2018        assert_eq!(
2019            gov.admit(
2020                &frame_with(8, MessagePriority::Critical),
2021                Some("b"),
2022                3,
2023                false
2024            ),
2025            RelayAdmission::Queued
2026        );
2027        assert_eq!(gov.pending_len(), 2);
2028        assert!(gov
2029            .take_due(Instant::now() + Duration::from_secs(1))
2030            .iter()
2031            .any(|r| r.message.priority == MessagePriority::Critical));
2032    }
2033
2034    #[test]
2035    fn a_frame_is_never_sent_back_where_it_came_from() {
2036        let gov = governor();
2037        let targets = gov.select_targets(
2038            [("alice", 80), ("b", 80), ("c", 80), ("d", 80)],
2039            &["b", "alice"], // arrival neighbor and the peer that wrote it
2040            "msg-1",
2041        );
2042
2043        assert!(!targets.contains(&"b".to_string()));
2044        assert!(!targets.contains(&"alice".to_string()));
2045        assert!(targets.contains(&"c".to_string()));
2046        assert!(targets.contains(&"d".to_string()));
2047    }
2048
2049    #[test]
2050    fn fanout_is_capped_and_stable_for_a_given_frame() {
2051        let gov = governor();
2052        // All equally good, so the tie-break is what decides — which is the
2053        // property under test.
2054        let neighbors = [
2055            ("n0", 70),
2056            ("n1", 70),
2057            ("n2", 70),
2058            ("n3", 70),
2059            ("n4", 70),
2060            ("n5", 70),
2061            ("n6", 70),
2062            ("n7", 70),
2063        ];
2064
2065        let first = gov.select_targets(neighbors, &[], "msg-1");
2066        let again = gov.select_targets(neighbors, &[], "msg-1");
2067
2068        assert_eq!(first.len(), DEFAULT_RELAY_FANOUT);
2069        assert_eq!(first, again, "a second copy must pick the same neighbors");
2070
2071        // Different frames spread across different neighbors rather than
2072        // hammering one set.
2073        let other = gov.select_targets(neighbors, &[], "msg-2");
2074        assert_eq!(other.len(), DEFAULT_RELAY_FANOUT);
2075    }
2076
2077    #[test]
2078    fn a_frame_we_originated_is_not_carried_back_for_someone_else() {
2079        // Our own message, handed to neighbors. When a neighbor's copy reaches
2080        // us again we must recognize it, not take it on as third-party traffic.
2081        let mut gov = governor();
2082        let msg = frame();
2083
2084        gov.mark_handled(&msg.id.as_str());
2085
2086        assert_eq!(
2087            gov.admit(&msg, Some("b"), 3, false),
2088            RelayAdmission::Rejected(RelayRejection::AlreadySeen)
2089        );
2090    }
2091
2092    #[test]
2093    fn the_suppression_cache_holds_a_saturated_node_for_its_full_window() {
2094        // Capacity has to outlast in-flight copies at the maximum rate the node
2095        // will accept traffic. If it did not, an id could be forgotten while
2096        // copies were still circulating and be forwarded a second time — by
2097        // every node, which is how a flood becomes a storm.
2098        let gov = governor();
2099        let max_admissions_per_sec = DEFAULT_RELAY_PEER_RATE_PER_SEC * MAX_RELAY_RATE_PEERS as f32;
2100        let sustained_ceiling = DEFAULT_RELAY_RATE_PER_SEC.min(max_admissions_per_sec);
2101        let ids_in_window = sustained_ceiling * gov.config().seen.retention.as_secs_f32();
2102
2103        assert!(
2104            (gov.config().seen.capacity as f32) >= ids_in_window,
2105            "cache holds {} ids but a saturated node sees {} within the retention window",
2106            gov.config().seen.capacity,
2107            ids_in_window
2108        );
2109    }
2110
2111    // ====================================================================
2112    // Capability bias
2113    // ====================================================================
2114
2115    /// A governor with a real jitter window, so bias has something to shift.
2116    fn biased_governor() -> MeshRelayGovernor {
2117        MeshRelayGovernor::with_config("relay-node", MeshRelayConfig::default())
2118    }
2119
2120    #[test]
2121    fn a_weak_device_always_waits_longer_than_a_capable_one() {
2122        // The whole mechanism: the weak device's earliest delay must be later
2123        // than the capable device's latest, or the two windows overlap and the
2124        // weak one sometimes transmits first — spending the airtime the bias
2125        // exists to save.
2126        let msg = frame();
2127
2128        let mut capable = biased_governor();
2129        capable.set_conditions(Some(100), false, false, 30);
2130
2131        let mut weak = biased_governor();
2132        weak.set_conditions(Some(30), false, false, 30);
2133
2134        let capable_delay = capable.jitter_for(&msg.id.as_str(), 3);
2135        let weak_delay = weak.jitter_for(&msg.id.as_str(), 3);
2136        assert!(
2137            weak_delay > capable_delay,
2138            "weak waited {weak_delay:?}, capable {capable_delay:?}"
2139        );
2140
2141        // Independent of which frame it is: the handicap shifts the window, so
2142        // the worst case for the weak device beats the best case for the
2143        // capable one on every id.
2144        //
2145        // The bound that has to clear the span is the handicap a device can
2146        // actually pay, not the configured ceiling. A device at the floor
2147        // scales to `bias_min_scale`, never to zero, so the most it ever pays
2148        // is `(1 - bias_min_scale) * bias_max_handicap` — mirroring
2149        // `jitter_for`'s expression, truncation included. Asserting the raw
2150        // ceiling instead would stay green while a raised min scale silently
2151        // let the two windows overlap.
2152        let span = DEFAULT_RELAY_JITTER_MAX_MS - DEFAULT_RELAY_JITTER_MIN_MS;
2153        let reachable_handicap_ms = ((1.0 - DEFAULT_RELAY_BIAS_MIN_SCALE)
2154            * DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS as f32) as u64;
2155        assert!(
2156            reachable_handicap_ms >= span,
2157            "the reachable handicap ({reachable_handicap_ms}ms: a \
2158             {DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS}ms ceiling scaled by 1 - \
2159             {DEFAULT_RELAY_BIAS_MIN_SCALE}) must cover the jitter span ({span}ms) or the two \
2160             windows overlap"
2161        );
2162    }
2163
2164    #[test]
2165    fn bias_adds_a_bounded_delay_rather_than_a_multiplied_one() {
2166        // Bias must cost redundancy, never delivery. The jitter span already
2167        // grows with density, so the one shape that must not exist is a
2168        // handicap that grows with it too: at every density the biased device
2169        // waits at most one fixed handicap longer than the capable one.
2170        let msg = frame();
2171        let cap = Duration::from_millis(DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS);
2172
2173        for degree in [0usize, 3, 6, 30, 100, MAX_RELAY_RATE_PEERS] {
2174            let mut capable = biased_governor();
2175            capable.set_conditions(Some(100), false, false, 30);
2176            let mut weak = biased_governor();
2177            weak.set_conditions(Some(0), false, false, 30);
2178
2179            let capable_delay = capable.jitter_for(&msg.id.as_str(), degree);
2180            let weak_delay = weak.jitter_for(&msg.id.as_str(), degree);
2181            assert!(weak_delay >= capable_delay, "degree {degree}");
2182            assert!(
2183                weak_delay - capable_delay <= cap,
2184                "at degree {degree} bias added {:?}, past its {cap:?} ceiling",
2185                weak_delay - capable_delay
2186            );
2187        }
2188    }
2189
2190    #[test]
2191    fn a_biased_forward_still_transmits_inside_the_overdue_cutoff() {
2192        // Asserted over *every* possible message id by computing the worst
2193        // case, not by sampling one: the delay is `min + handicap + hash %
2194        // span`, so a sampled id passes or fails on its hash and would make
2195        // this a coin toss rather than a guarantee.
2196        //
2197        // Bounded to the venue-sized neighborhood this design targets (~100
2198        // nodes). Past roughly 155 neighbors the delay does outrun the
2199        // cut-off — but the density scaling alone does that at ~167 with bias
2200        // switched off entirely, so it is a property of the pre-existing
2201        // density term rather than of bias, and out of scope here.
2202        let config = MeshRelayConfig::default();
2203        let min_ms = config.jitter_min.as_millis() as u64;
2204        let max_ms = config.jitter_max.as_millis() as u64;
2205        let degree = 100usize;
2206
2207        let density_scale = 1 + (degree / config.dense_degree.max(1)) as u64;
2208        let span = (max_ms - min_ms).saturating_mul(density_scale).max(1);
2209        // The full handicap, which only a device biased to the floor pays.
2210        let worst = Duration::from_millis(
2211            min_ms + config.bias_max_handicap.as_millis() as u64 + (span - 1),
2212        );
2213
2214        assert!(
2215            worst < RELAY_QUEUE_MAX_OVERDUE,
2216            "the worst biased wait at degree {degree} is {worst:?}, past the \
2217             {RELAY_QUEUE_MAX_OVERDUE:?} cut-off"
2218        );
2219    }
2220
2221    #[test]
2222    fn charging_and_eager_devices_are_not_biased_down() {
2223        let msg = frame();
2224        let mut unfed = biased_governor();
2225        let baseline = unfed.jitter_for(&msg.id.as_str(), 3);
2226
2227        // Charging at a level that would otherwise be heavily penalised.
2228        let mut charging = biased_governor();
2229        charging.set_conditions(Some(20), true, false, 30);
2230        assert_eq!(charging.jitter_for(&msg.id.as_str(), 3), baseline);
2231
2232        // Configured to relay eagerly, on battery.
2233        let mut eager = biased_governor();
2234        eager.set_conditions(Some(20), false, true, 30);
2235        assert_eq!(eager.jitter_for(&msg.id.as_str(), 3), baseline);
2236
2237        // And an unfed device is treated as fully capable, matching the
2238        // forwarding gate's "unknown means willing".
2239        unfed.set_conditions(None, false, false, 30);
2240        assert_eq!(unfed.jitter_for(&msg.id.as_str(), 3), baseline);
2241    }
2242
2243    #[test]
2244    fn a_weak_device_forwards_to_fewer_neighbors_but_never_none() {
2245        let neighbors: Vec<(&str, u8)> = vec![("a", 90), ("b", 80), ("c", 70), ("d", 60)];
2246
2247        let mut capable = biased_governor();
2248        capable.set_conditions(Some(100), false, false, 30);
2249        let capable_targets = capable.select_targets(neighbors.clone(), &[], "msg-1");
2250        assert_eq!(capable_targets.len(), DEFAULT_RELAY_FANOUT);
2251
2252        let mut weak = biased_governor();
2253        weak.set_conditions(Some(0), false, false, 30);
2254        let weak_targets = weak.select_targets(neighbors, &[], "msg-1");
2255        assert!(
2256            !weak_targets.is_empty(),
2257            "a fan-out of zero is a silent drop, not a cheaper forward"
2258        );
2259        assert!(
2260            weak_targets.len() < capable_targets.len(),
2261            "weak fanned out to {} neighbors, capable to {}",
2262            weak_targets.len(),
2263            capable_targets.len()
2264        );
2265    }
2266
2267    #[test]
2268    fn a_weak_device_offers_its_own_frames_to_every_neighbor_it_would_have() {
2269        // Bias narrows the fan-out for other people's traffic, where neighbors
2270        // hold copies and the sender is still retrying, so the cost is
2271        // redundancy. A frame this device originated has neither: the fan-out
2272        // *is* the delivery attempt, and because targets are chosen stably per
2273        // message id, a narrowed set would be the same narrowed set on every
2274        // retransmission — one dead-end neighbor forever instead of three
2275        // chances.
2276        let neighbors: Vec<(&str, u8)> = vec![("a", 90), ("b", 80), ("c", 70), ("d", 60)];
2277
2278        let mut weak = biased_governor();
2279        weak.set_conditions(Some(0), false, false, 30);
2280
2281        assert!(
2282            weak.select_targets(neighbors.clone(), &[], "msg-1").len() < DEFAULT_RELAY_FANOUT,
2283            "the forwarding path must still be biased",
2284        );
2285        assert_eq!(
2286            weak.select_origination_targets(neighbors.clone(), &[], "msg-1")
2287                .len(),
2288            DEFAULT_RELAY_FANOUT,
2289            "this device's own frame must reach as many neighbors as a capable device's",
2290        );
2291
2292        // And identical to what a fully capable device would have picked, not
2293        // merely as many: same neighbors, same order.
2294        let mut capable = biased_governor();
2295        capable.set_conditions(Some(100), false, false, 30);
2296        assert_eq!(
2297            weak.select_origination_targets(neighbors.clone(), &[], "msg-1"),
2298            capable.select_origination_targets(neighbors, &[], "msg-1"),
2299        );
2300    }
2301
2302    #[test]
2303    fn bias_does_not_throttle_this_devices_own_traffic() {
2304        // Bias scales the *forwarding* share of the airtime ceiling, not the
2305        // ceiling. Scaling the shared bucket would slow a low-battery device's
2306        // own messages and acknowledgements in step with its battery, which is
2307        // delivery rather than redundancy — and an absent acknowledgement is
2308        // what makes a delivered message look lost to its sender.
2309        let mut capable = MeshRelayGovernor::with_config("relay-node", immediate_config());
2310        capable.set_conditions(Some(100), false, false, 30);
2311        let capable_own = (0..1000).filter(|_| capable.take_own_send_token()).count();
2312
2313        let mut weak = MeshRelayGovernor::with_config("relay-node", immediate_config());
2314        weak.set_conditions(Some(0), false, false, 30);
2315        let weak_own = (0..1000).filter(|_| weak.take_own_send_token()).count();
2316
2317        assert_eq!(
2318            weak_own, capable_own,
2319            "a weak device got {weak_own} of its own transmissions against a capable \
2320             device's {capable_own}",
2321        );
2322        assert!(capable_own > 0, "the fixture must actually grant something");
2323    }
2324
2325    #[test]
2326    fn a_weak_device_transmits_less_before_hitting_its_ceiling() {
2327        let mut capable = MeshRelayGovernor::with_config("relay-node", immediate_config());
2328        capable.set_conditions(Some(100), false, false, 30);
2329        let capable_grants = (0..1000).filter(|_| capable.take_send_token()).count();
2330
2331        let mut weak = MeshRelayGovernor::with_config("relay-node", immediate_config());
2332        weak.set_conditions(Some(0), false, false, 30);
2333        let weak_grants = (0..1000).filter(|_| weak.take_send_token()).count();
2334
2335        assert!(
2336            weak_grants < capable_grants,
2337            "weak got {weak_grants} transmissions, capable {capable_grants}"
2338        );
2339        assert!(
2340            weak_grants > 0,
2341            "bias scales the budget down, it does not switch forwarding off"
2342        );
2343    }
2344
2345    #[test]
2346    fn a_worsening_battery_does_not_mint_a_fresh_burst() {
2347        // `set_conditions` runs every tick, so re-sizing the bucket must keep
2348        // what has been spent spent. If it refilled, a device whose battery
2349        // oscillates around any point in the ramp would transmit without limit.
2350        let mut gov = MeshRelayGovernor::with_config("relay-node", immediate_config());
2351        gov.set_conditions(Some(100), false, false, 30);
2352
2353        let first = (0..1000).filter(|_| gov.take_send_token()).count();
2354        assert!(first > 0);
2355        assert!(!gov.take_send_token(), "the budget must be spent");
2356
2357        for level in [90, 100, 90, 100] {
2358            gov.set_conditions(Some(level), false, false, 30);
2359            assert!(
2360                !gov.take_send_token(),
2361                "re-sizing the budget handed back tokens that were already spent"
2362            );
2363        }
2364    }
2365
2366    // ====================================================================
2367    // Activity-derived relay standing
2368    // ====================================================================
2369
2370    /// Short windows so activity transitions are reachable without waiting a
2371    /// real minute; `observe_activity` takes `now`, so the clock is explicit.
2372    fn activity_config() -> MeshRelayConfig {
2373        MeshRelayConfig {
2374            activity_window: Duration::from_secs(10),
2375            activity_min_forwards: 2,
2376            activity_idle_windows: 2,
2377            ..immediate_config()
2378        }
2379    }
2380
2381    #[test]
2382    fn carrying_traffic_is_what_makes_a_device_a_relay() {
2383        let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2384        let start = Instant::now();
2385        assert!(!gov.is_active_relay());
2386
2387        // A window that carried nothing changes nothing.
2388        assert_eq!(gov.observe_activity(start + Duration::from_secs(11)), None);
2389        assert!(!gov.is_active_relay());
2390
2391        // Carrying enough in one window promotes, exactly once.
2392        gov.record_forwarded();
2393        gov.record_forwarded();
2394        assert_eq!(
2395            gov.observe_activity(start + Duration::from_secs(22)),
2396            Some(RelayActivity::Began { forwarded: 2 })
2397        );
2398        assert!(gov.is_active_relay());
2399
2400        gov.record_forwarded();
2401        gov.record_forwarded();
2402        assert_eq!(
2403            gov.observe_activity(start + Duration::from_secs(33)),
2404            None,
2405            "a relay that keeps carrying traffic must not re-announce"
2406        );
2407        assert!(gov.is_active_relay());
2408    }
2409
2410    #[test]
2411    fn a_relay_stops_being_one_only_after_sustained_quiet() {
2412        // Asymmetric on purpose: one busy window promotes, several quiet ones
2413        // demote. A mesh with nothing to say for a window has not stopped
2414        // having a relay in it.
2415        let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2416        let start = Instant::now();
2417
2418        gov.record_forwarded();
2419        gov.record_forwarded();
2420        assert!(matches!(
2421            gov.observe_activity(start + Duration::from_secs(11)),
2422            Some(RelayActivity::Began { .. })
2423        ));
2424
2425        // One quiet window is not enough.
2426        assert_eq!(gov.observe_activity(start + Duration::from_secs(22)), None);
2427        assert!(gov.is_active_relay());
2428
2429        assert_eq!(
2430            gov.observe_activity(start + Duration::from_secs(33)),
2431            Some(RelayActivity::Ceased)
2432        );
2433        assert!(!gov.is_active_relay());
2434
2435        // And it stays ceased rather than re-announcing every window.
2436        assert_eq!(gov.observe_activity(start + Duration::from_secs(44)), None);
2437    }
2438
2439    #[test]
2440    fn traffic_within_a_window_resets_the_run_of_quiet_ones() {
2441        let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2442        let start = Instant::now();
2443
2444        gov.record_forwarded();
2445        gov.record_forwarded();
2446        assert!(matches!(
2447            gov.observe_activity(start + Duration::from_secs(11)),
2448            Some(RelayActivity::Began { .. })
2449        ));
2450
2451        // Quiet, then busy again, then quiet: the run restarts, so the second
2452        // quiet window must not be the one that demotes.
2453        assert_eq!(gov.observe_activity(start + Duration::from_secs(22)), None);
2454        gov.record_forwarded();
2455        gov.record_forwarded();
2456        assert_eq!(gov.observe_activity(start + Duration::from_secs(33)), None);
2457        assert_eq!(gov.observe_activity(start + Duration::from_secs(44)), None);
2458        assert!(gov.is_active_relay(), "the quiet run restarted");
2459
2460        assert_eq!(
2461            gov.observe_activity(start + Duration::from_secs(55)),
2462            Some(RelayActivity::Ceased)
2463        );
2464    }
2465
2466    #[test]
2467    fn observing_before_a_window_has_elapsed_does_nothing() {
2468        let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2469        let start = Instant::now();
2470
2471        gov.record_forwarded();
2472        gov.record_forwarded();
2473        assert_eq!(gov.observe_activity(start + Duration::from_secs(5)), None);
2474        assert!(
2475            !gov.is_active_relay(),
2476            "the window must complete before it counts"
2477        );
2478
2479        // And the frames carried are still counted toward it.
2480        assert!(matches!(
2481            gov.observe_activity(start + Duration::from_secs(11)),
2482            Some(RelayActivity::Began { forwarded: 2 })
2483        ));
2484    }
2485
2486    #[test]
2487    fn a_closing_gate_drops_the_relay_standing_at_once() {
2488        // Configuration or battery closing the forwarding gate stops traffic
2489        // immediately, so waiting out the idle windows would report a relay
2490        // that had already stopped being one.
2491        let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2492        let start = Instant::now();
2493
2494        gov.record_forwarded();
2495        gov.record_forwarded();
2496        assert!(matches!(
2497            gov.observe_activity(start + Duration::from_secs(11)),
2498            Some(RelayActivity::Began { .. })
2499        ));
2500
2501        assert!(gov.force_inactive(), "reports that the standing was held");
2502        assert!(!gov.is_active_relay());
2503        assert!(
2504            !gov.force_inactive(),
2505            "a device that was not a relay must not report a demotion"
2506        );
2507
2508        // The window's tally went with it, so re-opening the gate starts from
2509        // scratch rather than promoting on stale traffic.
2510        assert_eq!(gov.observe_activity(start + Duration::from_secs(22)), None);
2511        assert!(!gov.is_active_relay());
2512    }
2513}