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