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::{Message, MessagePriority};
64use offline_protocol_reliability::{RelaySeenCache, RelaySeenConfig};
65use std::collections::hash_map::DefaultHasher;
66use std::collections::HashMap;
67use std::hash::{Hash, Hasher};
68use std::time::{Duration, Instant};
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.
1302 //
1303 // Ties break on a hash of (id, peer, self), which does two things.
1304 // Within a group of equally good links it spreads different messages
1305 // across different neighbors rather than pinning every frame to the
1306 // same one. And it is stable per frame: a second copy of the same
1307 // message picks the same neighbors instead of a fresh set, so a
1308 // retransmission does not widen its own footprint.
1309 candidates.sort_by(|(a_peer, a_quality), (b_peer, b_quality)| {
1310 b_quality.cmp(a_quality).then_with(|| {
1311 Self::hash_of(&[message_id, a_peer, &self.local_id]).cmp(&Self::hash_of(&[
1312 message_id,
1313 b_peer,
1314 &self.local_id,
1315 ]))
1316 })
1317 });
1318 candidates
1319 .into_iter()
1320 .take(fanout)
1321 .map(|(peer, _)| peer.to_string())
1322 .collect()
1323 }
1324
1325 /// Drops per-neighbor rate state for a peer that has gone away.
1326 pub fn forget_peer(&mut self, peer_id: &str) {
1327 self.peer_budgets.remove(peer_id);
1328 }
1329
1330 /// Releases expired suppression entries. Safe to call from a periodic tick.
1331 pub fn maintain(&mut self, now: Instant) {
1332 self.seen.expire(now);
1333 }
1334
1335 /// Builds the copy to forward, or `None` when the frame has no hop budget
1336 /// left.
1337 fn prepare_hop(&mut self, message: &Message, degree: usize) -> Option<Message> {
1338 if message.hop_count.value() >= RELAY_MAX_HOP_COUNT {
1339 return None;
1340 }
1341
1342 let ceiling = if degree >= self.config.dense_degree {
1343 self.config.dense_max_ttl
1344 } else {
1345 self.config.max_ttl
1346 };
1347
1348 let claimed = message.ttl.value();
1349 let effective = claimed.min(ceiling);
1350 if effective < claimed {
1351 self.counters.ttl_clamped = self.counters.ttl_clamped.saturating_add(1);
1352 }
1353
1354 // A budget of 1 or less is spent: this node is the last that may hold
1355 // the frame, matching `TTL::is_exhausted`.
1356 let remaining = offline_protocol_core::TTL::from_value(effective).decrement()?;
1357
1358 let mut forwarded = message.clone();
1359 // Rewrite rather than decrement in place: the frame leaves with a
1360 // budget this node vouches for, so an inflated claim cannot survive
1361 // past this hop.
1362 forwarded.ttl = remaining;
1363 let _ = forwarded.increment_hop();
1364 // Every device that transmits a third-party frame strips the deposit
1365 // request from the copy it transmits, custody-enabled or not
1366 // (`docs/spec/custody.md`, "What a forwarder does"). The key is
1367 // unsigned metadata outside the sealed body, so nothing else stops
1368 // it travelling on; left in place, a two-hop network deposits a
1369 // frame at a custodian that never saw its sender, and a zone holds
1370 // every frame everywhere. Only the outer metadata is touched, never
1371 // `content`.
1372 forwarded.metadata.remove(CUSTODY_META_KEY);
1373 Some(forwarded)
1374 }
1375
1376 /// Makes room for a higher-priority forward by displacing the
1377 /// lowest-priority one that is not yet due.
1378 ///
1379 /// Frames already past their due time are left alone: they are on their way
1380 /// to the radio this tick, so taking one costs a transmission that was
1381 /// about to happen while the frame replacing it still has to wait out its
1382 /// hold. Only when every lower-priority frame is already due does it
1383 /// displace one of those instead of refusing outright.
1384 fn evict_for(&mut self, candidate: &Message, now: Instant) -> bool {
1385 let candidate_rank = priority_rank(candidate.priority);
1386 let lower_priority = |p: &PendingRelay| priority_rank(p.message.priority) < candidate_rank;
1387
1388 let victim = self
1389 .pending
1390 .iter()
1391 .enumerate()
1392 .filter(|(_, p)| lower_priority(p) && p.due_at > now)
1393 .min_by_key(|(_, p)| priority_rank(p.message.priority))
1394 .map(|(index, _)| index)
1395 .or_else(|| {
1396 self.pending
1397 .iter()
1398 .enumerate()
1399 .filter(|(_, p)| lower_priority(p))
1400 .min_by_key(|(_, p)| priority_rank(p.message.priority))
1401 .map(|(index, _)| index)
1402 });
1403
1404 match victim {
1405 Some(index) => {
1406 let displaced = self.pending.remove(index);
1407 // Displaced before it ever reached a link. Its id goes back to
1408 // being unknown, so the copy behind it — or the sender's
1409 // retransmission — can still be carried by this device once
1410 // there is room again.
1411 self.seen.forget(&displaced.message.id.as_str());
1412 self.counters.queue_evicted = self.counters.queue_evicted.saturating_add(1);
1413 true
1414 }
1415 None => false,
1416 }
1417 }
1418
1419 fn take_peer_token(&mut self, peer_id: &str, now: Instant) -> bool {
1420 if !self.peer_budgets.contains_key(peer_id) {
1421 // Bound the map: an attacker cannot grow it with invented peer ids
1422 // because entries are only created for peers we hold a link to,
1423 // but churn over a long session can still accumulate.
1424 if self.peer_budgets.len() >= MAX_RELAY_RATE_PEERS {
1425 // Make room by forgetting the link that is spending least,
1426 // rather than by clearing the table. A wholesale reset hands
1427 // every neighbor a fresh burst — including the noisy one the
1428 // per-link limit exists to hold back, which is the entry it can
1429 // least afford to lose. The bucket that has recovered the most
1430 // tokens is the one whose absence changes the least: at full
1431 // capacity it is indistinguishable from a link never tracked.
1432 //
1433 // Costs a scan of the table, but only once the ceiling is
1434 // reached and only when a genuinely new link appears.
1435 for bucket in self.peer_budgets.values_mut() {
1436 bucket.refill(now);
1437 }
1438 let most_recovered = self
1439 .peer_budgets
1440 .iter()
1441 .max_by(|(_, a), (_, b)| a.tokens.total_cmp(&b.tokens))
1442 .map(|(peer, _)| peer.clone());
1443 if let Some(peer) = most_recovered {
1444 self.peer_budgets.remove(&peer);
1445 }
1446 }
1447 self.peer_budgets.insert(
1448 peer_id.to_string(),
1449 TokenBucket::new(self.config.peer_burst, self.config.peer_rate_per_sec, now),
1450 );
1451 }
1452
1453 self.peer_budgets
1454 .get_mut(peer_id)
1455 .map(|bucket| bucket.try_take_above(0.0, now))
1456 .unwrap_or(true)
1457 }
1458
1459 /// The delay before a queued forward is transmitted.
1460 ///
1461 /// Derived from the frame and this node's identity, so two nodes holding
1462 /// the same frame wait different amounts and the later one's fan-out is
1463 /// narrowed by the earlier one's copy.
1464 /// The spread widens with density, because that is where the contention is.
1465 ///
1466 /// Capability shifts the whole window later rather than widening it, so
1467 /// between two neighbors holding the same frame the capable one usually
1468 /// transmits first and the weaker one's fan-out loses it before anything is
1469 /// spent. That is the point: the saving is the link that is never crossed,
1470 /// not a cheaper crossing.
1471 ///
1472 /// How reliably it wins depends on density, and the handicap is a fixed
1473 /// constant precisely so it cannot chase it. Below [`dense_degree`] the
1474 /// span is narrower than the handicap, so the weak device's earliest delay
1475 /// is later than the capable one's latest and the ordering is absolute.
1476 /// Past that the span grows and the two windows overlap, leaving a
1477 /// probabilistic bias — the capable device still wins most frames, and the
1478 /// weak one's wait stays bounded by one handicap rather than growing with
1479 /// the room. Widening the handicap to restore the absolute ordering is
1480 /// exactly the shape that would push a weak device in a crowded room past
1481 /// [`RELAY_QUEUE_MAX_OVERDUE`], where forwards are abandoned rather than
1482 /// merely late.
1483 ///
1484 /// Frames this device originated are not delayed at all — see
1485 /// [`Self::select_origination_targets`] and the caller in `send.rs`.
1486 ///
1487 /// [`dense_degree`]: MeshRelayConfig::dense_degree
1488 fn jitter_for(&self, message_id: &str, degree: usize) -> Duration {
1489 let min_ms = self.config.jitter_min.as_millis() as u64;
1490 let max_ms = (self.config.jitter_max.as_millis() as u64).max(min_ms);
1491
1492 // More neighbors means more nodes racing to forward the same frame, so
1493 // give them more room to separate.
1494 let density_scale = 1 + (degree / self.config.dense_degree.max(1)) as u64;
1495 let span = (max_ms - min_ms).saturating_mul(density_scale).max(1);
1496
1497 // Bounded by a constant rather than by the span, which already grows
1498 // with density: compounding the two would push a weak device in a
1499 // crowded room past the overdue cut-off, turning late forwards into
1500 // abandoned ones.
1501 let handicap_ms = ((1.0 - self.bias_scale()).clamp(0.0, 1.0)
1502 * self.config.bias_max_handicap.as_millis() as f32) as u64;
1503
1504 let hash = Self::hash_of(&[message_id, &self.local_id]);
1505 Duration::from_millis(min_ms + handicap_ms + (hash % span))
1506 }
1507
1508 /// Stable hash used to spread delays and fan-out choices.
1509 ///
1510 /// `DefaultHasher` is unseeded, so this is predictable to anyone who knows
1511 /// the inputs — both of which (a message id and a user id) are visible on
1512 /// the wire. That is accepted: knowing it buys only the ability to guess
1513 /// which neighbor fires first or which three links a frame takes, and every
1514 /// path it could steer a frame down is already bounded by the fan-out cap
1515 /// and the send budget. It must stay *stable*, which a randomly-seeded
1516 /// hasher would not be — a second copy of a frame has to pick the same
1517 /// neighbors, or a retransmission widens its own footprint.
1518 fn hash_of(parts: &[&str]) -> u64 {
1519 let mut hasher = DefaultHasher::new();
1520 for part in parts {
1521 part.hash(&mut hasher);
1522 }
1523 hasher.finish()
1524 }
1525}
1526
1527fn priority_rank(priority: MessagePriority) -> u8 {
1528 match priority {
1529 MessagePriority::Low => 0,
1530 MessagePriority::Medium => 1,
1531 MessagePriority::High => 2,
1532 MessagePriority::Critical => 3,
1533 }
1534}
1535
1536#[cfg(test)]
1537mod tests {
1538 use super::*;
1539 use offline_protocol_core::{AppId, UserId, TTL};
1540
1541 /// Zero jitter so `admit` queues a forward that is immediately due, which
1542 /// keeps the tests deterministic without a clock abstraction.
1543 fn immediate_config() -> MeshRelayConfig {
1544 MeshRelayConfig {
1545 jitter_min: Duration::from_millis(0),
1546 jitter_max: Duration::from_millis(0),
1547 ..Default::default()
1548 }
1549 }
1550
1551 fn governor() -> MeshRelayGovernor {
1552 MeshRelayGovernor::with_config("relay-node", immediate_config())
1553 }
1554
1555 fn frame_with(ttl: u8, priority: MessagePriority) -> Message {
1556 Message::builder(
1557 UserId::new("alice").unwrap(),
1558 UserId::new("bob").unwrap(),
1559 AppId::new("app").unwrap(),
1560 )
1561 .content("hello")
1562 .priority(priority)
1563 .ttl(TTL::new(ttl).unwrap())
1564 .build()
1565 }
1566
1567 fn frame() -> Message {
1568 frame_with(8, MessagePriority::Medium)
1569 }
1570
1571 #[test]
1572 fn a_frame_is_forwarded_once_however_many_copies_arrive() {
1573 let mut gov = governor();
1574 let msg = frame();
1575
1576 assert_eq!(gov.admit(&msg, Some("b"), 3), RelayAdmission::Queued);
1577
1578 // Copies arriving after ours has gone out are suppressed, not forwarded.
1579 let due = gov.take_due(Instant::now());
1580 assert_eq!(due.len(), 1);
1581
1582 for _ in 0..5 {
1583 assert_eq!(
1584 gov.admit(&msg, Some("c"), 3),
1585 RelayAdmission::Rejected(RelayRejection::AlreadySeen)
1586 );
1587 }
1588 assert_eq!(gov.counters().queued, 1);
1589 assert_eq!(gov.counters().duplicates_suppressed, 5);
1590 }
1591
1592 #[test]
1593 fn a_copy_arriving_while_we_wait_narrows_our_forward_rather_than_cancelling_it() {
1594 // A copy proves only that the neighbor who sent it holds the frame.
1595 // Carriers forward by unicast to a few chosen neighbors, so it says
1596 // nothing about the rest of ours: the forward stays, minus that
1597 // neighbor.
1598 let mut gov = MeshRelayGovernor::with_config(
1599 "relay-node",
1600 MeshRelayConfig {
1601 jitter_min: Duration::from_millis(50),
1602 jitter_max: Duration::from_millis(200),
1603 ..Default::default()
1604 },
1605 );
1606 let msg = frame();
1607
1608 assert_eq!(gov.admit(&msg, Some("b"), 6), RelayAdmission::Queued);
1609 assert_eq!(
1610 gov.admit(&msg, Some("c"), 6),
1611 RelayAdmission::Rejected(RelayRejection::NarrowedByDuplicate)
1612 );
1613 // A second copy from the same neighbor, and one from the neighbor it
1614 // first came from, teach nothing new.
1615 gov.admit(&msg, Some("c"), 6);
1616 gov.admit(&msg, Some("b"), 6);
1617
1618 assert_eq!(gov.pending_len(), 1, "the forward must survive a copy");
1619 assert_eq!(gov.counters().cancelled_by_duplicate, 0);
1620 let due = gov.take_due(Instant::now() + Duration::from_secs(1));
1621 assert_eq!(due.len(), 1);
1622 assert_eq!(due[0].arrival_peer.as_deref(), Some("b"));
1623 assert_eq!(due[0].heard_from, vec!["c".to_string()]);
1624 }
1625
1626 #[test]
1627 fn a_forward_every_neighbor_already_holds_is_dropped_as_covered() {
1628 let mut gov = governor();
1629 let msg = frame();
1630 gov.admit(&msg, Some("b"), 3);
1631 gov.admit(&msg, Some("c"), 3);
1632 let relay = gov
1633 .take_due(Instant::now() + Duration::from_secs(1))
1634 .pop()
1635 .expect("due");
1636
1637 gov.drop_covered(relay);
1638
1639 assert_eq!(gov.counters().cancelled_by_duplicate, 1);
1640 assert!(
1641 gov.is_suppressed(&msg.id.as_str()),
1642 "a covered frame is held all around, so later copies stay absorbed"
1643 );
1644 }
1645
1646 #[test]
1647 fn an_inflated_hop_budget_is_cut_to_local_policy() {
1648 // Nothing authenticates the hop budget at this layer, so a frame can
1649 // claim any value. It must not buy more than one hop past our own
1650 // ceiling.
1651 let mut gov = governor();
1652 let msg = frame_with(255, MessagePriority::Medium);
1653
1654 assert_eq!(gov.admit(&msg, Some("b"), 2), RelayAdmission::Queued);
1655 let due = gov.take_due(Instant::now());
1656
1657 assert_eq!(due.len(), 1);
1658 assert_eq!(due[0].message.ttl.value(), DEFAULT_RELAY_MAX_TTL - 1);
1659 assert_eq!(gov.counters().ttl_clamped, 1);
1660 }
1661
1662 #[test]
1663 fn a_dense_neighborhood_uses_the_lower_hop_ceiling() {
1664 let mut gov = governor();
1665 let msg = frame_with(255, MessagePriority::Medium);
1666
1667 assert_eq!(
1668 gov.admit(&msg, Some("b"), DEFAULT_RELAY_DENSE_DEGREE),
1669 RelayAdmission::Queued
1670 );
1671 let due = gov.take_due(Instant::now());
1672 assert_eq!(due[0].message.ttl.value(), DEFAULT_RELAY_DENSE_MAX_TTL - 1);
1673 }
1674
1675 #[test]
1676 fn a_frame_out_of_hops_is_not_forwarded() {
1677 let mut gov = governor();
1678 // A budget of 1 is spent: this node is the last that may hold it.
1679 let msg = frame_with(1, MessagePriority::Medium);
1680
1681 assert_eq!(
1682 gov.admit(&msg, Some("b"), 3),
1683 RelayAdmission::Rejected(RelayRejection::HopLimitReached)
1684 );
1685 assert_eq!(gov.counters().hop_limit_reached, 1);
1686 }
1687
1688 #[test]
1689 fn the_last_hop_is_forwarded_and_the_one_after_it_is_not() {
1690 let mut gov = governor();
1691
1692 // Two hops left: goes out with one, which the next node will refuse to
1693 // forward. Verifies the budget lands exactly on `TTL::is_exhausted`
1694 // rather than a hop early or late.
1695 assert_eq!(
1696 gov.admit(&frame_with(2, MessagePriority::Medium), Some("b"), 3),
1697 RelayAdmission::Queued
1698 );
1699 let due = gov.take_due(Instant::now());
1700 assert_eq!(due[0].message.ttl.value(), 1);
1701 assert!(due[0].message.is_ttl_exhausted());
1702 }
1703
1704 #[test]
1705 fn the_hop_ceiling_stops_a_frame_that_evaded_the_budget() {
1706 let mut gov = governor();
1707 let mut msg = frame_with(255, MessagePriority::Medium);
1708 for _ in 0..RELAY_MAX_HOP_COUNT {
1709 let _ = msg.increment_hop();
1710 }
1711
1712 assert_eq!(
1713 gov.admit(&msg, Some("b"), 3),
1714 RelayAdmission::Rejected(RelayRejection::HopLimitReached)
1715 );
1716 }
1717
1718 #[test]
1719 fn one_noisy_neighbor_cannot_consume_the_whole_node() {
1720 let mut gov = governor();
1721
1722 // Spend the noisy neighbor's allowance on unique frames — the shape
1723 // suppression cannot help with, since every id is new.
1724 let mut accepted_from_noisy = 0;
1725 for _ in 0..200 {
1726 if gov.admit(&frame(), Some("noisy"), 3) == RelayAdmission::Queued {
1727 accepted_from_noisy += 1;
1728 }
1729 }
1730
1731 assert!(
1732 accepted_from_noisy <= DEFAULT_RELAY_PEER_BURST as usize + 2,
1733 "accepted {accepted_from_noisy} frames from one neighbor"
1734 );
1735 assert!(gov.counters().peer_rate_limited > 0);
1736
1737 // A different neighbor still gets through: the limit is per link, so
1738 // one bad actor degrades only itself.
1739 assert_eq!(
1740 gov.admit(&frame(), Some("quiet"), 3),
1741 RelayAdmission::Queued
1742 );
1743 }
1744
1745 #[test]
1746 fn transmission_never_exceeds_the_node_budget() {
1747 // The invariant the whole design rests on: whatever arrives, what the
1748 // radio is asked to send is capped. Asserted on transmissions rather
1749 // than on queue releases, because one release can put a frame on
1750 // several links and it is the links that cost airtime.
1751 let mut gov = MeshRelayGovernor::with_config(
1752 "relay-node",
1753 MeshRelayConfig {
1754 queue_capacity: 512,
1755 // Admission is not what is under test here.
1756 peer_rate_per_sec: 10_000.0,
1757 peer_burst: 10_000.0,
1758 ..immediate_config()
1759 },
1760 );
1761
1762 for _ in 0..200 {
1763 gov.admit(&frame(), Some("b"), 3);
1764 }
1765 let due = gov.take_due(Instant::now());
1766 assert!(!due.is_empty());
1767
1768 // Ask for far more transmissions than the budget allows.
1769 let granted = (0..1000).filter(|_| gov.take_send_token()).count();
1770
1771 assert!(
1772 granted <= DEFAULT_RELAY_BURST as usize + 1,
1773 "granted {granted} transmissions against a burst of {DEFAULT_RELAY_BURST}"
1774 );
1775 assert_eq!(gov.counters().transmissions as usize, granted);
1776 assert!(gov.counters().rate_deferred > 0);
1777 }
1778
1779 #[test]
1780 fn a_frame_that_reached_no_neighbor_for_budget_goes_back_on_the_queue() {
1781 // `take_due` checks the budget without reserving it, so a batch of due
1782 // frames can be released against a single token: the first spends it
1783 // and the rest reach the radio with nothing left. Those frames have
1784 // been forwarded nowhere, and their ids are already recorded as
1785 // handled — dropping one would lose this copy and refuse both the
1786 // copies behind it and the sender's retransmissions for the whole
1787 // retention window. It has to go back on the queue.
1788 let mut gov = MeshRelayGovernor::with_config(
1789 "relay-node",
1790 MeshRelayConfig {
1791 peer_rate_per_sec: 10_000.0,
1792 peer_burst: 10_000.0,
1793 ..immediate_config()
1794 },
1795 );
1796
1797 for _ in 0..40 {
1798 gov.admit(&frame(), Some("b"), 3);
1799 }
1800 let due = gov.take_due(Instant::now());
1801 assert!(
1802 due.len() > DEFAULT_RELAY_BURST as usize,
1803 "the batch must outrun the budget for this to be the case under test"
1804 );
1805
1806 // Drain the budget the way a fan-out does, then hand back everything
1807 // that got nowhere.
1808 let mut requeued = 0;
1809 for relay in due {
1810 if gov.take_send_token() {
1811 continue;
1812 }
1813 gov.requeue(relay);
1814 requeued += 1;
1815 }
1816
1817 assert!(requeued > 0, "the budget must have run out mid-batch");
1818 assert_eq!(gov.pending_len(), requeued, "every starved frame is kept");
1819 assert_eq!(gov.counters().requeued as usize, requeued);
1820 }
1821
1822 #[test]
1823 fn a_requeued_frame_is_still_abandoned_once_it_is_far_past_its_turn() {
1824 // Re-queueing must not become an unbounded retry: a frame kept for
1825 // budget keeps its original due time, so the overdue cut-off still
1826 // reaches it.
1827 let mut gov = governor();
1828 gov.admit(&frame(), Some("b"), 3);
1829 let mut due = gov.take_due(Instant::now());
1830 assert_eq!(due.len(), 1);
1831
1832 gov.requeue(due.remove(0));
1833 assert_eq!(gov.pending_len(), 1);
1834
1835 let later = gov.take_due(Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_secs(1));
1836 assert!(later.is_empty());
1837 assert_eq!(gov.counters().abandoned_overdue, 1);
1838 assert_eq!(gov.pending_len(), 0);
1839 }
1840
1841 #[test]
1842 fn a_frame_abandoned_for_waiting_too_long_releases_its_id() {
1843 // A frame dropped for waiting past its turn never reached a link, so
1844 // nothing is circulating that a second forward could duplicate. Its id
1845 // must go back to being unknown, or the sender's retransmissions —
1846 // which carry the same id — are refused for the whole retention window
1847 // because of a few seconds of congestion.
1848 let mut gov = governor();
1849 let msg = frame();
1850 gov.admit(&msg, Some("b"), 3);
1851
1852 let due = gov.take_due(Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_secs(1));
1853 assert!(due.is_empty());
1854 assert_eq!(gov.counters().abandoned_overdue, 1);
1855
1856 assert_eq!(
1857 gov.admit(&msg, Some("c"), 3),
1858 RelayAdmission::Queued,
1859 "the sender's retransmission must still be carried"
1860 );
1861 }
1862
1863 #[test]
1864 fn a_frame_displaced_from_the_queue_releases_its_id() {
1865 // Same rule for a frame evicted to make room for an urgent one: it was
1866 // never transmitted, so the route through this device must stay open.
1867 let mut gov = MeshRelayGovernor::with_config(
1868 "relay-node",
1869 MeshRelayConfig {
1870 queue_capacity: 1,
1871 peer_rate_per_sec: 10_000.0,
1872 peer_burst: 10_000.0,
1873 ..immediate_config()
1874 },
1875 );
1876
1877 let routine = frame_with(8, MessagePriority::Low);
1878 gov.admit(&routine, Some("b"), 3);
1879 gov.admit(&frame_with(8, MessagePriority::Critical), Some("b"), 3);
1880 assert_eq!(gov.counters().queue_evicted, 1);
1881
1882 // The urgent frame goes out, leaving room again. A later copy of the
1883 // displaced one — or the sender's retransmission — must be carryable.
1884 gov.take_due(Instant::now());
1885 assert_eq!(
1886 gov.admit(&routine, Some("c"), 3),
1887 RelayAdmission::Queued,
1888 "the displaced frame's id must not be blackholed"
1889 );
1890 }
1891
1892 #[test]
1893 fn a_frame_refused_room_on_the_way_back_releases_its_id() {
1894 // The third drop that happens after the id was recorded: a frame that
1895 // reached no neighbor and finds the queue full when handed back.
1896 let mut gov = MeshRelayGovernor::with_config(
1897 "relay-node",
1898 MeshRelayConfig {
1899 queue_capacity: 1,
1900 peer_rate_per_sec: 10_000.0,
1901 peer_burst: 10_000.0,
1902 ..immediate_config()
1903 },
1904 );
1905
1906 let starved = frame();
1907 gov.admit(&starved, Some("b"), 3);
1908 let mut due = gov.take_due(Instant::now());
1909 assert_eq!(due.len(), 1);
1910
1911 // The queue fills while the frame is out being transmitted, so there is
1912 // no room for it on the way back.
1913 gov.admit(&frame(), Some("b"), 3);
1914 gov.requeue(due.remove(0));
1915 assert_eq!(gov.counters().queue_full, 1);
1916 assert_eq!(gov.pending_len(), 1, "the dropped frame is not held");
1917
1918 // Once the queue drains, the copy behind it must still be carryable.
1919 gov.take_due(Instant::now());
1920 assert_eq!(
1921 gov.admit(&starved, Some("c"), 3),
1922 RelayAdmission::Queued,
1923 "a frame that got nowhere and could not be kept must stay carryable"
1924 );
1925 }
1926
1927 #[test]
1928 fn forwarding_leaves_room_for_this_devices_own_traffic() {
1929 // One radio, one budget — but a device saturated with other people's
1930 // traffic must still be able to send its own, above all the delivery
1931 // acknowledgement whose absence makes a delivered message look lost.
1932 let mut gov = MeshRelayGovernor::with_config(
1933 "relay-node",
1934 MeshRelayConfig {
1935 queue_capacity: 512,
1936 peer_rate_per_sec: 10_000.0,
1937 peer_burst: 10_000.0,
1938 ..immediate_config()
1939 },
1940 );
1941
1942 for _ in 0..200 {
1943 gov.admit(&frame(), Some("b"), 3);
1944 }
1945 gov.take_due(Instant::now());
1946
1947 // Drain the budget the way a saturated forwarder does.
1948 let forwarded = (0..1000).filter(|_| gov.take_send_token()).count();
1949 assert!(forwarded > 0, "forwarding must get its share first");
1950 assert!(
1951 !gov.take_send_token(),
1952 "forwarding must stop at the reserve, not at empty"
1953 );
1954
1955 // The reserve is still there for us.
1956 let own = (0..1000).filter(|_| gov.take_own_send_token()).count();
1957 assert!(
1958 own >= RELAY_OWN_TRAFFIC_RESERVE as usize,
1959 "own traffic got {own} transmissions against a reserve of {RELAY_OWN_TRAFFIC_RESERVE}"
1960 );
1961
1962 // And the ceiling still holds across both: the reserve is carved out of
1963 // the burst, not added to it.
1964 assert!(
1965 (forwarded + own) <= DEFAULT_RELAY_BURST as usize + 1,
1966 "{forwarded} forwarded + {own} own exceeds the burst of {DEFAULT_RELAY_BURST}"
1967 );
1968 }
1969
1970 #[test]
1971 fn a_small_configured_burst_still_forwards() {
1972 // The reserve is clamped to half the burst, so a device configured with
1973 // a burst smaller than the reserve does not silently stop forwarding
1974 // altogether.
1975 let mut gov = MeshRelayGovernor::with_config(
1976 "relay-node",
1977 MeshRelayConfig {
1978 burst: 2.0,
1979 rate_per_sec: 1.0,
1980 peer_rate_per_sec: 10_000.0,
1981 peer_burst: 10_000.0,
1982 ..immediate_config()
1983 },
1984 );
1985
1986 gov.admit(&frame(), Some("b"), 3);
1987 assert!(
1988 !gov.take_due(Instant::now()).is_empty(),
1989 "a small burst must still release forwards"
1990 );
1991 assert!(gov.take_send_token(), "and still allow transmitting them");
1992 }
1993
1994 #[test]
1995 fn a_settled_duplicate_is_absorbed_without_the_full_admission_path() {
1996 let mut gov = governor();
1997 let msg = frame();
1998
1999 assert!(
2000 !gov.absorb_settled_duplicate(&msg, Some("b")),
2001 "an id never seen is not a duplicate"
2002 );
2003
2004 gov.admit(&msg, Some("b"), 3);
2005 // While our copy is still pending, a copy narrows our forward, so the
2006 // cheap path must decline it and leave it to `admit`.
2007 assert!(!gov.absorb_settled_duplicate(&msg, Some("b")));
2008
2009 // Once it has gone out, further copies are settled duplicates.
2010 gov.take_due(Instant::now());
2011 assert!(gov.absorb_settled_duplicate(&msg, Some("b")));
2012 assert_eq!(gov.counters().duplicates_suppressed, 1);
2013 }
2014
2015 /// The origin re-sending a frame this device already forwarded is a
2016 /// sender that heard no acknowledgement. Its recipient has to see the
2017 /// re-send to re-acknowledge the copy it holds, so neither intake may
2018 /// suppress it: suppressed, the sender retried into this device for the
2019 /// whole retention window (a Pi lab, 2026-10-13: the sender restarted
2020 /// after the recipient's acknowledgement went nowhere, and its one retry
2021 /// entry outlived the scenario by ten minutes). A copy from anyone else
2022 /// is still an echo.
2023 #[test]
2024 fn the_origin_re_sending_a_forwarded_frame_is_forwarded_again() {
2025 let mut gov = governor();
2026 let msg = frame();
2027 assert_eq!(gov.admit(&msg, Some("alice"), 3), RelayAdmission::Queued);
2028 assert_eq!(gov.take_due(Instant::now()).len(), 1);
2029
2030 assert!(
2031 gov.absorb_settled_duplicate(&msg, Some("carol")),
2032 "an echo from another neighbour is absorbed"
2033 );
2034 assert!(
2035 !gov.absorb_settled_duplicate(&msg, Some("alice")),
2036 "the origin's re-send is not"
2037 );
2038 assert_eq!(gov.admit(&msg, Some("alice"), 3), RelayAdmission::Queued);
2039 assert_eq!(gov.take_due(Instant::now()).len(), 1, "and goes out again");
2040
2041 // A forwarder's copy carries a spent hop, so it never qualifies, even
2042 // from a link that claims the sender's name.
2043 let mut echoed = msg.clone();
2044 echoed.hop_count = echoed.hop_count.increment().unwrap();
2045 assert!(gov.absorb_settled_duplicate(&echoed, Some("alice")));
2046 }
2047
2048 /// An origin re-send that admission refuses leaves the id suppressed. The
2049 /// release used to run before the rate, hop and queue checks, so a re-send
2050 /// refused for the neighbour's rate left the id forgotten, and the next
2051 /// forwarder's echo (hop spent, never an origin re-send) was carried a
2052 /// second time: the storm the suppression cache exists to prevent.
2053 #[test]
2054 fn a_refused_origin_re_send_leaves_the_id_suppressed() {
2055 let mut gov = MeshRelayGovernor::with_config(
2056 "relay-node",
2057 MeshRelayConfig {
2058 peer_burst: 1.0,
2059 peer_rate_per_sec: 0.0,
2060 ..immediate_config()
2061 },
2062 );
2063 let msg = frame();
2064 assert_eq!(gov.admit(&msg, Some("alice"), 3), RelayAdmission::Queued);
2065 assert_eq!(gov.take_due(Instant::now()).len(), 1);
2066
2067 assert_eq!(
2068 gov.admit(&msg, Some("alice"), 3),
2069 RelayAdmission::Rejected(RelayRejection::PeerRateLimited)
2070 );
2071 assert!(
2072 gov.is_suppressed(&msg.id.as_str()),
2073 "a refused re-send releases nothing"
2074 );
2075
2076 let mut echoed = msg.clone();
2077 echoed.hop_count = echoed.hop_count.increment().unwrap();
2078 assert!(gov.absorb_settled_duplicate(&echoed, Some("carol")));
2079 assert_eq!(
2080 gov.admit(&echoed, Some("carol"), 3),
2081 RelayAdmission::Rejected(RelayRejection::AlreadySeen)
2082 );
2083 assert!(
2084 gov.take_due(Instant::now()).is_empty(),
2085 "the echo stays put"
2086 );
2087 }
2088
2089 #[test]
2090 fn eviction_prefers_a_frame_that_has_not_yet_gone_out() {
2091 // A due frame is on its way to the radio this tick; taking it costs a
2092 // transmission that was about to happen, while the frame replacing it
2093 // still has to wait out its hold.
2094 let mut gov = MeshRelayGovernor::with_config(
2095 "relay-node",
2096 MeshRelayConfig {
2097 queue_capacity: 2,
2098 jitter_min: Duration::from_millis(0),
2099 jitter_max: Duration::from_millis(0),
2100 peer_rate_per_sec: 10_000.0,
2101 peer_burst: 10_000.0,
2102 ..Default::default()
2103 },
2104 );
2105
2106 // One low-priority frame due immediately.
2107 let due_now = frame_with(8, MessagePriority::Low);
2108 gov.admit(&due_now, Some("b"), 3);
2109
2110 // A second low-priority frame that is not due for a while.
2111 gov.config.jitter_min = Duration::from_millis(500);
2112 gov.config.jitter_max = Duration::from_millis(500);
2113 let waiting = frame_with(8, MessagePriority::Low);
2114 gov.admit(&waiting, Some("b"), 3);
2115
2116 // An urgent frame needs the room.
2117 gov.admit(&frame_with(8, MessagePriority::Critical), Some("b"), 3);
2118
2119 assert_eq!(gov.counters().queue_evicted, 1);
2120 assert_eq!(
2121 gov.counters().queue_full,
2122 0,
2123 "making room is not the same as refusing"
2124 );
2125 assert!(
2126 gov.pending
2127 .iter()
2128 .any(|p| p.message.id.as_str() == due_now.id.as_str()),
2129 "the frame already due must survive"
2130 );
2131 assert!(
2132 !gov.pending
2133 .iter()
2134 .any(|p| p.message.id.as_str() == waiting.id.as_str()),
2135 "the frame still waiting is the one displaced"
2136 );
2137 }
2138
2139 #[test]
2140 fn a_full_peer_table_keeps_the_links_that_are_spending() {
2141 // Clearing the table wholesale hands every neighbor a fresh burst,
2142 // including the noisy one the per-link limit exists to hold back.
2143 let mut gov = MeshRelayGovernor::with_config("relay-node", immediate_config());
2144
2145 // One neighbor spends most of its allowance.
2146 for _ in 0..(DEFAULT_RELAY_PEER_BURST as usize - 1) {
2147 gov.admit(&frame(), Some("noisy"), 3);
2148 }
2149
2150 // Fill the table with links that arrive once and go quiet.
2151 for i in 0..MAX_RELAY_RATE_PEERS {
2152 gov.admit(&frame(), Some(&format!("idle-{i}")), 3);
2153 }
2154
2155 assert!(gov.peer_budgets.len() <= MAX_RELAY_RATE_PEERS);
2156 // Still tracked at all: the entry the limit exists to hold back is the
2157 // one it can least afford to forget, and it never sends again here, so
2158 // an eviction would leave it absent rather than rebuilt.
2159 let noisy = gov
2160 .peer_budgets
2161 .get("noisy")
2162 .expect("a link mid-spend must not be forgotten to make room for idle ones");
2163 assert!(
2164 noisy.tokens < noisy.capacity,
2165 "the noisy link kept its spent allowance rather than being handed a fresh burst"
2166 );
2167 }
2168
2169 #[test]
2170 fn a_refused_frame_leaves_the_way_open_for_another_copy() {
2171 // A frame turned away must not be remembered as handled: the copy
2172 // arriving over a healthy link a moment later is the alternate path,
2173 // and suppressing it would blank every route through this device for
2174 // the whole retention window.
2175 let mut gov = governor();
2176
2177 // Refused for having no hops left — the cheapest thing an attacker can
2178 // forge, since nothing authenticates the claim.
2179 let msg = frame_with(1, MessagePriority::Medium);
2180 assert_eq!(
2181 gov.admit(&msg, Some("hostile"), 3),
2182 RelayAdmission::Rejected(RelayRejection::HopLimitReached)
2183 );
2184
2185 // The same message, arriving intact by another route.
2186 let mut healthy = msg.clone();
2187 healthy.ttl = TTL::new(8).unwrap();
2188 assert_eq!(
2189 gov.admit(&healthy, Some("good"), 3),
2190 RelayAdmission::Queued,
2191 "a refused frame must not blackhole its id"
2192 );
2193 }
2194
2195 #[test]
2196 fn a_rate_limited_neighbor_does_not_blackhole_the_ids_it_sent() {
2197 let mut gov = governor();
2198
2199 // Exhaust one neighbor's allowance with distinct frames.
2200 let mut refused = Vec::new();
2201 for _ in 0..200 {
2202 let msg = frame();
2203 if gov.admit(&msg, Some("noisy"), 3)
2204 == RelayAdmission::Rejected(RelayRejection::PeerRateLimited)
2205 {
2206 refused.push(msg);
2207 }
2208 }
2209 assert!(!refused.is_empty());
2210
2211 // Those same messages, reaching us through someone else, must still be
2212 // carried.
2213 let victim = &refused[0];
2214 assert_eq!(gov.admit(victim, Some("quiet"), 3), RelayAdmission::Queued);
2215 }
2216
2217 #[test]
2218 fn the_last_hop_does_not_stand_down_for_a_neighbor() {
2219 // Our copy is going to the recipient over a link only we are known to
2220 // hold. A neighbor's copy must leave it queued.
2221 let mut gov = MeshRelayGovernor::with_config(
2222 "relay-node",
2223 MeshRelayConfig {
2224 jitter_min: Duration::from_millis(50),
2225 jitter_max: Duration::from_millis(200),
2226 ..Default::default()
2227 },
2228 );
2229 let msg = frame();
2230
2231 assert_eq!(gov.admit(&msg, Some("b"), 3), RelayAdmission::Queued);
2232 assert_eq!(
2233 gov.admit(&msg, Some("c"), 3),
2234 RelayAdmission::Rejected(RelayRejection::NarrowedByDuplicate)
2235 );
2236
2237 assert_eq!(gov.pending_len(), 1, "the delivering copy must survive");
2238 assert_eq!(gov.counters().cancelled_by_duplicate, 0);
2239 }
2240
2241 #[test]
2242 fn a_forward_waiting_far_past_its_turn_is_abandoned() {
2243 let mut gov = governor();
2244 gov.admit(&frame(), Some("b"), 3);
2245
2246 // Long enough that other paths have carried it or the sender has
2247 // retransmitted; holding it would only displace newer traffic.
2248 let due = gov.take_due(Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_secs(1));
2249 assert!(due.is_empty());
2250 assert_eq!(gov.counters().abandoned_overdue, 1);
2251 assert_eq!(gov.pending_len(), 0);
2252 }
2253
2254 #[test]
2255 fn a_full_queue_refuses_rather_than_growing() {
2256 let mut gov = MeshRelayGovernor::with_config(
2257 "relay-node",
2258 MeshRelayConfig {
2259 queue_capacity: 4,
2260 jitter_min: Duration::from_millis(500),
2261 jitter_max: Duration::from_millis(500),
2262 peer_rate_per_sec: 10_000.0,
2263 peer_burst: 10_000.0,
2264 ..Default::default()
2265 },
2266 );
2267
2268 for _ in 0..4 {
2269 assert_eq!(gov.admit(&frame(), Some("b"), 3), RelayAdmission::Queued);
2270 }
2271 assert_eq!(
2272 gov.admit(&frame(), Some("b"), 3),
2273 RelayAdmission::Rejected(RelayRejection::QueueFull)
2274 );
2275 assert_eq!(gov.pending_len(), 4);
2276 }
2277
2278 #[test]
2279 fn a_full_queue_still_takes_an_urgent_frame_over_a_routine_one() {
2280 let mut gov = MeshRelayGovernor::with_config(
2281 "relay-node",
2282 MeshRelayConfig {
2283 queue_capacity: 2,
2284 jitter_min: Duration::from_millis(500),
2285 jitter_max: Duration::from_millis(500),
2286 peer_rate_per_sec: 10_000.0,
2287 peer_burst: 10_000.0,
2288 ..Default::default()
2289 },
2290 );
2291
2292 gov.admit(&frame_with(8, MessagePriority::Low), Some("b"), 3);
2293 gov.admit(&frame_with(8, MessagePriority::Low), Some("b"), 3);
2294
2295 assert_eq!(
2296 gov.admit(&frame_with(8, MessagePriority::Critical), Some("b"), 3,),
2297 RelayAdmission::Queued
2298 );
2299 assert_eq!(gov.pending_len(), 2);
2300 assert!(gov
2301 .take_due(Instant::now() + Duration::from_secs(1))
2302 .iter()
2303 .any(|r| r.message.priority == MessagePriority::Critical));
2304 }
2305
2306 #[test]
2307 fn a_frame_is_never_sent_back_where_it_came_from() {
2308 let gov = governor();
2309 let targets = gov.select_targets(
2310 [("alice", 80), ("b", 80), ("c", 80), ("d", 80)],
2311 &["b", "alice"], // arrival neighbor and the peer that wrote it
2312 "msg-1",
2313 );
2314
2315 assert!(!targets.contains(&"b".to_string()));
2316 assert!(!targets.contains(&"alice".to_string()));
2317 assert!(targets.contains(&"c".to_string()));
2318 assert!(targets.contains(&"d".to_string()));
2319 }
2320
2321 #[test]
2322 fn fanout_is_capped_and_stable_for_a_given_frame() {
2323 let gov = governor();
2324 // All equally good, so the tie-break is what decides — which is the
2325 // property under test.
2326 let neighbors = [
2327 ("n0", 70),
2328 ("n1", 70),
2329 ("n2", 70),
2330 ("n3", 70),
2331 ("n4", 70),
2332 ("n5", 70),
2333 ("n6", 70),
2334 ("n7", 70),
2335 ];
2336
2337 let first = gov.select_targets(neighbors, &[], "msg-1");
2338 let again = gov.select_targets(neighbors, &[], "msg-1");
2339
2340 assert_eq!(first.len(), DEFAULT_RELAY_FANOUT);
2341 assert_eq!(first, again, "a second copy must pick the same neighbors");
2342
2343 // Different frames spread across different neighbors rather than
2344 // hammering one set.
2345 let other = gov.select_targets(neighbors, &[], "msg-2");
2346 assert_eq!(other.len(), DEFAULT_RELAY_FANOUT);
2347 }
2348
2349 #[test]
2350 fn a_frame_we_originated_is_not_carried_back_for_someone_else() {
2351 // Our own message, handed to neighbors. When a neighbor's copy reaches
2352 // us again we must recognize it, not take it on as third-party traffic.
2353 let mut gov = governor();
2354 let msg = frame();
2355
2356 gov.mark_handled(&msg.id.as_str());
2357
2358 assert_eq!(
2359 gov.admit(&msg, Some("b"), 3),
2360 RelayAdmission::Rejected(RelayRejection::AlreadySeen)
2361 );
2362 }
2363
2364 #[test]
2365 fn the_suppression_cache_holds_a_saturated_node_for_its_full_window() {
2366 // Capacity has to outlast in-flight copies at the maximum rate the node
2367 // will accept traffic. If it did not, an id could be forgotten while
2368 // copies were still circulating and be forwarded a second time — by
2369 // every node, which is how a flood becomes a storm.
2370 let gov = governor();
2371 let max_admissions_per_sec = DEFAULT_RELAY_PEER_RATE_PER_SEC * MAX_RELAY_RATE_PEERS as f32;
2372 let sustained_ceiling = DEFAULT_RELAY_RATE_PER_SEC.min(max_admissions_per_sec);
2373 let ids_in_window = sustained_ceiling * gov.config().seen.retention.as_secs_f32();
2374
2375 assert!(
2376 (gov.config().seen.capacity as f32) >= ids_in_window,
2377 "cache holds {} ids but a saturated node sees {} within the retention window",
2378 gov.config().seen.capacity,
2379 ids_in_window
2380 );
2381 }
2382
2383 // ====================================================================
2384 // Capability bias
2385 // ====================================================================
2386
2387 /// A governor with a real jitter window, so bias has something to shift.
2388 fn biased_governor() -> MeshRelayGovernor {
2389 MeshRelayGovernor::with_config("relay-node", MeshRelayConfig::default())
2390 }
2391
2392 #[test]
2393 fn a_weak_device_always_waits_longer_than_a_capable_one() {
2394 // The whole mechanism: the weak device's earliest delay must be later
2395 // than the capable device's latest, or the two windows overlap and the
2396 // weak one sometimes transmits first — spending the airtime the bias
2397 // exists to save.
2398 let msg = frame();
2399
2400 let mut capable = biased_governor();
2401 capable.set_conditions(Some(100), false, false, 30);
2402
2403 let mut weak = biased_governor();
2404 weak.set_conditions(Some(30), false, false, 30);
2405
2406 let capable_delay = capable.jitter_for(&msg.id.as_str(), 3);
2407 let weak_delay = weak.jitter_for(&msg.id.as_str(), 3);
2408 assert!(
2409 weak_delay > capable_delay,
2410 "weak waited {weak_delay:?}, capable {capable_delay:?}"
2411 );
2412
2413 // Independent of which frame it is: the handicap shifts the window, so
2414 // the worst case for the weak device beats the best case for the
2415 // capable one on every id.
2416 //
2417 // The bound that has to clear the span is the handicap a device can
2418 // actually pay, not the configured ceiling. A device at the floor
2419 // scales to `bias_min_scale`, never to zero, so the most it ever pays
2420 // is `(1 - bias_min_scale) * bias_max_handicap` — mirroring
2421 // `jitter_for`'s expression, truncation included. Asserting the raw
2422 // ceiling instead would stay green while a raised min scale silently
2423 // let the two windows overlap.
2424 let span = DEFAULT_RELAY_JITTER_MAX_MS - DEFAULT_RELAY_JITTER_MIN_MS;
2425 let reachable_handicap_ms = ((1.0 - DEFAULT_RELAY_BIAS_MIN_SCALE)
2426 * DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS as f32) as u64;
2427 assert!(
2428 reachable_handicap_ms >= span,
2429 "the reachable handicap ({reachable_handicap_ms}ms: a \
2430 {DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS}ms ceiling scaled by 1 - \
2431 {DEFAULT_RELAY_BIAS_MIN_SCALE}) must cover the jitter span ({span}ms) or the two \
2432 windows overlap"
2433 );
2434 }
2435
2436 #[test]
2437 fn bias_adds_a_bounded_delay_rather_than_a_multiplied_one() {
2438 // Bias must cost redundancy, never delivery. The jitter span already
2439 // grows with density, so the one shape that must not exist is a
2440 // handicap that grows with it too: at every density the biased device
2441 // waits at most one fixed handicap longer than the capable one.
2442 let msg = frame();
2443 let cap = Duration::from_millis(DEFAULT_RELAY_BIAS_MAX_HANDICAP_MS);
2444
2445 for degree in [0usize, 3, 6, 30, 100, MAX_RELAY_RATE_PEERS] {
2446 let mut capable = biased_governor();
2447 capable.set_conditions(Some(100), false, false, 30);
2448 let mut weak = biased_governor();
2449 weak.set_conditions(Some(0), false, false, 30);
2450
2451 let capable_delay = capable.jitter_for(&msg.id.as_str(), degree);
2452 let weak_delay = weak.jitter_for(&msg.id.as_str(), degree);
2453 assert!(weak_delay >= capable_delay, "degree {degree}");
2454 assert!(
2455 weak_delay - capable_delay <= cap,
2456 "at degree {degree} bias added {:?}, past its {cap:?} ceiling",
2457 weak_delay - capable_delay
2458 );
2459 }
2460 }
2461
2462 #[test]
2463 fn a_biased_forward_still_transmits_inside_the_overdue_cutoff() {
2464 // Asserted over *every* possible message id by computing the worst
2465 // case, not by sampling one: the delay is `min + handicap + hash %
2466 // span`, so a sampled id passes or fails on its hash and would make
2467 // this a coin toss rather than a guarantee.
2468 //
2469 // Bounded to the venue-sized neighborhood this design targets (~100
2470 // nodes). Past roughly 155 neighbors the delay does outrun the
2471 // cut-off — but the density scaling alone does that at ~167 with bias
2472 // switched off entirely, so it is a property of the pre-existing
2473 // density term rather than of bias, and out of scope here.
2474 let config = MeshRelayConfig::default();
2475 let min_ms = config.jitter_min.as_millis() as u64;
2476 let max_ms = config.jitter_max.as_millis() as u64;
2477 let degree = 100usize;
2478
2479 let density_scale = 1 + (degree / config.dense_degree.max(1)) as u64;
2480 let span = (max_ms - min_ms).saturating_mul(density_scale).max(1);
2481 // The full handicap, which only a device biased to the floor pays.
2482 let worst = Duration::from_millis(
2483 min_ms + config.bias_max_handicap.as_millis() as u64 + (span - 1),
2484 );
2485
2486 assert!(
2487 worst < RELAY_QUEUE_MAX_OVERDUE,
2488 "the worst biased wait at degree {degree} is {worst:?}, past the \
2489 {RELAY_QUEUE_MAX_OVERDUE:?} cut-off"
2490 );
2491 }
2492
2493 #[test]
2494 fn charging_and_eager_devices_are_not_biased_down() {
2495 let msg = frame();
2496 let mut unfed = biased_governor();
2497 let baseline = unfed.jitter_for(&msg.id.as_str(), 3);
2498
2499 // Charging at a level that would otherwise be heavily penalised.
2500 let mut charging = biased_governor();
2501 charging.set_conditions(Some(20), true, false, 30);
2502 assert_eq!(charging.jitter_for(&msg.id.as_str(), 3), baseline);
2503
2504 // Configured to relay eagerly, on battery.
2505 let mut eager = biased_governor();
2506 eager.set_conditions(Some(20), false, true, 30);
2507 assert_eq!(eager.jitter_for(&msg.id.as_str(), 3), baseline);
2508
2509 // And an unfed device is treated as fully capable, matching the
2510 // forwarding gate's "unknown means willing".
2511 unfed.set_conditions(None, false, false, 30);
2512 assert_eq!(unfed.jitter_for(&msg.id.as_str(), 3), baseline);
2513 }
2514
2515 #[test]
2516 fn a_weak_device_forwards_to_fewer_neighbors_but_never_none() {
2517 let neighbors: Vec<(&str, u8)> = vec![("a", 90), ("b", 80), ("c", 70), ("d", 60)];
2518
2519 let mut capable = biased_governor();
2520 capable.set_conditions(Some(100), false, false, 30);
2521 let capable_targets = capable.select_targets(neighbors.clone(), &[], "msg-1");
2522 assert_eq!(capable_targets.len(), DEFAULT_RELAY_FANOUT);
2523
2524 let mut weak = biased_governor();
2525 weak.set_conditions(Some(0), false, false, 30);
2526 let weak_targets = weak.select_targets(neighbors, &[], "msg-1");
2527 assert!(
2528 !weak_targets.is_empty(),
2529 "a fan-out of zero is a silent drop, not a cheaper forward"
2530 );
2531 assert!(
2532 weak_targets.len() < capable_targets.len(),
2533 "weak fanned out to {} neighbors, capable to {}",
2534 weak_targets.len(),
2535 capable_targets.len()
2536 );
2537 }
2538
2539 #[test]
2540 fn a_weak_device_offers_its_own_frames_to_every_neighbor_it_would_have() {
2541 // Bias narrows the fan-out for other people's traffic, where neighbors
2542 // hold copies and the sender is still retrying, so the cost is
2543 // redundancy. A frame this device originated has neither: the fan-out
2544 // *is* the delivery attempt, and because targets are chosen stably per
2545 // message id, a narrowed set would be the same narrowed set on every
2546 // retransmission — one dead-end neighbor forever instead of three
2547 // chances.
2548 let neighbors: Vec<(&str, u8)> = vec![("a", 90), ("b", 80), ("c", 70), ("d", 60)];
2549
2550 let mut weak = biased_governor();
2551 weak.set_conditions(Some(0), false, false, 30);
2552
2553 assert!(
2554 weak.select_targets(neighbors.clone(), &[], "msg-1").len() < DEFAULT_RELAY_FANOUT,
2555 "the forwarding path must still be biased",
2556 );
2557 assert_eq!(
2558 weak.select_origination_targets(neighbors.clone(), &[], "msg-1")
2559 .len(),
2560 DEFAULT_RELAY_FANOUT,
2561 "this device's own frame must reach as many neighbors as a capable device's",
2562 );
2563
2564 // And identical to what a fully capable device would have picked, not
2565 // merely as many: same neighbors, same order.
2566 let mut capable = biased_governor();
2567 capable.set_conditions(Some(100), false, false, 30);
2568 assert_eq!(
2569 weak.select_origination_targets(neighbors.clone(), &[], "msg-1"),
2570 capable.select_origination_targets(neighbors, &[], "msg-1"),
2571 );
2572 }
2573
2574 #[test]
2575 fn bias_does_not_throttle_this_devices_own_traffic() {
2576 // Bias scales the *forwarding* share of the airtime ceiling, not the
2577 // ceiling. Scaling the shared bucket would slow a low-battery device's
2578 // own messages and acknowledgements in step with its battery, which is
2579 // delivery rather than redundancy — and an absent acknowledgement is
2580 // what makes a delivered message look lost to its sender.
2581 let mut capable = MeshRelayGovernor::with_config("relay-node", immediate_config());
2582 capable.set_conditions(Some(100), false, false, 30);
2583 let capable_own = (0..1000).filter(|_| capable.take_own_send_token()).count();
2584
2585 let mut weak = MeshRelayGovernor::with_config("relay-node", immediate_config());
2586 weak.set_conditions(Some(0), false, false, 30);
2587 let weak_own = (0..1000).filter(|_| weak.take_own_send_token()).count();
2588
2589 assert_eq!(
2590 weak_own, capable_own,
2591 "a weak device got {weak_own} of its own transmissions against a capable \
2592 device's {capable_own}",
2593 );
2594 assert!(capable_own > 0, "the fixture must actually grant something");
2595 }
2596
2597 #[test]
2598 fn a_weak_device_transmits_less_before_hitting_its_ceiling() {
2599 let mut capable = MeshRelayGovernor::with_config("relay-node", immediate_config());
2600 capable.set_conditions(Some(100), false, false, 30);
2601 let capable_grants = (0..1000).filter(|_| capable.take_send_token()).count();
2602
2603 let mut weak = MeshRelayGovernor::with_config("relay-node", immediate_config());
2604 weak.set_conditions(Some(0), false, false, 30);
2605 let weak_grants = (0..1000).filter(|_| weak.take_send_token()).count();
2606
2607 assert!(
2608 weak_grants < capable_grants,
2609 "weak got {weak_grants} transmissions, capable {capable_grants}"
2610 );
2611 assert!(
2612 weak_grants > 0,
2613 "bias scales the budget down, it does not switch forwarding off"
2614 );
2615 }
2616
2617 #[test]
2618 fn a_worsening_battery_does_not_mint_a_fresh_burst() {
2619 // `set_conditions` runs every tick, so re-sizing the bucket must keep
2620 // what has been spent spent. If it refilled, a device whose battery
2621 // oscillates around any point in the ramp would transmit without limit.
2622 let mut gov = MeshRelayGovernor::with_config("relay-node", immediate_config());
2623 gov.set_conditions(Some(100), false, false, 30);
2624
2625 let first = (0..1000).filter(|_| gov.take_send_token()).count();
2626 assert!(first > 0);
2627 assert!(!gov.take_send_token(), "the budget must be spent");
2628
2629 for level in [90, 100, 90, 100] {
2630 gov.set_conditions(Some(level), false, false, 30);
2631 assert!(
2632 !gov.take_send_token(),
2633 "re-sizing the budget handed back tokens that were already spent"
2634 );
2635 }
2636 }
2637
2638 // ====================================================================
2639 // Activity-derived relay standing
2640 // ====================================================================
2641
2642 /// Short windows so activity transitions are reachable without waiting a
2643 /// real minute; `observe_activity` takes `now`, so the clock is explicit.
2644 fn activity_config() -> MeshRelayConfig {
2645 MeshRelayConfig {
2646 activity_window: Duration::from_secs(10),
2647 activity_min_forwards: 2,
2648 activity_idle_windows: 2,
2649 ..immediate_config()
2650 }
2651 }
2652
2653 #[test]
2654 fn carrying_traffic_is_what_makes_a_device_a_relay() {
2655 let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2656 let start = Instant::now();
2657 assert!(!gov.is_active_relay());
2658
2659 // A window that carried nothing changes nothing.
2660 assert_eq!(gov.observe_activity(start + Duration::from_secs(11)), None);
2661 assert!(!gov.is_active_relay());
2662
2663 // Carrying enough in one window promotes, exactly once.
2664 gov.record_forwarded();
2665 gov.record_forwarded();
2666 assert_eq!(
2667 gov.observe_activity(start + Duration::from_secs(22)),
2668 Some(RelayActivity::Began { forwarded: 2 })
2669 );
2670 assert!(gov.is_active_relay());
2671
2672 gov.record_forwarded();
2673 gov.record_forwarded();
2674 assert_eq!(
2675 gov.observe_activity(start + Duration::from_secs(33)),
2676 None,
2677 "a relay that keeps carrying traffic must not re-announce"
2678 );
2679 assert!(gov.is_active_relay());
2680 }
2681
2682 #[test]
2683 fn a_relay_stops_being_one_only_after_sustained_quiet() {
2684 // Asymmetric on purpose: one busy window promotes, several quiet ones
2685 // demote. A mesh with nothing to say for a window has not stopped
2686 // having a relay in it.
2687 let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2688 let start = Instant::now();
2689
2690 gov.record_forwarded();
2691 gov.record_forwarded();
2692 assert!(matches!(
2693 gov.observe_activity(start + Duration::from_secs(11)),
2694 Some(RelayActivity::Began { .. })
2695 ));
2696
2697 // One quiet window is not enough.
2698 assert_eq!(gov.observe_activity(start + Duration::from_secs(22)), None);
2699 assert!(gov.is_active_relay());
2700
2701 assert_eq!(
2702 gov.observe_activity(start + Duration::from_secs(33)),
2703 Some(RelayActivity::Ceased)
2704 );
2705 assert!(!gov.is_active_relay());
2706
2707 // And it stays ceased rather than re-announcing every window.
2708 assert_eq!(gov.observe_activity(start + Duration::from_secs(44)), None);
2709 }
2710
2711 #[test]
2712 fn traffic_within_a_window_resets_the_run_of_quiet_ones() {
2713 let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2714 let start = Instant::now();
2715
2716 gov.record_forwarded();
2717 gov.record_forwarded();
2718 assert!(matches!(
2719 gov.observe_activity(start + Duration::from_secs(11)),
2720 Some(RelayActivity::Began { .. })
2721 ));
2722
2723 // Quiet, then busy again, then quiet: the run restarts, so the second
2724 // quiet window must not be the one that demotes.
2725 assert_eq!(gov.observe_activity(start + Duration::from_secs(22)), None);
2726 gov.record_forwarded();
2727 gov.record_forwarded();
2728 assert_eq!(gov.observe_activity(start + Duration::from_secs(33)), None);
2729 assert_eq!(gov.observe_activity(start + Duration::from_secs(44)), None);
2730 assert!(gov.is_active_relay(), "the quiet run restarted");
2731
2732 assert_eq!(
2733 gov.observe_activity(start + Duration::from_secs(55)),
2734 Some(RelayActivity::Ceased)
2735 );
2736 }
2737
2738 #[test]
2739 fn observing_before_a_window_has_elapsed_does_nothing() {
2740 let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2741 let start = Instant::now();
2742
2743 gov.record_forwarded();
2744 gov.record_forwarded();
2745 assert_eq!(gov.observe_activity(start + Duration::from_secs(5)), None);
2746 assert!(
2747 !gov.is_active_relay(),
2748 "the window must complete before it counts"
2749 );
2750
2751 // And the frames carried are still counted toward it.
2752 assert!(matches!(
2753 gov.observe_activity(start + Duration::from_secs(11)),
2754 Some(RelayActivity::Began { forwarded: 2 })
2755 ));
2756 }
2757
2758 #[test]
2759 fn a_closing_gate_drops_the_relay_standing_at_once() {
2760 // Configuration or battery closing the forwarding gate stops traffic
2761 // immediately, so waiting out the idle windows would report a relay
2762 // that had already stopped being one.
2763 let mut gov = MeshRelayGovernor::with_config("relay-node", activity_config());
2764 let start = Instant::now();
2765
2766 gov.record_forwarded();
2767 gov.record_forwarded();
2768 assert!(matches!(
2769 gov.observe_activity(start + Duration::from_secs(11)),
2770 Some(RelayActivity::Began { .. })
2771 ));
2772
2773 assert!(gov.force_inactive(), "reports that the standing was held");
2774 assert!(!gov.is_active_relay());
2775 assert!(
2776 !gov.force_inactive(),
2777 "a device that was not a relay must not report a demotion"
2778 );
2779
2780 // The window's tally went with it, so re-opening the gate starts from
2781 // scratch rather than promoting on stale traffic.
2782 assert_eq!(gov.observe_activity(start + Duration::from_secs(22)), None);
2783 assert!(!gov.is_active_relay());
2784 }
2785
2786 // ====================================================================
2787 // Custody: the held-frame intake and the drop point
2788 // ====================================================================
2789
2790 /// A frame carrying a deposit request, as a depositor offers it.
2791 fn deposit_frame() -> Message {
2792 let mut msg = frame();
2793 msg.metadata
2794 .insert(CUSTODY_META_KEY.to_string(), "data".to_string());
2795 msg
2796 }
2797
2798 #[test]
2799 fn a_forwarder_strips_the_deposit_request_and_the_drop_point_keeps_it() {
2800 // The key is unsigned metadata outside the sealed body, so nothing
2801 // but this strip keeps a deposit to one hop: left in place, a two-hop
2802 // network deposits a frame at a custodian that never saw its sender.
2803 let mut gov = governor();
2804 let msg = deposit_frame();
2805 assert_eq!(gov.admit(&msg, Some("alice"), 3), RelayAdmission::Queued);
2806
2807 let (due, abandoned) = gov.release_due(Instant::now());
2808 assert!(abandoned.is_empty());
2809 let relay = &due[0];
2810 assert!(
2811 !relay.message.metadata.contains_key(CUSTODY_META_KEY),
2812 "the copy that travels on carries no request"
2813 );
2814 assert_eq!(
2815 relay.custody_request.as_deref(),
2816 Some("data"),
2817 "the request is kept beside the copy, for the drop point alone"
2818 );
2819 assert_eq!(
2820 relay.custody_target, None,
2821 "an ordinary forward is not marked"
2822 );
2823 assert_eq!(
2824 relay.message.content, msg.content,
2825 "only the outer metadata is touched"
2826 );
2827 }
2828
2829 #[test]
2830 fn an_abandoned_forward_is_handed_to_the_drop_point_with_its_id_released() {
2831 let mut gov = governor();
2832 let msg = deposit_frame();
2833 gov.admit(&msg, Some("alice"), 3);
2834 let (due, _) = gov.release_due(Instant::now());
2835 gov.requeue(due.into_iter().next().unwrap());
2836
2837 let later = Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_millis(1);
2838 let (due, abandoned) = gov.release_due(later);
2839 assert!(due.is_empty());
2840 assert_eq!(
2841 abandoned.len(),
2842 1,
2843 "the drop point sees the abandoned forward"
2844 );
2845 assert_eq!(abandoned[0].custody_request.as_deref(), Some("data"));
2846 assert!(
2847 !gov.is_suppressed(&msg.id.as_str()),
2848 "released before custody judges it, so a custodian never blanks its own route"
2849 );
2850 assert_eq!(gov.counters().abandoned_overdue, 1);
2851 }
2852
2853 #[test]
2854 fn the_held_intake_skips_suppression_and_hop_accounting() {
2855 let mut gov = governor();
2856 let msg = frame_with(3, MessagePriority::Medium);
2857
2858 // The ordinary path has handled this id: a copy would be refused (one
2859 // from a neighbour; the origin's own re-send is forwarded again).
2860 assert_eq!(gov.admit(&msg, Some("alice"), 3), RelayAdmission::Queued);
2861 let (due, _) = gov.release_due(Instant::now());
2862 assert_eq!(due.len(), 1);
2863 assert_eq!(
2864 gov.admit(&msg, Some("carol"), 3),
2865 RelayAdmission::Rejected(RelayRejection::AlreadySeen)
2866 );
2867
2868 // The held intake queues it anyway, toward one neighbor, with the hop
2869 // fields exactly as handed in.
2870 let held = due.into_iter().next().unwrap().message;
2871 let hop_before = held.hop_count.value();
2872 let ttl_before = held.ttl.value();
2873 gov.admit_held(held, "dave", Instant::now())
2874 .expect("queued");
2875 let (due, _) = gov.release_due(Instant::now());
2876 assert_eq!(due.len(), 1);
2877 let relay = &due[0];
2878 assert_eq!(relay.custody_target.as_deref(), Some("dave"));
2879 assert_eq!(relay.arrival_peer, None);
2880 assert_eq!(relay.message.hop_count.value(), hop_before, "no hop spent");
2881 assert_eq!(relay.message.ttl.value(), ttl_before, "no budget spent");
2882
2883 // And it recorded nothing: the cache is exactly as it was.
2884 let seen_before = gov.seen.len();
2885 gov.admit_held(relay.message.clone(), "erin", Instant::now())
2886 .expect("queued");
2887 assert_eq!(gov.seen.len(), seen_before);
2888 }
2889
2890 #[test]
2891 fn a_marked_forward_never_touches_the_suppression_cache() {
2892 let mut gov = governor();
2893 let msg = frame();
2894 // The depositor's own retransmission is legitimately handled: taken
2895 // on, released to the radio, and its id recorded for the window.
2896 gov.admit(&msg, Some("alice"), 3);
2897 let (due, _) = gov.release_due(Instant::now());
2898 assert_eq!(due.len(), 1, "transmitted");
2899 assert!(gov.is_suppressed(&msg.id.as_str()));
2900
2901 // A held copy of the same id, abandoned: the ordinary entry stays.
2902 gov.admit_held(msg.clone(), "dave", Instant::now())
2903 .expect("queued");
2904 let later = Instant::now() + RELAY_QUEUE_MAX_OVERDUE + Duration::from_millis(1);
2905 let (_, abandoned) = gov.release_due(later);
2906 let held: Vec<_> = abandoned
2907 .iter()
2908 .filter(|r| r.custody_target.is_some())
2909 .collect();
2910 assert_eq!(held.len(), 1);
2911 assert!(
2912 gov.is_suppressed(&msg.id.as_str()),
2913 "abandoning a marked forward releases nothing"
2914 );
2915 // Nor does refusing it room on the way back.
2916 let counters_before = gov.counters().clone();
2917 let mut full = MeshRelayGovernor::with_config(
2918 "relay-node",
2919 MeshRelayConfig {
2920 queue_capacity: 1,
2921 ..immediate_config()
2922 },
2923 );
2924 full.admit(&frame(), Some("alice"), 3);
2925 let returned = full.requeue(PendingRelay {
2926 message: frame(),
2927 arrival_peer: None,
2928 heard_from: Vec::new(),
2929 due_at: Instant::now(),
2930 custody_request: None,
2931 custody_target: Some("dave".to_string()),
2932 });
2933 assert!(returned.is_some(), "handed back to the custody store");
2934 assert_eq!(full.counters().queue_full, 0, "not counted as a loss");
2935 assert_eq!(
2936 gov.counters().abandoned_overdue,
2937 counters_before.abandoned_overdue
2938 );
2939 }
2940
2941 #[test]
2942 fn dropping_held_forwards_leaves_the_ordinary_ones_queued() {
2943 let mut gov = governor();
2944 gov.admit(&frame(), Some("alice"), 3);
2945 gov.admit_held(frame(), "dave", Instant::now())
2946 .expect("queued");
2947 gov.admit_held(frame(), "erin", Instant::now())
2948 .expect("queued");
2949 assert_eq!(gov.pending_len(), 3);
2950
2951 gov.drop_held();
2952
2953 assert_eq!(gov.pending_len(), 1);
2954 let (due, _) = gov.release_due(Instant::now());
2955 assert_eq!(due.len(), 1);
2956 assert!(due[0].custody_target.is_none());
2957 }
2958
2959 #[test]
2960 fn the_held_intake_still_takes_the_queue_and_the_neighbor_rate() {
2961 let mut gov = MeshRelayGovernor::with_config(
2962 "relay-node",
2963 MeshRelayConfig {
2964 queue_capacity: 1,
2965 peer_burst: 1.0,
2966 peer_rate_per_sec: 0.0001,
2967 ..immediate_config()
2968 },
2969 );
2970 let now = Instant::now();
2971 gov.admit_held(frame(), "dave", now)
2972 .expect("first one queues");
2973 assert_eq!(
2974 gov.admit_held(frame(), "erin", now).err(),
2975 Some(RelayRejection::QueueFull),
2976 "a full queue is a full queue"
2977 );
2978 let (due, _) = gov.release_due(now);
2979 assert_eq!(due.len(), 1);
2980 assert_eq!(
2981 gov.admit_held(frame(), "dave", now).err(),
2982 Some(RelayRejection::PeerRateLimited),
2983 "one neighbor's share is spent"
2984 );
2985 // Erin's share went with the refusal above, as it does on the
2986 // ordinary path; a neighbor nobody has charged still admits one.
2987 assert!(gov.admit_held(frame(), "frank", now).is_ok());
2988 }
2989}