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