umsh_mac/
coordinator.rs

1use core::num::NonZeroU8;
2use core::{future::poll_fn, task::Poll};
3
4use hamaddr::HamAddr;
5use heapless::{LinearMap, Vec};
6use rand::{Rng, RngExt as _};
7use umsh_core::{
8    BuildError, ChannelId, ChannelKey, FloodHops, NodeHint, OptionNumber, PacketBuilder,
9    PacketHeader, PacketType, ParseError, ParsedOptions, PayloadType, PublicKey, RouterHint,
10    SourceAddrRef, UnsealedPacket, feed_aad,
11    options::{OptionEncoder, TraceSignalEntry},
12};
13use umsh_crypto::{CryptoEngine, CryptoError, DerivedChannelKeys, NodeIdentity, PairwiseKeys};
14use umsh_hal::{Clock, CounterStore, Radio, RxInfo, RxOrigin, Snr, TxError, TxOptions};
15
16use crate::{
17    AddPeerError, CapacityError, DEFAULT_ACKS, DEFAULT_CHANNEL_HINT_REPLAY, DEFAULT_CHANNEL_REPLAY,
18    DEFAULT_CHANNELS, DEFAULT_DUP, DEFAULT_IDENTITIES, DEFAULT_PEERS, DEFAULT_TX,
19    ESTABLISHED_ROUTE_EXTRA_HOPS, MAX_CAD_ATTEMPTS, MAX_FLOOD_HOPS, MAX_FORWARD_RETRIES,
20    MAX_RESEND_FRAME_LEN, MAX_SOURCE_ROUTE_HOPS, Platform, ReplayVerdict, ReplayWindow,
21    cache::{DupCacheKey, DuplicateCache},
22    peers::CachedRoute,
23    peers::{ChannelTable, PeerCryptoMap, PeerId, PeerRegistry},
24    send::{
25        CompletionSignal, PendingAck, PendingAckError, ResendRecord, SendOptions, SendReceipt,
26        TxPriority, TxQueue,
27    },
28};
29
30/// Why [`Mac::poll_wait_for_wake`] returned ready.
31///
32/// Returned by the sync phase-2 poll so that callers sharing a coordinator
33/// across tasks can decide when to re-acquire the exclusive borrow for
34/// [`Mac::process_wake_reason`].
35pub enum WakeReason {
36    /// A frame was received; its metadata is attached and the caller-provided
37    /// buffer has been populated with `rx.len` bytes.
38    Received(RxInfo),
39    /// At least one coordinator timer has elapsed (ACK deadline, post-TX
40    /// listen window, or a deferred transmit becoming ready).
41    TimerExpired,
42}
43
44pub(crate) const COUNTER_PERSIST_BLOCK_SIZE: u32 = 128;
45const COUNTER_PERSIST_BLOCK_MASK: u32 = COUNTER_PERSIST_BLOCK_SIZE - 1;
46const COUNTER_PERSIST_SCHEDULE_OFFSET: u32 = 100;
47const MAC_COMMAND_ECHO_REQUEST_ID: u8 = 4;
48const MAC_COMMAND_ECHO_RESPONSE_ID: u8 = 5;
49const COUNTER_RESYNC_NONCE_LEN: usize = 4;
50const COUNTER_RESYNC_REQUEST_RETRY_MS: u64 = 5_000;
51/// Minimum backward gap (peer's `last_accepted` minus received counter) that
52/// will trigger a counter-resync exchange. Gaps smaller than this are silently
53/// dropped: they're indistinguishable from ordinary mesh reordering and would
54/// otherwise cause resync churn whenever a slightly-late packet arrives. A
55/// real desync (peer reboot, lost persistence boundary) typically produces a
56/// jump much larger than this.
57const COUNTER_RESYNC_GAP_THRESHOLD: u32 = 1024;
58
59/// The carriage a request arrived on, and so the carriage its response owes
60/// it back.
61///
62/// A blind unicast hides both endpoints from anyone without the channel key.
63/// A response that left the channel would name the pair the request took care
64/// to conceal, so a responder mirrors the carriage rather than choosing one.
65#[derive(Clone, Copy)]
66pub(crate) enum ReplyCarriage<'a> {
67    Unicast,
68    Blind(&'a DerivedChannelKeys),
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72struct PendingCounterResync {
73    nonce: u32,
74    requested_ms: u64,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
78struct DeferredCounterResyncFrame<const FRAME: usize> {
79    local_id: LocalIdentityId,
80    peer_id: PeerId,
81    frame: Vec<u8, FRAME>,
82    rssi: i16,
83    snr: Snr,
84    lqi: Option<NonZeroU8>,
85    origin: RxOrigin,
86    received_at_ms: u64,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90struct ResolvedMulticastSource {
91    peer_id: Option<PeerId>,
92    public_key: Option<PublicKey>,
93    hint: Option<NodeHint>,
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
97struct PostTxListen {
98    identity_id: LocalIdentityId,
99    receipt: SendReceipt,
100    confirm_key: DupCacheKey,
101    deadline_ms: u64,
102}
103
104/// Opaque handle that identifies a locally registered identity within the [`Mac`] coordinator.
105///
106/// Every UMSH node presents one or more Ed25519 public keys to the network. When a key is
107/// registered via [`Mac::add_identity`] (or [`Mac::register_ephemeral`] for PFS sessions),
108/// the coordinator allocates a slot and returns a `LocalIdentityId` that permanently names it.
109///
110/// The inner `u8` is a stable zero-based slot index — slot `0` is the first identity
111/// registered, slot `1` the second, and so on. All per-identity coordinator operations
112/// (`queue_unicast`, `queue_multicast`, ACK tracking, key installation, frame-counter
113/// persistence) accept a `LocalIdentityId` to select which local keypair to use, allowing a
114/// single coordinator instance to operate multiple identities simultaneously — for example, a
115/// persistent long-term identity alongside an ephemeral PFS session identity.
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117pub struct LocalIdentityId(pub u8);
118
119/// A local node identity that the [`Mac`] coordinator owns and acts on behalf of.
120///
121/// UMSH nodes are identified by Ed25519 public keys. An identity provides the public key and
122/// the ability to derive pairwise keys via ECDH with the corresponding private key.
123/// Two variants are supported:
124///
125/// - **`LongTerm(I)`** — wraps the platform-supplied `I: NodeIdentity`, which is typically
126///   backed by secure-element storage, an HSM, or a platform keystore. Long-term identities
127///   persist across reboots; their frame counters are saved to the [`umsh_hal::CounterStore`]
128///   so that replay protection remains valid after a power cycle.
129///
130/// - **`Ephemeral`** — wraps an in-memory [`SoftwareIdentity`](umsh_crypto::software::SoftwareIdentity)
131///   generated fresh at runtime for Perfect Forward Secrecy sessions. Because the key material
132///   itself vanishes on power loss, ephemeral identities do not persist their frame counters;
133///   replay protection is meaningful only within a single session. Requires the
134///   `software-crypto` crate feature.
135///
136/// Use [`LocalIdentity::public_key`] or [`LocalIdentity::hint`] to inspect the address
137/// presented to the network without matching on the variant.
138pub enum LocalIdentity<I: NodeIdentity> {
139    /// Long-term platform identity.
140    LongTerm(I),
141    #[cfg(feature = "software-crypto")]
142    /// Software ephemeral identity used for PFS sessions.
143    Ephemeral(umsh_crypto::software::SoftwareIdentity),
144}
145
146impl<I: NodeIdentity> LocalIdentity<I> {
147    /// Return the public key for this identity.
148    pub fn public_key(&self) -> &PublicKey {
149        match self {
150            Self::LongTerm(identity) => identity.public_key(),
151            #[cfg(feature = "software-crypto")]
152            Self::Ephemeral(identity) => identity.public_key(),
153        }
154    }
155
156    /// Return the derived node hint for this identity.
157    pub fn hint(&self) -> umsh_core::NodeHint {
158        self.public_key().hint()
159    }
160
161    /// Return whether this identity is ephemeral.
162    pub fn is_ephemeral(&self) -> bool {
163        match self {
164            Self::LongTerm(_) => false,
165            #[cfg(feature = "software-crypto")]
166            Self::Ephemeral(_) => true,
167        }
168    }
169}
170
171impl<I: NodeIdentity> From<I> for LocalIdentity<I> {
172    fn from(value: I) -> Self {
173        Self::LongTerm(value)
174    }
175}
176
177/// Per-identity runtime state owned by the [`Mac`] coordinator.
178///
179/// There is exactly one `IdentitySlot` per registered local identity ([`LocalIdentityId`]).
180/// The slot bundles everything the coordinator needs to send, receive, and authenticate
181/// on behalf of a single local keypair:
182///
183/// - The [`LocalIdentity`] (public key + ECDH capability).
184/// - A [`PeerCryptoMap`](crate::peers::PeerCryptoMap) mapping each known remote peer to its
185///   established [`umsh_crypto::PairwiseKeys`] and replay window. Entries are populated on
186///   first secure contact, or through the advanced manual-install escape hatch.
187/// - A monotonically increasing **frame counter** stamped into SECINFO of every sealed
188///   packet, plus the bookkeeping needed to persist it safely (see below).
189/// - A [`LinearMap`] of in-flight [`PendingAck`](crate::send::PendingAck) records keyed by
190///   [`SendReceipt`](crate::send::SendReceipt), one entry per ACK-requested send awaiting
191///   either forwarding confirmation or a final transport ACK.
192/// - An internal `next_receipt` counter used to issue unique
193///   [`SendReceipt`](crate::send::SendReceipt) values without allocation.
194///
195/// ## Frame-counter persistence
196///
197/// UMSH uses a monotonic frame counter instead of a timestamp for replay protection.
198/// If the counter resets to a previously-seen value after a reboot, replayed old frames
199/// might be accepted. To prevent this, the coordinator "reserves" counter ranges by writing
200/// boundary values to the [`umsh_hal::CounterStore`] *before* using them. The slot tracks
201/// three values:
202///
203/// - `frame_counter` — the live in-use value, advanced on every secured send.
204/// - `persisted_counter` — the last boundary safely committed to the store.
205/// - `pending_persist_target` — a scheduled future boundary written on the next call to
206///   [`Mac::service_counter_persistence`].
207///
208/// If the live counter reaches `persisted_counter + COUNTER_PERSIST_BLOCK_SIZE` without a
209/// successful flush, secure sends on that identity are blocked
210/// ([`SendError::CounterPersistenceLag`]) until the store catches up. Ephemeral identities
211/// opt out of this mechanism entirely.
212pub struct IdentitySlot<
213    I: NodeIdentity,
214    const PEERS: usize,
215    const ACKS: usize,
216    const FRAME: usize = MAX_RESEND_FRAME_LEN,
217> {
218    identity: LocalIdentity<I>,
219    peer_crypto: PeerCryptoMap<PEERS>,
220    frame_counter: u32,
221    persisted_counter: u32,
222    pending_persist_target: Option<u32>,
223    save_scheduled_since_boot: bool,
224    counter_persistence_enabled: bool,
225    pending_acks: LinearMap<SendReceipt, PendingAck<FRAME>, ACKS>,
226    next_receipt: u32,
227    pfs_parent: Option<LocalIdentityId>,
228    pending_counter_resync: LinearMap<PeerId, PendingCounterResync, PEERS>,
229}
230
231impl<I: NodeIdentity, const PEERS: usize, const ACKS: usize, const FRAME: usize>
232    IdentitySlot<I, PEERS, ACKS, FRAME>
233{
234    /// Create a new identity slot.
235    pub fn new(
236        identity: LocalIdentity<I>,
237        frame_counter: u32,
238        pfs_parent: Option<LocalIdentityId>,
239    ) -> Self {
240        let counter_persistence_enabled = !identity.is_ephemeral();
241        Self {
242            identity,
243            peer_crypto: PeerCryptoMap::new(),
244            frame_counter,
245            persisted_counter: frame_counter,
246            pending_persist_target: None,
247            save_scheduled_since_boot: false,
248            counter_persistence_enabled,
249            pending_acks: LinearMap::new(),
250            next_receipt: 0,
251            pfs_parent,
252            pending_counter_resync: LinearMap::new(),
253        }
254    }
255
256    /// Borrow the underlying identity.
257    pub fn identity(&self) -> &LocalIdentity<I> {
258        &self.identity
259    }
260    /// Borrow the per-peer secure-state map.
261    pub fn peer_crypto(&self) -> &PeerCryptoMap<PEERS> {
262        &self.peer_crypto
263    }
264    /// Mutably borrow the per-peer secure-state map.
265    pub fn peer_crypto_mut(&mut self) -> &mut PeerCryptoMap<PEERS> {
266        &mut self.peer_crypto
267    }
268    /// Return the current frame counter.
269    pub fn frame_counter(&self) -> u32 {
270        self.frame_counter
271    }
272    /// Return the persisted frame-counter reservation boundary.
273    pub fn persisted_counter(&self) -> u32 {
274        self.persisted_counter
275    }
276    /// Overwrite the current frame counter.
277    ///
278    /// # Safety (logical)
279    /// Misuse can break replay protection.
280    #[cfg(test)]
281    pub(crate) fn set_frame_counter(&mut self, value: u32) {
282        self.frame_counter = value;
283    }
284    /// Return the next scheduled persist target, if any.
285    pub fn pending_persist_target(&self) -> Option<u32> {
286        self.pending_persist_target
287    }
288
289    /// Return whether counter persistence is enabled for this identity.
290    pub fn counter_persistence_enabled(&self) -> bool {
291        self.counter_persistence_enabled
292    }
293
294    /// Return the current frame counter and advance it with wrapping semantics.
295    pub(crate) fn advance_frame_counter(&mut self) -> u32 {
296        let current = self.frame_counter;
297        self.frame_counter = self.frame_counter.wrapping_add(1);
298        current
299    }
300
301    /// Load a persisted counter boundary for this identity.
302    pub fn load_persisted_counter(&mut self, value: u32) {
303        let aligned = align_counter_boundary(value);
304        self.frame_counter = aligned;
305        self.persisted_counter = aligned;
306        self.pending_persist_target = None;
307        self.save_scheduled_since_boot = false;
308    }
309
310    fn schedule_counter_persist_if_needed(&mut self) {
311        if !self.counter_persistence_enabled {
312            return;
313        }
314
315        let should_schedule = !self.save_scheduled_since_boot
316            || (self.frame_counter & COUNTER_PERSIST_BLOCK_MASK) >= COUNTER_PERSIST_SCHEDULE_OFFSET;
317        if !should_schedule {
318            return;
319        }
320
321        // Reserve with `BLOCK - OFFSET` counters of lead beyond the live
322        // counter, so the boundary lands one block out for a counter early in
323        // its block and two blocks out for one already inside the renewal
324        // zone. Deriving the target from the bare counter here once made the
325        // offset-triggered renewal a no-op — it re-derived the boundary that
326        // was already persisted — so an identity that reached that boundary
327        // had nothing scheduled, and since a refused send advances nothing,
328        // it stayed refusing secure sends until reboot.
329        let target = next_counter_persist_target(
330            self.frame_counter
331                .wrapping_add(COUNTER_PERSIST_BLOCK_SIZE - COUNTER_PERSIST_SCHEDULE_OFFSET),
332        );
333        // A late-block trigger after its renewal already committed re-derives
334        // a boundary at or behind `persisted_counter`; writing that back
335        // would regress the reservation.
336        if self.persisted_counter.wrapping_sub(target) <= 2 * COUNTER_PERSIST_BLOCK_SIZE {
337            return;
338        }
339        if self.pending_persist_target == Some(target) {
340            return;
341        }
342        self.pending_persist_target = Some(
343            self.pending_persist_target
344                .map(|existing| existing.max(target))
345                .unwrap_or(target),
346        );
347        self.save_scheduled_since_boot = true;
348    }
349
350    fn mark_counter_persisted(&mut self, value: u32) {
351        let aligned = align_counter_boundary(value);
352        self.persisted_counter = aligned;
353        if self.pending_persist_target == Some(aligned) {
354            self.pending_persist_target = None;
355        }
356    }
357
358    fn counter_window_exhausted(&self) -> bool {
359        if !self.counter_persistence_enabled {
360            return false;
361        }
362
363        // Two blocks, not one: a renewal committed from late in the current
364        // block puts the boundary up to `2 * COUNTER_PERSIST_BLOCK_SIZE`
365        // ahead of the live counter, and any counter below the committed
366        // boundary is safe to use regardless of distance.
367        let ahead = self.persisted_counter.wrapping_sub(self.frame_counter);
368        if ahead > 0 && ahead <= 2 * COUNTER_PERSIST_BLOCK_SIZE {
369            return false;
370        }
371
372        if ahead == 0 {
373            return self.save_scheduled_since_boot;
374        }
375
376        self.frame_counter.wrapping_sub(self.persisted_counter) >= COUNTER_PERSIST_BLOCK_SIZE
377    }
378
379    /// Allocate the next send receipt.
380    pub fn next_receipt(&mut self) -> SendReceipt {
381        let receipt = SendReceipt(self.next_receipt);
382        self.next_receipt = self.next_receipt.wrapping_add(1);
383        receipt
384    }
385
386    /// Overrides the next send receipt value in tests that exercise wraparound behavior.
387    #[cfg(test)]
388    pub(crate) fn set_next_receipt_for_test(&mut self, value: u32) {
389        self.next_receipt = value;
390    }
391
392    /// Insert or replace pending-ACK state for a send receipt.
393    pub fn try_insert_pending_ack(
394        &mut self,
395        receipt: SendReceipt,
396        pending: PendingAck<FRAME>,
397    ) -> Result<Option<PendingAck<FRAME>>, PendingAckError> {
398        self.pending_acks
399            .insert(receipt, pending)
400            .map_err(|_| PendingAckError::TableFull)
401    }
402
403    /// Borrow pending-ACK state by receipt.
404    pub fn pending_ack(&self, receipt: &SendReceipt) -> Option<&PendingAck<FRAME>> {
405        self.pending_acks.get(receipt)
406    }
407
408    /// Iterate every tracked in-flight send in this slot.
409    ///
410    /// Includes internally tracked repeat-confirmed sends, whose receipts are
411    /// never returned to the application.
412    pub fn pending_acks(&self) -> impl Iterator<Item = (&SendReceipt, &PendingAck<FRAME>)> {
413        self.pending_acks.iter()
414    }
415
416    /// Mutably borrow pending-ACK state by receipt.
417    pub fn pending_ack_mut(&mut self, receipt: &SendReceipt) -> Option<&mut PendingAck<FRAME>> {
418        self.pending_acks.get_mut(receipt)
419    }
420
421    /// Remove pending-ACK state by receipt.
422    pub fn remove_pending_ack(&mut self, receipt: &SendReceipt) -> Option<PendingAck<FRAME>> {
423        self.pending_acks.remove(receipt)
424    }
425
426    /// Return the parent long-term identity if this slot is ephemeral.
427    pub fn pfs_parent(&self) -> Option<LocalIdentityId> {
428        self.pfs_parent
429    }
430
431    /// Borrow the pending counter-resynchronization table.
432    fn pending_counter_resync(&self) -> &LinearMap<PeerId, PendingCounterResync, PEERS> {
433        &self.pending_counter_resync
434    }
435
436    /// Mutably borrow the pending counter-resynchronization table.
437    fn pending_counter_resync_mut(
438        &mut self,
439    ) -> &mut LinearMap<PeerId, PendingCounterResync, PEERS> {
440        &mut self.pending_counter_resync
441    }
442}
443
444/// Per-channel operating-policy overrides enforced on outgoing traffic.
445///
446/// [`OperatingPolicy`] holds a small list of `ChannelPolicy` entries, one per channel that
447/// requires non-default behavior. When the coordinator builds a multicast or blind-unicast
448/// frame, it checks whether the target `channel_id` appears in this list and applies any
449/// overrides before sealing the packet.
450///
451/// Typical use cases:
452/// - **Unlicensed spectrum compliance** — force `require_unencrypted = true` for channels
453///   that must operate under Part 15 / ISM-band rules where encryption is permissible but
454///   the channel operator has chosen to run openly.
455/// - **Metadata reduction** — force `require_full_source = true` when receiving nodes need
456///   to resolve the sender without a prior key-exchange round-trip (e.g., a public beacon
457///   channel where all senders are first-contact).
458/// - **Propagation budget** — set `max_flood_hops` for high-density channels where
459///   uncontrolled flooding would waste airtime.
460///
461/// Channels absent from the policy list use the permissive defaults inherited from
462/// [`SendOptions`](crate::send::SendOptions).
463#[derive(Clone, Debug, PartialEq, Eq)]
464pub struct ChannelPolicy {
465    /// Channel to which this policy applies.
466    pub channel_id: ChannelId,
467    /// Whether the channel must be sent unencrypted.
468    pub require_unencrypted: bool,
469    /// Whether the channel requires the full source public key.
470    pub require_full_source: bool,
471    /// Optional maximum flood-hop budget.
472    pub max_flood_hops: Option<u8>,
473}
474
475/// Controls how the coordinator and optional repeater handle amateur-radio legal requirements.
476///
477/// Amateur (ham) radio law in most jurisdictions prohibits encrypted transmissions and requires
478/// station identification on all transmitted frames. UMSH supports three operating modes to
479/// accommodate networks that mix licensed and unlicensed nodes, or that operate exclusively
480/// under one regulatory regime.
481///
482/// | Mode | Encryption | Operator callsign | Repeater station callsign |
483/// |------|------------|------------------|--------------------------|
484/// | `Unlicensed` | Allowed | Optional | Not added |
485/// | `LicensedOnly` | Prohibited | Required | Required |
486/// | `Hybrid` | Allowed (local) | Optional | Added to forwarded frames |
487///
488/// The mode appears on both [`OperatingPolicy`] (for locally-originated traffic) and
489/// [`RepeaterConfig`] (for forwarding decisions) and they may differ independently — a node
490/// might transmit its own encrypted application traffic (`Unlicensed`) while acting as a
491/// licensed-identified repeater (`LicensedOnly`) for third-party frames it forwards.
492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub enum AmateurRadioMode {
494    /// Treat traffic as unlicensed operation only.
495    ///
496    /// Local transmit policy does not require operator callsigns or amateur-only
497    /// restrictions. Repeaters operating in this mode must not add a station
498    /// callsign when forwarding and should only retransmit packets that can be
499    /// handled under unlicensed rules.
500    Unlicensed,
501    /// Treat forwarded and locally originated traffic as amateur-only.
502    ///
503    /// Encryption and blind unicast are disallowed, operator callsigns are
504    /// required on originated packets, and repeaters must identify themselves
505    /// with a station callsign on forwarded traffic.
506    LicensedOnly,
507    /// Permit both unlicensed and amateur-qualified forwarding behavior.
508    ///
509    /// Local transmit policy remains permissive, but repeaters identify
510    /// forwarded packets with their station callsign and may still forward
511    /// packets lacking an operator callsign when they can do so under
512    /// unlicensed rules.
513    Hybrid,
514}
515
516#[derive(Clone, Copy, Debug, PartialEq, Eq)]
517enum TransmitAuthority {
518    Unlicensed,
519    Amateur,
520}
521
522#[derive(Clone, Copy, Debug, PartialEq, Eq)]
523enum ForwardStationAction {
524    Remove,
525    Replace,
526}
527
528#[derive(Clone, Copy, Debug, PartialEq, Eq)]
529struct ForwardPlan {
530    router_hint: RouterHint,
531    consume_source_route: bool,
532    decrement_flood_hops: bool,
533    insert_region_code: Option<[u8; 2]>,
534    delay_ms: u64,
535    station_action: ForwardStationAction,
536    /// Signal quality this repeater received the frame at, prepended to a
537    /// trace-signal option so its entries pair with the trace-route hints.
538    signal: TraceSignalEntry,
539}
540
541/// Package a reception's physical-layer observations for delivery.
542///
543/// A frame that reached the MAC without crossing a radio has no such
544/// observations, and reporting the placeholders it carries as readings
545/// would put invented numbers in front of anything that charts them.
546fn rx_metadata(rx: &RxInfo, received_at_ms: u64) -> crate::send::RxMetadata {
547    if rx.origin.is_measured() {
548        crate::send::RxMetadata::new(Some(rx.rssi), Some(rx.snr), rx.lqi, Some(received_at_ms))
549    } else {
550        crate::send::RxMetadata::new(None, None, None, Some(received_at_ms))
551    }
552}
553
554fn trace_signal_entry(rx: &RxInfo) -> TraceSignalEntry {
555    if rx.origin.is_measured() {
556        TraceSignalEntry::new(rx.rssi, rx.snr.as_centibels())
557    } else {
558        // A hop that measured nothing still owes the trace an entry, or
559        // every reading after it pairs with the wrong hop.
560        TraceSignalEntry::UNMEASURED
561    }
562}
563
564/// Local transmission policy enforced by the [`Mac`] coordinator on all outgoing frames.
565///
566/// `OperatingPolicy` governs what the coordinator is *allowed to send*, independent of what
567/// the application requests. It is consulted at the start of every `queue_*` call via an
568/// internal policy check, which returns [`SendError::PolicyViolation`] if the requested send
569/// would violate it. This policy applies only to locally-originated frames; forwarding
570/// decisions are governed separately by [`RepeaterConfig`].
571///
572/// - **`amateur_radio_mode`** — determines whether encryption and blind-unicast are permitted
573///   and whether an operator callsign must be appended to originated frames.
574///   See [`AmateurRadioMode`].
575/// - **`operator_callsign`** — the ARNCE/HAM-64 callsign automatically appended to every
576///   locally-originated frame when set. Required in `LicensedOnly` mode; optional otherwise.
577/// - **`channel_policies`** — a small list of per-channel overrides for multicast and
578///   blind-unicast traffic. Channels absent from the list use permissive defaults.
579///
580/// The default configuration (via [`Default`]) sets `Unlicensed` mode with no callsign and
581/// no per-channel overrides.
582#[derive(Clone, Debug, PartialEq, Eq)]
583pub struct OperatingPolicy {
584    /// Amateur-radio operating mode.
585    pub amateur_radio_mode: AmateurRadioMode,
586    /// Optional local operator callsign.
587    pub operator_callsign: Option<HamAddr>,
588    /// Per-channel overrides.
589    pub channel_policies: Vec<ChannelPolicy, 4>,
590}
591
592impl Default for OperatingPolicy {
593    fn default() -> Self {
594        Self {
595            amateur_radio_mode: AmateurRadioMode::Unlicensed,
596            operator_callsign: None,
597            channel_policies: Vec::new(),
598        }
599    }
600}
601
602/// Configuration governing whether and how the node forwards received frames.
603///
604/// The UMSH MAC layer includes an optional built-in repeater that forwards packets it
605/// successfully receives, extending the effective range of the network without requiring
606/// dedicated infrastructure. `RepeaterConfig` controls every facet of that behavior:
607///
608/// - **`enabled`** — master on/off switch. When `false`, all inbound forwarding logic is
609///   skipped even if the other fields are populated.
610/// - **`regions`** — a local list of 2-byte ARNCE region codes used as the flood-forwarding
611///   eligibility filter. When non-empty, packets carrying region codes are flood-forwarded only
612///   if at least one of those codes appears here; when empty, forwarding imposes no region check
613///   and a tagged packet is forwarded whatever its region.
614/// - **`default_region`** — the region code inserted into a flood-forwarded packet that carries
615///   none. `None` — the default — means the repeater never tags: untagged packets are forwarded
616///   untagged. Tagging is deliberately opt-in and independent of `regions`, because inserting a
617///   code asserts where the packet *is*, not merely which regions the repeater will carry. An
618///   already-tagged packet is always forwarded with its codes unchanged.
619/// - **`min_rssi` / `min_snr`** — signal-quality thresholds for flood forwarding. Packets
620///   received below these values are not flood-forwarded; this prevents marginal receptions
621///   from being re-injected into the network at full power, which would degrade SNR for
622///   nearby nodes rather than help. These thresholds do not apply to source-routed hops.
623/// - **Flood contention tuning** — controls the signal-to-delay mapping used when several
624///   eligible repeaters contend to flood-forward the same frame. The window is deterministic
625///   in the reception's SNR and RSSI, with a small random tie-breaker on top. These values
626///   should usually remain aligned across the mesh.
627/// - **`amateur_radio_mode`** — determines whether the repeater may forward encrypted or
628///   blind-unicast frames, and whether it must inject a station callsign. See
629///   [`AmateurRadioMode`].
630/// - **`station_callsign`** — the ARNCE/HAM-64 callsign injected into the options block of
631///   every forwarded frame when operating in `LicensedOnly` or `Hybrid` mode, satisfying the
632///   third-party identification requirements of FCC §97.119 and equivalent regulations.
633///
634/// The default configuration has `enabled: false`; repeating must be explicitly opted in.
635#[derive(Clone, Debug, PartialEq, Eq)]
636pub struct RepeaterConfig {
637    /// Whether repeater forwarding is enabled.
638    pub enabled: bool,
639    /// Allowed repeater region codes. Empty imposes no region check.
640    pub regions: Vec<[u8; 2], 8>,
641    /// Region code inserted into an untagged flood-forwarded packet.
642    /// `None` forwards untagged packets untagged.
643    pub default_region: Option<[u8; 2]>,
644    /// Minimum RSSI threshold for flood forwarding.
645    pub min_rssi: Option<i16>,
646    /// Minimum SNR threshold for flood forwarding.
647    pub min_snr: Option<i8>,
648    /// Lower clamp bound for the SNR quality term of flood forwarding contention.
649    pub flood_contention_snr_low_db: i8,
650    /// Upper clamp bound for the SNR quality term of flood forwarding contention.
651    pub flood_contention_snr_high_db: i8,
652    /// Lower clamp bound for the RSSI signal term of flood forwarding contention.
653    pub flood_contention_rssi_low_dbm: i16,
654    /// Upper clamp bound for the RSSI signal term of flood forwarding contention.
655    pub flood_contention_rssi_high_dbm: i16,
656    /// Minimum forwarding contention window as a percentage of `T_frame`.
657    pub flood_contention_min_window_percent: u8,
658    /// Maximum forwarding contention window as a percentage of `T_frame`.
659    pub flood_contention_max_window_percent: u8,
660    /// Random tie-breaker added to the deterministic contention window, as a
661    /// percentage of `T_frame`.
662    pub flood_contention_jitter_percent: u8,
663    /// ACK protection interval as a percentage of `T_frame`, added to the
664    /// contention delay when flood-forwarding an ack-requested packet that was
665    /// received with no remaining source-route hops (see channel-access.md
666    /// § ACK Protection Interval).
667    pub flood_contention_ack_guard_percent: u8,
668    /// Maximum number of overheard-repeat deferrals before abandoning a pending forward.
669    pub flood_contention_max_deferrals: u8,
670    /// Amateur-radio operating mode for forwarding.
671    pub amateur_radio_mode: AmateurRadioMode,
672    /// Optional station callsign injected on forwarded traffic.
673    pub station_callsign: Option<HamAddr>,
674}
675
676impl Default for RepeaterConfig {
677    fn default() -> Self {
678        Self {
679            enabled: false,
680            regions: Vec::new(),
681            default_region: None,
682            min_rssi: None,
683            min_snr: None,
684            flood_contention_snr_low_db: -9,
685            flood_contention_snr_high_db: 3,
686            flood_contention_rssi_low_dbm: -100,
687            flood_contention_rssi_high_dbm: -70,
688            flood_contention_min_window_percent: 0,
689            flood_contention_max_window_percent: 50,
690            flood_contention_jitter_percent: 10,
691            flood_contention_ack_guard_percent: 25,
692            flood_contention_max_deferrals: 3,
693            amateur_radio_mode: AmateurRadioMode::Unlicensed,
694            station_callsign: None,
695        }
696    }
697}
698
699/// Errors returned by the [`Mac`] coordinator when queueing an outbound send.
700///
701/// Returned synchronously by `queue_broadcast`, `queue_unicast`, `queue_multicast`, and
702/// related methods. An error here means the send could not be *enqueued* — it says nothing
703/// about the fate of frames already in the transmit queue.
704#[derive(Clone, Debug, PartialEq, Eq)]
705pub enum SendError {
706    /// The [`LocalIdentityId`] passed to the queue call does not correspond to an occupied
707    /// identity slot. This indicates the identity was never registered or was removed.
708    IdentityMissing,
709    /// The destination [`umsh_core::PublicKey`] is not present in the peer registry.
710    /// Register the peer first via [`Mac::add_peer`].
711    PeerMissing,
712    /// No cached pairwise session keys exist for the target peer on this identity.
713    /// This is only returned by the low-level `queue_*` APIs; the public async send APIs
714    /// derive and cache peer state automatically.
715    PairwiseKeysMissing,
716    /// The local identity failed to derive a shared secret for this peer.
717    IdentityAgreementFailed,
718    /// The target [`umsh_core::ChannelId`] is not present in the channel table.
719    /// Register the channel first via [`Mac::add_channel`] or [`Mac::add_named_channel`].
720    ChannelMissing,
721    /// The [`OperatingPolicy`] rejected this send — for example, attempting to send an
722    /// encrypted frame while operating in [`AmateurRadioMode::LicensedOnly`] mode.
723    PolicyViolation,
724    /// The low-level packet builder failed, typically because the frame buffer is too small
725    /// for the requested options and payload.
726    Build(BuildError),
727    /// Packet parsing failed while reprocessing a freshly-built frame, indicating an
728    /// internal inconsistency in the packet construction logic.
729    Parse(ParseError),
730    /// The cryptographic seal operation failed. This typically indicates a mismatched key
731    /// length or an internal crypto engine error.
732    Crypto(CryptoError),
733    /// The transmit queue is at the configured `TX` capacity. Back off and retry after
734    /// the event loop has drained some entries.
735    QueueFull,
736    /// The in-flight ACK table for this identity is at the configured `ACKS` capacity.
737    /// Wait for an existing ACK-requested send to complete or time out before sending another.
738    PendingAckFull,
739    /// Secure sends are blocked because the live frame counter has reached the persisted
740    /// reservation boundary. Call [`Mac::service_counter_persistence`] to flush a new
741    /// boundary to the counter store before retrying.
742    CounterPersistenceLag,
743}
744
745impl From<BuildError> for SendError {
746    fn from(value: BuildError) -> Self {
747        Self::Build(value)
748    }
749}
750
751impl From<ParseError> for SendError {
752    fn from(value: ParseError) -> Self {
753        Self::Parse(value)
754    }
755}
756
757impl From<CryptoError> for SendError {
758    fn from(value: CryptoError) -> Self {
759        Self::Crypto(value)
760    }
761}
762
763/// Runtime errors produced by the [`Mac`] coordinator's async event loop.
764///
765/// Unlike [`SendError`], which is returned synchronously when *enqueueing* a send,
766/// `MacError` surfaces from the async methods (`next_event`,
767/// `service_counter_persistence`) that actually drive the coordinator forward.
768#[derive(Clone, Debug, PartialEq, Eq)]
769pub enum MacError<RadioError> {
770    /// The underlying [`umsh_hal::Radio`] driver returned an error during a receive or
771    /// channel-sense operation. The inner type is platform-specific (e.g., SPI fault on
772    /// embedded hardware, socket error on a UDP transport).
773    Radio(RadioError),
774    /// A transmit-phase error from the radio. The frame was not sent.
775    ///
776    /// Channel-busy (CAD) verdicts are not surfaced here: they are retried
777    /// with backoff, and exhausting the retry budget emits
778    /// [`MacEventRef::TxAbandoned`](crate::MacEventRef::TxAbandoned) instead.
779    Transmit(TxError<RadioError>),
780    /// An internal capacity invariant was violated: the coordinator needed to enqueue a
781    /// control frame (MAC ACK, forwarded packet) but the transmit queue was full.
782    /// Increase the `TX` const generic on [`Mac`] to give the queue more headroom.
783    QueueFull,
784}
785
786/// Errors returned while loading persisted frame-counter boundaries via [`Mac::load_persisted_counter`].
787///
788/// On startup, applications should call [`Mac::load_persisted_counter`] for each registered
789/// long-term identity to restore the safe starting point for the frame counter from
790/// non-volatile storage.
791#[derive(Clone, Debug, PartialEq, Eq)]
792pub enum CounterPersistenceError<StoreError> {
793    /// The [`LocalIdentityId`] supplied does not correspond to an occupied identity slot.
794    IdentityMissing,
795    /// The underlying [`umsh_hal::CounterStore`] read operation failed. The application
796    /// should decide whether to halt, retry, or start the counter at a conservatively high
797    /// value to avoid replay-window collisions with pre-reset traffic.
798    Store(StoreError),
799}
800
801impl<RadioError> From<RadioError> for MacError<RadioError> {
802    fn from(value: RadioError) -> Self {
803        Self::Radio(value)
804    }
805}
806
807impl<RadioError> From<TxError<RadioError>> for MacError<RadioError> {
808    fn from(value: TxError<RadioError>) -> Self {
809        Self::Transmit(value)
810    }
811}
812
813/// Central MAC coordinator that owns and drives the full UMSH radio-facing state machine.
814///
815/// `Mac` is the top-level entry point for UMSH protocol operation. It combines a radio driver,
816/// cryptographic engine, clock, RNG, counter store, and all protocol state into a single
817/// fully-typed, allocation-free structure. All const-generic capacity parameters are enforced
818/// at compile time via `heapless` collections — there are no heap allocations inside `Mac`.
819///
820/// ## Generic parameters
821///
822/// - **`P: Platform`** — a trait bundle supplying the concrete driver types for `Radio`,
823///   `Aes`/`Sha` (crypto), `Clock`, `Rng`, and `CounterStore`. Implement [`Platform`] once
824///   per deployment target to swap in real hardware drivers, software stubs, or test doubles.
825/// - **`IDENTITIES`** — maximum simultaneously active local identities (default
826///   [`DEFAULT_IDENTITIES`]).
827/// - **`PEERS`** — maximum known remote peers and their per-identity pairwise key entries
828///   (default [`DEFAULT_PEERS`]).
829/// - **`CHANNELS`** — maximum registered multicast channel keys (default
830///   [`DEFAULT_CHANNELS`]).
831/// - **`ACKS`** — maximum simultaneously in-flight ACK-requested sends per identity
832///   (default [`DEFAULT_ACKS`]).
833/// - **`TX`** — depth of the transmit queue (default [`DEFAULT_TX`]). Must be large enough
834///   to absorb a burst of control frames (MAC ACKs + forwarded frames) alongside any
835///   backlogged application sends.
836/// - **`FRAME`** — maximum byte length of a stored frame buffer for retransmission
837///   (default [`MAX_RESEND_FRAME_LEN`]).
838/// - **`DUP`** — capacity of the duplicate-detection cache (default [`DEFAULT_DUP`]).
839///
840/// ## Lifecycle
841///
842/// 1. **Construct** with [`Mac::new`], supplying concrete driver instances and policy.
843/// 2. **Register identities** via [`Mac::add_identity`]; call
844///    [`Mac::load_persisted_counter`] on each long-term identity to restore the safe
845///    frame-counter start point from non-volatile storage.
846/// 3. **Register peers** via [`Mac::add_peer`]. Secure unicast and blind-unicast state is
847///    derived lazily from the local private key and peer public key on first use.
848/// 4. **Register channels** via [`Mac::add_channel`] or [`Mac::add_named_channel`].
849/// 5. **Drive the event loop** via [`Mac::run`] / [`Mac::run_quiet`] for long-lived tasks,
850///    or by awaiting [`Mac::next_event`] when you need to multiplex MAC progress with other
851///    async work. The coordinator handles incoming frames, outgoing transmits, forwarding,
852///    ACK matching, retransmission scheduling, and timer deadlines — no external polling
853///    required.
854/// 6. **Send traffic** by calling `queue_broadcast`, `queue_unicast`, `queue_multicast`,
855///    etc. from application code between (or concurrent with) event-loop iterations.
856/// 7. **Persist counters** by calling [`Mac::service_counter_persistence`] whenever
857///    `next_event` signals that pending persistence work is ready to flush.
858///
859/// ## Example (pseudo-code)
860///
861/// ```rust,ignore
862/// let mut mac = Mac::<MyPlatform>::new(
863///     radio, crypto, clock, rng, counter_store,
864///     RepeaterConfig::default(), OperatingPolicy::default(),
865/// );
866/// let id = mac.add_identity(my_identity)?;
867/// mac.load_persisted_counter(id).await?;
868///
869/// mac.run(|id, event| {
870///     let _ = (id, event);
871///     // handle deliveries / ACKs here and schedule persistence work as needed
872/// }).await?;
873/// ```
874/// Cumulative frame tallies for the coordinator, since construction.
875///
876/// Diagnostic only: nothing in the protocol depends on them, and they are
877/// deliberately not persisted. They exist so an operator can tell a
878/// working node from a deaf one without a capture — a radio whose
879/// `rx_frames` never moves is not hearing anybody.
880///
881/// Every field saturates rather than wraps. A counter that rolled over
882/// would make a long-running node look freshly booted; pinning at the
883/// maximum is at least monotone, which is the property a reader is
884/// actually using them for.
885#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
886pub struct MacCounters {
887    /// Frames the radio accepted for transmission, including forwards.
888    pub tx_frames: u32,
889    /// Frames given up on after [`MAX_CAD_ATTEMPTS`] busy channels.
890    pub tx_abandoned: u32,
891    /// Frames the radio handed up, whoever they were addressed to.
892    pub rx_frames: u32,
893    /// Receptions that produced an event or a side effect. The shortfall
894    /// against [`Self::rx_frames`] is other people's traffic, duplicates,
895    /// and undecodable noise.
896    pub rx_accepted: u32,
897    /// Receptions this node repeated onward.
898    pub forwarded: u32,
899    /// Queued forwards dropped because the destination's ack was overheard
900    /// before they went out. Airtime this node did not have to spend.
901    pub forward_cancelled: u32,
902    /// Receptions this node would have repeated and declined under the
903    /// operator's forwarding policy: an exhausted flood budget, a signal
904    /// under the configured minimum RSSI or SNR, or a region this
905    /// repeater does not serve.
906    ///
907    /// Deliberately narrow. Duplicates, traffic addressed to us, and
908    /// frames arriving while forwarding is switched off are all reasons
909    /// not to repeat something, and none of them is a decision the
910    /// operator made about *this* frame — counting them here would bury
911    /// the four numbers a repeater's settings can actually move.
912    pub forward_dropped_policy: u32,
913}
914
915impl MacCounters {
916    fn bump(slot: &mut u32) {
917        *slot = slot.saturating_add(1);
918    }
919}
920
921pub struct Mac<
922    P: Platform,
923    const IDENTITIES: usize = DEFAULT_IDENTITIES,
924    const PEERS: usize = DEFAULT_PEERS,
925    const CHANNELS: usize = DEFAULT_CHANNELS,
926    const ACKS: usize = DEFAULT_ACKS,
927    const TX: usize = DEFAULT_TX,
928    const FRAME: usize = MAX_RESEND_FRAME_LEN,
929    const DUP: usize = DEFAULT_DUP,
930    const RN: usize = DEFAULT_CHANNEL_REPLAY,
931    const HN: usize = DEFAULT_CHANNEL_HINT_REPLAY,
932> {
933    radio: P::Radio,
934    crypto: CryptoEngine<P::Aes, P::Sha>,
935    clock: P::Clock,
936    rng: P::Rng,
937    counter_store: P::CounterStore,
938    identities: Vec<Option<IdentitySlot<P::Identity, PEERS, ACKS, FRAME>>, IDENTITIES>,
939    peer_registry: PeerRegistry<PEERS>,
940    transmitter_observations: crate::TransmitterObservations,
941    channels: ChannelTable<CHANNELS, RN, HN>,
942    dup_cache: DuplicateCache<DUP>,
943    multicast_unknown_dup_cache: DuplicateCache<DUP>,
944    tx_queue: TxQueue<TX, FRAME>,
945    post_tx_listen: Option<PostTxListen>,
946    repeater: RepeaterConfig,
947    operating_policy: OperatingPolicy,
948    auto_register_full_key_peers: bool,
949    deferred_counter_resync_frame: Option<DeferredCounterResyncFrame<FRAME>>,
950    counters: MacCounters,
951}
952
953impl<
954    P: Platform,
955    const IDENTITIES: usize,
956    const PEERS: usize,
957    const CHANNELS: usize,
958    const ACKS: usize,
959    const TX: usize,
960    const FRAME: usize,
961    const DUP: usize,
962    const RN: usize,
963    const HN: usize,
964> Mac<P, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP, RN, HN>
965{
966    /// Creates a MAC coordinator with the supplied radio, crypto, timing, and policy state.
967    pub fn new(
968        radio: P::Radio,
969        crypto: CryptoEngine<P::Aes, P::Sha>,
970        clock: P::Clock,
971        rng: P::Rng,
972        counter_store: P::CounterStore,
973        repeater: RepeaterConfig,
974        operating_policy: OperatingPolicy,
975    ) -> Self {
976        Self {
977            radio,
978            crypto,
979            clock,
980            rng,
981            counter_store,
982            identities: Vec::new(),
983            peer_registry: PeerRegistry::new(),
984            transmitter_observations: crate::TransmitterObservations::new(),
985            channels: ChannelTable::new(),
986            dup_cache: DuplicateCache::new(),
987            multicast_unknown_dup_cache: DuplicateCache::new(),
988            tx_queue: TxQueue::new(),
989            post_tx_listen: None,
990            repeater,
991            operating_policy,
992            auto_register_full_key_peers: false,
993            deferred_counter_resync_frame: None,
994            counters: MacCounters::default(),
995        }
996    }
997
998    /// Cumulative frame tallies since construction.
999    pub const fn counters(&self) -> MacCounters {
1000        self.counters
1001    }
1002
1003    /// What has been heard from whom, most recently — the signal half of a
1004    /// peer-repeater listing.
1005    pub fn transmitter_observations(&self) -> &crate::TransmitterObservations {
1006        &self.transmitter_observations
1007    }
1008
1009    /// Borrow the underlying radio.
1010    pub fn radio(&self) -> &P::Radio {
1011        &self.radio
1012    }
1013
1014    /// Mutably borrow the underlying radio.
1015    pub fn radio_mut(&mut self) -> &mut P::Radio {
1016        &mut self.radio
1017    }
1018
1019    /// Give up this coordinator and recover the radio underneath it.
1020    ///
1021    /// A host tool that borrows a device for the duration of one mesh
1022    /// operation gets its handle back this way, rather than dropping the
1023    /// attachment and paying for another.
1024    pub fn into_radio(self) -> P::Radio {
1025        self.radio
1026    }
1027
1028    /// Borrow the crypto engine.
1029    pub fn crypto(&self) -> &CryptoEngine<P::Aes, P::Sha> {
1030        &self.crypto
1031    }
1032
1033    /// Borrow the monotonic clock.
1034    pub fn clock(&self) -> &P::Clock {
1035        &self.clock
1036    }
1037
1038    /// Borrow the RNG.
1039    pub fn rng(&self) -> &P::Rng {
1040        &self.rng
1041    }
1042
1043    /// Mutably borrow the RNG.
1044    pub fn rng_mut(&mut self) -> &mut P::Rng {
1045        &mut self.rng
1046    }
1047
1048    /// Borrow the counter store.
1049    pub fn counter_store(&self) -> &P::CounterStore {
1050        &self.counter_store
1051    }
1052    /// Borrow the transmit queue.
1053    pub fn tx_queue(&self) -> &TxQueue<TX, FRAME> {
1054        &self.tx_queue
1055    }
1056    /// Mutably borrow the transmit queue.
1057    pub fn tx_queue_mut(&mut self) -> &mut TxQueue<TX, FRAME> {
1058        &mut self.tx_queue
1059    }
1060    /// Borrow the duplicate cache.
1061    pub fn dup_cache(&self) -> &DuplicateCache<DUP> {
1062        &self.dup_cache
1063    }
1064    /// Borrow the peer registry.
1065    pub fn peer_registry(&self) -> &PeerRegistry<PEERS> {
1066        &self.peer_registry
1067    }
1068    /// Mutably borrow the peer registry.
1069    pub fn peer_registry_mut(&mut self) -> &mut PeerRegistry<PEERS> {
1070        &mut self.peer_registry
1071    }
1072    /// Borrow the channel table.
1073    pub fn channels(&self) -> &ChannelTable<CHANNELS, RN, HN> {
1074        &self.channels
1075    }
1076    /// Mutably borrow the channel table.
1077    pub fn channels_mut(&mut self) -> &mut ChannelTable<CHANNELS, RN, HN> {
1078        &mut self.channels
1079    }
1080    /// Borrow repeater configuration.
1081    pub fn repeater_config(&self) -> &RepeaterConfig {
1082        &self.repeater
1083    }
1084    /// Mutably borrow repeater configuration.
1085    pub fn repeater_config_mut(&mut self) -> &mut RepeaterConfig {
1086        &mut self.repeater
1087    }
1088    /// Borrow the local operating policy.
1089    pub fn operating_policy(&self) -> &OperatingPolicy {
1090        &self.operating_policy
1091    }
1092    /// Mutably borrow the local operating policy.
1093    pub fn operating_policy_mut(&mut self) -> &mut OperatingPolicy {
1094        &mut self.operating_policy
1095    }
1096
1097    /// Return whether inbound secure packets carrying a full source key may auto-register peers.
1098    pub fn auto_register_full_key_peers(&self) -> bool {
1099        self.auto_register_full_key_peers
1100    }
1101
1102    /// Enable or disable inbound full-key peer auto-registration.
1103    pub fn set_auto_register_full_key_peers(&mut self, enabled: bool) {
1104        self.auto_register_full_key_peers = enabled;
1105    }
1106
1107    /// Register one long-term local identity.
1108    pub fn add_identity(
1109        &mut self,
1110        identity: P::Identity,
1111    ) -> Result<LocalIdentityId, CapacityError> {
1112        self.insert_identity(LocalIdentity::LongTerm(identity), None)
1113    }
1114
1115    /// Load the persisted frame-counter boundary for `id` from the counter store.
1116    ///
1117    /// # Flash-wear invariant
1118    ///
1119    /// This method must remain read-only. In particular, booting or repeatedly
1120    /// rebooting without transmitting must never write a new reservation and
1121    /// wear out embedded flash. The first authenticated send schedules the
1122    /// future reservation; the MAC driver services that pending write because
1123    /// actual transmission has made persistence necessary.
1124    pub async fn load_persisted_counter(
1125        &mut self,
1126        id: LocalIdentityId,
1127    ) -> Result<u32, CounterPersistenceError<<P::CounterStore as CounterStore>::Error>> {
1128        let context = {
1129            let slot = self
1130                .identity(id)
1131                .ok_or(CounterPersistenceError::IdentityMissing)?;
1132            if !slot.counter_persistence_enabled() {
1133                return Ok(slot.frame_counter());
1134            }
1135            *slot.identity().public_key()
1136        };
1137        let loaded = self
1138            .counter_store
1139            .load(&context.0)
1140            .await
1141            .map_err(CounterPersistenceError::Store)?;
1142        // Zero is the store's missing-record sentinel. Keep the random non-zero
1143        // initial value in that case; it covers both a fresh identity and
1144        // counter-state loss for an identity that survived in a separate secret
1145        // store. Do not persist here: see the flash-wear invariant above.
1146        if loaded == 0 {
1147            let slot = self
1148                .identity(id)
1149                .ok_or(CounterPersistenceError::IdentityMissing)?;
1150            return Ok(slot.frame_counter());
1151        }
1152        let aligned = align_counter_boundary(loaded);
1153        let slot = self
1154            .identity_mut(id)
1155            .ok_or(CounterPersistenceError::IdentityMissing)?;
1156        slot.load_persisted_counter(aligned);
1157        Ok(aligned)
1158    }
1159
1160    /// Persist all currently scheduled frame-counter reservations.
1161    pub async fn service_counter_persistence(
1162        &mut self,
1163    ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
1164        let mut pending = Vec::<(LocalIdentityId, [u8; 32], u32), IDENTITIES>::new();
1165        for (index, slot) in self.identities.iter().enumerate() {
1166            let Some(slot) = slot.as_ref() else {
1167                continue;
1168            };
1169            let Some(target) = slot.pending_persist_target() else {
1170                continue;
1171            };
1172            if !slot.counter_persistence_enabled() {
1173                continue;
1174            }
1175            pending
1176                .push((
1177                    LocalIdentityId(index as u8),
1178                    slot.identity().public_key().0,
1179                    target,
1180                ))
1181                .expect("identity enumeration must fit configured identity capacity");
1182        }
1183
1184        let mut wrote = 0usize;
1185        for (_, context, target) in pending.iter() {
1186            self.counter_store
1187                .store(context, align_counter_boundary(*target))
1188                .await?;
1189            wrote += 1;
1190        }
1191        if wrote > 0 {
1192            self.counter_store.flush().await?;
1193            for (id, _, target) in pending {
1194                if let Some(slot) = self.identity_mut(id) {
1195                    slot.mark_counter_persisted(target);
1196                }
1197            }
1198        }
1199        Ok(wrote)
1200    }
1201
1202    /// Persist RX frame-counter boundaries for all peers that have received
1203    /// enough frames since the last flush.
1204    ///
1205    /// Called automatically from [`Self::next_event`] so applications need not
1206    /// invoke this directly.
1207    pub(crate) async fn service_rx_counter_persistence(
1208        &mut self,
1209    ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
1210        // Collect all (identity_index, peer_id, peer_pk, last_accepted) tuples
1211        // that need persisting. We collect before awaiting to avoid holding
1212        // live references across the async CounterStore calls.
1213        let mut pending: heapless::Vec<(u8, crate::peers::PeerId, [u8; 32], u32), PEERS> =
1214            heapless::Vec::new();
1215
1216        for (identity_index, slot) in self.identities.iter().enumerate() {
1217            let Some(slot) = slot.as_ref() else {
1218                continue;
1219            };
1220            for (peer_id, state) in slot.peer_crypto().iter() {
1221                if !state.needs_rx_persist {
1222                    continue;
1223                }
1224                let Some(info) = self.peer_registry.get(*peer_id) else {
1225                    continue;
1226                };
1227                let _ = pending.push((
1228                    identity_index as u8,
1229                    *peer_id,
1230                    info.public_key.0,
1231                    state.replay_window.last_accepted,
1232                ));
1233            }
1234        }
1235
1236        let mut wrote = 0usize;
1237        for (_, _, pk, last) in pending.iter() {
1238            let key = rx_counter_key_bytes(pk);
1239            self.counter_store.store(&key, *last).await?;
1240            wrote += 1;
1241        }
1242        if wrote > 0 {
1243            self.counter_store.flush().await?;
1244            // Clear flags and update persisted_rx_counter only after a
1245            // successful flush.
1246            for (identity_index, peer_id, _, last) in pending.iter() {
1247                if let Some(slot) = self.identities[*identity_index as usize].as_mut() {
1248                    if let Some(state) = slot.peer_crypto_mut().get_mut(peer_id) {
1249                        state.persisted_rx_counter = *last;
1250                        state.needs_rx_persist = false;
1251                    }
1252                }
1253            }
1254        }
1255        Ok(wrote)
1256    }
1257
1258    /// Persist every RX frame-counter boundary that has advanced past what
1259    /// the store holds, regardless of how little it advanced.
1260    ///
1261    /// The block-cadence persistence above trades a bounded replay window
1262    /// for flash wear: up to [`COUNTER_PERSIST_BLOCK_SIZE`] frames per peer
1263    /// ride in RAM only, and an abrupt power loss re-opens that window. A
1264    /// *deliberate* restart must not: a command the device executed and
1265    /// then rebooted on would be executed again when the sender's retry
1266    /// lands inside the stale window. Call this before any intentional
1267    /// reset so the boundary on flash is the boundary that was live.
1268    pub async fn persist_all_rx_counters(
1269        &mut self,
1270    ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
1271        for slot in self.identities.iter_mut() {
1272            let Some(slot) = slot.as_mut() else {
1273                continue;
1274            };
1275            for (_, state) in slot.peer_crypto_mut().iter_mut() {
1276                if state.replay_window.last_accepted != state.persisted_rx_counter {
1277                    state.needs_rx_persist = true;
1278                }
1279            }
1280        }
1281        self.service_rx_counter_persistence().await
1282    }
1283
1284    /// Load persisted RX frame-counter boundaries for all registered peers and
1285    /// store them in [`PeerInfo::initial_rx_counter`].
1286    ///
1287    /// Call this once at boot after registering all known peers. When pairwise
1288    /// keys are first derived for each peer, the replay window is initialized
1289    /// to the loaded boundary so frames replayed from before the reboot are
1290    /// rejected.
1291    pub async fn load_all_persisted_rx_counters(
1292        &mut self,
1293    ) -> Result<usize, <P::CounterStore as CounterStore>::Error> {
1294        let mut loaded = 0usize;
1295        // PeerRegistry is a dense Vec; PeerId(i) == index i. Break on first None.
1296        for peer_index in 0..PEERS {
1297            let peer_id = crate::peers::PeerId(peer_index as u8);
1298            let Some(info) = self.peer_registry.get(peer_id) else {
1299                break;
1300            };
1301            let pk = info.public_key;
1302            let key = rx_counter_key_bytes(&pk.0);
1303            let stored = self.counter_store.load(&key).await?;
1304            if stored > 0 {
1305                if let Some(info) = self.peer_registry.get_mut(peer_id) {
1306                    info.initial_rx_counter = stored;
1307                    loaded += 1;
1308                }
1309            }
1310        }
1311        Ok(loaded)
1312    }
1313
1314    #[cfg(feature = "software-crypto")]
1315    /// Register an ephemeral software identity linked to `parent`.
1316    pub fn register_ephemeral(
1317        &mut self,
1318        parent: LocalIdentityId,
1319        identity: umsh_crypto::software::SoftwareIdentity,
1320    ) -> Result<LocalIdentityId, CapacityError> {
1321        self.insert_identity(LocalIdentity::Ephemeral(identity), Some(parent))
1322    }
1323
1324    #[cfg(feature = "software-crypto")]
1325    /// Remove an ephemeral identity slot if one exists at `id`.
1326    pub fn remove_ephemeral(&mut self, id: LocalIdentityId) -> bool {
1327        if let Some(slot) = self.identities.get_mut(id.0 as usize) {
1328            let should_remove = slot
1329                .as_ref()
1330                .map(|identity_slot| identity_slot.identity().is_ephemeral())
1331                .unwrap_or(false);
1332            if should_remove {
1333                *slot = None;
1334                return true;
1335            }
1336        }
1337        false
1338    }
1339
1340    /// Borrow an identity slot by identifier.
1341    pub fn identity(
1342        &self,
1343        id: LocalIdentityId,
1344    ) -> Option<&IdentitySlot<P::Identity, PEERS, ACKS, FRAME>> {
1345        self.identities.get(id.0 as usize)?.as_ref()
1346    }
1347
1348    /// Mutably borrow an identity slot by identifier.
1349    pub fn identity_mut(
1350        &mut self,
1351        id: LocalIdentityId,
1352    ) -> Option<&mut IdentitySlot<P::Identity, PEERS, ACKS, FRAME>> {
1353        self.identities.get_mut(id.0 as usize)?.as_mut()
1354    }
1355
1356    /// Registers or refreshes a known remote peer in the shared registry.
1357    ///
1358    /// When the `software-crypto` feature is enabled, the supplied public key
1359    /// is validated as a well-formed Ed25519 compressed point on the curve.
1360    /// Malformed keys are rejected with [`AddPeerError::InvalidPublicKey`]
1361    /// before they can pollute the peer registry.
1362    pub fn add_peer(&mut self, key: PublicKey) -> Result<PeerId, AddPeerError> {
1363        #[cfg(feature = "software-crypto")]
1364        {
1365            if !umsh_crypto::is_valid_ed25519_public_key(&key) {
1366                return Err(AddPeerError::InvalidPublicKey);
1367            }
1368        }
1369        Ok(self.peer_registry.try_insert_or_update(key)?)
1370    }
1371
1372    /// Removes a registered peer and every piece of per-peer transport state:
1373    /// pairwise crypto, replay windows, pending counter resyncs, and any
1374    /// deferred inbound frame. Returns whether the peer was registered.
1375    ///
1376    /// The peer registry is dense, so removal moves the last entry into the
1377    /// freed slot; state keyed by the moved peer's old identifier is re-keyed
1378    /// here. Persisted RX counter boundaries are deliberately retained: if
1379    /// the peer is re-added later, replay protection resumes from the stored
1380    /// boundary instead of accepting replays from before the removal.
1381    pub fn remove_peer(&mut self, key: &PublicKey) -> bool {
1382        let Some((peer_id, _)) = self.peer_registry.lookup_by_key(key) else {
1383            return false;
1384        };
1385        self.clear_peer_slot_state(peer_id);
1386        if self
1387            .deferred_counter_resync_frame
1388            .as_ref()
1389            .is_some_and(|deferred| deferred.peer_id == peer_id)
1390        {
1391            self.deferred_counter_resync_frame = None;
1392        }
1393        let Some(removal) = self.peer_registry.remove(peer_id) else {
1394            return false;
1395        };
1396        if let Some((old_id, new_id)) = removal.moved {
1397            self.rekey_peer_slot_state(old_id, new_id);
1398        }
1399        true
1400    }
1401
1402    /// Adds or updates a shared channel and derives its multicast keys.
1403    pub fn add_channel(&mut self, key: ChannelKey) -> Result<(), CapacityError> {
1404        let derived = self.crypto.derive_channel_keys(&key);
1405        self.channels.try_add(key, derived)
1406    }
1407
1408    /// Removes a previously added channel by its exact key, discarding
1409    /// the channel's replay state with it (re-adding the key later
1410    /// starts at first contact). Returns whether a channel was removed.
1411    pub fn remove_channel(&mut self, key: &ChannelKey) -> bool {
1412        self.channels.remove_by_key(key)
1413    }
1414
1415    /// Adds or updates a named channel using the coordinator's channel-key derivation.
1416    ///
1417    /// The name is canonicalized (ASCII lowercase fold) before derivation, so
1418    /// `Public` and `public` register the same channel.
1419    pub fn add_named_channel(&mut self, name: &str) -> Result<(), crate::AddChannelError> {
1420        let key = self
1421            .crypto
1422            .derive_named_channel_key(name)
1423            .map_err(crate::AddChannelError::InvalidName)?;
1424        self.add_channel(key)?;
1425        Ok(())
1426    }
1427
1428    /// Return the number of occupied identity slots.
1429    pub fn identity_count(&self) -> usize {
1430        self.identities.iter().filter(|slot| slot.is_some()).count()
1431    }
1432
1433    /// Installs pairwise transport keys for one local identity and remote peer.
1434    ///
1435    /// # Safety (logical)
1436    /// Installing wrong keys will silently corrupt the session. This method
1437    /// is crate-internal; external callers should use the `unsafe-advanced`
1438    /// feature or go through the node-layer PFS session manager.
1439    #[cfg(any(feature = "unsafe-advanced", test))]
1440    pub(crate) fn install_pairwise_keys(
1441        &mut self,
1442        identity_id: LocalIdentityId,
1443        peer_id: PeerId,
1444        pairwise_keys: PairwiseKeys,
1445    ) -> Result<Option<crate::peers::PeerCryptoState>, SendError> {
1446        // Read initial_rx_counter and clock before the mutable identity borrow.
1447        let initial = self
1448            .peer_registry
1449            .get(peer_id)
1450            .map(|info| info.initial_rx_counter)
1451            .unwrap_or(0);
1452        let now_ms = self.clock.now_ms();
1453        let mut replay_window = ReplayWindow::new();
1454        if initial > 0 {
1455            replay_window.reset(initial, now_ms);
1456        }
1457        let slot = self
1458            .identity_mut(identity_id)
1459            .ok_or(SendError::IdentityMissing)?;
1460        slot.peer_crypto_mut()
1461            .insert(
1462                peer_id,
1463                crate::peers::PeerCryptoState {
1464                    pairwise_keys,
1465                    replay_window,
1466                    persisted_rx_counter: initial,
1467                    needs_rx_persist: false,
1468                },
1469            )
1470            .map_err(|_| SendError::QueueFull)
1471    }
1472
1473    /// Installs pairwise transport keys for one local identity and remote peer.
1474    ///
1475    /// # Safety (logical)
1476    /// Installing wrong keys will silently corrupt the session. This method
1477    /// is deliberately gated behind the `unsafe-advanced` feature. Prefer
1478    /// going through the node-layer PFS session manager instead.
1479    #[cfg(feature = "unsafe-advanced")]
1480    pub fn install_pairwise_keys_advanced(
1481        &mut self,
1482        identity_id: LocalIdentityId,
1483        peer_id: PeerId,
1484        pairwise_keys: PairwiseKeys,
1485    ) -> Result<Option<crate::peers::PeerCryptoState>, SendError> {
1486        self.install_pairwise_keys(identity_id, peer_id, pairwise_keys)
1487    }
1488
1489    /// Enqueues a broadcast frame for transmission.
1490    ///
1491    /// Note: the `encrypted`, `ack_requested`, and `salt` flags on `options`
1492    /// are silently forced to `false` because broadcasts cannot carry any of
1493    /// them on the wire.
1494    ///
1495    /// TODO: this sanitization is silent — a caller that explicitly set
1496    /// `ack_requested = true` for a broadcast will get a successful
1497    /// `SendReceipt` and never learn the flag was dropped. Surface this as a
1498    /// debug-level event (or split `SendOptions` into kind-specific
1499    /// builders) so the loss is observable.
1500    pub fn queue_broadcast(
1501        &mut self,
1502        from: LocalIdentityId,
1503        payload: &[u8],
1504        options: &SendOptions,
1505    ) -> Result<SendReceipt, SendError> {
1506        // Broadcasts are always unencrypted, never ack-requested, and never
1507        // salted by the MAC. Sanitize before policy classification so a reused
1508        // `SendOptions` (whose default `encrypted = true`) doesn't get rejected
1509        // as a `PolicyViolation` in `LicensedOnly` mode for a send that will
1510        // in fact go out unencrypted.
1511        let mut options = options.clone();
1512        options.encrypted = false;
1513        options.ack_requested = false;
1514        options.salt = false;
1515        let options = &options;
1516        self.enforce_send_policy(None, options, false)?;
1517
1518        let slot = self.identity_mut(from).ok_or(SendError::IdentityMissing)?;
1519        let source_key = *slot.identity().public_key();
1520        let receipt = slot.next_receipt();
1521        let mut buf = [0u8; FRAME];
1522        let builder = PacketBuilder::new(&mut buf).broadcast();
1523        let mut builder = if options.full_source {
1524            builder.source_full(&source_key)
1525        } else {
1526            builder.source_hint(source_key.hint())
1527        };
1528        let repeatable =
1529            Self::frame_is_repeatable(options.flood_hops, options.source_route.as_deref());
1530        if let Some(hops) = options.flood_hops {
1531            builder = builder.flood_hops(hops);
1532        }
1533        if options.trace_route && repeatable {
1534            builder = builder.trace_route();
1535        }
1536        // A route that constrains no hop is not attached: only a repeater
1537        // consuming the final hint may leave an empty SourceRoute option.
1538        if let Some(route) = options
1539            .source_route
1540            .as_ref()
1541            .filter(|route| !route.is_empty())
1542        {
1543            builder = builder.source_route(route.as_slice());
1544        }
1545        if let Some(callsign) = self.operating_policy.operator_callsign {
1546            builder = builder.option(OptionNumber::OperatorCallsign, callsign.as_trimmed_slice());
1547        }
1548        if options.trace_signal && repeatable {
1549            builder = builder.trace_signal();
1550        }
1551        if let Some(region_code) = options.region_code {
1552            builder = builder.region_code(region_code);
1553        }
1554        let frame = builder.payload(payload).build()?;
1555        if frame.len() > self.radio.max_frame_size() {
1556            return Err(SendError::Build(BuildError::BufferTooSmall));
1557        }
1558        let not_before_ms = self.tx_not_before_ms(options);
1559        self.tx_queue
1560            .enqueue_with_state(
1561                TxPriority::Application,
1562                frame,
1563                Some(receipt),
1564                Some(from),
1565                not_before_ms,
1566                0,
1567                0,
1568            )
1569            .map_err(|_| SendError::QueueFull)?;
1570        Ok(receipt)
1571    }
1572
1573    /// Enqueue a broadcast frame for transmission.
1574    pub async fn send_broadcast(
1575        &mut self,
1576        from: LocalIdentityId,
1577        payload: &[u8],
1578        options: &SendOptions,
1579    ) -> Result<SendReceipt, SendError> {
1580        self.queue_broadcast(from, payload, options)
1581    }
1582
1583    /// Enqueues a multicast frame using the configured channel keys.
1584    pub fn queue_multicast(
1585        &mut self,
1586        from: LocalIdentityId,
1587        channel_id: &ChannelId,
1588        payload: &[u8],
1589        options: &SendOptions,
1590    ) -> Result<SendReceipt, SendError> {
1591        self.enforce_send_policy(Some(*channel_id), options, false)?;
1592        // Multicast has no unicast receiver to ack, so an `ack_requested`
1593        // flag carried over from shared `SendOptions` is silently ignored.
1594        // TODO: surface this drop as a debug-level event (see the matching
1595        // TODO on `queue_broadcast`) so callers can detect when their
1596        // requested flags were not applied.
1597
1598        let derived = self
1599            .channels
1600            .lookup_by_id(channel_id)
1601            .next()
1602            .ok_or(SendError::ChannelMissing)?
1603            .derived
1604            .clone();
1605        let keys = PairwiseKeys {
1606            k_enc: derived.k_enc,
1607            k_mic: derived.k_mic,
1608        };
1609        let receipt = self
1610            .identity_mut(from)
1611            .ok_or(SendError::IdentityMissing)?
1612            .next_receipt();
1613        let (source_key, frame_counter) = self.identity_and_advance(from)?;
1614        let salt = self.take_salt(options);
1615        let mut buf = [0u8; FRAME];
1616        let builder = PacketBuilder::new(&mut buf).multicast(*channel_id);
1617        let builder = if options.full_source {
1618            builder.source_full(&source_key)
1619        } else {
1620            builder.source_hint(source_key.hint())
1621        };
1622        let mut builder = builder.frame_counter(frame_counter);
1623        if options.encrypted {
1624            builder = builder.encrypted();
1625        }
1626        builder = builder.mic_size(options.mic_size);
1627        if let Some(salt) = salt {
1628            builder = builder.salt(salt);
1629        }
1630        let repeatable =
1631            Self::frame_is_repeatable(options.flood_hops, options.source_route.as_deref());
1632        if let Some(hops) = options.flood_hops {
1633            builder = builder.flood_hops(hops);
1634        }
1635        if options.trace_route && repeatable {
1636            builder = builder.trace_route();
1637        }
1638        // A route that constrains no hop is not attached: only a repeater
1639        // consuming the final hint may leave an empty SourceRoute option.
1640        if let Some(route) = options
1641            .source_route
1642            .as_ref()
1643            .filter(|route| !route.is_empty())
1644        {
1645            builder = builder.source_route(route.as_slice());
1646        }
1647        if let Some(callsign) = self.operating_policy.operator_callsign {
1648            builder = builder.option(OptionNumber::OperatorCallsign, callsign.as_trimmed_slice());
1649        }
1650        if options.trace_signal && repeatable {
1651            builder = builder.trace_signal();
1652        }
1653        if let Some(region_code) = options.region_code {
1654            builder = builder.region_code(region_code);
1655        }
1656        let mut packet = builder.payload(payload).build()?;
1657        self.crypto.seal_packet(&mut packet, &keys)?;
1658        let not_before_ms = self.tx_not_before_ms(options);
1659        self.enqueue_packet(packet, Some(receipt), Some(from), not_before_ms)?;
1660        Ok(receipt)
1661    }
1662
1663    /// Enqueue a multicast frame for transmission.
1664    pub async fn send_multicast(
1665        &mut self,
1666        from: LocalIdentityId,
1667        channel_id: &ChannelId,
1668        payload: &[u8],
1669        options: &SendOptions,
1670    ) -> Result<SendReceipt, SendError> {
1671        self.queue_multicast(from, channel_id, payload, options)
1672    }
1673
1674    /// Enqueues a MAC ACK frame, using any cached route to `peer_id` when available.
1675    ///
1676    /// `trace_route` mirrors the acknowledged frame: a sender that attached a
1677    /// trace route was discovering a path, and the trace it collected only
1678    /// taught *us* the way back. The matching trace on this ack is what closes
1679    /// the other direction, and is the only thing an ack-only exchange gives
1680    /// the sender to learn a route from.
1681    ///
1682    /// It is honored only when the route attached below gives this ack some
1683    /// way to be repeated. An ack going back to a peer we hear directly
1684    /// carries neither a flood budget nor a source route, so no repeater may
1685    /// touch it and the trace would arrive as empty as it left. The sender
1686    /// reads the direct link off the ack's own shape instead — see
1687    /// [`Mac::learn_route_for_peer`].
1688    ///
1689    /// An ack that does ride through repeaters is tracked like any other
1690    /// repeat-confirmed send: silence where the repeat should be means the
1691    /// first hop never got it, and the retry ladder retransmits. The ladder
1692    /// ends in a silent abandon — the sender's own data retries are the
1693    /// backstop; this just spares them in the common case.
1694    pub fn queue_mac_ack_for_peer(
1695        &mut self,
1696        from: LocalIdentityId,
1697        peer_id: PeerId,
1698        ack_trailer: [u8; 8],
1699        trace_route: bool,
1700    ) -> Result<(), SendError> {
1701        // Settled before the frame is built, because a trace route has to be
1702        // encoded ahead of the route options that decide whether to keep it.
1703        let repeatable = match self
1704            .peer_registry
1705            .get(peer_id)
1706            .and_then(|peer| peer.route.as_ref())
1707        {
1708            // Matching the arms below: a route that constrains no hop is not
1709            // attached, so it leaves nothing to carry the ack either.
1710            Some(CachedRoute::Source(route)) => !route.is_empty(),
1711            Some(CachedRoute::Flood { .. }) => true,
1712            _ => false,
1713        };
1714        let mut buf = [0u8; FRAME];
1715        let mut builder = PacketBuilder::new(&mut buf).mac_ack(ack_trailer);
1716        // Ahead of the route options below: the builder rejects options
1717        // encoded out of ascending number order.
1718        if trace_route && repeatable {
1719            builder = builder.trace_route();
1720        }
1721        if let Some(peer) = self.peer_registry.get(peer_id) {
1722            match peer.route.as_ref() {
1723                Some(CachedRoute::Source(route)) if !route.is_empty() => {
1724                    builder = builder.source_route(route.as_slice());
1725                }
1726                Some(CachedRoute::Flood { hops, regions }) => {
1727                    builder = builder.flood_hops((*hops).clamp(1, MAX_FLOOD_HOPS));
1728                    for region in regions {
1729                        builder = builder.region_code(*region);
1730                    }
1731                }
1732                // Direct, unknown, or a route that constrains no hop: there
1733                // is nothing to attach, and an empty option must not be
1734                // originated.
1735                _ => {}
1736            }
1737        }
1738        let frame = builder.build()?;
1739        if frame.len() > self.radio.max_frame_size() {
1740            return Err(SendError::Build(BuildError::BufferTooSmall));
1741        }
1742        let tracked_receipt = if repeatable {
1743            self.prepare_repeat_confirmed_ack(from, peer_id, frame)
1744        } else {
1745            None
1746        };
1747        if self
1748            .tx_queue
1749            .enqueue(
1750                TxPriority::ImmediateAck,
1751                frame,
1752                tracked_receipt,
1753                tracked_receipt.map(|_| from),
1754            )
1755            .is_err()
1756        {
1757            if let Some(receipt) = tracked_receipt {
1758                let _ = self
1759                    .identity_mut(from)
1760                    .and_then(|slot| slot.remove_pending_ack(&receipt));
1761            }
1762            return Err(SendError::QueueFull);
1763        }
1764        Ok(())
1765    }
1766
1767    /// Track a routed MAC ack so silence — no repeat overheard — retries it.
1768    ///
1769    /// Returns `None`, leaving the ack fire-and-forget, whenever tracking
1770    /// cannot help or cannot be afforded: an unknown peer, a full pending
1771    /// table (an ack is not worth displacing a send anyone is waiting on),
1772    /// or an identical ack already in flight — re-arming a second ladder
1773    /// for the same trailer would just double the retransmissions.
1774    fn prepare_repeat_confirmed_ack(
1775        &mut self,
1776        from: LocalIdentityId,
1777        peer_id: PeerId,
1778        frame: &[u8],
1779    ) -> Option<SendReceipt> {
1780        let peer = self.peer_registry.get(peer_id)?.public_key;
1781        let confirm_key = Self::confirmation_key(frame);
1782        let resend = ResendRecord::try_new(frame, None).ok()?;
1783        let slot = self.identity_mut(from)?;
1784        if confirm_key.is_some()
1785            && slot.pending_acks().any(|(_, pending)| {
1786                Self::confirmation_key(pending.resend.frame.as_slice()) == confirm_key
1787            })
1788        {
1789            return None;
1790        }
1791        let receipt = slot.next_receipt();
1792        slot.try_insert_pending_ack(receipt, PendingAck::repeat_only(peer, resend))
1793            .ok()?;
1794        Some(receipt)
1795    }
1796
1797    /// Enqueues an immediate direct MAC ACK frame.
1798    pub fn queue_mac_ack(&mut self, ack_trailer: [u8; 8]) -> Result<(), SendError> {
1799        let mut buf = [0u8; FRAME];
1800        let frame = PacketBuilder::new(&mut buf).mac_ack(ack_trailer).build()?;
1801        if frame.len() > self.radio.max_frame_size() {
1802            return Err(SendError::Build(BuildError::BufferTooSmall));
1803        }
1804        self.tx_queue
1805            .enqueue(TxPriority::ImmediateAck, frame, None, None)
1806            .map_err(|_| SendError::QueueFull)?;
1807        Ok(())
1808    }
1809
1810    /// Enqueues a unicast frame and optional pending-ACK state.
1811    pub fn queue_unicast(
1812        &mut self,
1813        from: LocalIdentityId,
1814        peer: &PublicKey,
1815        payload: &[u8],
1816        options: &SendOptions,
1817    ) -> Result<Option<SendReceipt>, SendError> {
1818        self.enforce_send_policy(None, options, false)?;
1819        let (peer_id, _) = self
1820            .peer_registry
1821            .lookup_by_key(peer)
1822            .ok_or(SendError::PeerMissing)?;
1823        let pairwise_keys = self
1824            .identity(from)
1825            .ok_or(SendError::IdentityMissing)?
1826            .peer_crypto()
1827            .get(&peer_id)
1828            .ok_or(SendError::PairwiseKeysMissing)?
1829            .pairwise_keys
1830            .clone();
1831        let effective_source_route = self.effective_source_route(peer_id, options);
1832        let effective_flood_hops =
1833            self.effective_flood_hops(peer_id, options, effective_source_route.as_ref());
1834
1835        let (source_key, frame_counter) = self.identity_and_advance(from)?;
1836        let salt = self.take_salt(options);
1837        let mut buf = [0u8; FRAME];
1838        let builder = PacketBuilder::new(&mut buf).unicast(peer.hint());
1839        let builder = if options.full_source {
1840            builder.source_full(&source_key)
1841        } else {
1842            builder.source_hint(source_key.hint())
1843        };
1844        let mut builder = builder.frame_counter(frame_counter);
1845        if options.ack_requested {
1846            builder = builder.ack_requested();
1847        }
1848        if options.encrypted {
1849            builder = builder.encrypted();
1850        }
1851        builder = builder.mic_size(options.mic_size);
1852        if let Some(salt) = salt {
1853            builder = builder.salt(salt);
1854        }
1855        // A caller asking for a trace is asking for a path to be recorded,
1856        // not for an empty option: a frame no repeater may carry has no path
1857        // to record, and the request is dropped rather than honored blind.
1858        let repeatable =
1859            Self::frame_is_repeatable(effective_flood_hops, effective_source_route.as_deref());
1860        if let Some(hops) = effective_flood_hops {
1861            builder = builder.flood_hops(hops);
1862        }
1863        if repeatable
1864            && (options.trace_route
1865                || self.needs_route_discovery(
1866                    peer_id,
1867                    effective_source_route.as_ref(),
1868                    effective_flood_hops,
1869                    options.ack_requested,
1870                ))
1871        {
1872            builder = builder.trace_route();
1873        }
1874        if let Some(route) = effective_source_route.as_ref() {
1875            builder = builder.source_route(route.as_slice());
1876        }
1877        if let Some(callsign) = self.operating_policy.operator_callsign {
1878            builder = builder.option(OptionNumber::OperatorCallsign, callsign.as_trimmed_slice());
1879        }
1880        if options.trace_signal && repeatable {
1881            builder = builder.trace_signal();
1882        }
1883        if let Some(region_code) = options.region_code {
1884            builder = builder.region_code(region_code);
1885        }
1886        let mut packet = builder.payload(payload).build()?;
1887
1888        let receipt = if options.ack_requested {
1889            Some(self.prepare_pending_ack(from, *peer, &packet, &pairwise_keys, options)?)
1890        } else {
1891            None
1892        };
1893
1894        self.crypto.seal_packet(&mut packet, &pairwise_keys)?;
1895        if let Some(receipt) = receipt {
1896            self.refresh_pending_resend(
1897                from,
1898                receipt,
1899                packet.as_bytes(),
1900                effective_source_route
1901                    .as_ref()
1902                    .map(|route| route.as_slice()),
1903                options.flood_hops,
1904            )?;
1905        }
1906        // A non-ACK send that rides through repeaters is tracked under an
1907        // internal receipt so it can retry until a repeat is heard. The caller
1908        // asked for no tracking and sees none.
1909        let tracked_receipt = match receipt {
1910            Some(receipt) => Some(receipt),
1911            None if Self::frame_solicits_repeat(packet.as_bytes()) => {
1912                Some(self.prepare_repeat_confirmed_send(
1913                    from,
1914                    *peer,
1915                    packet.as_bytes(),
1916                    effective_source_route.as_ref(),
1917                    options.flood_hops,
1918                )?)
1919            }
1920            None => None,
1921        };
1922        let not_before_ms = self.tx_not_before_ms(options);
1923        if let Err(err) = self.enqueue_packet(packet, tracked_receipt, Some(from), not_before_ms) {
1924            if let Some(tracked_receipt) = tracked_receipt {
1925                let _ = self
1926                    .identity_mut(from)
1927                    .and_then(|slot| slot.remove_pending_ack(&tracked_receipt));
1928            }
1929            return Err(err);
1930        }
1931        Ok(receipt)
1932    }
1933
1934    /// Enqueue a unicast frame for transmission, deriving secure peer state on first use.
1935    pub async fn send_unicast(
1936        &mut self,
1937        from: LocalIdentityId,
1938        peer: &PublicKey,
1939        payload: &[u8],
1940        options: &SendOptions,
1941    ) -> Result<Option<SendReceipt>, SendError> {
1942        let (peer_id, _) = self
1943            .peer_registry
1944            .lookup_by_key(peer)
1945            .ok_or(SendError::PeerMissing)?;
1946        let _ = self.ensure_peer_crypto(from, peer_id).await?;
1947        self.queue_unicast(from, peer, payload, options)
1948    }
1949
1950    /// Enqueues a blind-unicast frame and optional pending-ACK state.
1951    pub fn queue_blind_unicast(
1952        &mut self,
1953        from: LocalIdentityId,
1954        peer: &PublicKey,
1955        channel_id: &ChannelId,
1956        payload: &[u8],
1957        options: &SendOptions,
1958    ) -> Result<Option<SendReceipt>, SendError> {
1959        let channel_keys = self
1960            .channels
1961            .lookup_by_id(channel_id)
1962            .next()
1963            .ok_or(SendError::ChannelMissing)?
1964            .derived
1965            .clone();
1966        self.queue_blind_unicast_with_keys(from, peer, &channel_keys, payload, options)
1967    }
1968
1969    /// Enqueues a blind-unicast frame sealed under channel keys the caller
1970    /// already holds.
1971    ///
1972    /// A reply takes this path rather than [`Self::queue_blind_unicast`]:
1973    /// looking the channel up again by its two-byte identifier would pick the
1974    /// first key that derives to it, which need not be the key that opened the
1975    /// frame being answered.
1976    fn queue_blind_unicast_with_keys(
1977        &mut self,
1978        from: LocalIdentityId,
1979        peer: &PublicKey,
1980        channel_keys: &DerivedChannelKeys,
1981        payload: &[u8],
1982        options: &SendOptions,
1983    ) -> Result<Option<SendReceipt>, SendError> {
1984        let channel_id = channel_keys.channel_id;
1985        self.enforce_send_policy(Some(channel_id), options, true)?;
1986        let (peer_id, _) = self
1987            .peer_registry
1988            .lookup_by_key(peer)
1989            .ok_or(SendError::PeerMissing)?;
1990        let pairwise_keys = self
1991            .identity(from)
1992            .ok_or(SendError::IdentityMissing)?
1993            .peer_crypto()
1994            .get(&peer_id)
1995            .ok_or(SendError::PairwiseKeysMissing)?
1996            .pairwise_keys
1997            .clone();
1998        let blind_keys = self.crypto.derive_blind_keys(&pairwise_keys, channel_keys);
1999        let effective_source_route = self.effective_source_route(peer_id, options);
2000        let effective_flood_hops =
2001            self.effective_flood_hops(peer_id, options, effective_source_route.as_ref());
2002
2003        let (source_key, frame_counter) = self.identity_and_advance(from)?;
2004        let salt = self.take_salt(options);
2005        let mut buf = [0u8; FRAME];
2006        let builder = PacketBuilder::new(&mut buf).blind_unicast(channel_id, peer.hint());
2007        let builder = if options.full_source {
2008            builder.source_full(&source_key)
2009        } else {
2010            builder.source_hint(source_key.hint())
2011        };
2012        let mut builder = builder.frame_counter(frame_counter);
2013        if options.ack_requested {
2014            builder = builder.ack_requested();
2015        }
2016        if !options.encrypted {
2017            builder = builder.unencrypted();
2018        }
2019        builder = builder.mic_size(options.mic_size);
2020        if let Some(salt) = salt {
2021            builder = builder.salt(salt);
2022        }
2023        // Same rule as the plain unicast above: no repeater may carry an
2024        // unrepeatable frame, so neither trace option has anything to collect.
2025        let repeatable =
2026            Self::frame_is_repeatable(effective_flood_hops, effective_source_route.as_deref());
2027        if let Some(hops) = effective_flood_hops {
2028            builder = builder.flood_hops(hops);
2029        }
2030        if repeatable
2031            && (options.trace_route
2032                || self.needs_route_discovery(
2033                    peer_id,
2034                    effective_source_route.as_ref(),
2035                    effective_flood_hops,
2036                    options.ack_requested,
2037                ))
2038        {
2039            builder = builder.trace_route();
2040        }
2041        if let Some(route) = effective_source_route.as_ref() {
2042            builder = builder.source_route(route.as_slice());
2043        }
2044        if let Some(callsign) = self.operating_policy.operator_callsign {
2045            builder = builder.option(OptionNumber::OperatorCallsign, callsign.as_trimmed_slice());
2046        }
2047        if options.trace_signal && repeatable {
2048            builder = builder.trace_signal();
2049        }
2050        if let Some(region_code) = options.region_code {
2051            builder = builder.region_code(region_code);
2052        }
2053        let mut packet = builder.payload(payload).build()?;
2054
2055        let receipt = if options.ack_requested {
2056            Some(self.prepare_pending_ack(from, *peer, &packet, &blind_keys, options)?)
2057        } else {
2058            None
2059        };
2060
2061        self.crypto
2062            .seal_blind_packet(&mut packet, &blind_keys, channel_keys)
2063            .map_err(SendError::Crypto)?;
2064        if let Some(receipt) = receipt {
2065            self.refresh_pending_resend(
2066                from,
2067                receipt,
2068                packet.as_bytes(),
2069                effective_source_route
2070                    .as_ref()
2071                    .map(|route| route.as_slice()),
2072                options.flood_hops,
2073            )?;
2074        }
2075        // A non-ACK send that rides through repeaters is tracked under an
2076        // internal receipt so it can retry until a repeat is heard. The caller
2077        // asked for no tracking and sees none.
2078        let tracked_receipt = match receipt {
2079            Some(receipt) => Some(receipt),
2080            None if Self::frame_solicits_repeat(packet.as_bytes()) => {
2081                Some(self.prepare_repeat_confirmed_send(
2082                    from,
2083                    *peer,
2084                    packet.as_bytes(),
2085                    effective_source_route.as_ref(),
2086                    options.flood_hops,
2087                )?)
2088            }
2089            None => None,
2090        };
2091        let not_before_ms = self.tx_not_before_ms(options);
2092        if let Err(err) = self.enqueue_packet(packet, tracked_receipt, Some(from), not_before_ms) {
2093            if let Some(tracked_receipt) = tracked_receipt {
2094                let _ = self
2095                    .identity_mut(from)
2096                    .and_then(|slot| slot.remove_pending_ack(&tracked_receipt));
2097            }
2098            return Err(err);
2099        }
2100        Ok(receipt)
2101    }
2102
2103    /// Enqueue a blind-unicast frame for transmission, deriving secure peer state on first use.
2104    pub async fn send_blind_unicast(
2105        &mut self,
2106        from: LocalIdentityId,
2107        peer: &PublicKey,
2108        channel_id: &ChannelId,
2109        payload: &[u8],
2110        options: &SendOptions,
2111    ) -> Result<Option<SendReceipt>, SendError> {
2112        let (peer_id, _) = self
2113            .peer_registry
2114            .lookup_by_key(peer)
2115            .ok_or(SendError::PeerMissing)?;
2116        let _ = self.ensure_peer_crypto(from, peer_id).await?;
2117        self.queue_blind_unicast(from, peer, channel_id, payload, options)
2118    }
2119
2120    /// Send a response over the carriage its request arrived on.
2121    ///
2122    /// The one place the MAC decides how an answer travels, so that answering
2123    /// never widens the audience the question had.
2124    async fn send_response(
2125        &mut self,
2126        from: LocalIdentityId,
2127        peer: &PublicKey,
2128        carriage: ReplyCarriage<'_>,
2129        payload: &[u8],
2130        options: &SendOptions,
2131    ) -> Result<Option<SendReceipt>, SendError> {
2132        match carriage {
2133            ReplyCarriage::Unicast => self.send_unicast(from, peer, payload, options).await,
2134            ReplyCarriage::Blind(channel_keys) => {
2135                let (peer_id, _) = self
2136                    .peer_registry
2137                    .lookup_by_key(peer)
2138                    .ok_or(SendError::PeerMissing)?;
2139                let _ = self.ensure_peer_crypto(from, peer_id).await?;
2140                self.queue_blind_unicast_with_keys(from, peer, channel_keys, payload, options)
2141            }
2142        }
2143    }
2144
2145    /// Transmit the next eligible queued frame, if any.
2146    ///
2147    /// While a post-transmit forwarding listen window is active, only immediate MAC
2148    /// ACK traffic is permitted to bypass the listen state. Forwarded sends arm a new
2149    /// listen window after the radio transmit completes. Non-immediate traffic honors
2150    /// queued CAD backoff state and gives up after the configured maximum number of
2151    /// CAD attempts.
2152    pub async fn transmit_next(
2153        &mut self,
2154        on_event: &mut impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2155    ) -> Result<Option<SendReceipt>, MacError<<P::Radio as Radio>::Error>> {
2156        self.expire_post_tx_listen_if_needed();
2157        let Some(queued) = self.tx_queue.pop_next() else {
2158            return Ok(None);
2159        };
2160        let now_ms = self.clock.now_ms();
2161
2162        if queued.not_before_ms > now_ms {
2163            self.requeue_tx(&queued).map_err(|_| MacError::QueueFull)?;
2164            return Ok(None);
2165        }
2166
2167        if self.post_tx_listen.is_some() && queued.priority != TxPriority::ImmediateAck {
2168            self.requeue_tx(&queued).map_err(|_| MacError::QueueFull)?;
2169            return Ok(None);
2170        }
2171
2172        let receipt = queued.receipt;
2173        let identity_id = queued.identity_id;
2174
2175        // A retransmission exists only to serve a send still waiting on an
2176        // outcome, and that send may have reached one while the frame sat in
2177        // the queue — acknowledged, confirmed by an overheard repeat, timed
2178        // out, or cancelled. All of those drop the pending entry, so its
2179        // absence is what makes the frame unwanted. Only retries are judged
2180        // this way: a first transmission carries a receipt for reporting
2181        // (broadcast and multicast do so with no pending entry at all) and is
2182        // always sent.
2183        if queued.priority == TxPriority::Retry
2184            && let (Some(receipt), Some(identity_id)) = (receipt, identity_id)
2185            && self
2186                .identity(identity_id)
2187                .map(|slot| slot.pending_ack(&receipt).is_none())
2188                .unwrap_or(true)
2189        {
2190            return Ok(None);
2191        }
2192
2193        let tx_options = if queued.priority == TxPriority::ImmediateAck {
2194            // Immediate ACK: the channel was clear when the packet ended, so
2195            // transmit without CAD (see channel-access.md § Immediate ACK).
2196            TxOptions {
2197                cad: umsh_hal::CadPolicy::Skip,
2198            }
2199        } else {
2200            TxOptions {
2201                cad: umsh_hal::CadPolicy::Gate,
2202            }
2203        };
2204        match self
2205            .radio
2206            .transmit(queued.frame.as_slice(), tx_options)
2207            .await
2208        {
2209            Ok(()) => {}
2210            Err(TxError::CadTimeout) => {
2211                let next_attempt = queued.cad_attempts.saturating_add(1);
2212                if next_attempt >= MAX_CAD_ATTEMPTS {
2213                    // Counted whether or not anyone is told: a forwarded
2214                    // frame is dropped without an event, and a node
2215                    // abandoning every forward is exactly the condition
2216                    // this tally exists to make visible.
2217                    MacCounters::bump(&mut self.counters.tx_abandoned);
2218                    // Drop with accounting: locally-originated frames report
2219                    // the abandonment to the owning identity (a send that
2220                    // never aired will see no AckReceived/AckTimeout, so this
2221                    // is its terminal state). Forwarded frames (no identity)
2222                    // are best-effort and dropped without an event. A
2223                    // *retransmission* that lost CAD is neither: its send has
2224                    // aired before and its deadlines still stand, so the
2225                    // attempt is dropped quietly and the timers judge the
2226                    // send.
2227                    if let Some(identity_id) = identity_id
2228                        && queued.priority != TxPriority::Retry
2229                    {
2230                        // Terminal means the tracking entry goes too; left
2231                        // behind in `Queued` it would outlive every timer,
2232                        // holding its slot until the identity is removed.
2233                        if let Some(receipt) = receipt
2234                            && let Some(slot) = self.identity_mut(identity_id)
2235                        {
2236                            let _ = slot.pending_acks.remove(&receipt);
2237                        }
2238                        on_event(
2239                            identity_id,
2240                            crate::MacEventRef::TxAbandoned {
2241                                identity_id,
2242                                receipt,
2243                            },
2244                        );
2245                    }
2246                    return Ok(None);
2247                }
2248                // Uniform over [T_frame/40, T_frame/4] (channel-access.md
2249                // § Backoff Procedure): long enough that a frame already on the
2250                // air has a chance to finish, short enough that sixteen
2251                // attempts still fit inside the sender's patience.
2252                let t_frame_ms = self.radio.t_frame_ms();
2253                let backoff_lo_ms = t_frame_ms / 40;
2254                let backoff_hi_ms = (t_frame_ms / 4).max(backoff_lo_ms);
2255                let backoff_ms = u64::from(
2256                    backoff_lo_ms.saturating_add(
2257                        self.rng
2258                            .random_range(..(backoff_hi_ms - backoff_lo_ms).saturating_add(1)),
2259                    ),
2260                );
2261                self.tx_queue
2262                    .enqueue_with_state(
2263                        queued.priority,
2264                        queued.frame.as_slice(),
2265                        queued.receipt,
2266                        queued.identity_id,
2267                        now_ms.saturating_add(backoff_ms),
2268                        next_attempt,
2269                        queued.forward_deferrals,
2270                    )
2271                    .map_err(|_| MacError::QueueFull)?;
2272                return Ok(None);
2273            }
2274            Err(error) => return Err(MacError::Transmit(error)),
2275        }
2276        // Counted before the event, which only locally-originated frames
2277        // get: a repeater's whole output would otherwise be invisible.
2278        MacCounters::bump(&mut self.counters.tx_frames);
2279        if let Some(identity_id) = identity_id {
2280            on_event(
2281                identity_id,
2282                crate::MacEventRef::Transmitted {
2283                    identity_id,
2284                    receipt,
2285                    wire_bytes: queued.frame.as_slice(),
2286                },
2287            );
2288        }
2289        if let Some(receipt) = receipt {
2290            self.note_transmitted_tracked(receipt, queued.frame.as_slice());
2291        }
2292        Ok(receipt)
2293    }
2294
2295    /// Keep transmitting until the queue is empty.
2296    ///
2297    /// Progress stops when CAD keeps reporting busy, when a post-transmit listen window blocks
2298    /// normal traffic, or when the queue is otherwise unable to shrink further in the current cycle.
2299    pub async fn drain_tx_queue(
2300        &mut self,
2301        on_event: &mut impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2302    ) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
2303        while !self.tx_queue.is_empty() {
2304            let queue_len = self.tx_queue.len();
2305            let _ = self.transmit_next(on_event).await?;
2306            if self.tx_queue.len() >= queue_len {
2307                break;
2308            }
2309        }
2310        Ok(())
2311    }
2312
2313    /// Runs one coordinator cycle over the current MAC state.
2314    ///
2315    /// The cycle performs four ordered phases:
2316    ///
2317    /// 1. Drain any queued transmit work.
2318    /// 2. Receive and process at most one inbound frame.
2319    /// 3. Drain any immediate ACK generated during receive handling.
2320    /// 4. Service pending ACK timers and emit timeout events.
2321    ///
2322    /// The callback may be invoked zero or more times depending on what the
2323    /// receive and timeout phases accept or resolve.
2324    /// Service one MAC coordinator cycle.
2325    pub async fn poll_cycle(
2326        &mut self,
2327        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2328    ) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
2329        self.drain_tx_queue(&mut on_event).await?;
2330        if self.post_tx_listen.is_some() {
2331            self.service_post_tx_listen(&mut on_event).await?;
2332        } else {
2333            let _ = self.receive_one(&mut on_event).await?;
2334        }
2335        self.drain_tx_queue(&mut on_event).await?;
2336        self.service_pending_ack_timeouts(&mut on_event)
2337            .map_err(|_| MacError::QueueFull)?;
2338        Ok(())
2339    }
2340
2341    /// Compute the earliest deadline across all coordinator timers.
2342    ///
2343    /// Returns `None` when there are no pending timers.  The returned value
2344    /// covers pending ACK deadlines (both `ack_deadline_ms` and forwarding
2345    /// `confirm_deadline_ms`), the post-transmit listen window, and deferred
2346    /// transmit-queue entries.
2347    pub fn earliest_deadline_ms(&self) -> Option<u64> {
2348        let mut earliest: Option<u64> = None;
2349
2350        if let Some(listen) = &self.post_tx_listen {
2351            earliest =
2352                Some(earliest.map_or(listen.deadline_ms, |e: u64| e.min(listen.deadline_ms)));
2353        }
2354
2355        for slot in self.identities.iter().filter_map(|s| s.as_ref()) {
2356            for (_, pending) in slot.pending_acks.iter() {
2357                if !matches!(pending.state, crate::AckState::Queued { .. }) {
2358                    earliest = Some(earliest.map_or(pending.ack_deadline_ms, |e: u64| {
2359                        e.min(pending.ack_deadline_ms)
2360                    }));
2361                }
2362                // A confirm deadline is only a wake-up if its expiry can
2363                // still queue a retry. With the retry budget spent it would
2364                // report a moment that services nothing, and a deadline in
2365                // the past that never clears pins the caller's timer loop.
2366                if let crate::AckState::AwaitingForward {
2367                    confirm_deadline_ms,
2368                } = pending.state
2369                    && pending.retries < MAX_FORWARD_RETRIES
2370                {
2371                    earliest = Some(
2372                        earliest.map_or(confirm_deadline_ms, |e: u64| e.min(confirm_deadline_ms)),
2373                    );
2374                }
2375            }
2376        }
2377
2378        if let Some(nb) = self.tx_queue.earliest_not_before_ms() {
2379            earliest = Some(earliest.map_or(nb, |e: u64| e.min(nb)));
2380        }
2381
2382        earliest
2383    }
2384
2385    /// Run the coordinator's event loop until at least one event is delivered
2386    /// or a timer-driven action (retransmit, timeout) is processed.
2387    ///
2388    /// Unlike [`poll_cycle`](Self::poll_cycle), this method properly awaits the
2389    /// radio and timer deadlines instead of returning immediately when nothing
2390    /// is ready.  Callers can use `tokio::select!` (or equivalent) to multiplex
2391    /// user input alongside MAC events:
2392    ///
2393    /// ```ignore
2394    /// loop {
2395    ///     tokio::select! {
2396    ///         line = stdin.next_line() => { /* handle input */ }
2397    ///         result = mac.next_event(|id, event| { /* handle event */ }) => {
2398    ///             result?;
2399    ///         }
2400    ///     }
2401    /// }
2402    /// ```
2403    pub async fn next_event(
2404        &mut self,
2405        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2406    ) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
2407        loop {
2408            // Phase 1: Drain ready transmit work.
2409            self.drain_tx_queue(&mut on_event).await?;
2410
2411            // Phase 2: Wait for a radio frame or the earliest timer deadline.
2412            let mut buf = [0u8; FRAME];
2413            let reason = poll_fn(|cx| self.poll_wait_for_wake(cx, &mut buf))
2414                .await
2415                .map_err(MacError::Radio)?;
2416
2417            // Phases 3-5: process the wake, drain any follow-up sends, and run timeouts.
2418            self.process_wake_reason(reason, &mut buf, &mut on_event)
2419                .await?;
2420
2421            // Flush any pending TX or RX counter boundaries to durable storage.
2422            // Errors are intentionally ignored — persistence is best-effort and
2423            // must not block the radio event loop.
2424            let _ = self.service_counter_persistence().await;
2425            let _ = self.service_rx_counter_persistence().await;
2426
2427            // If the tx_queue has new work (e.g. retransmits just enqueued),
2428            // loop back to drain it before waiting again.
2429            if !self.tx_queue.is_empty() {
2430                continue;
2431            }
2432
2433            return Ok(());
2434        }
2435    }
2436
2437    /// Register radio/timer wakers and report what has become ready.
2438    ///
2439    /// This is Phase 2 of [`next_event`](Self::next_event) exposed as a sync
2440    /// poll method so that callers sharing the coordinator through an
2441    /// `AsyncRefCell` can release the exclusive borrow between polls. The
2442    /// caller-provided `buf` is populated with the received frame when the
2443    /// return value is [`WakeReason::Received`].
2444    pub fn poll_wait_for_wake(
2445        &mut self,
2446        cx: &mut core::task::Context<'_>,
2447        buf: &mut [u8; FRAME],
2448    ) -> Poll<Result<WakeReason, <P::Radio as Radio>::Error>> {
2449        match self.radio.poll_receive(cx, buf) {
2450            Poll::Ready(Ok(rx)) => return Poll::Ready(Ok(WakeReason::Received(rx))),
2451            Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
2452            Poll::Pending => {}
2453        }
2454
2455        let now_ms = self.clock.now_ms();
2456        if let Some(deadline) = self.earliest_deadline_ms() {
2457            if now_ms >= deadline {
2458                return Poll::Ready(Ok(WakeReason::TimerExpired));
2459            }
2460            // A `Ready` here means the deadline elapsed between the `now_ms`
2461            // snapshot above and this poll. Treat it as a timer expiry so the
2462            // deadline is never silently dropped; a `Pending` result must have
2463            // arranged a wake (a registered timer, or the default impl's
2464            // immediate self-wake busy-poll).
2465            if self.clock.poll_delay_until(cx, deadline).is_ready() {
2466                return Poll::Ready(Ok(WakeReason::TimerExpired));
2467            }
2468        }
2469
2470        // Ready queue entries are only actionable when they could actually
2471        // transmit: during a post-transmit listen window only immediate-ACK
2472        // frames may go out, and the window's own expiry is already covered
2473        // by `earliest_deadline_ms` above. Reporting a blocked-but-ready
2474        // frame here would spin this poll hot for the whole window —
2475        // starving every other task sharing the executor — since the drain
2476        // it triggers requeues the frame without progress.
2477        if self.tx_queue.has_ready(now_ms)
2478            && (self.post_tx_listen.is_none() || self.tx_queue.has_ready_immediate_ack(now_ms))
2479        {
2480            return Poll::Ready(Ok(WakeReason::TimerExpired));
2481        }
2482
2483        Poll::Pending
2484    }
2485
2486    /// Run Phases 3-5 after [`poll_wait_for_wake`](Self::poll_wait_for_wake)
2487    /// has reported a wake reason: process the received frame (if any),
2488    /// drain immediate ACKs, and service pending ACK timeouts.
2489    ///
2490    /// # Precondition
2491    ///
2492    /// When `reason` is [`WakeReason::Received`], `buf` **must** be the same
2493    /// buffer — containing the same bytes — that was passed to the matching
2494    /// [`poll_wait_for_wake`](Self::poll_wait_for_wake) call. The received
2495    /// frame bytes live in `buf[..rx.len]`; `reason` only carries the
2496    /// accompanying metadata (`RxInfo`). Passing a different or reinitialized
2497    /// buffer will cause the received frame to be silently discarded and the
2498    /// contents of `buf` to be misinterpreted as a MAC frame.
2499    ///
2500    /// This precondition only matters when using the split-phase API for
2501    /// [`AsyncRefCell`](umsh_sync::AsyncRefCell) sharing. When driving the
2502    /// coordinator through [`next_event`](Self::next_event) the buffer is a
2503    /// local stack variable and the pairing is guaranteed automatically.
2504    pub async fn process_wake_reason(
2505        &mut self,
2506        reason: WakeReason,
2507        buf: &mut [u8; FRAME],
2508        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2509    ) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
2510        match reason {
2511            WakeReason::Received(rx) => {
2512                let frame_len = rx.len.min(buf.len());
2513                let _ = self
2514                    .process_received_frame(buf, frame_len, &rx, &mut on_event)
2515                    .await;
2516            }
2517            WakeReason::TimerExpired => {}
2518        }
2519
2520        self.drain_tx_queue(&mut on_event).await?;
2521
2522        self.service_pending_ack_timeouts(&mut on_event)
2523            .map_err(|_| MacError::QueueFull)?;
2524
2525        Ok(())
2526    }
2527
2528    /// Drive the coordinator forever, invoking `on_event` for each delivered event.
2529    ///
2530    /// This is the preferred long-lived run loop for standalone MAC-driven tasks such as
2531    /// repeaters or dedicated radio services. Unlike manually calling
2532    /// [`poll_cycle`](Self::poll_cycle) in a loop, `run` keeps the wake/sleep policy inside
2533    /// the coordinator by delegating to [`next_event`](Self::next_event), which already
2534    /// waits for radio activity and protocol deadlines.
2535    pub async fn run(
2536        &mut self,
2537        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2538    ) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
2539        loop {
2540            self.next_event(&mut on_event).await?;
2541        }
2542    }
2543
2544    /// Drive the coordinator forever while ignoring emitted events.
2545    ///
2546    /// Useful for standalone repeaters or bridge tasks that do not need to observe inbound
2547    /// deliveries directly but still need the coordinator to service forwarding, ACKs, and
2548    /// retransmissions without an app-owned polling loop.
2549    pub async fn run_quiet(&mut self) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
2550        self.run(|_, _| {}).await
2551    }
2552
2553    /// Process a received frame, dispatching events through `on_event`.
2554    ///
2555    /// This is the shared implementation used by both [`receive_one`](Self::receive_one)
2556    /// and [`next_event`](Self::next_event).  Returns `true` when the frame
2557    /// produced at least one event or side-effect.
2558    ///
2559    /// Every reception passes through here exactly once, which is what
2560    /// makes it the place to tally them: the `rx_frames`/`rx_accepted`
2561    /// pair counts one radio reception and whether anything came of it.
2562    pub async fn process_received_frame(
2563        &mut self,
2564        buf: &mut [u8; FRAME],
2565        frame_len: usize,
2566        rx: &RxInfo,
2567        on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2568    ) -> bool {
2569        MacCounters::bump(&mut self.counters.rx_frames);
2570        self.note_transmitter_observation(&buf[..frame_len], rx);
2571        let handled = self
2572            .process_received_frame_inner(buf, frame_len, rx, on_event)
2573            .await;
2574        if handled {
2575            MacCounters::bump(&mut self.counters.rx_accepted);
2576        }
2577        handled
2578    }
2579
2580    async fn process_received_frame_inner(
2581        &mut self,
2582        buf: &mut [u8; FRAME],
2583        frame_len: usize,
2584        rx: &RxInfo,
2585        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2586    ) -> bool {
2587        let received_at_ms = self.clock.now_ms();
2588        let mut current_len = frame_len;
2589        let mut current_rx = RxInfo {
2590            len: frame_len,
2591            rssi: rx.rssi,
2592            snr: rx.snr,
2593            lqi: rx.lqi,
2594            origin: rx.origin,
2595        };
2596        let mut current_received_at_ms = received_at_ms;
2597        let mut handled_any = false;
2598
2599        loop {
2600            let Ok(header) = PacketHeader::parse(&buf[..current_len]) else {
2601                return handled_any;
2602            };
2603            // A piggy-backed Ack MIC option is read here, before any
2604            // decryption, because it is aimed at whoever happens to be
2605            // carrying the acknowledged packet — not at this frame's
2606            // addressee.
2607            self.cancel_forwards_for_ack_mic_option(&buf[..current_len], &header);
2608            let forwarding_confirmed = if let Some((identity_id, receipt)) =
2609                self.observe_forwarding_confirmation(&buf[..current_len])
2610            {
2611                let hint = match header.source {
2612                    SourceAddrRef::Hint(h) => Some(RouterHint([h.0[0], h.0[1]])),
2613                    SourceAddrRef::FullKeyAt { offset } => {
2614                        let mut key_bytes = [0u8; 32];
2615                        key_bytes.copy_from_slice(&buf[offset..offset + 32]);
2616                        let h = PublicKey(key_bytes).hint();
2617                        Some(RouterHint([h.0[0], h.0[1]]))
2618                    }
2619                    _ => None,
2620                };
2621                on_event(
2622                    identity_id,
2623                    crate::MacEventRef::Forwarded {
2624                        identity_id,
2625                        receipt,
2626                        hint,
2627                    },
2628                );
2629                true
2630            } else {
2631                false
2632            };
2633
2634            let (handled, replay_target) = match header.packet_type() {
2635                PacketType::Broadcast => (
2636                    self.process_broadcast(
2637                        buf,
2638                        current_len,
2639                        &header,
2640                        &current_rx,
2641                        current_received_at_ms,
2642                        &mut on_event,
2643                    ),
2644                    None,
2645                ),
2646                PacketType::MacAck => (
2647                    self.process_mac_ack(
2648                        buf,
2649                        current_len,
2650                        &header,
2651                        &current_rx,
2652                        forwarding_confirmed,
2653                        &mut on_event,
2654                    ),
2655                    None,
2656                ),
2657                PacketType::Unicast | PacketType::UnicastAckReq => {
2658                    self.process_unicast(
2659                        buf,
2660                        current_len,
2661                        &header,
2662                        &current_rx,
2663                        current_received_at_ms,
2664                        forwarding_confirmed,
2665                        &mut on_event,
2666                    )
2667                    .await
2668                }
2669                PacketType::Multicast => (
2670                    self.process_multicast(
2671                        buf,
2672                        current_len,
2673                        &header,
2674                        &current_rx,
2675                        current_received_at_ms,
2676                        forwarding_confirmed,
2677                        &mut on_event,
2678                    ),
2679                    None,
2680                ),
2681                PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {
2682                    self.process_blind_unicast(
2683                        buf,
2684                        current_len,
2685                        &header,
2686                        &current_rx,
2687                        current_received_at_ms,
2688                        forwarding_confirmed,
2689                        &mut on_event,
2690                    )
2691                    .await
2692                }
2693                PacketType::Reserved5 => (false, None),
2694            };
2695            handled_any |= handled;
2696
2697            let Some((local_id, peer_id)) = replay_target else {
2698                return handled_any;
2699            };
2700            let Some(deferred) = self.take_deferred_counter_resync_frame(local_id, peer_id) else {
2701                return handled_any;
2702            };
2703            current_len = deferred.frame.len();
2704            buf[..current_len].copy_from_slice(deferred.frame.as_slice());
2705            current_rx = RxInfo {
2706                len: current_len,
2707                rssi: deferred.rssi,
2708                snr: deferred.snr,
2709                lqi: deferred.lqi,
2710                origin: deferred.origin,
2711            };
2712            current_received_at_ms = deferred.received_at_ms;
2713        }
2714    }
2715
2716    fn process_broadcast(
2717        &mut self,
2718        buf: &[u8; FRAME],
2719        frame_len: usize,
2720        header: &PacketHeader,
2721        rx: &RxInfo,
2722        received_at_ms: u64,
2723        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2724    ) -> bool {
2725        let Some((from_hint, from_key)) = Self::resolve_broadcast_source(&buf[..frame_len], header)
2726        else {
2727            return false;
2728        };
2729        if !Self::payload_is_allowed(header.packet_type(), &buf[header.body_range.clone()]) {
2730            return false;
2731        }
2732        let mut delivered = false;
2733        for (index, slot) in self.identities.iter().enumerate() {
2734            if slot.is_none() {
2735                continue;
2736            }
2737            delivered = true;
2738            on_event(
2739                LocalIdentityId(index as u8),
2740                crate::MacEventRef::Received(crate::ReceivedPacketRef::new(
2741                    &buf[..frame_len],
2742                    &buf[header.body_range.clone()],
2743                    header.clone(),
2744                    ParsedOptions::extract(&buf[..frame_len], header.options_range.clone())
2745                        .unwrap_or_default(),
2746                    from_key,
2747                    Some(from_hint),
2748                    false,
2749                    None,
2750                    rx_metadata(rx, received_at_ms),
2751                )),
2752            );
2753        }
2754        // Broadcast delivery does not consume the packet. Broadcast remains a
2755        // routable mesh packet and may still be forwarded by a repeater after
2756        // local delivery.
2757        let forwarded = self.maybe_forward_received(&buf[..frame_len], header, rx, false);
2758        delivered || forwarded
2759    }
2760
2761    fn process_mac_ack(
2762        &mut self,
2763        buf: &[u8; FRAME],
2764        frame_len: usize,
2765        header: &PacketHeader,
2766        rx: &RxInfo,
2767        forwarding_confirmed: bool,
2768        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2769    ) -> bool {
2770        // MAC acks carry no destination hint. Correlation is by the 8-byte
2771        // ack trailer (`ack_mic || ack_tag`): the public `ack_mic` half links
2772        // the ack to an outstanding request, and the keyed `ack_tag` half
2773        // authenticates it. A colliding `ack_mic` from an unrelated exchange
2774        // fails the full-trailer comparison and falls through to forwarding.
2775        if header.mic_range.len() != 8 {
2776            return forwarding_confirmed
2777                || self.maybe_forward_received(&buf[..frame_len], header, rx, false);
2778        }
2779        let mut ack_trailer = [0u8; 8];
2780        ack_trailer.copy_from_slice(&buf[header.mic_range.clone()]);
2781        // Independent of whether this node is the ack's addressee: a node can
2782        // be the origin of one exchange and a repeater for another, and the
2783        // queued forward it may be holding is not this ack's business to
2784        // match against.
2785        self.cancel_forwards_for_ack_mic(&ack_trailer[..4]);
2786        if let Some(target_peer) = self.peer_for_ack_trailer(&ack_trailer)
2787            && let Some((identity_id, receipt)) = self.complete_ack(&target_peer, &ack_trailer)
2788        {
2789            // An ack is a packet the peer sent us, and route learning applies
2790            // to it like any other. It is also the only packet an ack-only
2791            // exchange produces, so without this a sender that never receives
2792            // application traffic back from a peer never learns a route to it
2793            // — the trace the peer mirrored onto the ack would arrive and be
2794            // discarded.
2795            if let Some((peer_id, _)) = self.peer_registry.lookup_by_key(&target_peer) {
2796                self.learn_route_for_peer(peer_id, &buf[..frame_len], header);
2797            }
2798            on_event(
2799                identity_id,
2800                crate::MacEventRef::AckReceived {
2801                    peer: target_peer,
2802                    receipt,
2803                },
2804            );
2805            return true;
2806        }
2807        forwarding_confirmed || self.maybe_forward_received(&buf[..frame_len], header, rx, false)
2808    }
2809
2810    async fn process_unicast(
2811        &mut self,
2812        buf: &mut [u8; FRAME],
2813        frame_len: usize,
2814        header: &PacketHeader,
2815        rx: &RxInfo,
2816        received_at_ms: u64,
2817        forwarding_confirmed: bool,
2818        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
2819    ) -> (bool, Option<(LocalIdentityId, PeerId)>) {
2820        let mut original = [0u8; FRAME];
2821        original[..frame_len].copy_from_slice(&buf[..frame_len]);
2822        let mut replay_target = None;
2823        let handled = if let Some(local_id) = self.find_local_identity_for_dst(header.dst) {
2824            let mut handled = false;
2825            for (peer_id, peer_key) in
2826                self.resolve_source_peer_candidates(&buf[..frame_len], header)
2827            {
2828                let Ok(keys) = self.ensure_peer_crypto(local_id, peer_id).await else {
2829                    continue;
2830                };
2831                let Ok(body_range) = self
2832                    .crypto
2833                    .open_packet(&mut buf[..frame_len], header, &keys)
2834                else {
2835                    continue;
2836                };
2837                let payload = &buf[body_range.clone()];
2838                if !Self::payload_is_allowed(header.packet_type(), payload) {
2839                    continue;
2840                }
2841                if header.ack_requested()
2842                    && self.should_emit_destination_ack(&buf[..frame_len], header)
2843                    && self.note_acknowledgeable_unicast_duplicate(
2844                        local_id,
2845                        peer_id,
2846                        header,
2847                        &buf[..frame_len],
2848                    )
2849                {
2850                    // Before the ack is composed, because the ack is built
2851                    // from whatever route this teaches. A retransmission is
2852                    // the one frame carrying route information we did not
2853                    // already have: the sender reached the retry ladder
2854                    // precisely because its first attempt went unanswered,
2855                    // and a route-recovery retry attaches a fresh trace to
2856                    // say so. Composing the re-ack from the stale cache
2857                    // instead throws that away and re-sends the same
2858                    // unroutable frame the sender is retrying against.
2859                    self.learn_route_for_peer(peer_id, &buf[..frame_len], header);
2860                    let ack_trailer = self.compute_received_ack_trailer(
2861                        &buf[..frame_len],
2862                        header,
2863                        body_range.clone(),
2864                        &keys,
2865                    );
2866                    let trace_route = Self::frame_requests_trace_route(&buf[..frame_len], header);
2867                    self.queue_mac_ack_for_peer(local_id, peer_id, ack_trailer, trace_route)
2868                        .ok();
2869                    handled = true;
2870                    break;
2871                }
2872                match self.unicast_replay_verdict(local_id, peer_id, header, &buf[..frame_len]) {
2873                    Some(ReplayVerdict::Accept) => {
2874                        let _ = self.accept_unicast_replay(
2875                            local_id,
2876                            peer_id,
2877                            header,
2878                            &buf[..frame_len],
2879                        );
2880                    }
2881                    Some(ReplayVerdict::OutOfWindow | ReplayVerdict::Stale) => {
2882                        if self.try_accept_counter_resync_response(
2883                            local_id,
2884                            peer_id,
2885                            header,
2886                            &buf[..frame_len],
2887                            payload,
2888                        ) {
2889                            replay_target = Some((local_id, peer_id));
2890                        } else {
2891                            // Only initiate a counter resync if the backward
2892                            // gap is large enough that out-of-order delivery
2893                            // can't explain it. Small gaps (handful of packets)
2894                            // are silently dropped to avoid resync churn from
2895                            // normal mesh reordering. See
2896                            // `COUNTER_RESYNC_GAP_THRESHOLD`.
2897                            let counter = Self::replay_metadata(header, &buf[..frame_len])
2898                                .map(|(c, _)| c)
2899                                .unwrap_or(0);
2900                            let gap = self
2901                                .identity(local_id)
2902                                .and_then(|slot| slot.peer_crypto().get(&peer_id))
2903                                .map(|state| {
2904                                    state.replay_window.last_accepted.wrapping_sub(counter)
2905                                })
2906                                .unwrap_or(0);
2907                            if gap >= COUNTER_RESYNC_GAP_THRESHOLD {
2908                                self.store_deferred_counter_resync_frame(
2909                                    local_id,
2910                                    peer_id,
2911                                    &original[..frame_len],
2912                                    rx,
2913                                    received_at_ms,
2914                                );
2915                                self.maybe_request_counter_resync(
2916                                    local_id,
2917                                    peer_id,
2918                                    peer_key,
2919                                    ReplyCarriage::Unicast,
2920                                )
2921                                .await;
2922                            }
2923                            continue;
2924                        }
2925                    }
2926                    Some(ReplayVerdict::Replay) | None => continue,
2927                }
2928                self.learn_route_for_peer(peer_id, &buf[..frame_len], header);
2929
2930                if header.ack_requested()
2931                    && self.should_emit_destination_ack(&buf[..frame_len], header)
2932                {
2933                    let ack_trailer = self.compute_received_ack_trailer(
2934                        &buf[..frame_len],
2935                        header,
2936                        body_range.clone(),
2937                        &keys,
2938                    );
2939                    let trace_route = Self::frame_requests_trace_route(&buf[..frame_len], header);
2940                    self.queue_mac_ack_for_peer(local_id, peer_id, ack_trailer, trace_route)
2941                        .ok();
2942                }
2943
2944                if let Some(data) = Self::echo_request_data(payload) {
2945                    let response =
2946                        Self::build_echo_command_payload(MAC_COMMAND_ECHO_RESPONSE_ID, data);
2947                    let options = Self::echo_response_options(&original[..frame_len], header);
2948                    let _ = self
2949                        .send_response(
2950                            local_id,
2951                            &peer_key,
2952                            ReplyCarriage::Unicast,
2953                            response.as_slice(),
2954                            &options,
2955                        )
2956                        .await;
2957                }
2958
2959                on_event(
2960                    local_id,
2961                    crate::MacEventRef::Received(crate::ReceivedPacketRef::new(
2962                        &original[..frame_len],
2963                        &buf[body_range],
2964                        header.clone(),
2965                        ParsedOptions::extract(
2966                            &original[..frame_len],
2967                            header.options_range.clone(),
2968                        )
2969                        .unwrap_or_default(),
2970                        Some(peer_key),
2971                        Some(peer_key.hint()),
2972                        true,
2973                        None,
2974                        rx_metadata(rx, received_at_ms),
2975                    )),
2976                );
2977                handled = true;
2978                break;
2979            }
2980            handled
2981        } else {
2982            false
2983        };
2984        let forwarded = self.maybe_forward_received(&original[..frame_len], header, rx, handled);
2985        (
2986            handled || forwarding_confirmed || forwarded,
2987            handled.then_some(()).and(replay_target),
2988        )
2989    }
2990
2991    fn process_multicast(
2992        &mut self,
2993        buf: &mut [u8; FRAME],
2994        frame_len: usize,
2995        header: &PacketHeader,
2996        rx: &RxInfo,
2997        received_at_ms: u64,
2998        forwarding_confirmed: bool,
2999        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
3000    ) -> bool {
3001        let mut original = [0u8; FRAME];
3002        original[..frame_len].copy_from_slice(&buf[..frame_len]);
3003        let delivered = if let Some(channel_id) = header.channel {
3004            let channel_info = {
3005                self.channels
3006                    .lookup_by_id(&channel_id)
3007                    .next()
3008                    .map(|channel| (channel.channel_key.clone(), channel.derived.clone()))
3009            };
3010            if let Some((channel_key, derived)) = channel_info {
3011                let keys = PairwiseKeys {
3012                    k_enc: derived.k_enc,
3013                    k_mic: derived.k_mic,
3014                };
3015                if let Ok(body_range) =
3016                    self.crypto
3017                        .open_packet(&mut buf[..frame_len], header, &keys)
3018                {
3019                    if !Self::payload_is_allowed(header.packet_type(), &buf[body_range.clone()]) {
3020                        false
3021                    } else if let Some(source) =
3022                        self.resolve_multicast_source(&buf[..frame_len], header)
3023                    {
3024                        let accepted = if let Some(peer_id) = source.peer_id {
3025                            let accepted = self.accept_multicast_replay(
3026                                channel_id,
3027                                peer_id,
3028                                header,
3029                                &buf[..frame_len],
3030                            );
3031                            if accepted {
3032                                self.learn_route_for_peer(peer_id, &buf[..frame_len], header);
3033                            }
3034                            accepted
3035                        } else {
3036                            self.accept_unknown_multicast_replay(header, &buf[..frame_len])
3037                        };
3038                        if accepted {
3039                            let mut delivered = false;
3040                            for (index, slot) in self.identities.iter().enumerate() {
3041                                if slot.is_none() {
3042                                    continue;
3043                                }
3044                                delivered = true;
3045                                on_event(
3046                                    LocalIdentityId(index as u8),
3047                                    crate::MacEventRef::Received(crate::ReceivedPacketRef::new(
3048                                        &original[..frame_len],
3049                                        &buf[body_range.clone()],
3050                                        header.clone(),
3051                                        ParsedOptions::extract(
3052                                            &original[..frame_len],
3053                                            header.options_range.clone(),
3054                                        )
3055                                        .unwrap_or_default(),
3056                                        source.public_key,
3057                                        source
3058                                            .hint
3059                                            .or_else(|| source.public_key.map(|key| key.hint())),
3060                                        true,
3061                                        Some(crate::ChannelInfoRef {
3062                                            id: channel_id,
3063                                            key: &channel_key,
3064                                        }),
3065                                        rx_metadata(rx, received_at_ms),
3066                                    )),
3067                                );
3068                            }
3069                            delivered
3070                        } else {
3071                            false
3072                        }
3073                    } else {
3074                        false
3075                    }
3076                } else {
3077                    false
3078                }
3079            } else {
3080                false
3081            }
3082        } else {
3083            false
3084        };
3085        let forwarded = self.maybe_forward_received(&original[..frame_len], header, rx, false);
3086        delivered || forwarding_confirmed || forwarded
3087    }
3088
3089    async fn process_blind_unicast(
3090        &mut self,
3091        buf: &mut [u8; FRAME],
3092        frame_len: usize,
3093        header: &PacketHeader,
3094        rx: &RxInfo,
3095        received_at_ms: u64,
3096        forwarding_confirmed: bool,
3097        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
3098    ) -> (bool, Option<(LocalIdentityId, PeerId)>) {
3099        let mut original = [0u8; FRAME];
3100        original[..frame_len].copy_from_slice(&buf[..frame_len]);
3101        let mut replay_target = None;
3102        let handled = if let Some(channel_id) = header.channel {
3103            let channel_candidates: Vec<(ChannelKey, DerivedChannelKeys), CHANNELS> = self
3104                .channels
3105                .lookup_by_id(&channel_id)
3106                .map(|channel| (channel.channel_key.clone(), channel.derived.clone()))
3107                .collect();
3108            if channel_candidates.is_empty() {
3109                false
3110            } else {
3111                let mut handled = false;
3112                for (resolved_channel_key, channel_keys) in channel_candidates {
3113                    buf[..frame_len].copy_from_slice(&original[..frame_len]);
3114                    let Ok((dst, source_addr)) = self.crypto.decrypt_blind_addr(
3115                        &mut buf[..frame_len],
3116                        header,
3117                        &channel_keys,
3118                    ) else {
3119                        continue;
3120                    };
3121                    let Some(local_id) = self.find_local_identity_for_dst(Some(dst)) else {
3122                        continue;
3123                    };
3124                    for (peer_id, peer_key) in
3125                        self.resolve_blind_source_peer_candidates(&buf[..frame_len], source_addr)
3126                    {
3127                        let Ok(pairwise_keys) = self.ensure_peer_crypto(local_id, peer_id).await
3128                        else {
3129                            continue;
3130                        };
3131                        let blind_keys =
3132                            self.crypto.derive_blind_keys(&pairwise_keys, &channel_keys);
3133                        let body_range = match self.crypto.open_packet(
3134                            &mut buf[..frame_len],
3135                            header,
3136                            &blind_keys,
3137                        ) {
3138                            Ok(range) => range,
3139                            Err(_) => continue,
3140                        };
3141                        let payload = &buf[body_range.clone()];
3142                        if !Self::payload_is_allowed(header.packet_type(), payload) {
3143                            continue;
3144                        }
3145                        if header.ack_requested()
3146                            && self.should_emit_destination_ack(&buf[..frame_len], header)
3147                            && self.note_acknowledgeable_unicast_duplicate(
3148                                local_id,
3149                                peer_id,
3150                                header,
3151                                &buf[..frame_len],
3152                            )
3153                        {
3154                            // Same reason as the unicast path: the retry is
3155                            // what carries the route the first attempt
3156                            // lacked, and the re-ack is composed from it.
3157                            self.learn_route_for_peer(peer_id, &buf[..frame_len], header);
3158                            let ack_trailer = self.compute_received_ack_trailer(
3159                                &buf[..frame_len],
3160                                header,
3161                                body_range.clone(),
3162                                &blind_keys,
3163                            );
3164                            let trace_route =
3165                                Self::frame_requests_trace_route(&buf[..frame_len], header);
3166                            self.queue_mac_ack_for_peer(
3167                                local_id,
3168                                peer_id,
3169                                ack_trailer,
3170                                trace_route,
3171                            )
3172                            .ok();
3173                            handled = true;
3174                            break;
3175                        }
3176                        match self.unicast_replay_verdict(
3177                            local_id,
3178                            peer_id,
3179                            header,
3180                            &buf[..frame_len],
3181                        ) {
3182                            Some(ReplayVerdict::Accept) => {
3183                                let _ = self.accept_unicast_replay(
3184                                    local_id,
3185                                    peer_id,
3186                                    header,
3187                                    &buf[..frame_len],
3188                                );
3189                            }
3190                            Some(ReplayVerdict::OutOfWindow | ReplayVerdict::Stale) => {
3191                                if self.try_accept_counter_resync_response(
3192                                    local_id,
3193                                    peer_id,
3194                                    header,
3195                                    &buf[..frame_len],
3196                                    payload,
3197                                ) {
3198                                    replay_target = Some((local_id, peer_id));
3199                                } else {
3200                                    self.store_deferred_counter_resync_frame(
3201                                        local_id,
3202                                        peer_id,
3203                                        &original[..frame_len],
3204                                        rx,
3205                                        received_at_ms,
3206                                    );
3207                                    self.maybe_request_counter_resync(
3208                                        local_id,
3209                                        peer_id,
3210                                        peer_key,
3211                                        ReplyCarriage::Blind(&channel_keys),
3212                                    )
3213                                    .await;
3214                                    continue;
3215                                }
3216                            }
3217                            Some(ReplayVerdict::Replay) | None => continue,
3218                        }
3219                        self.learn_route_for_peer(peer_id, &buf[..frame_len], header);
3220
3221                        if header.ack_requested()
3222                            && self.should_emit_destination_ack(&buf[..frame_len], header)
3223                        {
3224                            let ack_trailer = self.compute_received_ack_trailer(
3225                                &buf[..frame_len],
3226                                header,
3227                                body_range.clone(),
3228                                &blind_keys,
3229                            );
3230                            let trace_route =
3231                                Self::frame_requests_trace_route(&buf[..frame_len], header);
3232                            self.queue_mac_ack_for_peer(
3233                                local_id,
3234                                peer_id,
3235                                ack_trailer,
3236                                trace_route,
3237                            )
3238                            .ok();
3239                        }
3240
3241                        if let Some(data) = Self::echo_request_data(payload) {
3242                            let response = Self::build_echo_command_payload(
3243                                MAC_COMMAND_ECHO_RESPONSE_ID,
3244                                data,
3245                            );
3246                            let options =
3247                                Self::echo_response_options(&original[..frame_len], header);
3248                            let _ = self
3249                                .send_response(
3250                                    local_id,
3251                                    &peer_key,
3252                                    ReplyCarriage::Blind(&channel_keys),
3253                                    response.as_slice(),
3254                                    &options,
3255                                )
3256                                .await;
3257                        }
3258
3259                        on_event(
3260                            local_id,
3261                            crate::MacEventRef::Received(crate::ReceivedPacketRef::new(
3262                                &original[..frame_len],
3263                                &buf[body_range],
3264                                header.clone(),
3265                                ParsedOptions::extract(
3266                                    &original[..frame_len],
3267                                    header.options_range.clone(),
3268                                )
3269                                .unwrap_or_default(),
3270                                Some(peer_key),
3271                                Some(peer_key.hint()),
3272                                true,
3273                                Some(crate::ChannelInfoRef {
3274                                    id: channel_id,
3275                                    key: &resolved_channel_key,
3276                                }),
3277                                rx_metadata(rx, received_at_ms),
3278                            )),
3279                        );
3280                        handled = true;
3281                        break;
3282                    }
3283                    if handled {
3284                        break;
3285                    }
3286                }
3287                handled
3288            }
3289        } else {
3290            false
3291        };
3292        let forwarded = self.maybe_forward_received(&original[..frame_len], header, rx, handled);
3293        (
3294            handled || forwarding_confirmed || forwarded,
3295            handled.then_some(()).and(replay_target),
3296        )
3297    }
3298
3299    /// Non-blocking receive: polls the radio once and processes a frame if available.
3300    ///
3301    /// This is the legacy non-blocking API used by [`poll_cycle`](Self::poll_cycle).
3302    /// For new code, prefer [`next_event`](Self::next_event) which properly awaits
3303    /// the radio and timer deadlines.
3304    pub async fn receive_one(
3305        &mut self,
3306        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
3307    ) -> Result<bool, MacError<<P::Radio as Radio>::Error>> {
3308        let mut buf = [0u8; FRAME];
3309        let Some(rx) = poll_fn(|cx| match self.radio.poll_receive(cx, &mut buf) {
3310            Poll::Ready(Ok(rx)) => Poll::Ready(Ok(Some(rx))),
3311            Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
3312            Poll::Pending => Poll::Ready(Ok(None)),
3313        })
3314        .await
3315        .map_err(MacError::Radio)?
3316        else {
3317            return Ok(false);
3318        };
3319
3320        let frame_len = rx.len.min(buf.len());
3321        Ok(self
3322            .process_received_frame(&mut buf, frame_len, &rx, &mut on_event)
3323            .await)
3324    }
3325
3326    /// Mark a pending receipt as acknowledged and emit an event through `on_event`.
3327    pub fn complete_ack(
3328        &mut self,
3329        peer: &PublicKey,
3330        ack_trailer: &[u8; 8],
3331    ) -> Option<(LocalIdentityId, SendReceipt)> {
3332        for (index, slot) in self.identities.iter_mut().enumerate() {
3333            let Some(slot) = slot.as_mut() else {
3334                continue;
3335            };
3336
3337            let receipt = slot.pending_acks.iter().find_map(|(receipt, pending)| {
3338                (pending.expects_ack()
3339                    && pending.peer == *peer
3340                    && pending.ack_trailer == *ack_trailer)
3341                    .then_some(*receipt)
3342            });
3343
3344            if let Some(receipt) = receipt {
3345                slot.pending_acks.remove(&receipt);
3346                let identity_id = LocalIdentityId(index as u8);
3347                // A retransmission may already be queued behind this ack.
3348                // Nothing downstream re-checks, so it would otherwise go out
3349                // after the send it belongs to has been confirmed.
3350                self.tx_queue.remove_all_matching(|entry| {
3351                    entry.receipt == Some(receipt) && entry.identity_id == Some(identity_id)
3352                });
3353                if self
3354                    .post_tx_listen
3355                    .as_ref()
3356                    .map(|listen| listen.identity_id == identity_id && listen.receipt == receipt)
3357                    .unwrap_or(false)
3358                {
3359                    self.post_tx_listen = None;
3360                }
3361                return Some((identity_id, receipt));
3362            }
3363        }
3364
3365        None
3366    }
3367
3368    /// Expire or retry pending ACK state based on `now_ms`.
3369    pub fn service_pending_ack_timeouts(
3370        &mut self,
3371        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
3372    ) -> Result<(), CapacityError> {
3373        self.expire_post_tx_listen_if_needed();
3374
3375        #[derive(Clone)]
3376        enum Action<const FRAME: usize> {
3377            Retry {
3378                receipt: SendReceipt,
3379                resend: ResendRecord<FRAME>,
3380                not_before_ms: u64,
3381            },
3382            RouteRetry {
3383                receipt: SendReceipt,
3384                peer: PublicKey,
3385                resend: ResendRecord<FRAME>,
3386                not_before_ms: u64,
3387            },
3388            Timeout {
3389                receipt: SendReceipt,
3390                peer: PublicKey,
3391            },
3392            /// A repeat-confirmed send whose ladder ran out. Nobody is
3393            /// waiting on the outcome, so the entry just goes away.
3394            Abandon { receipt: SendReceipt },
3395        }
3396
3397        let now_ms = self.clock.now_ms();
3398        let t_frame_ms = self.radio.t_frame_ms();
3399
3400        for index in 0..self.identities.len() {
3401            let identity_id = LocalIdentityId(index as u8);
3402            let actions = {
3403                let Some(slot) = self.identities[index].as_mut() else {
3404                    continue;
3405                };
3406
3407                let mut actions: Vec<Action<FRAME>, ACKS> = Vec::new();
3408                for (receipt, pending) in slot.pending_acks.iter_mut() {
3409                    if !matches!(pending.state, crate::AckState::Queued { .. })
3410                        && now_ms >= pending.ack_deadline_ms
3411                    {
3412                        if !pending.expects_ack() {
3413                            actions
3414                                .push(Action::Abandon { receipt: *receipt })
3415                                .map_err(|_| CapacityError)?;
3416                        } else if Self::can_attempt_route_retry(pending) {
3417                            let backoff_cap_ms = t_frame_ms;
3418                            let backoff_ms = if backoff_cap_ms == 0 {
3419                                0
3420                            } else {
3421                                u64::from(self.rng.random_range(..backoff_cap_ms.saturating_add(1)))
3422                            };
3423                            actions
3424                                .push(Action::RouteRetry {
3425                                    receipt: *receipt,
3426                                    peer: pending.peer,
3427                                    resend: pending.resend.clone(),
3428                                    not_before_ms: now_ms.saturating_add(backoff_ms),
3429                                })
3430                                .map_err(|_| CapacityError)?;
3431                        } else {
3432                            actions
3433                                .push(Action::Timeout {
3434                                    receipt: *receipt,
3435                                    peer: pending.peer,
3436                                })
3437                                .map_err(|_| CapacityError)?;
3438                        }
3439                        continue;
3440                    }
3441
3442                    if let crate::AckState::AwaitingForward {
3443                        confirm_deadline_ms,
3444                    } = pending.state
3445                    {
3446                        if now_ms >= confirm_deadline_ms && pending.retries < MAX_FORWARD_RETRIES {
3447                            pending.retries = pending.retries.saturating_add(1);
3448                            let backoff_cap_ms = t_frame_ms;
3449                            let backoff_ms = if backoff_cap_ms == 0 {
3450                                0
3451                            } else {
3452                                u64::from(self.rng.random_range(..backoff_cap_ms.saturating_add(1)))
3453                            };
3454                            let not_before_ms = now_ms.saturating_add(backoff_ms);
3455                            pending.state = crate::AckState::RetryQueued;
3456                            actions
3457                                .push(Action::Retry {
3458                                    receipt: *receipt,
3459                                    resend: pending.resend.clone(),
3460                                    not_before_ms,
3461                                })
3462                                .map_err(|_| CapacityError)?;
3463                        }
3464                    }
3465                }
3466                actions
3467            };
3468
3469            for action in actions {
3470                match action {
3471                    Action::Retry {
3472                        receipt,
3473                        resend,
3474                        not_before_ms,
3475                    } => {
3476                        self.tx_queue.enqueue_with_state(
3477                            TxPriority::Retry,
3478                            resend.frame.as_slice(),
3479                            Some(receipt),
3480                            Some(identity_id),
3481                            not_before_ms,
3482                            0,
3483                            0,
3484                        )?;
3485                    }
3486                    Action::RouteRetry {
3487                        receipt,
3488                        peer,
3489                        resend,
3490                        not_before_ms,
3491                    } => {
3492                        if let Some(rewritten) = self.synthesize_route_retry_resend(&peer, &resend)
3493                        {
3494                            let rewritten_key = Self::confirmation_key(rewritten.frame.as_slice());
3495                            // The rewritten attempt always floods, so it earns
3496                            // the forwarded window. It is armed here, from the
3497                            // moment the frame becomes eligible to air, rather
3498                            // than left at zero for the transmit path to fill
3499                            // in: an unarmed deadline reads as *already
3500                            // expired* to both the timeout sweep and
3501                            // `earliest_deadline_ms`, which would retire the
3502                            // send — and drop the frame back out of the queue —
3503                            // before the retry it just scheduled ever aired.
3504                            let retry_deadline_ms =
3505                                not_before_ms.saturating_add(self.forwarded_ack_timeout_ms(
3506                                    Self::allowed_hop_distance(rewritten.frame.as_slice()),
3507                                ));
3508                            if let Some(pending) = self
3509                                .identity_mut(identity_id)
3510                                .and_then(|slot| slot.pending_ack_mut(&receipt))
3511                            {
3512                                pending.resend = rewritten.clone();
3513                                pending.retries = 0;
3514                                pending.sent_ms = 0;
3515                                pending.ack_deadline_ms = retry_deadline_ms;
3516                                pending.state = crate::AckState::RetryQueued;
3517                                // The rebuilt frame is a distinct forwarding
3518                                // identity; a repeat of the abandoned one no
3519                                // longer confirms this attempt.
3520                                pending.confirm_key = rewritten_key;
3521                            }
3522                            self.tx_queue.enqueue_with_state(
3523                                TxPriority::Retry,
3524                                rewritten.frame.as_slice(),
3525                                Some(receipt),
3526                                Some(identity_id),
3527                                not_before_ms,
3528                                0,
3529                                0,
3530                            )?;
3531                        } else {
3532                            if let Some(slot) = self.identity_mut(identity_id) {
3533                                slot.pending_acks.remove(&receipt);
3534                            }
3535                            on_event(
3536                                identity_id,
3537                                crate::MacEventRef::AckTimeout { peer, receipt },
3538                            );
3539                        }
3540                    }
3541                    Action::Timeout { receipt, peer } => {
3542                        if let Some(slot) = self.identity_mut(identity_id) {
3543                            slot.pending_acks.remove(&receipt);
3544                        }
3545                        self.tx_queue.remove_all_matching(|entry| {
3546                            entry.receipt == Some(receipt) && entry.identity_id == Some(identity_id)
3547                        });
3548                        on_event(
3549                            identity_id,
3550                            crate::MacEventRef::AckTimeout { peer, receipt },
3551                        );
3552                    }
3553                    Action::Abandon { receipt } => {
3554                        if let Some(slot) = self.identity_mut(identity_id) {
3555                            slot.pending_acks.remove(&receipt);
3556                        }
3557                        self.tx_queue.remove_all_matching(|entry| {
3558                            entry.receipt == Some(receipt) && entry.identity_id == Some(identity_id)
3559                        });
3560                    }
3561                }
3562            }
3563        }
3564
3565        Ok(())
3566    }
3567
3568    /// Cancel a pending ACK-requested send, stopping retransmissions.
3569    ///
3570    /// Removes the pending ACK entry for the given identity slot and receipt,
3571    /// and removes any matching entry from the transmit queue. Returns `true`
3572    /// if a pending ACK was found and removed.
3573    pub fn cancel_pending_ack(
3574        &mut self,
3575        identity_id: LocalIdentityId,
3576        receipt: SendReceipt,
3577    ) -> bool {
3578        let removed = self
3579            .identity_mut(identity_id)
3580            .and_then(|slot| slot.remove_pending_ack(&receipt))
3581            .is_some();
3582
3583        // Also remove any queued retransmission for this receipt.
3584        self.tx_queue.remove_first_matching(|entry| {
3585            entry.receipt == Some(receipt) && entry.identity_id == Some(identity_id)
3586        });
3587
3588        // Clear the post-tx listen if it was tracking this receipt.
3589        if let Some(listen) = &self.post_tx_listen {
3590            if listen.identity_id == identity_id && listen.receipt == receipt {
3591                self.post_tx_listen = None;
3592            }
3593        }
3594
3595        removed
3596    }
3597
3598    fn identity_and_advance(
3599        &mut self,
3600        from: LocalIdentityId,
3601    ) -> Result<(PublicKey, u32), SendError> {
3602        let slot = self.identity_mut(from).ok_or(SendError::IdentityMissing)?;
3603        if slot.counter_window_exhausted() {
3604            return Err(SendError::CounterPersistenceLag);
3605        }
3606        let source_key = *slot.identity().public_key();
3607        let frame_counter = slot.advance_frame_counter();
3608        slot.schedule_counter_persist_if_needed();
3609        Ok((source_key, frame_counter))
3610    }
3611
3612    fn take_salt(&mut self, options: &SendOptions) -> Option<u16> {
3613        options.salt.then(|| self.rng.next_u32() as u16)
3614    }
3615
3616    fn enforce_send_policy(
3617        &self,
3618        channel_id: Option<ChannelId>,
3619        options: &SendOptions,
3620        blind_unicast: bool,
3621    ) -> Result<(), SendError> {
3622        let _authority = self.classify_send_authority(options, blind_unicast)?;
3623
3624        let Some(channel_id) = channel_id else {
3625            return Ok(());
3626        };
3627        let Some(policy) = self
3628            .operating_policy
3629            .channel_policies
3630            .iter()
3631            .find(|policy| policy.channel_id == channel_id)
3632        else {
3633            return Ok(());
3634        };
3635
3636        if policy.require_unencrypted && options.encrypted {
3637            return Err(SendError::PolicyViolation);
3638        }
3639        if policy.require_full_source && !options.full_source {
3640            return Err(SendError::PolicyViolation);
3641        }
3642        if let Some(max_flood_hops) = policy.max_flood_hops {
3643            if options
3644                .flood_hops
3645                .map(|hops| hops > max_flood_hops)
3646                .unwrap_or(false)
3647            {
3648                return Err(SendError::PolicyViolation);
3649            }
3650        }
3651
3652        Ok(())
3653    }
3654
3655    fn classify_send_authority(
3656        &self,
3657        options: &SendOptions,
3658        _blind_unicast: bool,
3659    ) -> Result<TransmitAuthority, SendError> {
3660        match self.operating_policy.amateur_radio_mode {
3661            AmateurRadioMode::Unlicensed => Ok(TransmitAuthority::Unlicensed),
3662            AmateurRadioMode::LicensedOnly => {
3663                if options.encrypted || self.operating_policy.operator_callsign.is_none() {
3664                    return Err(SendError::PolicyViolation);
3665                }
3666                Ok(TransmitAuthority::Amateur)
3667            }
3668            AmateurRadioMode::Hybrid => {
3669                if options.encrypted {
3670                    // Hybrid encrypted traffic must be treated as unlicensed
3671                    // traffic for downstream transmit-power and duty-cycle policy.
3672                    Ok(TransmitAuthority::Unlicensed)
3673                } else if self.operating_policy.operator_callsign.is_some() {
3674                    Ok(TransmitAuthority::Amateur)
3675                } else {
3676                    Ok(TransmitAuthority::Unlicensed)
3677                }
3678            }
3679        }
3680    }
3681
3682    fn enqueue_packet(
3683        &mut self,
3684        packet: UnsealedPacket<'_>,
3685        receipt: Option<SendReceipt>,
3686        identity_id: Option<LocalIdentityId>,
3687        not_before_ms: u64,
3688    ) -> Result<(), SendError> {
3689        if packet.total_len() > self.radio.max_frame_size() {
3690            return Err(SendError::Build(BuildError::BufferTooSmall));
3691        }
3692        self.tx_queue
3693            .enqueue_with_state(
3694                TxPriority::Application,
3695                packet.as_bytes(),
3696                receipt,
3697                identity_id,
3698                not_before_ms,
3699                0,
3700                0,
3701            )
3702            .map_err(|_| SendError::QueueFull)?;
3703        Ok(())
3704    }
3705
3706    /// The earliest transmit instant a send's options allow: now plus the
3707    /// requested [`SendOptions::tx_delay_ms`], or zero for immediate.
3708    fn tx_not_before_ms(&self, options: &SendOptions) -> u64 {
3709        options
3710            .tx_delay_ms
3711            .map(|delay| self.clock.now_ms() + u64::from(delay))
3712            .unwrap_or(0)
3713    }
3714
3715    fn refresh_pending_resend(
3716        &mut self,
3717        from: LocalIdentityId,
3718        receipt: SendReceipt,
3719        frame: &[u8],
3720        source_route: Option<&[RouterHint]>,
3721        requested_flood_hops: Option<u8>,
3722    ) -> Result<(), SendError> {
3723        let resend = ResendRecord::try_new(frame, source_route)
3724            .map_err(|_| SendError::QueueFull)?
3725            .with_requested_flood_hops(requested_flood_hops);
3726        let pending = self
3727            .identity_mut(from)
3728            .ok_or(SendError::IdentityMissing)?
3729            .pending_ack_mut(&receipt)
3730            .ok_or(SendError::IdentityMissing)?;
3731        pending.resend = resend;
3732        Ok(())
3733    }
3734
3735    /// Whether the frame, as built, asks a repeater to carry it: a nonzero
3736    /// `FHOPS_REM` nibble or a non-empty source-route option.
3737    ///
3738    /// This is the arming condition for every retry a non-acknowledged send
3739    /// gets: such a send is confirmed only by overhearing a repeat, so a retry
3740    /// is defensible if and only if the frame visibly solicits one. The
3741    /// question is answered by parsing the frame itself rather than by the
3742    /// option values the send was built from — the builder is allowed to
3743    /// narrow or drop what was requested, and a prediction that drifts from
3744    /// the wire arms a retry ladder for a frame nothing will ever repeat.
3745    fn frame_solicits_repeat(frame: &[u8]) -> bool {
3746        let Ok(header) = PacketHeader::parse(frame) else {
3747            return false;
3748        };
3749        if header
3750            .flood_hops
3751            .map(|hops| hops.remaining() > 0)
3752            .unwrap_or(false)
3753        {
3754            return true;
3755        }
3756        ParsedOptions::extract(frame, header.options_range.clone())
3757            .ok()
3758            .and_then(|options| options.source_route)
3759            .map(|range| !range.is_empty())
3760            .unwrap_or(false)
3761    }
3762
3763    /// Track a non-ACK send that travels through repeaters.
3764    ///
3765    /// Such a send has no acknowledgement coming, but it is not therefore
3766    /// unverifiable: hearing the next hop carry the frame onward says it was
3767    /// received, and hearing nothing says the one transmission may have been
3768    /// the only chance it got. The receipt is the coordinator's own — the
3769    /// caller asked for no tracking and gets none — and exists so the retry
3770    /// ladder has something to hang on.
3771    ///
3772    /// A send with no hops is left alone: there is no repeater to hear, and
3773    /// repeating a direct packet nobody asked to acknowledge would be noise.
3774    fn prepare_repeat_confirmed_send(
3775        &mut self,
3776        from: LocalIdentityId,
3777        peer: PublicKey,
3778        frame: &[u8],
3779        source_route: Option<&Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>>,
3780        requested_flood_hops: Option<u8>,
3781    ) -> Result<SendReceipt, SendError> {
3782        let resend = ResendRecord::try_new(frame, source_route.map(|route| route.as_slice()))
3783            .map_err(|_| SendError::QueueFull)?
3784            .with_requested_flood_hops(requested_flood_hops);
3785        let slot = self.identity_mut(from).ok_or(SendError::IdentityMissing)?;
3786        let receipt = slot.next_receipt();
3787        slot.try_insert_pending_ack(receipt, PendingAck::repeat_only(peer, resend))
3788            .map_err(|_| SendError::PendingAckFull)?;
3789        Ok(receipt)
3790    }
3791
3792    fn prepare_pending_ack(
3793        &mut self,
3794        from: LocalIdentityId,
3795        peer: PublicKey,
3796        packet: &UnsealedPacket<'_>,
3797        keys: &PairwiseKeys,
3798        options: &SendOptions,
3799    ) -> Result<SendReceipt, SendError> {
3800        let header = PacketHeader::parse(packet.as_bytes())?;
3801        let full_mac = self.crypto.s2v_tag(
3802            &keys.k_mic,
3803            |cmac| feed_aad(&header, packet.as_bytes(), |chunk| cmac.update(chunk)),
3804            packet.body(),
3805        );
3806        let ack_trailer = self.crypto.compute_ack_trailer(&full_mac, &keys.k_enc);
3807        // Judged from the frame, not the requested options: whether the wait
3808        // for the ack includes a forwarding-confirmation phase depends on
3809        // whether the frame as built actually asks anyone to forward it.
3810        let is_forwarded = Self::frame_solicits_repeat(packet.as_bytes());
3811        let resend = ResendRecord::try_new(
3812            packet.as_bytes(),
3813            options.source_route.as_ref().map(|route| route.as_slice()),
3814        )
3815        .map_err(|_| SendError::QueueFull)?
3816        .with_requested_flood_hops(options.flood_hops);
3817
3818        let slot = self.identity_mut(from).ok_or(SendError::IdentityMissing)?;
3819        let receipt = slot.next_receipt();
3820        let pending = if is_forwarded {
3821            PendingAck::forwarded(ack_trailer, peer, resend)
3822        } else {
3823            PendingAck::direct(ack_trailer, peer, resend)
3824        };
3825        slot.try_insert_pending_ack(receipt, pending)
3826            .map_err(|_| SendError::PendingAckFull)?;
3827        Ok(receipt)
3828    }
3829
3830    async fn derive_pairwise_keys_for_peer(
3831        &self,
3832        local_id: LocalIdentityId,
3833        peer_key: &PublicKey,
3834    ) -> Result<PairwiseKeys, SendError> {
3835        let shared_secret = {
3836            let slot = self.identity(local_id).ok_or(SendError::IdentityMissing)?;
3837            match slot.identity() {
3838                LocalIdentity::LongTerm(identity) => identity
3839                    .agree(peer_key)
3840                    .await
3841                    .map_err(|_| SendError::IdentityAgreementFailed)?,
3842                #[cfg(feature = "software-crypto")]
3843                LocalIdentity::Ephemeral(identity) => identity
3844                    .agree(peer_key)
3845                    .await
3846                    .map_err(|_| SendError::IdentityAgreementFailed)?,
3847            }
3848        };
3849
3850        Ok(self.crypto.derive_pairwise_keys(&shared_secret))
3851    }
3852
3853    fn effective_source_route(
3854        &self,
3855        peer_id: PeerId,
3856        options: &SendOptions,
3857    ) -> Option<Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>> {
3858        // An explicitly supplied route that constrains no hop is the same as
3859        // no route at all, and must not reach the wire as an empty option.
3860        if let Some(route) = options.source_route.as_ref() {
3861            return (!route.is_empty()).then(|| route.clone());
3862        }
3863
3864        let Some(peer) = self.peer_registry.get(peer_id) else {
3865            return None;
3866        };
3867        match peer.route.as_ref() {
3868            Some(CachedRoute::Source(route)) => Some(route.clone()),
3869            _ => None,
3870        }
3871    }
3872
3873    /// Whether this send should carry a trace route the caller did not ask
3874    /// for, because nothing yet describes the path to the peer.
3875    ///
3876    /// A trace route asks the repeaters that carry a frame to record
3877    /// themselves, so it is only worth its byte on a frame some repeater can
3878    /// carry. A frame with no flood budget and no source route is not one:
3879    /// nothing may forward it, the trace is guaranteed to arrive empty, and
3880    /// the destination learns the link is direct from the frame's own shape
3881    /// anyway — see [`Mac::learn_route_for_peer`].
3882    ///
3883    /// Past that, a peer heard directly has no repeaters for a trace to
3884    /// record. Everything else floods toward a destination whose distance we
3885    /// can at best estimate, and that is exactly the frame whose path is worth
3886    /// writing down: it costs one byte on the way out and buys the
3887    /// destination a precise route back. The destination mirrors the option
3888    /// onto its ack, which closes the return direction, so a single exchange
3889    /// leaves both ends holding a route and this condition stops firing.
3890    /// Discovery is paid for once per path rather than per packet — the
3891    /// always-on variant is [proactive route refresh], which the spec
3892    /// deliberately leaves unspecified.
3893    ///
3894    /// A source route is the one case where knowing the path is not enough.
3895    /// The frame reaches the destination with its hints consumed, so nothing
3896    /// on it describes the way back, and an ack composed against an empty
3897    /// route cache carries neither a flood budget nor a route of its own —
3898    /// it dies at the first hop, and the sender retries against a peer that
3899    /// has been answering all along. So a routed frame that asks for an ack
3900    /// traces as well: the routed hops record themselves, and the ack has a
3901    /// path home.
3902    ///
3903    /// This is unconditional for now. The narrower form only traces when the
3904    /// peer has not shown it can reach us — a frame arriving from it with a
3905    /// source-route option present, or with accumulated flood hops, is that
3906    /// proof, and [`Mac::learn_route_for_peer`] already sees both.
3907    /// Relax to that once there is enough field data to say the evidence bit
3908    /// tracks reality; until then the extra byte is cheaper than a delivery
3909    /// that silently never lands.
3910    ///
3911    /// [proactive route refresh]: https://darconeous.github.io/umsh/docs/protocol/beacons.html#potential-improvement-proactive-route-refresh
3912    fn needs_route_discovery(
3913        &self,
3914        peer_id: PeerId,
3915        source_route: Option<&Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>>,
3916        flood_hops: Option<u8>,
3917        ack_requested: bool,
3918    ) -> bool {
3919        if source_route.is_some_and(|route| !route.is_empty()) {
3920            // Routed hops prepend to a trace like flooded ones do, so the
3921            // option arrives populated even though `FHOPS` never moved.
3922            return ack_requested;
3923        }
3924        if flood_hops.unwrap_or(0) == 0 {
3925            return false;
3926        }
3927        !matches!(
3928            self.peer_registry
3929                .get(peer_id)
3930                .and_then(|peer| peer.route.as_ref()),
3931            Some(CachedRoute::Direct)
3932        )
3933    }
3934
3935    /// Whether a frame this node is about to originate gives any repeater
3936    /// permission to carry it.
3937    ///
3938    /// Both trace options are filled in by repeaters and by nobody else. With
3939    /// no flood budget and no source route, nothing on the frame lets a
3940    /// repeater touch it, so a trace arrives exactly as empty as it left: an
3941    /// option header spent on a question no one is in a position to answer,
3942    /// and one more option in the block than the frame needs. The receiver
3943    /// reads the direct link off the frame's own shape instead of out of an
3944    /// empty trace, which is what makes the option redundant rather than
3945    /// merely unanswered; see [`Mac::learn_route_for_peer`].
3946    ///
3947    /// A flood budget of zero is not permission. The field goes on the wire
3948    /// but no repeater may spend it, so it forwards nothing.
3949    ///
3950    /// [`Mac::queue_mac_ack_for_peer`] asks the same question of the
3951    /// cached route it is about to attach, before it has fields to read.
3952    fn frame_is_repeatable(flood_hops: Option<u8>, source_route: Option<&[RouterHint]>) -> bool {
3953        flood_hops.is_some_and(|hops| hops > 0)
3954            || source_route.is_some_and(|route| !route.is_empty())
3955    }
3956
3957    /// Flood budget for a unicast send, narrowed by whatever is already known
3958    /// about how to reach the peer.
3959    ///
3960    /// `options.flood_hops` is a ceiling, not a target: a learned route only
3961    /// lowers it, and `no_flood()` still emits no flood-hop field at all. Each
3962    /// route form keeps [`ESTABLISHED_ROUTE_EXTRA_HOPS`] of slack beyond the
3963    /// flood distance it already covers. A source route covers that distance
3964    /// without spending flood budget, so its slack is the entire budget.
3965    ///
3966    /// A budget of zero is returned as `None` rather than `Some(0)`. The two
3967    /// say the same thing to every reader — no repeater may carry this frame —
3968    /// but the field costs a byte and an FCF bit to say it, and an absent
3969    /// `FHOPS` is what tells the receiver it heard us directly rather than
3970    /// across a zero-hop flood.
3971    ///
3972    /// The result is clamped to [`MAX_FLOOD_HOPS`], the largest value the
3973    /// `FHOPS_REM` nibble can hold.
3974    fn effective_flood_hops(
3975        &self,
3976        peer_id: PeerId,
3977        options: &SendOptions,
3978        source_route: Option<&Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>>,
3979    ) -> Option<u8> {
3980        let requested = options.flood_hops?;
3981        let cached = self
3982            .peer_registry
3983            .get(peer_id)
3984            .and_then(|peer| peer.route.as_ref());
3985        // An empty source route is carried for provenance only; it constrains
3986        // no hop, so it costs nothing.
3987        let source_route = source_route.filter(|route| !route.is_empty());
3988
3989        let ceiling = match (source_route, cached) {
3990            // A source route costs no flood budget at all — routed hops do not
3991            // touch `FHOPS` — so the whole budget is slack past the route's
3992            // end. Heard directly is the same shape: our own transmission is
3993            // the delivery, and any slack only buys a repeater backstop.
3994            (Some(_), _) | (None, Some(CachedRoute::Direct)) => ESTABLISHED_ROUTE_EXTRA_HOPS,
3995            // Flooding has to cover the distance the peer was last heard from.
3996            (None, Some(CachedRoute::Flood { hops, .. })) => {
3997                hops.saturating_add(ESTABLISHED_ROUTE_EXTRA_HOPS)
3998            }
3999            // A cached source route not attached to this send (the caller
4000            // suppressed it) has to be flooded end to end instead.
4001            (None, Some(CachedRoute::Source(route))) => u8::try_from(route.len())
4002                .unwrap_or(u8::MAX)
4003                .saturating_add(ESTABLISHED_ROUTE_EXTRA_HOPS),
4004            // Nothing known about the peer: first contact floods as asked.
4005            (None, None) => MAX_FLOOD_HOPS,
4006        };
4007
4008        // The ceiling arithmetic can exceed what the nibble holds (a cached
4009        // flood distance of 15 plus slack); an unclamped value would be
4010        // silently dropped by the builder, sending the frame with no flood
4011        // hops at all.
4012        let hops = requested.min(ceiling).min(MAX_FLOOD_HOPS);
4013        (hops > 0).then_some(hops)
4014    }
4015
4016    fn cache_peer_crypto(
4017        &mut self,
4018        local_id: LocalIdentityId,
4019        peer_id: PeerId,
4020        pairwise_keys: PairwiseKeys,
4021    ) -> Result<(), SendError> {
4022        let slot = self
4023            .identity_mut(local_id)
4024            .ok_or(SendError::IdentityMissing)?;
4025        if slot.peer_crypto().get(&peer_id).is_some() {
4026            return Ok(());
4027        }
4028        let initial = self
4029            .peer_registry
4030            .get(peer_id)
4031            .map(|info| info.initial_rx_counter)
4032            .unwrap_or(0);
4033        let now_ms = self.clock.now_ms();
4034        let mut replay_window = ReplayWindow::new();
4035        if initial > 0 {
4036            replay_window.reset(initial, now_ms);
4037        }
4038        let slot = self
4039            .identity_mut(local_id)
4040            .ok_or(SendError::IdentityMissing)?;
4041        slot.peer_crypto_mut()
4042            .insert(
4043                peer_id,
4044                crate::peers::PeerCryptoState {
4045                    pairwise_keys,
4046                    replay_window,
4047                    persisted_rx_counter: initial,
4048                    needs_rx_persist: false,
4049                },
4050            )
4051            .map_err(|_| SendError::QueueFull)?;
4052        Ok(())
4053    }
4054
4055    async fn ensure_peer_crypto(
4056        &mut self,
4057        local_id: LocalIdentityId,
4058        peer_id: PeerId,
4059    ) -> Result<PairwiseKeys, SendError> {
4060        if let Some(keys) = self
4061            .identity(local_id)
4062            .and_then(|slot| slot.peer_crypto().get(&peer_id))
4063            .map(|state| state.pairwise_keys.clone())
4064        {
4065            return Ok(keys);
4066        }
4067
4068        let peer_key = self
4069            .peer_registry
4070            .get(peer_id)
4071            .ok_or(SendError::PeerMissing)?
4072            .public_key;
4073        let pairwise_keys = self
4074            .derive_pairwise_keys_for_peer(local_id, &peer_key)
4075            .await?;
4076        self.cache_peer_crypto(local_id, peer_id, pairwise_keys.clone())?;
4077        Ok(pairwise_keys)
4078    }
4079
4080    fn insert_identity(
4081        &mut self,
4082        identity: LocalIdentity<P::Identity>,
4083        pfs_parent: Option<LocalIdentityId>,
4084    ) -> Result<LocalIdentityId, CapacityError> {
4085        let initial_frame_counter = nonzero_initial_frame_counter(self.rng.next_u32());
4086
4087        if let Some((index, slot)) = self
4088            .identities
4089            .iter_mut()
4090            .enumerate()
4091            .find(|(_, slot)| slot.is_none())
4092        {
4093            *slot = Some(IdentitySlot::new(
4094                identity,
4095                initial_frame_counter,
4096                pfs_parent,
4097            ));
4098            return Ok(LocalIdentityId(index as u8));
4099        }
4100
4101        let next_id = self.identities.len();
4102        self.identities
4103            .push(Some(IdentitySlot::new(
4104                identity,
4105                initial_frame_counter,
4106                pfs_parent,
4107            )))
4108            .map_err(|_| CapacityError)?;
4109        Ok(LocalIdentityId(next_id as u8))
4110    }
4111
4112    fn compute_received_ack_trailer(
4113        &self,
4114        buf: &[u8],
4115        header: &PacketHeader,
4116        body_range: core::ops::Range<usize>,
4117        keys: &PairwiseKeys,
4118    ) -> [u8; 8] {
4119        let full_mac = self.crypto.s2v_tag(
4120            &keys.k_mic,
4121            |cmac| feed_aad(header, buf, |chunk| cmac.update(chunk)),
4122            &buf[body_range],
4123        );
4124        self.crypto.compute_ack_trailer(&full_mac, &keys.k_enc)
4125    }
4126
4127    fn requeue_tx(&mut self, queued: &crate::QueuedTx<FRAME>) -> Result<u32, CapacityError> {
4128        self.tx_queue.enqueue_with_state(
4129            queued.priority,
4130            queued.frame.as_slice(),
4131            queued.receipt,
4132            queued.identity_id,
4133            queued.not_before_ms,
4134            queued.cad_attempts,
4135            queued.forward_deferrals,
4136        )
4137    }
4138
4139    fn accept_unicast_replay(
4140        &mut self,
4141        local_id: LocalIdentityId,
4142        peer_id: PeerId,
4143        header: &PacketHeader,
4144        frame: &[u8],
4145    ) -> bool {
4146        let Some((counter, mic)) = Self::replay_metadata(header, frame) else {
4147            return false;
4148        };
4149        let now_ms = self.clock.now_ms();
4150        let Some(state) = self
4151            .identity_mut(local_id)
4152            .and_then(|slot| slot.peer_crypto_mut().get_mut(&peer_id))
4153        else {
4154            return false;
4155        };
4156
4157        if state.replay_window.check(counter, mic, now_ms) != ReplayVerdict::Accept {
4158            return false;
4159        }
4160
4161        state.replay_window.accept(counter, mic, now_ms);
4162        // Schedule an RX counter flush when we've advanced one full persist block.
4163        let last = state.replay_window.last_accepted;
4164        if last.wrapping_sub(state.persisted_rx_counter) >= COUNTER_PERSIST_BLOCK_SIZE {
4165            state.needs_rx_persist = true;
4166        }
4167        true
4168    }
4169
4170    fn unicast_replay_verdict(
4171        &mut self,
4172        local_id: LocalIdentityId,
4173        peer_id: PeerId,
4174        header: &PacketHeader,
4175        frame: &[u8],
4176    ) -> Option<ReplayVerdict> {
4177        let Some((counter, mic)) = Self::replay_metadata(header, frame) else {
4178            return None;
4179        };
4180        let now_ms = self.clock.now_ms();
4181        self.identity_mut(local_id)
4182            .and_then(|slot| slot.peer_crypto_mut().get_mut(&peer_id))
4183            .map(|state| state.replay_window.check(counter, mic, now_ms))
4184    }
4185
4186    /// Whether this duplicate has earned a fresh acknowledgement, recording
4187    /// the acknowledgement when it has.
4188    ///
4189    /// The holdoff is one forwarding-confirmation window: flood copies of a
4190    /// single transmission arrive within it, while a sender that lost its
4191    /// ack cannot have retransmitted before it lapsed.
4192    fn note_acknowledgeable_unicast_duplicate(
4193        &mut self,
4194        local_id: LocalIdentityId,
4195        peer_id: PeerId,
4196        header: &PacketHeader,
4197        frame: &[u8],
4198    ) -> bool {
4199        let Some((counter, mic)) = Self::replay_metadata(header, frame) else {
4200            return false;
4201        };
4202        let now_ms = self.clock.now_ms();
4203        let holdoff_ms = self.forward_confirm_timeout_ms();
4204        self.identity_mut(local_id)
4205            .and_then(|slot| slot.peer_crypto_mut().get_mut(&peer_id))
4206            .is_some_and(|state| {
4207                state
4208                    .replay_window
4209                    .note_acknowledgeable_duplicate(counter, mic, now_ms, holdoff_ms)
4210            })
4211    }
4212
4213    fn try_accept_counter_resync_response(
4214        &mut self,
4215        local_id: LocalIdentityId,
4216        peer_id: PeerId,
4217        header: &PacketHeader,
4218        frame: &[u8],
4219        payload: &[u8],
4220    ) -> bool {
4221        let Some(nonce) = Self::echo_response_nonce(payload) else {
4222            return false;
4223        };
4224        let Some((counter, mic)) = Self::replay_metadata(header, frame) else {
4225            return false;
4226        };
4227        let now_ms = self.clock.now_ms();
4228        let Some(slot) = self.identity_mut(local_id) else {
4229            return false;
4230        };
4231        let Some(pending) = slot.pending_counter_resync().get(&peer_id).copied() else {
4232            return false;
4233        };
4234        if pending.nonce != nonce {
4235            return false;
4236        }
4237        let Some(state) = slot.peer_crypto_mut().get_mut(&peer_id) else {
4238            return false;
4239        };
4240        state.replay_window.reset(counter, now_ms);
4241        state.replay_window.accept(counter, mic, now_ms);
4242        // A resync resets the window; treat the new counter as a fresh baseline
4243        // and schedule a persist so the new boundary survives a reboot.
4244        state.persisted_rx_counter = 0;
4245        state.needs_rx_persist = true;
4246        let _ = slot.pending_counter_resync_mut().remove(&peer_id);
4247        true
4248    }
4249
4250    /// Probe a desynchronized peer with an echo request.
4251    ///
4252    /// The probe rides the same carriage as the frame that provoked it: a peer
4253    /// heard only through a channel is answered only through that channel.
4254    async fn maybe_request_counter_resync(
4255        &mut self,
4256        local_id: LocalIdentityId,
4257        peer_id: PeerId,
4258        peer_key: PublicKey,
4259        carriage: ReplyCarriage<'_>,
4260    ) {
4261        let now_ms = self.clock.now_ms();
4262        let should_send = {
4263            let Some(slot) = self.identity(local_id) else {
4264                return;
4265            };
4266            match slot.pending_counter_resync().get(&peer_id).copied() {
4267                Some(pending) => {
4268                    now_ms.saturating_sub(pending.requested_ms) >= COUNTER_RESYNC_REQUEST_RETRY_MS
4269                }
4270                None => true,
4271            }
4272        };
4273        if !should_send {
4274            return;
4275        }
4276
4277        let nonce = self.rng.next_u32();
4278        let payload =
4279            Self::build_echo_command_payload(MAC_COMMAND_ECHO_REQUEST_ID, &nonce.to_be_bytes());
4280        let options = SendOptions::default();
4281        if self
4282            .send_response(local_id, &peer_key, carriage, payload.as_slice(), &options)
4283            .await
4284            .is_ok()
4285        {
4286            if let Some(slot) = self.identity_mut(local_id) {
4287                let _ = slot.pending_counter_resync_mut().insert(
4288                    peer_id,
4289                    PendingCounterResync {
4290                        nonce,
4291                        requested_ms: now_ms,
4292                    },
4293                );
4294            }
4295        }
4296    }
4297
4298    fn store_deferred_counter_resync_frame(
4299        &mut self,
4300        local_id: LocalIdentityId,
4301        peer_id: PeerId,
4302        frame: &[u8],
4303        rx: &RxInfo,
4304        received_at_ms: u64,
4305    ) {
4306        let mut stored = Vec::new();
4307        stored
4308            .extend_from_slice(frame)
4309            .expect("received frame length must fit configured frame capacity");
4310        self.deferred_counter_resync_frame = Some(DeferredCounterResyncFrame {
4311            local_id,
4312            peer_id,
4313            frame: stored,
4314            rssi: rx.rssi,
4315            snr: rx.snr,
4316            lqi: rx.lqi,
4317            origin: rx.origin,
4318            received_at_ms,
4319        });
4320    }
4321
4322    fn take_deferred_counter_resync_frame(
4323        &mut self,
4324        local_id: LocalIdentityId,
4325        peer_id: PeerId,
4326    ) -> Option<DeferredCounterResyncFrame<FRAME>> {
4327        match self.deferred_counter_resync_frame.as_ref() {
4328            Some(deferred) if deferred.local_id == local_id && deferred.peer_id == peer_id => {
4329                self.deferred_counter_resync_frame.take()
4330            }
4331            _ => None,
4332        }
4333    }
4334
4335    fn accept_multicast_replay(
4336        &mut self,
4337        channel_id: ChannelId,
4338        peer_id: PeerId,
4339        header: &PacketHeader,
4340        frame: &[u8],
4341    ) -> bool {
4342        let Some((counter, mic)) = Self::replay_metadata(header, frame) else {
4343            return false;
4344        };
4345        let now_ms = self.clock.now_ms();
4346        let Some(channel) = self.channels.get_mut_by_id(&channel_id) else {
4347            return false;
4348        };
4349
4350        if let Some(window) = channel.replay.get_mut(&peer_id) {
4351            if window.check(counter, mic, now_ms) != ReplayVerdict::Accept {
4352                return false;
4353            }
4354            window.accept(counter, mic, now_ms);
4355            return true;
4356        }
4357
4358        let mut window = ReplayWindow::new();
4359        window.accept(counter, mic, now_ms);
4360        channel.replay.insert(peer_id, window).is_ok()
4361    }
4362
4363    fn clear_peer_slot_state(&mut self, peer_id: PeerId) {
4364        for slot in self.identities.iter_mut().filter_map(|slot| slot.as_mut()) {
4365            let _ = slot.peer_crypto_mut().remove(&peer_id);
4366            let _ = slot.pending_counter_resync_mut().remove(&peer_id);
4367        }
4368        for channel in self.channels.iter_mut() {
4369            let _ = channel.replay.remove(&peer_id);
4370        }
4371    }
4372
4373    /// Move every piece of `PeerId`-keyed state from `old_id` to `new_id`
4374    /// after a registry swap-remove relocated that peer. Mirrors the
4375    /// structures [`Self::clear_peer_slot_state`] clears; the destination
4376    /// slots were freed by the removal, so the inserts cannot overflow.
4377    fn rekey_peer_slot_state(&mut self, old_id: PeerId, new_id: PeerId) {
4378        for slot in self.identities.iter_mut().filter_map(|slot| slot.as_mut()) {
4379            if let Some(state) = slot.peer_crypto_mut().remove(&old_id) {
4380                let _ = slot.peer_crypto_mut().insert(new_id, state);
4381            }
4382            if let Some(pending) = slot.pending_counter_resync_mut().remove(&old_id) {
4383                let _ = slot.pending_counter_resync_mut().insert(new_id, pending);
4384            }
4385        }
4386        for channel in self.channels.iter_mut() {
4387            if let Some(window) = channel.replay.remove(&old_id) {
4388                let _ = channel.replay.insert(new_id, window);
4389            }
4390        }
4391        if let Some(deferred) = self.deferred_counter_resync_frame.as_mut() {
4392            if deferred.peer_id == old_id {
4393                deferred.peer_id = new_id;
4394            }
4395        }
4396    }
4397
4398    /// Ensure `key` is registered at least transiently, returning its slot.
4399    ///
4400    /// A known peer is returned as-is; an unknown one is auto-registered
4401    /// exactly like a full-source sender would be — unpinned and
4402    /// LRU-evictable, never promoted. This is the node layer's hook for
4403    /// answering a stranger (an Identity Request reply, say) whose frame
4404    /// arrived on a path that does not auto-register its source, such as a
4405    /// broadcast. Deliberately not gated on
4406    /// [`auto_register_full_key_peers`](Self::auto_register_full_key_peers):
4407    /// the caller is making an explicit per-peer decision, not opting into
4408    /// registering every full-source sender.
4409    pub fn ensure_transient_peer(&mut self, key: &PublicKey) -> Result<PeerId, AddPeerError> {
4410        if let Some((peer_id, _)) = self.peer_registry.lookup_by_key(key) {
4411            return Ok(peer_id);
4412        }
4413        self.try_auto_register_peer(*key)
4414    }
4415
4416    fn try_auto_register_peer(&mut self, key: PublicKey) -> Result<PeerId, AddPeerError> {
4417        #[cfg(feature = "software-crypto")]
4418        {
4419            if !umsh_crypto::is_valid_ed25519_public_key(&key) {
4420                return Err(AddPeerError::InvalidPublicKey);
4421            }
4422        }
4423        let now_ms = self.clock.now_ms();
4424        let outcome = self.peer_registry.try_insert_or_update_auto(key, now_ms)?;
4425        if outcome.evicted_key.is_some() {
4426            self.clear_peer_slot_state(outcome.peer_id);
4427        }
4428        Ok(outcome.peer_id)
4429    }
4430
4431    fn replay_metadata<'a>(header: &PacketHeader, frame: &'a [u8]) -> Option<(u32, &'a [u8])> {
4432        let counter = header.sec_info?.frame_counter;
4433        let mic = frame.get(header.mic_range.clone())?;
4434        Some((counter, mic))
4435    }
4436
4437    fn build_echo_command_payload(command_id: u8, data: &[u8]) -> Vec<u8, FRAME> {
4438        let mut payload = Vec::new();
4439        let _ = payload.push(PayloadType::MacCommand as u8);
4440        let _ = payload.push(command_id);
4441        let _ = payload.extend_from_slice(data);
4442        payload
4443    }
4444
4445    fn echo_request_data(payload: &[u8]) -> Option<&[u8]> {
4446        let (&payload_type, rest) = payload.split_first()?;
4447        let (&command_id, data) = rest.split_first()?;
4448        if PayloadType::from_byte(payload_type)? != PayloadType::MacCommand
4449            || command_id != MAC_COMMAND_ECHO_REQUEST_ID
4450        {
4451            return None;
4452        }
4453        Some(data)
4454    }
4455
4456    /// Whether a frame carries a trace-route option, and so is discovering a
4457    /// path whose reply has to trace its own way back.
4458    fn frame_requests_trace_route(frame: &[u8], header: &PacketHeader) -> bool {
4459        ParsedOptions::extract(frame, header.options_range.clone())
4460            .map(|parsed| parsed.trace_route.is_some())
4461            .unwrap_or(false)
4462    }
4463
4464    /// Mirror the requester's trace options onto the echo response.
4465    ///
4466    /// Both are mirrored rather than just the route: a ping measures a link,
4467    /// and the signal entries are what make that measurement per-hop instead
4468    /// of end-to-end. Mirroring also keeps the reply the same shape as the
4469    /// request, which is the point of a ping — a response routed or sized
4470    /// differently from the traffic it stands in for measures a path that
4471    /// traffic will not take.
4472    ///
4473    /// Both are requests, not guarantees. A response that ends up with no
4474    /// flood budget and no source route drops them again on the way out; see
4475    /// [`Mac::frame_is_repeatable`].
4476    fn echo_response_options(frame: &[u8], header: &PacketHeader) -> SendOptions {
4477        let mut options = SendOptions::default();
4478        let Ok(parsed) = ParsedOptions::extract(frame, header.options_range.clone()) else {
4479            return options;
4480        };
4481        if parsed.trace_route.is_some() {
4482            options = options.with_trace_route();
4483        }
4484        if parsed.trace_signal.is_some() {
4485            options = options.with_trace_signal();
4486        }
4487        options
4488    }
4489
4490    fn echo_response_nonce(payload: &[u8]) -> Option<u32> {
4491        let (&payload_type, rest) = payload.split_first()?;
4492        let (&command_id, data) = rest.split_first()?;
4493        if PayloadType::from_byte(payload_type)? != PayloadType::MacCommand
4494            || command_id != MAC_COMMAND_ECHO_RESPONSE_ID
4495            || data.len() != COUNTER_RESYNC_NONCE_LEN
4496        {
4497            return None;
4498        }
4499        Some(u32::from_be_bytes(data.try_into().ok()?))
4500    }
4501
4502    fn full_key_at(frame: &[u8], offset: usize) -> Option<PublicKey> {
4503        let mut key = [0u8; 32];
4504        key.copy_from_slice(frame.get(offset..offset + 32)?);
4505        Some(PublicKey(key))
4506    }
4507
4508    fn find_local_identity_for_dst(
4509        &self,
4510        dst: Option<umsh_core::NodeHint>,
4511    ) -> Option<LocalIdentityId> {
4512        let dst = dst?;
4513        self.identities
4514            .iter()
4515            .enumerate()
4516            .find(|(_, slot)| {
4517                slot.as_ref()
4518                    .map(|slot| slot.identity().public_key().hint() == dst)
4519                    .unwrap_or(false)
4520            })
4521            .map(|(index, _)| LocalIdentityId(index as u8))
4522    }
4523
4524    /// True when this frame names one of our own identities as its source.
4525    ///
4526    /// A repeater is not a relay for itself. Re-flooding a packet we sent
4527    /// wastes airtime, and because the forwarding rewrite prepends our router
4528    /// hint to the trace route it also fabricates a hop that never happened —
4529    /// the destination then learns a return path that starts by routing back
4530    /// through the originator.
4531    fn is_locally_originated(&self, frame: &[u8], header: &PacketHeader) -> bool {
4532        let mut locals = self.identities.iter().filter_map(|slot| slot.as_ref());
4533        match header.source {
4534            SourceAddrRef::Hint(hint) => {
4535                locals.any(|slot| slot.identity().public_key().hint() == hint)
4536            }
4537            SourceAddrRef::FullKeyAt { offset } => match Self::full_key_at(frame, offset) {
4538                Some(key) => locals.any(|slot| *slot.identity().public_key() == key),
4539                None => false,
4540            },
4541            // An encrypted blind-unicast source is unreadable, and a packet
4542            // with no source field names nobody.
4543            SourceAddrRef::Encrypted { .. } | SourceAddrRef::None => false,
4544        }
4545    }
4546
4547    fn resolve_source_peer_candidates(
4548        &mut self,
4549        frame: &[u8],
4550        header: &PacketHeader,
4551    ) -> Vec<(PeerId, PublicKey), PEERS> {
4552        match header.source {
4553            SourceAddrRef::FullKeyAt { offset } => {
4554                let Some(peer_key) = Self::full_key_at(frame, offset) else {
4555                    return Vec::new();
4556                };
4557
4558                if let Some((peer_id, _)) = self.peer_registry.lookup_by_key(&peer_key) {
4559                    let mut out = Vec::new();
4560                    let _ = out.push((peer_id, peer_key));
4561                    return out;
4562                }
4563
4564                if self.auto_register_full_key_peers {
4565                    if let Ok(peer_id) = self.try_auto_register_peer(peer_key) {
4566                        let mut out = Vec::new();
4567                        let _ = out.push((peer_id, peer_key));
4568                        return out;
4569                    }
4570                }
4571
4572                Vec::new()
4573            }
4574            SourceAddrRef::Hint(hint) => self
4575                .peer_registry
4576                .lookup_by_hint(&hint)
4577                .map(|(peer_id, info)| (peer_id, info.public_key))
4578                .collect(),
4579            SourceAddrRef::Encrypted { .. } | SourceAddrRef::None => Vec::new(),
4580        }
4581    }
4582
4583    fn resolve_multicast_source(
4584        &mut self,
4585        frame: &[u8],
4586        header: &PacketHeader,
4587    ) -> Option<ResolvedMulticastSource> {
4588        match header.source {
4589            SourceAddrRef::FullKeyAt { offset } => {
4590                let mut key = [0u8; 32];
4591                key.copy_from_slice(frame.get(offset..offset + 32)?);
4592                let public_key = PublicKey(key);
4593                let peer_id = self
4594                    .peer_registry
4595                    .lookup_by_key(&public_key)
4596                    .map(|(peer_id, _)| peer_id);
4597                Some(ResolvedMulticastSource {
4598                    peer_id,
4599                    public_key: Some(public_key),
4600                    hint: Some(public_key.hint()),
4601                })
4602            }
4603            SourceAddrRef::Hint(hint) => {
4604                let resolved = self.resolve_unique_hint(hint);
4605                Some(ResolvedMulticastSource {
4606                    peer_id: resolved.map(|(peer_id, _)| peer_id),
4607                    public_key: resolved.map(|(_, key)| key),
4608                    hint: Some(hint),
4609                })
4610            }
4611            SourceAddrRef::Encrypted { offset, len } => match len {
4612                32 => {
4613                    let mut key = [0u8; 32];
4614                    key.copy_from_slice(frame.get(offset..offset + 32)?);
4615                    let public_key = PublicKey(key);
4616                    let peer_id = self
4617                        .peer_registry
4618                        .lookup_by_key(&public_key)
4619                        .map(|(peer_id, _)| peer_id);
4620                    Some(ResolvedMulticastSource {
4621                        peer_id,
4622                        public_key: Some(public_key),
4623                        hint: Some(public_key.hint()),
4624                    })
4625                }
4626                3 => {
4627                    let hint = umsh_core::NodeHint([
4628                        *frame.get(offset)?,
4629                        *frame.get(offset + 1)?,
4630                        *frame.get(offset + 2)?,
4631                    ]);
4632                    let resolved = self.resolve_unique_hint(hint);
4633                    Some(ResolvedMulticastSource {
4634                        peer_id: resolved.map(|(peer_id, _)| peer_id),
4635                        public_key: resolved.map(|(_, key)| key),
4636                        hint: Some(hint),
4637                    })
4638                }
4639                _ => None,
4640            },
4641            SourceAddrRef::None => None,
4642        }
4643    }
4644
4645    fn accept_unknown_multicast_replay(&mut self, header: &PacketHeader, frame: &[u8]) -> bool {
4646        let Some(cache_key) = Self::forward_dup_key(header, frame) else {
4647            return false;
4648        };
4649        let now_ms = self.clock.now_ms();
4650        if self
4651            .multicast_unknown_dup_cache
4652            .contains(&cache_key, now_ms)
4653        {
4654            return false;
4655        }
4656        self.multicast_unknown_dup_cache.insert(cache_key, now_ms);
4657        true
4658    }
4659
4660    fn resolve_blind_source_peer_candidates(
4661        &mut self,
4662        frame: &[u8],
4663        source: SourceAddrRef,
4664    ) -> Vec<(PeerId, PublicKey), PEERS> {
4665        match source {
4666            SourceAddrRef::FullKeyAt { offset } => {
4667                let Some(peer_key) = Self::full_key_at(frame, offset) else {
4668                    return Vec::new();
4669                };
4670
4671                if let Some((peer_id, _)) = self.peer_registry.lookup_by_key(&peer_key) {
4672                    let mut out = Vec::new();
4673                    let _ = out.push((peer_id, peer_key));
4674                    return out;
4675                }
4676
4677                if self.auto_register_full_key_peers {
4678                    if let Ok(peer_id) = self.try_auto_register_peer(peer_key) {
4679                        let mut out = Vec::new();
4680                        let _ = out.push((peer_id, peer_key));
4681                        return out;
4682                    }
4683                }
4684
4685                Vec::new()
4686            }
4687            SourceAddrRef::Hint(hint) => self
4688                .peer_registry
4689                .lookup_by_hint(&hint)
4690                .map(|(peer_id, info)| (peer_id, info.public_key))
4691                .collect(),
4692            SourceAddrRef::Encrypted { .. } | SourceAddrRef::None => Vec::new(),
4693        }
4694    }
4695
4696    fn resolve_unique_hint(&self, hint: umsh_core::NodeHint) -> Option<(PeerId, PublicKey)> {
4697        let mut matches = self.peer_registry.lookup_by_hint(&hint);
4698        let (peer_id, info) = matches.next()?;
4699        if matches.next().is_some() {
4700            return None;
4701        }
4702        Some((peer_id, info.public_key))
4703    }
4704
4705    /// Record who was heard, and how well, from any frame off the air.
4706    ///
4707    /// Distinct from route learning, which records the way to a *peer*: this
4708    /// is the transmitter, whoever it was talking to. A frame forwarded past
4709    /// this node or simply overheard teaches nothing about a peer and never
4710    /// reaches the host, but it is exactly what proves a neighboring repeater
4711    /// is on the air — which is what a peer-repeater listing reports.
4712    ///
4713    /// Repeaters prepend to a trace route, so a trace's first hint is the hop
4714    /// just heard. Without one the frame came off the originator's own
4715    /// transmitter, and the originator is who was heard.
4716    fn note_transmitter_observation(&mut self, frame: &[u8], rx: &RxInfo) {
4717        // Only a real reception carries measurements; a loopback or a
4718        // backhauled frame would record a link that has no radio in it.
4719        if !rx.origin.is_measured() {
4720            return;
4721        }
4722        let Ok(header) = PacketHeader::parse(frame) else {
4723            return;
4724        };
4725        let hint = ParsedOptions::extract(frame, header.options_range.clone())
4726            .ok()
4727            .and_then(|options| options.trace_route)
4728            .and_then(|range| frame.get(range))
4729            .and_then(|trace| trace.get(..2))
4730            .map(|hint| RouterHint([hint[0], hint[1]]))
4731            .or_else(|| match header.source {
4732                SourceAddrRef::Hint(hint) => Some(RouterHint([hint.0[0], hint.0[1]])),
4733                SourceAddrRef::FullKeyAt { offset } => frame
4734                    .get(offset..offset + 32)
4735                    .and_then(|bytes| <[u8; 32]>::try_from(bytes).ok())
4736                    .map(|bytes| {
4737                        let node = PublicKey(bytes).hint();
4738                        RouterHint([node.0[0], node.0[1]])
4739                    }),
4740                // A blind frame hides who sent it, so there is nobody here
4741                // to record.
4742                SourceAddrRef::None | SourceAddrRef::Encrypted { .. } => None,
4743            });
4744        let Some(hint) = hint else {
4745            return;
4746        };
4747        let now_ms = self.clock.now_ms();
4748        self.transmitter_observations
4749            .observe(hint, rx.rssi, rx.snr, now_ms);
4750    }
4751
4752    fn learn_route_for_peer(&mut self, peer_id: PeerId, frame: &[u8], header: &PacketHeader) {
4753        let now_ms = self.clock.now_ms();
4754        self.peer_registry.touch(peer_id, now_ms);
4755
4756        let Ok(options) = ParsedOptions::extract(frame, header.options_range.clone()) else {
4757            return;
4758        };
4759
4760        if let Some(trace_range) = options.trace_route {
4761            if let Some(route) = self.source_route_from_trace(frame.get(trace_range).unwrap_or(&[]))
4762            {
4763                // A trace route that accumulated no hints means the packet
4764                // reached us without passing through a repeater. That is a
4765                // direct neighbour, not a zero-hop source route — caching it
4766                // as a route would attach an empty SourceRoute option to
4767                // everything we send back. Only a repeater consuming the
4768                // final hint may leave an empty option behind, for
4769                // provenance; an originator must never add one.
4770                let learned = if route.is_empty() {
4771                    crate::CachedRoute::Direct
4772                } else {
4773                    crate::CachedRoute::Source(route)
4774                };
4775                self.peer_registry.update_route(peer_id, learned);
4776                return;
4777            }
4778        }
4779
4780        // A source-routed packet — including one whose hints are all consumed,
4781        // since the emptied option is preserved for provenance — spends flood
4782        // budget only after the route runs out. Its `FHOPS_ACC` therefore
4783        // counts the tail of the path, not its length, and would understate
4784        // how far away the peer is. Leave whatever route is already cached
4785        // (that route is what carried this packet here) rather than replacing
4786        // it with a distance estimate that is known to be short.
4787        if options.source_route.is_some() {
4788            return;
4789        }
4790
4791        let Some(flood_hops) = header.flood_hops else {
4792            // No flood budget and no source route: nothing on this frame gave
4793            // any repeater permission to carry it, so the only way it reached
4794            // us is off the sender's own transmitter. That is the same
4795            // evidence an empty trace route carries, read off the frame's
4796            // shape instead of out of an option — which is why a frame like
4797            // this is not worth spending a trace route on.
4798            self.peer_registry
4799                .update_route(peer_id, crate::CachedRoute::Direct);
4800            return;
4801        };
4802
4803        let regions = Self::region_codes_from_options(frame, header.options_range.clone());
4804        self.peer_registry.update_route(
4805            peer_id,
4806            crate::CachedRoute::Flood {
4807                hops: flood_hops.accumulated(),
4808                regions,
4809            },
4810        );
4811    }
4812
4813    fn region_codes_from_options(
4814        frame: &[u8],
4815        options_range: core::ops::Range<usize>,
4816    ) -> Vec<[u8; 2], 8> {
4817        let mut regions = Vec::new();
4818        if options_range.is_empty() {
4819            return regions;
4820        }
4821        for entry in umsh_core::iter_options(frame, options_range) {
4822            let Ok((number, value)) = entry else {
4823                continue;
4824            };
4825            if OptionNumber::from(number) != OptionNumber::RegionCode || value.len() != 2 {
4826                continue;
4827            }
4828            if regions.push([value[0], value[1]]).is_err() {
4829                break;
4830            }
4831        }
4832        regions
4833    }
4834
4835    fn resolve_broadcast_source(
4836        frame: &[u8],
4837        header: &PacketHeader,
4838    ) -> Option<(umsh_core::NodeHint, Option<PublicKey>)> {
4839        match header.source {
4840            SourceAddrRef::Hint(hint) => Some((hint, None)),
4841            SourceAddrRef::FullKeyAt { offset } => {
4842                let mut key = [0u8; 32];
4843                key.copy_from_slice(frame.get(offset..offset + 32)?);
4844                let public_key = PublicKey(key);
4845                Some((public_key.hint(), Some(public_key)))
4846            }
4847            SourceAddrRef::Encrypted { .. } | SourceAddrRef::None => None,
4848        }
4849    }
4850
4851    fn payload_is_allowed(packet_type: PacketType, payload: &[u8]) -> bool {
4852        if payload.is_empty() {
4853            return true;
4854        }
4855
4856        PayloadType::from_byte(payload[0])
4857            .unwrap_or(PayloadType::Empty)
4858            .allowed_for(packet_type)
4859    }
4860
4861    fn source_route_from_trace(
4862        &self,
4863        trace_bytes: &[u8],
4864    ) -> Option<heapless::Vec<RouterHint, { crate::MAX_SOURCE_ROUTE_HOPS }>> {
4865        if trace_bytes.len() % 2 != 0 {
4866            return None;
4867        }
4868
4869        let mut route = heapless::Vec::new();
4870        for chunk in trace_bytes.chunks_exact(2) {
4871            route.push(RouterHint([chunk[0], chunk[1]])).ok()?;
4872        }
4873        Some(route)
4874    }
4875
4876    fn should_emit_destination_ack(&self, frame: &[u8], header: &PacketHeader) -> bool {
4877        let Ok(options) = ParsedOptions::extract(frame, header.options_range.clone()) else {
4878            return false;
4879        };
4880
4881        // Destination ACKs are emitted only once a source route is fully
4882        // consumed. An empty SourceRoute still matters for provenance, but it
4883        // no longer constrains forwarding and therefore counts as "at the
4884        // destination" for ACK purposes.
4885        options
4886            .source_route
4887            .map(|range| range.is_empty())
4888            .unwrap_or(true)
4889    }
4890
4891    fn maybe_forward_received(
4892        &mut self,
4893        frame: &[u8],
4894        header: &PacketHeader,
4895        rx: &RxInfo,
4896        locally_handled_unicast: bool,
4897    ) -> bool {
4898        if !self.repeater.enabled {
4899            return false;
4900        }
4901        if !header.packet_type().is_routable() {
4902            return false;
4903        }
4904        // A frame this radio just put on the air must never be repeated,
4905        // whoever composed it. `is_locally_originated` cannot see this:
4906        // the source is whichever stack sharing the antenna sent it, and
4907        // an attached host's identity is not one of ours. Recording the
4908        // duplicate key is the other half — a neighbor's repeat of the
4909        // same frame will arrive shortly, and repeating that would put a
4910        // frame back on the air that already left this antenna once.
4911        if rx.origin == RxOrigin::LocalTx {
4912            if let Some(cache_key) = Self::forward_dup_key(header, frame) {
4913                self.dup_cache.insert(cache_key, self.clock.now_ms());
4914            }
4915            return false;
4916        }
4917        if self.is_locally_originated(frame, header) {
4918            return false;
4919        }
4920        // A packet that names one of our identities as its destination has
4921        // arrived. Whether we could actually process it — we may lack the key,
4922        // or it may be a replay — is a separate question from whether it still
4923        // needs carrying, and it does not.
4924        if self.find_local_identity_for_dst(header.dst).is_some() {
4925            return false;
4926        }
4927        // Once a point-to-point packet is handled by its actual destination, it
4928        // should not also be repeated by that same node. This still matters for
4929        // encrypted blind unicast, whose destination hint is not on the wire.
4930        // Other packet classes, including broadcast and MAC ACK, remain
4931        // routable.
4932        if locally_handled_unicast
4933            && matches!(
4934                header.packet_type(),
4935                PacketType::Unicast
4936                    | PacketType::UnicastAckReq
4937                    | PacketType::BlindUnicast
4938                    | PacketType::BlindUnicastAckReq
4939            )
4940        {
4941            return false;
4942        }
4943
4944        let Ok(options) = ParsedOptions::extract(frame, header.options_range.clone()) else {
4945            return false;
4946        };
4947        let Some(cache_key) = Self::forward_dup_key(header, frame) else {
4948            return false;
4949        };
4950        if self.dup_cache.contains(&cache_key, self.clock.now_ms()) {
4951            self.defer_pending_forward(&cache_key, header, rx, &options);
4952            return false;
4953        }
4954
4955        let Some(plan) = self.plan_forwarding(frame, header, &options, rx) else {
4956            return false;
4957        };
4958
4959        let mut rewritten = [0u8; FRAME];
4960        let Ok(total_len) =
4961            self.rewrite_forwarded_frame(frame, header, &options, plan, &mut rewritten)
4962        else {
4963            return false;
4964        };
4965        if total_len > self.radio.max_frame_size() {
4966            return false;
4967        }
4968
4969        let now_ms = self.clock.now_ms();
4970        if self
4971            .tx_queue
4972            .enqueue_with_state(
4973                TxPriority::Forward,
4974                &rewritten[..total_len],
4975                None,
4976                None,
4977                now_ms.saturating_add(plan.delay_ms),
4978                0,
4979                0,
4980            )
4981            .is_err()
4982        {
4983            return false;
4984        }
4985        self.dup_cache.insert(cache_key, now_ms);
4986        // Counted at the decision to repeat rather than at the eventual
4987        // transmit, so the tally measures what this node chose to carry.
4988        // The frame is also counted again in `tx_frames` when it actually
4989        // goes out, which is the correct relationship: a repeat that CAD
4990        // abandons shows up here and not there.
4991        MacCounters::bump(&mut self.counters.forwarded);
4992        true
4993    }
4994
4995    fn plan_forwarding(
4996        &mut self,
4997        frame: &[u8],
4998        header: &PacketHeader,
4999        options: &ParsedOptions,
5000        rx: &RxInfo,
5001    ) -> Option<ForwardPlan> {
5002        if options.has_unknown_critical {
5003            return None;
5004        }
5005
5006        let router_hint = self.repeater_router_hint()?;
5007        let station_action = self.classify_forward_station_action(frame, header)?;
5008
5009        let source_route_bytes = options
5010            .source_route
5011            .as_ref()
5012            .and_then(|range| frame.get(range.clone()))
5013            .unwrap_or(&[]);
5014        if source_route_bytes.len() % 2 != 0 {
5015            return None;
5016        }
5017
5018        let mut consume_source_route = false;
5019        let mut decrement_flood_hops = false;
5020        let mut insert_region_code = None;
5021        let mut delay_ms = 0u64;
5022
5023        if !source_route_bytes.is_empty() {
5024            // A source-routed hop costs no flood budget, even the one that
5025            // consumes the last hint: `FHOPS` counts flood hops only
5026            // (packet-structure.md § Flood Hop Count). Emptying the route
5027            // makes the packet floodable, but the transition is observed by
5028            // the *next* repeater, which sees an empty route and pays for the
5029            // first real flood hop. Everything gated below — hop accounting,
5030            // signal thresholds, region policy, contention delay — is flood
5031            // behavior and does not apply to a hop that was named explicitly.
5032            if source_route_bytes[..2] != router_hint.0 {
5033                return None;
5034            }
5035            consume_source_route = true;
5036        } else {
5037            decrement_flood_hops = true;
5038        }
5039
5040        if decrement_flood_hops {
5041            let flood_hops = header.flood_hops?;
5042            // From here to the end of this branch, every refusal is the
5043            // operator's policy talking about a frame this node would
5044            // otherwise have carried — which is exactly what
5045            // `forward_dropped_policy` means. The `?` above is not one of
5046            // them: a packet with no flood-hop field was never floodable.
5047            if flood_hops.remaining() == 0 {
5048                MacCounters::bump(&mut self.counters.forward_dropped_policy);
5049                return None;
5050            }
5051            // Signal-quality filtering applies only to flood forwarding,
5052            // not to source-routed hops — and only to frames that arrived
5053            // over a radio. A minimum RSSI asks how far away the sender
5054            // was; over a wire the question has no answer, and any value
5055            // the comparison reads is one nobody measured.
5056            if rx.origin.is_measured() {
5057                if let Some(min_rssi) = Self::effective_min_rssi(options, &self.repeater) {
5058                    if rx.rssi < min_rssi {
5059                        MacCounters::bump(&mut self.counters.forward_dropped_policy);
5060                        return None;
5061                    }
5062                }
5063                if let Some(min_snr) = Self::effective_min_snr(options, &self.repeater) {
5064                    if rx.snr < Snr::from_decibels(min_snr) {
5065                        MacCounters::bump(&mut self.counters.forward_dropped_policy);
5066                        return None;
5067                    }
5068                }
5069            }
5070            let mut saw_region_code = false;
5071            let mut matched_region_code = false;
5072            if !header.options_range.is_empty() {
5073                for entry in umsh_core::iter_options(frame, header.options_range.clone()) {
5074                    let (number, value) = entry.ok()?;
5075                    if OptionNumber::from(number) != OptionNumber::RegionCode || value.len() != 2 {
5076                        continue;
5077                    }
5078                    saw_region_code = true;
5079                    let region_code = [value[0], value[1]];
5080                    if self
5081                        .repeater
5082                        .regions
5083                        .iter()
5084                        .any(|configured| *configured == region_code)
5085                    {
5086                        matched_region_code = true;
5087                    }
5088                }
5089            }
5090            if saw_region_code {
5091                // An empty configured list imposes no regional restriction.
5092                if !self.repeater.regions.is_empty() && !matched_region_code {
5093                    MacCounters::bump(&mut self.counters.forward_dropped_policy);
5094                    return None;
5095                }
5096            } else {
5097                insert_region_code = self.repeater.default_region;
5098            }
5099            delay_ms = self.sample_flood_contention_delay_ms(rx, options);
5100            // An ack-requested packet flood-forwarded with no remaining
5101            // source-route hops elicits an immediate, CAD-skipping ACK from its
5102            // destination (channel-access.md § Immediate ACK Transmission);
5103            // hold the forward back long enough for that ACK to clear. Reaching
5104            // this branch already means the source route is exhausted, so the
5105            // "no remaining hops" half of the condition is satisfied here by
5106            // construction; the deferral path, which judges an overheard copy
5107            // that may still be routed, tests it explicitly.
5108            if header.ack_requested() {
5109                delay_ms = delay_ms.saturating_add(self.ack_guard_delay_ms());
5110            }
5111        }
5112
5113        Some(ForwardPlan {
5114            router_hint,
5115            consume_source_route,
5116            decrement_flood_hops,
5117            insert_region_code,
5118            delay_ms,
5119            station_action,
5120            signal: trace_signal_entry(rx),
5121        })
5122    }
5123
5124    fn classify_forward_station_action(
5125        &self,
5126        frame: &[u8],
5127        header: &PacketHeader,
5128    ) -> Option<ForwardStationAction> {
5129        let has_operator_callsign = if header.options_range.is_empty() {
5130            false
5131        } else {
5132            umsh_core::iter_options(frame, header.options_range.clone())
5133                .filter_map(Result::ok)
5134                .any(|(number, _)| OptionNumber::from(number) == OptionNumber::OperatorCallsign)
5135        };
5136        let encrypted = header
5137            .sec_info
5138            .map(|sec| sec.scf.encrypted())
5139            .unwrap_or(false);
5140
5141        match self.repeater.amateur_radio_mode {
5142            AmateurRadioMode::Unlicensed => Some(ForwardStationAction::Remove),
5143            AmateurRadioMode::LicensedOnly => {
5144                if encrypted || !has_operator_callsign || self.repeater.station_callsign.is_none() {
5145                    None
5146                } else {
5147                    Some(ForwardStationAction::Replace)
5148                }
5149            }
5150            AmateurRadioMode::Hybrid => {
5151                if !encrypted && has_operator_callsign {
5152                    self.repeater
5153                        .station_callsign
5154                        .as_ref()
5155                        .map(|_| ForwardStationAction::Replace)
5156                } else {
5157                    Some(ForwardStationAction::Remove)
5158                }
5159            }
5160        }
5161    }
5162
5163    fn rewrite_forwarded_frame(
5164        &self,
5165        src: &[u8],
5166        header: &PacketHeader,
5167        options: &ParsedOptions,
5168        plan: ForwardPlan,
5169        dst: &mut [u8],
5170    ) -> Result<usize, CapacityError> {
5171        if dst.is_empty() {
5172            return Err(CapacityError);
5173        }
5174
5175        // FCF (no options bit in new format)
5176        dst[0] = umsh_core::Fcf::new(
5177            header.packet_type(),
5178            header.fcf.full_source(),
5179            header.flood_hops.is_some(),
5180        )
5181        .0;
5182        let mut cursor = 1;
5183
5184        // FHOPS
5185        if let Some(flood_hops) = header.flood_hops {
5186            let next = if plan.decrement_flood_hops {
5187                flood_hops.decremented().0
5188            } else {
5189                flood_hops.0
5190            };
5191            *dst.get_mut(cursor).ok_or(CapacityError)? = next;
5192            cursor += 1;
5193        }
5194
5195        // Fixed core: DST/CHANNEL/SRC/SECINFO from original (between FHOPS and options)
5196        let fhops_len = usize::from(header.flood_hops.is_some());
5197        let fixed_core = src
5198            .get(1 + fhops_len..header.options_range.start)
5199            .ok_or(CapacityError)?;
5200        let core_end = cursor + fixed_core.len();
5201        dst.get_mut(cursor..core_end)
5202            .ok_or(CapacityError)?
5203            .copy_from_slice(fixed_core);
5204        cursor = core_end;
5205
5206        // Re-encoded options (without 0xFF — caller emits marker)
5207        let options_len =
5208            self.encode_forwarded_options(src, header, options, plan, &mut dst[cursor..])?;
5209        cursor += options_len;
5210
5211        // 0xFF marker when a body follows (not needed for MAC ack whose trailer is at fixed offset)
5212        let needs_marker = !matches!(header.packet_type(), PacketType::MacAck)
5213            && header.options_range.end < header.total_len;
5214        if needs_marker {
5215            *dst.get_mut(cursor).ok_or(CapacityError)? = 0xFF;
5216            cursor += 1;
5217        }
5218
5219        // Body + MIC from original (src[options_range.end] is already past the original 0xFF)
5220        let tail = src
5221            .get(header.options_range.end..header.total_len)
5222            .ok_or(CapacityError)?;
5223        let end = cursor + tail.len();
5224        dst.get_mut(cursor..end)
5225            .ok_or(CapacityError)?
5226            .copy_from_slice(tail);
5227        Ok(end)
5228    }
5229
5230    fn encode_forwarded_options(
5231        &self,
5232        src: &[u8],
5233        header: &PacketHeader,
5234        _options: &ParsedOptions,
5235        plan: ForwardPlan,
5236        dst: &mut [u8],
5237    ) -> Result<usize, CapacityError> {
5238        let mut encoder = OptionEncoder::new(dst);
5239        let mut inserted_region = false;
5240        let mut inserted_station = false;
5241        let mut saw_station = false;
5242
5243        if !header.options_range.is_empty() {
5244            for entry in umsh_core::iter_options(src, header.options_range.clone()) {
5245                let (number, value) = entry.map_err(|_| CapacityError)?;
5246                if !inserted_region {
5247                    if let Some(region_code) = plan.insert_region_code {
5248                        if number > OptionNumber::RegionCode.as_u16() {
5249                            encoder
5250                                .put(OptionNumber::RegionCode.as_u16(), &region_code)
5251                                .map_err(|_| CapacityError)?;
5252                            inserted_region = true;
5253                        }
5254                    }
5255                }
5256                if !inserted_station
5257                    && matches!(plan.station_action, ForwardStationAction::Replace)
5258                    && number > OptionNumber::StationCallsign.as_u16()
5259                {
5260                    encoder
5261                        .put(
5262                            OptionNumber::StationCallsign.as_u16(),
5263                            self.repeater
5264                                .station_callsign
5265                                .as_ref()
5266                                .ok_or(CapacityError)?
5267                                .as_trimmed_slice(),
5268                        )
5269                        .map_err(|_| CapacityError)?;
5270                    inserted_station = true;
5271                }
5272
5273                match OptionNumber::from(number) {
5274                    OptionNumber::RegionCode => {
5275                        inserted_region = true;
5276                        encoder.put(number, value).map_err(|_| CapacityError)?;
5277                    }
5278                    // Both trace arms bound the incoming value before the
5279                    // copy: the accumulated trace arrives from the air, and
5280                    // one grown past what the local buffer can extend is
5281                    // over-limit input to drop, not to index by.
5282                    OptionNumber::TraceRoute => {
5283                        let mut trace = [0u8; crate::MAX_SOURCE_ROUTE_HOPS * 2 + 2];
5284                        if value.len() > crate::MAX_SOURCE_ROUTE_HOPS * 2 {
5285                            return Err(CapacityError);
5286                        }
5287                        trace[..2].copy_from_slice(&plan.router_hint.0);
5288                        trace[2..2 + value.len()].copy_from_slice(value);
5289                        encoder
5290                            .put(number, &trace[..2 + value.len()])
5291                            .map_err(|_| CapacityError)?;
5292                    }
5293                    // Prepended in lockstep with the router hint above: entry
5294                    // N of this option is the signal quality at which the
5295                    // repeater named by hint N received the frame.
5296                    OptionNumber::TraceSignal => {
5297                        let mut trace = [0u8; crate::MAX_SOURCE_ROUTE_HOPS * 2 + 2];
5298                        if value.len() > crate::MAX_SOURCE_ROUTE_HOPS * 2 {
5299                            return Err(CapacityError);
5300                        }
5301                        trace[..2].copy_from_slice(&plan.signal.as_bytes());
5302                        trace[2..2 + value.len()].copy_from_slice(value);
5303                        encoder
5304                            .put(number, &trace[..2 + value.len()])
5305                            .map_err(|_| CapacityError)?;
5306                    }
5307                    OptionNumber::SourceRoute if plan.consume_source_route => {
5308                        if value.len() < 2 || value.len() % 2 != 0 {
5309                            return Err(CapacityError);
5310                        }
5311                        let remaining = if value.len() > 2 { &value[2..] } else { &[] };
5312                        encoder.put(number, remaining).map_err(|_| CapacityError)?;
5313                    }
5314                    OptionNumber::StationCallsign => {
5315                        saw_station = true;
5316                        match plan.station_action {
5317                            ForwardStationAction::Remove => {}
5318                            ForwardStationAction::Replace => {
5319                                encoder
5320                                    .put(
5321                                        number,
5322                                        self.repeater
5323                                            .station_callsign
5324                                            .as_ref()
5325                                            .ok_or(CapacityError)?
5326                                            .as_trimmed_slice(),
5327                                    )
5328                                    .map_err(|_| CapacityError)?;
5329                                inserted_station = true;
5330                            }
5331                        }
5332                    }
5333                    _ => {
5334                        encoder.put(number, value).map_err(|_| CapacityError)?;
5335                    }
5336                }
5337            }
5338        }
5339
5340        if matches!(plan.station_action, ForwardStationAction::Replace)
5341            && !inserted_station
5342            && !saw_station
5343        {
5344            encoder
5345                .put(
5346                    OptionNumber::StationCallsign.as_u16(),
5347                    self.repeater
5348                        .station_callsign
5349                        .as_ref()
5350                        .ok_or(CapacityError)?
5351                        .as_trimmed_slice(),
5352                )
5353                .map_err(|_| CapacityError)?;
5354        }
5355        if let Some(region_code) = plan.insert_region_code {
5356            if !inserted_region {
5357                encoder
5358                    .put(OptionNumber::RegionCode.as_u16(), &region_code)
5359                    .map_err(|_| CapacityError)?;
5360            }
5361        }
5362        Ok(encoder.finish())
5363    }
5364
5365    fn synthesize_route_retry_resend(
5366        &self,
5367        peer: &PublicKey,
5368        resend: &ResendRecord<FRAME>,
5369    ) -> Option<ResendRecord<FRAME>> {
5370        let header = PacketHeader::parse(resend.frame.as_slice()).ok()?;
5371        let options =
5372            ParsedOptions::extract(resend.frame.as_slice(), header.options_range.clone()).ok()?;
5373        if options.route_retry {
5374            return None;
5375        }
5376        let source_route = resend
5377            .source_route
5378            .as_ref()
5379            .filter(|route| !route.is_empty());
5380        if source_route.is_none() && resend.requested_flood_hops.is_none() {
5381            // Neither a route to abandon nor an application budget to restore.
5382            return None;
5383        }
5384
5385        let flood_hops =
5386            self.route_retry_flood_hops(peer, &header, source_route, resend.requested_flood_hops)?;
5387        let has_flood_hops = flood_hops > 0;
5388        let mut rewritten = [0u8; FRAME];
5389
5390        // FCF
5391        rewritten[0] = umsh_core::Fcf::new(
5392            header.packet_type(),
5393            header.fcf.full_source(),
5394            has_flood_hops,
5395        )
5396        .0;
5397        let mut cursor = 1;
5398
5399        // FHOPS
5400        if has_flood_hops {
5401            *rewritten.get_mut(cursor)? = FloodHops::new(flood_hops, 0)?.0;
5402            cursor += 1;
5403        }
5404
5405        // Fixed core: DST/CHANNEL/SRC/SECINFO from original
5406        let fhops_len = usize::from(header.flood_hops.is_some());
5407        let fixed_core = resend
5408            .frame
5409            .get(1 + fhops_len..header.options_range.start)?;
5410        let core_end = cursor + fixed_core.len();
5411        rewritten
5412            .get_mut(cursor..core_end)?
5413            .copy_from_slice(fixed_core);
5414        cursor = core_end;
5415
5416        // Re-encoded options
5417        let options_len = self
5418            .encode_route_retry_options(
5419                resend.frame.as_slice(),
5420                header.options_range.clone(),
5421                &options,
5422                &mut rewritten[cursor..],
5423            )
5424            .ok()?;
5425        cursor += options_len;
5426
5427        // 0xFF marker when body follows
5428        let needs_marker = !matches!(header.packet_type(), PacketType::MacAck)
5429            && header.options_range.end < header.total_len;
5430        if needs_marker {
5431            *rewritten.get_mut(cursor)? = 0xFF;
5432            cursor += 1;
5433        }
5434
5435        // Body + MIC from original
5436        let tail = resend
5437            .frame
5438            .get(header.options_range.end..header.total_len)?;
5439        let end = cursor + tail.len();
5440        rewritten.get_mut(cursor..end)?.copy_from_slice(tail);
5441
5442        ResendRecord::try_new(&rewritten[..end], None)
5443            .ok()
5444            .map(|record| record.with_requested_flood_hops(resend.requested_flood_hops))
5445    }
5446
5447    fn encode_route_retry_options(
5448        &self,
5449        src: &[u8],
5450        options_range: core::ops::Range<usize>,
5451        _options: &ParsedOptions,
5452        dst: &mut [u8],
5453    ) -> Result<usize, CapacityError> {
5454        let mut encoder = OptionEncoder::new(dst);
5455        let mut inserted_trace_route = false;
5456        let mut inserted_route_retry = false;
5457
5458        if !options_range.is_empty() {
5459            for entry in umsh_core::iter_options(src, options_range) {
5460                let (number, value) = entry.map_err(|_| CapacityError)?;
5461                if !inserted_trace_route && number > OptionNumber::TraceRoute.as_u16() {
5462                    encoder
5463                        .put(OptionNumber::TraceRoute.as_u16(), &[])
5464                        .map_err(|_| CapacityError)?;
5465                    inserted_trace_route = true;
5466                }
5467                if !inserted_route_retry && number > OptionNumber::RouteRetry.as_u16() {
5468                    encoder
5469                        .put(OptionNumber::RouteRetry.as_u16(), &[])
5470                        .map_err(|_| CapacityError)?;
5471                    inserted_route_retry = true;
5472                }
5473                match OptionNumber::from(number) {
5474                    OptionNumber::SourceRoute => {}
5475                    OptionNumber::TraceRoute => {
5476                        encoder.put(number, value).map_err(|_| CapacityError)?;
5477                        inserted_trace_route = true;
5478                    }
5479                    OptionNumber::RouteRetry => {}
5480                    _ => {
5481                        encoder.put(number, value).map_err(|_| CapacityError)?;
5482                    }
5483                }
5484            }
5485        }
5486
5487        if !inserted_trace_route {
5488            encoder
5489                .put(OptionNumber::TraceRoute.as_u16(), &[])
5490                .map_err(|_| CapacityError)?;
5491        }
5492        if !inserted_route_retry {
5493            encoder
5494                .put(OptionNumber::RouteRetry.as_u16(), &[])
5495                .map_err(|_| CapacityError)?;
5496        }
5497
5498        Ok(encoder.finish())
5499    }
5500
5501    fn route_retry_flood_hops(
5502        &self,
5503        peer: &PublicKey,
5504        header: &PacketHeader,
5505        source_route: Option<&heapless::Vec<RouterHint, MAX_SOURCE_ROUTE_HOPS>>,
5506        requested: Option<u8>,
5507    ) -> Option<u8> {
5508        // The failed attempt was narrowed against a route assumption — either
5509        // an attached source route, or a cached route that clamped `FHOPS_REM`
5510        // below what was asked for. Rediscovery has to reach past the break,
5511        // so prefer the budget the application was willing to spend; it is
5512        // also the ceiling, since route recovery may undo the MAC's narrowing
5513        // but not exceed the caller's own limit.
5514        let requested = requested
5515            .filter(|hops| *hops > 0)
5516            .map(|hops| hops.clamp(1, MAX_FLOOD_HOPS));
5517        let existing = header
5518            .flood_hops
5519            .map(|hops| hops.remaining())
5520            .filter(|hops| *hops > 0);
5521        let cached = self
5522            .peer_registry
5523            .lookup_by_key(peer)
5524            .and_then(|(_, info)| match info.route.as_ref() {
5525                Some(crate::CachedRoute::Flood { hops, .. }) => {
5526                    Some((*hops).clamp(1, MAX_FLOOD_HOPS))
5527                }
5528                _ => None,
5529            });
5530        let route_len = source_route
5531            .and_then(|route| u8::try_from(route.len()).ok())
5532            .map(|hops| hops.clamp(1, MAX_FLOOD_HOPS));
5533
5534        requested.or(existing).or(cached).or(route_len).or(Some(5))
5535    }
5536
5537    fn repeater_router_hint(&self) -> Option<RouterHint> {
5538        self.identities
5539            .iter()
5540            .filter_map(|slot| slot.as_ref())
5541            .next()
5542            .map(|slot| slot.identity().public_key().router_hint())
5543    }
5544
5545    fn effective_min_rssi(options: &ParsedOptions, repeater: &RepeaterConfig) -> Option<i16> {
5546        match (options.min_rssi, repeater.min_rssi) {
5547            (Some(packet), Some(local)) => Some(packet.max(local)),
5548            (Some(packet), None) => Some(packet),
5549            (None, Some(local)) => Some(local),
5550            (None, None) => None,
5551        }
5552    }
5553
5554    fn effective_min_snr(options: &ParsedOptions, repeater: &RepeaterConfig) -> Option<i8> {
5555        match (options.min_snr, repeater.min_snr) {
5556            (Some(packet), Some(local)) => Some(packet.max(local)),
5557            (Some(packet), None) => Some(packet),
5558            (None, Some(local)) => Some(local),
5559            (None, None) => None,
5560        }
5561    }
5562
5563    /// Deterministic flood-forwarding contention window, plus its random
5564    /// tie-breaker (channel-access.md § Flood Forwarding Contention).
5565    ///
5566    /// Two clamped terms describe the reception: `quality` from SNR, which says
5567    /// how cleanly the frame was demodulated, and `signal` from RSSI, which says
5568    /// how loud its transmitter is here. The window takes
5569    /// `max(1 − quality, signal)`, so a repeater waits longer if either the copy
5570    /// it holds is poor *or* it sits close enough to the previous hop that its
5571    /// forward would mostly cover ground already covered. Clean, distant
5572    /// receptions — the ones that carry the flood outward — go first.
5573    ///
5574    /// The quality scale's floor is raised from the spec's constant to the
5575    /// effective minimum-SNR forwarding threshold when one is set: eligibility
5576    /// already cut the range off there, and grading receptions against ground
5577    /// none of them can occupy would compress every survivor toward "clean".
5578    fn sample_flood_contention_delay_ms(&mut self, rx: &RxInfo, options: &ParsedOptions) -> u64 {
5579        // Contention delay staggers the repeaters that all heard one
5580        // transmission, weighted so the best-placed one goes first. A
5581        // frame handed over a point-to-point link was heard by exactly
5582        // one repeater, so there is no race to stagger.
5583        if !rx.origin.is_measured() {
5584            return 0;
5585        }
5586        let effective_threshold_db =
5587            Self::effective_min_snr(options, &self.repeater).unwrap_or(i8::MIN);
5588        let snr_low_db = self
5589            .repeater
5590            .flood_contention_snr_low_db
5591            .max(effective_threshold_db);
5592        let snr_high_db = self
5593            .repeater
5594            .flood_contention_snr_high_db
5595            .max(snr_low_db.saturating_add(1));
5596        let snr_low = i32::from(Snr::from_decibels(snr_low_db).as_centibels());
5597        let snr_high = i32::from(Snr::from_decibels(snr_high_db).as_centibels());
5598        let snr_received = i32::from(rx.snr.as_centibels());
5599        let quality_den = (snr_high - snr_low) as u64;
5600        let quality_num = (snr_received - snr_low).clamp(0, snr_high - snr_low) as u64;
5601
5602        let rssi_low = i32::from(self.repeater.flood_contention_rssi_low_dbm);
5603        let rssi_high =
5604            i32::from(self.repeater.flood_contention_rssi_high_dbm).max(rssi_low.saturating_add(1));
5605        let rssi_received = i32::from(rx.rssi);
5606        let signal_den = (rssi_high - rssi_low) as u64;
5607        let signal_num = (rssi_received - rssi_low).clamp(0, rssi_high - rssi_low) as u64;
5608
5609        let t_frame_ms = u64::from(self.radio.t_frame_ms());
5610        let min_window_ms = t_frame_ms
5611            .saturating_mul(u64::from(self.repeater.flood_contention_min_window_percent))
5612            / 100;
5613        let max_window_ms = t_frame_ms
5614            .saturating_mul(u64::from(self.repeater.flood_contention_max_window_percent))
5615            / 100;
5616        let max_window_ms = max_window_ms.max(min_window_ms);
5617        let span_ms = max_window_ms.saturating_sub(min_window_ms);
5618        // span × (1 − quality) and span × signal, each as one integer product.
5619        let quality_term = span_ms.saturating_mul(quality_den - quality_num) / quality_den;
5620        let signal_term = span_ms.saturating_mul(signal_num) / signal_den;
5621        let window_ms = min_window_ms.saturating_add(quality_term.max(signal_term));
5622
5623        let jitter_ms = t_frame_ms
5624            .saturating_mul(u64::from(self.repeater.flood_contention_jitter_percent))
5625            / 100;
5626        if jitter_ms == 0 {
5627            window_ms
5628        } else {
5629            window_ms.saturating_add(self.rng.random_range(..jitter_ms.saturating_add(1)))
5630        }
5631    }
5632
5633    /// ACK protection interval: minimum head start granted to a destination's
5634    /// immediate ACK before any flood forward of the same packet may transmit.
5635    fn ack_guard_delay_ms(&self) -> u64 {
5636        u64::from(self.radio.t_frame_ms())
5637            .saturating_mul(u64::from(self.repeater.flood_contention_ack_guard_percent))
5638            / 100
5639    }
5640
5641    /// Routing identity used for duplicate suppression at repeaters.
5642    ///
5643    /// Defined in [`crate::forward_id`], which every forwarder in the mesh
5644    /// shares.
5645    pub(crate) fn forward_dup_key(header: &PacketHeader, frame: &[u8]) -> Option<DupCacheKey> {
5646        crate::forward_id::forwarding_dup_key_parsed(header, frame)
5647    }
5648
5649    fn defer_pending_forward(
5650        &mut self,
5651        key: &DupCacheKey,
5652        header: &PacketHeader,
5653        rx: &RxInfo,
5654        options: &ParsedOptions,
5655    ) {
5656        let Some(queued) = self.tx_queue.remove_first_matching(|entry| {
5657            entry.priority == TxPriority::Forward
5658                && Self::confirmation_key(entry.frame.as_slice())
5659                    .map(|entry_key| &entry_key == key)
5660                    .unwrap_or(false)
5661        }) else {
5662            return;
5663        };
5664
5665        if queued.forward_deferrals >= self.repeater.flood_contention_max_deferrals {
5666            return;
5667        }
5668
5669        let now_ms = self.clock.now_ms();
5670        let mut delay_ms = self.sample_flood_contention_delay_ms(rx, options);
5671        // The overheard copy may itself elicit an immediate ACK from the
5672        // destination; give that ACK the same head start as on first receipt.
5673        let overheard_has_route_hops = options
5674            .source_route
5675            .as_ref()
5676            .map(|range| !range.is_empty())
5677            .unwrap_or(false);
5678        if header.ack_requested() && !overheard_has_route_hops {
5679            delay_ms = delay_ms.saturating_add(self.ack_guard_delay_ms());
5680        }
5681        let _ = self.tx_queue.enqueue_with_state(
5682            queued.priority,
5683            queued.frame.as_slice(),
5684            queued.receipt,
5685            queued.identity_id,
5686            now_ms.saturating_add(delay_ms),
5687            queued.cad_attempts,
5688            queued.forward_deferrals.saturating_add(1),
5689        );
5690    }
5691
5692    /// Drop any queued forward the destination has already acknowledged.
5693    ///
5694    /// A MAC ack echoes `ack_mic` — the first four bytes of the acknowledged
5695    /// packet's on-wire MIC — which every forwarder can read without keys, and
5696    /// which survives the rewrites a repeater performs. A forward still sitting
5697    /// in the transmit queue when that ack is overheard has been overtaken by
5698    /// events: the destination has the packet, so repeating it buys nothing but
5699    /// airtime. The [ACK protection interval] exists to put the ack on the air
5700    /// first, which is precisely what makes this observation available.
5701    ///
5702    /// This cancels what is queued at this instant and leaves nothing behind. A
5703    /// [route-retry] copy that arrives later carries a distinct forwarding
5704    /// identity, is queued and forwarded normally — the origin resorted to it
5705    /// because the ack never reached it, and carrying it prompts the
5706    /// destination to acknowledge again — and is in turn cancelable by another
5707    /// overheard ack.
5708    ///
5709    /// Only unattributed forwards are eligible. A queued frame carrying a
5710    /// receipt is this node's own send, whose completion is decided by the
5711    /// authenticated `ack_tag` in [`Mac::complete_ack`], never by a four-byte
5712    /// public prefix.
5713    ///
5714    /// [ACK protection interval]: https://darconeous.github.io/umsh/docs/protocol/channel-access.html#ack-protection-interval
5715    /// [route-retry]: https://darconeous.github.io/umsh/docs/protocol/packet-options.html#route-retry-option-6
5716    fn cancel_forwards_for_ack_mic(&mut self, ack_mic: &[u8]) -> usize {
5717        if ack_mic.len() != 4 {
5718            return 0;
5719        }
5720        let removed = self.tx_queue.remove_all_matching(|entry| {
5721            if entry.priority != TxPriority::Forward || entry.receipt.is_some() {
5722                return false;
5723            }
5724            let frame = entry.frame.as_slice();
5725            let Ok(header) = PacketHeader::parse(frame) else {
5726                return false;
5727            };
5728            // Only the packet types whose destination emits a MAC ack can be
5729            // the subject of one. Without this, a forwarded ack awaiting
5730            // transmission would be cancelled by a second copy of itself: a
5731            // MAC ack's own trailer opens with the same four bytes it echoes.
5732            if !matches!(
5733                header.packet_type(),
5734                PacketType::UnicastAckReq | PacketType::BlindUnicastAckReq
5735            ) {
5736                return false;
5737            }
5738            frame
5739                .get(header.mic_range.clone())
5740                .and_then(|mic| mic.get(..4))
5741                .map(|prefix| prefix == ack_mic)
5742                .unwrap_or(false)
5743        });
5744        if removed > 0 {
5745            MacCounters::bump(&mut self.counters.forward_cancelled);
5746        }
5747        removed
5748    }
5749
5750    /// Cancel queued forwards named by a piggy-backed [Ack MIC option].
5751    ///
5752    /// The option carries the same correlation handle as a standalone ack, on
5753    /// an ordinary authenticated packet travelling the other way. It sits in
5754    /// the options block, which is not encrypted, so a forwarder reads it
5755    /// under the same terms as the standalone form.
5756    ///
5757    /// [Ack MIC option]: https://darconeous.github.io/umsh/docs/protocol/packet-options.html#ack-mic-option-8
5758    fn cancel_forwards_for_ack_mic_option(&mut self, frame: &[u8], header: &PacketHeader) {
5759        if header.options_range.is_empty() {
5760            return;
5761        }
5762        let mut ack_mic = None;
5763        for entry in umsh_core::iter_options(frame, header.options_range.clone()) {
5764            let Ok((number, value)) = entry else {
5765                continue;
5766            };
5767            if OptionNumber::from(number) == OptionNumber::AckMic && value.len() == 4 {
5768                ack_mic = Some([value[0], value[1], value[2], value[3]]);
5769                break;
5770            }
5771        }
5772        if let Some(ack_mic) = ack_mic {
5773            self.cancel_forwards_for_ack_mic(&ack_mic);
5774        }
5775    }
5776
5777    async fn service_post_tx_listen(
5778        &mut self,
5779        mut on_event: impl FnMut(LocalIdentityId, crate::MacEventRef<'_>),
5780    ) -> Result<(), MacError<<P::Radio as Radio>::Error>> {
5781        loop {
5782            self.expire_post_tx_listen_if_needed();
5783            if self.post_tx_listen.is_none() {
5784                return Ok(());
5785            }
5786
5787            let handled = self.receive_one(&mut on_event).await?;
5788            if !handled {
5789                return Ok(());
5790            }
5791        }
5792    }
5793
5794    /// Start the completion timers for a tracked send that just went on the air.
5795    fn note_transmitted_tracked(&mut self, receipt: SendReceipt, frame: &[u8]) {
5796        let sent_ms = self.clock.now_ms();
5797        let direct_ack_deadline_ms = sent_ms.saturating_add(self.direct_ack_timeout_ms());
5798        let forwarded_ack_deadline_ms = sent_ms
5799            .saturating_add(self.forwarded_ack_timeout_ms(Self::allowed_hop_distance(frame)));
5800        let repeat_only_deadline_ms = sent_ms.saturating_add(self.repeat_confirm_timeout_ms());
5801        let confirm_timeout_ms = self.forward_confirm_timeout_ms();
5802        let confirm_key = Self::confirmation_key(frame);
5803
5804        let post_tx_listen = {
5805            let Some((identity_id, pending)) = self.pending_ack_mut(receipt) else {
5806                return;
5807            };
5808
5809            let needs_forward_confirmation = match pending.state {
5810                crate::AckState::Queued {
5811                    needs_forward_confirmation,
5812                } => needs_forward_confirmation,
5813                crate::AckState::RetryQueued => true,
5814                _ => return,
5815            };
5816
5817            pending.sent_ms = sent_ms;
5818            pending.confirm_key = confirm_key.clone();
5819            // Zero means the send has never aired and its window is still to
5820            // be set. A route retry arrives here already armed — it has to be,
5821            // to survive the wait in the queue — and keeps the deadline it was
5822            // scheduled with.
5823            if pending.ack_deadline_ms == 0 {
5824                pending.ack_deadline_ms = match (pending.completion, needs_forward_confirmation) {
5825                    (CompletionSignal::RepeatOnly, _) => repeat_only_deadline_ms,
5826                    (CompletionSignal::Ack, true) => forwarded_ack_deadline_ms,
5827                    (CompletionSignal::Ack, false) => direct_ack_deadline_ms,
5828                };
5829            }
5830
5831            if needs_forward_confirmation {
5832                let deadline_ms = sent_ms.saturating_add(confirm_timeout_ms);
5833                pending.state = crate::AckState::AwaitingForward {
5834                    confirm_deadline_ms: deadline_ms,
5835                };
5836                // The dedicated listen window — which holds all other
5837                // transmissions back for the whole confirmation wait — is
5838                // reserved for sends with an ACK on the line. A repeat-only
5839                // send is best-effort by construction; the pending entry
5840                // matches an overheard repeat whenever the radio hears one,
5841                // and stalling unrelated traffic for seconds is a price
5842                // best-effort delivery does not get to charge.
5843                if pending.expects_ack() {
5844                    confirm_key.map(|confirm_key| PostTxListen {
5845                        identity_id,
5846                        receipt,
5847                        confirm_key,
5848                        deadline_ms,
5849                    })
5850                } else {
5851                    None
5852                }
5853            } else {
5854                pending.state = crate::AckState::AwaitingAck;
5855                None
5856            }
5857        };
5858
5859        self.post_tx_listen = post_tx_listen;
5860    }
5861
5862    fn expire_post_tx_listen_if_needed(&mut self) {
5863        let should_clear = self
5864            .post_tx_listen
5865            .as_ref()
5866            .map(|listen| self.clock.now_ms() >= listen.deadline_ms)
5867            .unwrap_or(false);
5868        if should_clear {
5869            self.post_tx_listen = None;
5870        }
5871    }
5872
5873    /// How long to wait for the next hop to retransmit before retrying.
5874    ///
5875    /// The next hop must hear the frame out, wait out its contention window and
5876    /// jitter, hold back for the destination's immediate ACK, and then transmit:
5877    /// `2 × T_frame + W_max + W_jitter + D_ack`, which at the default tuning is
5878    /// `2.85 × T_frame` (repeater-operation.md § Forwarding Confirmation).
5879    fn forward_confirm_timeout_ms(&self) -> u64 {
5880        let t_frame_ms = u64::from(self.radio.t_frame_ms());
5881        t_frame_ms
5882            .saturating_add(self.max_forward_contention_delay_ms())
5883            .saturating_add(self.ack_guard_delay_ms())
5884            .saturating_add(t_frame_ms)
5885    }
5886
5887    /// Longest contention delay a forwarding repeater can draw: the deterministic
5888    /// window at its maximum plus the whole jitter range.
5889    fn max_forward_contention_delay_ms(&self) -> u64 {
5890        let t_frame_ms = u64::from(self.radio.t_frame_ms());
5891        let max_window_ms = t_frame_ms
5892            .saturating_mul(u64::from(self.repeater.flood_contention_max_window_percent))
5893            / 100;
5894        let jitter_ms = t_frame_ms
5895            .saturating_mul(u64::from(self.repeater.flood_contention_jitter_percent))
5896            / 100;
5897        max_window_ms.saturating_add(jitter_ms)
5898    }
5899
5900    /// Jitter cap for a forwarding-confirmation retry: one frame time, flat
5901    /// across the ladder.
5902    ///
5903    /// The jitter exists to decorrelate retries between nodes, and one frame
5904    /// time is all that takes. Growing the delay would only stretch the
5905    /// ladder: every window it adds is time the payload spends undelivered,
5906    /// and a packet the mesh cannot carry after three prompt attempts is
5907    /// better dropped than delivered tens of seconds late.
5908    fn forward_retry_backoff_cap_ms(&self) -> u32 {
5909        self.radio.t_frame_ms()
5910    }
5911
5912    /// Whether a timed-out send carries a route assumption worth abandoning.
5913    ///
5914    /// An attached source route is the obvious case: the named path did not
5915    /// deliver. The subtler one is a flood budget the MAC narrowed on the
5916    /// sender's behalf. A peer cached as [`CachedRoute::Direct`] transmits at
5917    /// [`ESTABLISHED_ROUTE_EXTRA_HOPS`] however wide a flood the application
5918    /// asked for, so a peer that has since moved out of direct range cannot be
5919    /// reached by repeating the same frame — and nothing in the options records
5920    /// that a route was ever assumed. That is the same staleness as a dead
5921    /// source-route hint, kept in `FHOPS` instead of in an option.
5922    ///
5923    /// A budget the *application* chose is left alone. Route recovery exists to
5924    /// undo the MAC's own narrowing, not to flood wider than the caller was
5925    /// willing to.
5926    fn can_attempt_route_retry(pending: &PendingAck<FRAME>) -> bool {
5927        // Route recovery re-attempts delivery of a packet whose fate is
5928        // knowable. A send with no ack to wait for never learns whether the
5929        // route failed, so it has nothing to recover from.
5930        if !pending.expects_ack() {
5931            return false;
5932        }
5933        let Ok(header) = PacketHeader::parse(pending.resend.frame.as_slice()) else {
5934            return false;
5935        };
5936        let Ok(options) = ParsedOptions::extract(
5937            pending.resend.frame.as_slice(),
5938            header.options_range.clone(),
5939        ) else {
5940            return false;
5941        };
5942        if options.route_retry {
5943            return false;
5944        }
5945        let has_source_route = pending
5946            .resend
5947            .source_route
5948            .as_ref()
5949            .map(|route| !route.is_empty())
5950            .unwrap_or(false);
5951        if has_source_route {
5952            return true;
5953        }
5954        // An absent `FHOPS` byte is a budget of zero: the send went out direct.
5955        let sent = header
5956            .flood_hops
5957            .map(|hops| hops.remaining())
5958            .unwrap_or_default();
5959        pending
5960            .resend
5961            .requested_flood_hops
5962            .is_some_and(|requested| requested > sent)
5963    }
5964
5965    fn direct_ack_timeout_ms(&self) -> u64 {
5966        u64::from(self.radio.t_frame_ms()).saturating_mul(10)
5967    }
5968
5969    /// How long an ACK-requested send that travels through repeaters waits
5970    /// before the ACK is declared lost.
5971    ///
5972    /// The retry ladder covers the first hop — that is the only hop this node
5973    /// can observe. Everything past it is distance: the packet has to cross
5974    /// `hops` forwards to arrive and the ACK has to cross them back, and each
5975    /// crossing costs a frame time plus the forwarder's contention window and
5976    /// ACK guard. Charging for that distance is what keeps a delivery that
5977    /// genuinely succeeded from reporting a timeout to the application; a
5978    /// single frame time, which is what this allowed before, only ever
5979    /// sufficed for a one-hop return.
5980    fn forwarded_ack_timeout_ms(&self, hops: u8) -> u64 {
5981        let t_frame_ms = u64::from(self.radio.t_frame_ms());
5982        let per_hop_ms = self
5983            .forward_confirm_timeout_ms()
5984            .saturating_sub(t_frame_ms)
5985            .max(t_frame_ms);
5986        self.repeat_confirm_timeout_ms().saturating_add(
5987            per_hop_ms
5988                .saturating_mul(2)
5989                .saturating_mul(u64::from(hops.max(1))),
5990        )
5991    }
5992
5993    /// Hops the sender allowed this frame, and so the distance its ACK has to
5994    /// come back over.
5995    ///
5996    /// A hybrid route spends both budgets in turn, so they add. The floor of
5997    /// one keeps a frame whose header cannot be re-read from collapsing the
5998    /// return allowance to nothing.
5999    fn allowed_hop_distance(frame: &[u8]) -> u8 {
6000        let Ok(header) = PacketHeader::parse(frame) else {
6001            return 1;
6002        };
6003        let mut hops = header
6004            .flood_hops
6005            .map(|flood_hops| flood_hops.remaining())
6006            .unwrap_or(0);
6007        if let Ok(options) = ParsedOptions::extract(frame, header.options_range.clone())
6008            && let Some(range) = options.source_route
6009        {
6010            hops = hops.saturating_add(u8::try_from(range.len() / 2).unwrap_or(u8::MAX));
6011        }
6012        hops.max(1)
6013    }
6014
6015    /// How long a send waits to overhear its own packet carried onward before
6016    /// the attempt is over.
6017    ///
6018    /// This spans the full retry ladder: every confirmation window plus the
6019    /// backoff that separates them. A send with no ACK to wait for ends here;
6020    /// an ACK-requested send allows a further frame time for the ack to
6021    /// return.
6022    fn repeat_confirm_timeout_ms(&self) -> u64 {
6023        let per_retry_ms = u64::from(self.forward_retry_backoff_cap_ms())
6024            .saturating_add(self.forward_confirm_timeout_ms());
6025        self.forward_confirm_timeout_ms()
6026            .saturating_add(per_retry_ms.saturating_mul(u64::from(MAX_FORWARD_RETRIES)))
6027    }
6028
6029    fn pending_ack_mut(
6030        &mut self,
6031        receipt: SendReceipt,
6032    ) -> Option<(LocalIdentityId, &mut PendingAck<FRAME>)> {
6033        for (index, slot) in self.identities.iter_mut().enumerate() {
6034            let Some(slot) = slot.as_mut() else {
6035                continue;
6036            };
6037            if let Some(pending) = slot.pending_ack_mut(&receipt) {
6038                return Some((LocalIdentityId(index as u8), pending));
6039            }
6040        }
6041        None
6042    }
6043
6044    pub(crate) fn confirmation_key(frame: &[u8]) -> Option<DupCacheKey> {
6045        crate::forward_id::forwarding_dup_key(frame)
6046    }
6047
6048    /// Check if a received frame confirms forwarding of a pending send.
6049    ///
6050    /// Confirmation is not tied to the post-transmit listen window. That
6051    /// window governs when this node stays off the air waiting; the sender's
6052    /// interest in hearing its packet carried onward outlives it. A repeat
6053    /// that arrives late — or while a retransmission is already sitting in
6054    /// backoff — is the same evidence it would have been a moment earlier, so
6055    /// every pending send still waiting for a repeat is matched, and a retry
6056    /// queued on its behalf is withdrawn.
6057    ///
6058    /// Returns `Some((identity_id, receipt))` on successful confirmation,
6059    /// `None` otherwise. A send expecting an ACK moves on to wait for it; a
6060    /// [`CompletionSignal::RepeatOnly`] send is finished by the repeat itself.
6061    fn observe_forwarding_confirmation(
6062        &mut self,
6063        frame: &[u8],
6064    ) -> Option<(LocalIdentityId, SendReceipt)> {
6065        self.expire_post_tx_listen_if_needed();
6066        let received_key = Self::confirmation_key(frame)?;
6067
6068        // Whatever the pending table decides below, the wait itself is over.
6069        if self
6070            .post_tx_listen
6071            .as_ref()
6072            .map(|listen| listen.confirm_key == received_key)
6073            .unwrap_or(false)
6074        {
6075            self.post_tx_listen = None;
6076        }
6077
6078        let mut found = None;
6079        'search: for (index, slot) in self.identities.iter().enumerate() {
6080            let Some(slot) = slot.as_ref() else {
6081                continue;
6082            };
6083            for (receipt, pending) in slot.pending_acks.iter() {
6084                if !matches!(
6085                    pending.state,
6086                    crate::AckState::AwaitingForward { .. } | crate::AckState::RetryQueued
6087                ) {
6088                    continue;
6089                }
6090                if pending.confirm_key.as_ref() != Some(&received_key) {
6091                    continue;
6092                }
6093                found = Some((LocalIdentityId(index as u8), *receipt, pending.completion));
6094                break 'search;
6095            }
6096        }
6097        let (identity_id, receipt, completion) = found?;
6098
6099        // A retransmission scheduled while this confirmation was in flight has
6100        // been overtaken by it.
6101        self.tx_queue.remove_all_matching(|entry| {
6102            entry.receipt == Some(receipt) && entry.identity_id == Some(identity_id)
6103        });
6104
6105        match completion {
6106            CompletionSignal::Ack => {
6107                if let Some(pending) = self
6108                    .identity_mut(identity_id)
6109                    .and_then(|slot| slot.pending_ack_mut(&receipt))
6110                {
6111                    pending.state = crate::AckState::AwaitingAck;
6112                }
6113            }
6114            // Nothing further is coming: the repeat was the whole point.
6115            CompletionSignal::RepeatOnly => {
6116                if let Some(slot) = self.identity_mut(identity_id) {
6117                    slot.pending_acks.remove(&receipt);
6118                }
6119            }
6120        }
6121        Some((identity_id, receipt))
6122    }
6123
6124    /// Find the destination peer of the outstanding ack-requested send whose
6125    /// expected ack trailer (`ack_mic || ack_tag`) equals `ack_trailer`,
6126    /// searching every local identity's pending table.
6127    fn peer_for_ack_trailer(&self, ack_trailer: &[u8; 8]) -> Option<PublicKey> {
6128        self.identities
6129            .iter()
6130            .filter_map(|slot| slot.as_ref())
6131            .find_map(|slot| self.match_pending_peer_for_ack(slot, ack_trailer))
6132    }
6133
6134    fn match_pending_peer_for_ack(
6135        &self,
6136        slot: &IdentitySlot<P::Identity, PEERS, ACKS, FRAME>,
6137        ack_trailer_bytes: &[u8],
6138    ) -> Option<PublicKey> {
6139        if ack_trailer_bytes.len() != 8 {
6140            return None;
6141        }
6142
6143        slot.pending_acks.iter().find_map(|(_, pending)| {
6144            (pending.expects_ack() && pending.ack_trailer == ack_trailer_bytes)
6145                .then_some(pending.peer)
6146        })
6147    }
6148}
6149
6150fn align_counter_boundary(value: u32) -> u32 {
6151    value & !COUNTER_PERSIST_BLOCK_MASK
6152}
6153
6154pub(crate) const fn nonzero_initial_frame_counter(random: u32) -> u32 {
6155    if random == 0 { 1 } else { random }
6156}
6157
6158fn next_counter_persist_target(next_counter: u32) -> u32 {
6159    next_counter.wrapping_add(COUNTER_PERSIST_BLOCK_SIZE) & !COUNTER_PERSIST_BLOCK_MASK
6160}
6161
6162/// Build the CounterStore context key for a peer's RX boundary.
6163///
6164/// Format: `mac.rx:` (7 bytes) + raw Ed25519 pubkey (32 bytes) = 39 bytes.
6165/// This is distinct from the TX key (which is the bare 32-byte local pubkey)
6166/// and from peer record keys in the flash KV store.
6167fn rx_counter_key_bytes(pk: &[u8; 32]) -> [u8; 39] {
6168    let mut key = [0u8; 39];
6169    key[..7].copy_from_slice(b"mac.rx:");
6170    key[7..].copy_from_slice(pk);
6171    key
6172}