umsh_text/node_adapter/
transmit.rs

1//! Engine transmissions to `LocalNode` sends, and their delivery back to the
2//! engine.
3
4use alloc::collections::{BTreeSet, VecDeque};
5use alloc::vec::Vec;
6
7use umsh_core::{ChannelKey, ChannelTag, PayloadType, PublicKey};
8use umsh_mac::{SendOptions, SendReceipt};
9#[cfg(feature = "software-crypto")]
10use umsh_node::{BoundChannel, Transport};
11use umsh_node::{LocalNode, MacBackend, SendProgressTicket};
12
13use crate::chat::{ChannelRegistry, ChatFacade, Outbound, TransmitUpdate};
14use crate::engine::sequence::MessageHandle;
15use crate::engine::{Destination, Transmission};
16use crate::node_adapter::WellKnownChannels;
17use crate::validate::TextProfile;
18
19/// The flood-hop ceiling the protocol fixes for `public` and `EMERGENCY`.
20pub const WELL_KNOWN_CHANNEL_MAX_FLOOD_HOPS: u8 = 10;
21
22/// How long an in-flight transmission may go unresolved before it is called
23/// out. Longer than any ordinary acknowledgment wait, so this fires on
24/// trouble rather than on a slow link.
25pub const TRANSMISSION_STALL_MS: u64 = 60_000;
26
27/// What a send needs to know beyond the frame itself.
28#[derive(Clone, Copy)]
29pub struct TransmitPolicy {
30    /// Ceiling on how far a send travels when nothing narrower applies. Zero
31    /// sends no flood at all.
32    pub default_flood_hops: u8,
33    pub well_known: WellKnownChannels,
34}
35
36/// Spend `hops` on `options`: a budget for a flood, or no flood at all for
37/// zero.
38pub fn with_flood_reach(options: SendOptions, hops: u8) -> SendOptions {
39    if hops == 0 {
40        options.no_flood()
41    } else {
42        options.with_flood_hops(hops)
43    }
44}
45
46/// How far traffic on a channel may travel: the ceiling the user registered
47/// for it, otherwise `default_hops`—and never past the ceiling the protocol
48/// fixes for the well-known `public` and `EMERGENCY` channels.
49pub fn channel_reach(
50    channels: &ChannelRegistry,
51    well_known: &WellKnownChannels,
52    channel: &ChannelTag,
53    default_hops: u8,
54) -> u8 {
55    let asked = channels.max_flood_hops(channel).unwrap_or(default_hops);
56    match channels.key(channel) {
57        Some(key) if well_known.contains(&key) => asked.min(WELL_KNOWN_CHANNEL_MAX_FLOOD_HOPS),
58        _ => asked,
59    }
60}
61
62/// Bind a held channel, so it can be sent over.
63///
64/// Looked up per send rather than cached: leaving and rejoining a channel
65/// invalidates the binding, and the registry is the one place that knows.
66#[cfg(feature = "software-crypto")]
67pub fn bound_channel<M: MacBackend>(
68    node: &LocalNode<M>,
69    channels: &ChannelRegistry,
70    channel: &ChannelTag,
71) -> Option<BoundChannel<M>> {
72    let key = channels.key(channel)?;
73    node.bound_channel(&umsh_node::Channel::private(key, ""))
74}
75
76/// A transmission that has stayed in flight past [`TRANSMISSION_STALL_MS`].
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct Stall {
79    pub transmission_id: u32,
80    pub waited_ms: u64,
81    /// Window slots in use, of `window`.
82    pub occupied: usize,
83    pub window: usize,
84    /// Transmissions waiting behind the window.
85    pub pending: usize,
86}
87
88/// The chat transmit queue.
89///
90/// Frames go to the node through a bounded window, so a message larger than
91/// the window waits here rather than in the MAC's own queue. A frame leaves
92/// the window when nothing more can happen to it: acknowledged or failed, or
93/// transmitted when no acknowledgment will follow.
94pub struct TransmitQueue {
95    window: usize,
96    pending: VecDeque<Queued>,
97    in_flight: Vec<InFlight>,
98    /// Peers that have acknowledged a frame since the last one failed:
99    /// they know our key.
100    pipeline_ready: BTreeSet<[u8; 32]>,
101}
102
103struct Queued {
104    session_id: u64,
105    transmission: Transmission,
106}
107
108struct InFlight {
109    session_id: u64,
110    transmission_id: u32,
111    /// The peer whose first-contact pipeline this send gates on. Channel
112    /// sends have no such peer: multicast is unaddressed and blind unicast
113    /// carries no acknowledgment to confirm with.
114    gate_peer: Option<PublicKey>,
115    ticket: SendProgressTicket,
116    sent_reported: bool,
117    /// No acknowledgment will ever arrive, so transmission is the terminal
118    /// success state rather than a step toward one.
119    non_ack: bool,
120    /// When this entry entered the window, and whether it has already been
121    /// reported as overdue. A frame that never resolves holds a slot; a full
122    /// window of them stops chat entirely, which from the outside looks like
123    /// messages hanging on "Sending" for no reason.
124    queued_at_ms: u64,
125    stall_reported: bool,
126}
127
128impl TransmitQueue {
129    /// A queue keeping at most `window` frames in flight, aligned with the
130    /// MAC's transmit queue.
131    pub fn new(window: usize) -> Self {
132        Self {
133            window: window.max(1),
134            pending: VecDeque::new(),
135            in_flight: Vec::new(),
136            pipeline_ready: BTreeSet::new(),
137        }
138    }
139
140    pub fn enqueue(&mut self, outbound: Outbound) {
141        let session_id = outbound.session_id();
142        self.pending.extend(
143            outbound
144                .into_transmissions()
145                .into_iter()
146                .map(|transmission| Queued {
147                    session_id,
148                    transmission,
149                }),
150        );
151    }
152
153    pub fn has_pending(&self) -> bool {
154        !self.pending.is_empty()
155    }
156
157    pub fn pending_len(&self) -> usize {
158        self.pending.len()
159    }
160
161    pub fn in_flight_len(&self) -> usize {
162        self.in_flight.len()
163    }
164
165    /// Hand waiting frames to the node while the window has room. Returns
166    /// how many were sent: each is an authenticated send that advanced the
167    /// frame counter. It is [`TransmitQueue::depart`], [`transmit`], and
168    /// [`TransmitQueue::departed`] in a loop, for a caller that holds the
169    /// facade across the sends.
170    pub async fn pump<M, P, const SLOTS: usize, const PAGES: usize>(
171        &mut self,
172        node: &LocalNode<M>,
173        chat: &mut ChatFacade<P, SLOTS, PAGES>,
174        policy: &TransmitPolicy,
175        now_ms: u64,
176    ) -> usize
177    where
178        M: MacBackend,
179        P: TextProfile + Clone,
180    {
181        let mut sent_count = 0;
182        while let Some(next) = self.depart(chat.channels(), policy) {
183            let departure = match next {
184                Ok(departure) => departure,
185                Err(undeliverable) => {
186                    undeliverable.fail(chat, now_ms);
187                    continue;
188                }
189            };
190            let sent = transmit(node, &departure).await;
191            match self.departed(departure, sent, now_ms) {
192                Departed::Sent => sent_count += 1,
193                Departed::Failed(undeliverable) => undeliverable.fail(chat, now_ms),
194                Departed::Busy => break,
195            }
196        }
197        sent_count
198    }
199
200    /// The next frame the window has room for, with where it goes resolved
201    /// against `channels`. `None` when nothing may leave now.
202    ///
203    /// - A unicast asks for an acknowledgment, and a peer gets one frame in
204    ///   flight until its first is acknowledged: first contact may need
205    ///   counter synchronization. Until then it names us by full key, since
206    ///   a peer that has not registered us cannot read a frame that names
207    ///   us by hint.
208    /// - A channel multicast carries the full source key, so a member who
209    ///   misses a fragment can address a resend request to us.
210    /// - A channel-direct frame is a blind unicast, registering the peer on
211    ///   the way out.
212    /// - `EMERGENCY` goes out unencrypted, so any node in range can read it.
213    ///
214    /// A frame on a channel no longer held comes back undeliverable.
215    pub fn depart(
216        &mut self,
217        channels: &ChannelRegistry,
218        policy: &TransmitPolicy,
219    ) -> Option<Result<Departure, Undeliverable>> {
220        if self.in_flight.len() >= self.window {
221            return None;
222        }
223        let queued = self.pending.pop_front()?;
224        let transmission = &queued.transmission;
225        let gate_peer = match transmission.destination {
226            Destination::Peer(peer) => Some(peer),
227            Destination::Channel(_) | Destination::ChannelPeer { .. } => None,
228        };
229        if let Some(peer) = gate_peer
230            && !self.pipeline_ready.contains(&peer.0)
231            && self
232                .in_flight
233                .iter()
234                .any(|entry| entry.gate_peer == Some(peer))
235        {
236            self.pending.push_front(queued);
237            return None;
238        }
239        let route = match transmission.destination {
240            Destination::Peer(peer) => {
241                let options = with_flood_reach(SendOptions::default(), policy.default_flood_hops)
242                    .with_ack_requested(true);
243                let options = if self.pipeline_ready.contains(&peer.0) {
244                    options
245                } else {
246                    options.with_full_source()
247                };
248                Route::Peer { peer, options }
249            }
250            Destination::Channel(channel) => {
251                let Some(key) = channels.key(&channel) else {
252                    return Some(Err(Undeliverable::of(&queued)));
253                };
254                let options = channel_options(channels, policy, &channel, &key);
255                Route::Channel { key, options }
256            }
257            // A repair request; the engine owns retrying it, so no
258            // acknowledgment is asked for. Repairs carry the same message
259            // the multicast did, so they are held to the same rules.
260            Destination::ChannelPeer { channel, peer } => {
261                let Some(key) = channels.key(&channel) else {
262                    return Some(Err(Undeliverable::of(&queued)));
263                };
264                let options = channel_options(channels, policy, &channel, &key);
265                Route::ChannelPeer { key, peer, options }
266            }
267        };
268        let mut payload = Vec::with_capacity(transmission.payload.len() + 1);
269        payload.push(PayloadType::TextMessage as u8);
270        payload.extend_from_slice(transmission.payload.as_slice());
271        Some(Ok(Departure {
272            queued,
273            gate_peer,
274            route,
275            payload,
276            forward_secret: None,
277        }))
278    }
279
280    /// What became of a departure's send. A frame the node refused for
281    /// now—with a registered peer and an engine-bounded payload, its queues
282    /// are full—goes back to the head of the queue, to wait for tickets to
283    /// advance.
284    pub fn departed(&mut self, departure: Departure, sent: Sent, now_ms: u64) -> Departed {
285        let ticket = match sent {
286            Sent::Ticket(ticket) => ticket,
287            Sent::Unsendable => return Departed::Failed(Undeliverable::of(&departure.queued)),
288            Sent::Busy => {
289                self.pending.push_front(departure.queued);
290                return Departed::Busy;
291            }
292        };
293        self.in_flight.push(InFlight {
294            session_id: departure.queued.session_id,
295            transmission_id: departure.queued.transmission.transmission_id,
296            gate_peer: departure.gate_peer,
297            ticket,
298            sent_reported: false,
299            non_ack: departure.gate_peer.is_none(),
300            queued_at_ms: now_ms,
301            stall_reported: false,
302        });
303        Departed::Sent
304    }
305
306    /// Report what the tickets say to the engine: transmitted, acknowledged,
307    /// or failed. Returns the frames newly found overdue.
308    pub fn service<P, const SLOTS: usize, const PAGES: usize>(
309        &mut self,
310        chat: &mut ChatFacade<P, SLOTS, PAGES>,
311        now_ms: u64,
312    ) -> Vec<Stall>
313    where
314        P: TextProfile + Clone,
315    {
316        let occupied = self.in_flight.len();
317        let pending = self.pending.len();
318        let mut stalls = Vec::new();
319        let mut index = 0;
320        while index < self.in_flight.len() {
321            let entry = &mut self.in_flight[index];
322            // Checked before the state transitions below, so an entry about
323            // to retire this pass is not accused on its way out.
324            let waited_ms = now_ms.saturating_sub(entry.queued_at_ms);
325            if !entry.stall_reported
326                && !entry.ticket.was_transmitted()
327                && waited_ms >= TRANSMISSION_STALL_MS
328            {
329                entry.stall_reported = true;
330                stalls.push(Stall {
331                    transmission_id: entry.transmission_id,
332                    waited_ms,
333                    occupied,
334                    window: self.window,
335                    pending,
336                });
337            }
338            if entry.ticket.was_transmitted() && !entry.sent_reported {
339                entry.sent_reported = true;
340                chat.transmit_update(
341                    entry.session_id,
342                    entry.transmission_id,
343                    TransmitUpdate::Sent,
344                    now_ms,
345                );
346            }
347            let entry = &self.in_flight[index];
348            if entry.non_ack && entry.sent_reported {
349                // Nothing further can happen to this one; retiring it here is
350                // what keeps channel sends from accumulating in flight.
351                self.in_flight.swap_remove(index);
352            } else if entry.ticket.was_acked() {
353                if let Some(peer) = entry.gate_peer {
354                    self.pipeline_ready.insert(peer.0);
355                }
356                chat.transmit_update(
357                    entry.session_id,
358                    entry.transmission_id,
359                    TransmitUpdate::Acked {
360                        via_flood: entry.ticket.was_acked_via_route_retry(),
361                    },
362                    now_ms,
363                );
364                self.in_flight.swap_remove(index);
365            } else if entry.ticket.has_failed() {
366                // Perhaps it no longer knows us: the next frame introduces
367                // us again.
368                if let Some(peer) = entry.gate_peer {
369                    self.pipeline_ready.remove(&peer.0);
370                }
371                chat.transmit_update(
372                    entry.session_id,
373                    entry.transmission_id,
374                    TransmitUpdate::Failed,
375                    now_ms,
376                );
377                self.in_flight.swap_remove(index);
378            } else {
379                index += 1;
380            }
381        }
382        stalls
383    }
384
385    /// Fail every frame the queue holds, waiting or in flight. Returns the
386    /// receipts of in-flight sends still awaiting an acknowledgment, for the
387    /// caller to cancel at the MAC.
388    pub fn fail_all<P, const SLOTS: usize, const PAGES: usize>(
389        &mut self,
390        chat: &mut ChatFacade<P, SLOTS, PAGES>,
391        now_ms: u64,
392    ) -> Vec<SendReceipt>
393    where
394        P: TextProfile + Clone,
395    {
396        for queued in self.pending.drain(..) {
397            chat.transmit_update(
398                queued.session_id,
399                queued.transmission.transmission_id,
400                TransmitUpdate::Failed,
401                now_ms,
402            );
403        }
404        let mut receipts = Vec::new();
405        for entry in self.in_flight.drain(..) {
406            if let Some(receipt) = entry.ticket.receipt() {
407                receipts.push(receipt);
408            }
409            chat.transmit_update(
410                entry.session_id,
411                entry.transmission_id,
412                TransmitUpdate::Failed,
413                now_ms,
414            );
415        }
416        receipts
417    }
418}
419
420/// A frame let go by [`TransmitQueue::depart`], with where it goes, until
421/// [`TransmitQueue::departed`] is told how its send went.
422pub struct Departure {
423    queued: Queued,
424    gate_peer: Option<PublicKey>,
425    route: Route,
426    /// The frame, with its payload-type byte.
427    payload: Vec<u8>,
428    /// Whether a frame to a peer goes under a session with forward secrecy
429    /// or never does; `None` leaves it to whether one is held.
430    forward_secret: Option<bool>,
431}
432
433impl Departure {
434    /// The frame's payload, after its payload-type byte.
435    pub fn payload(&self) -> &[u8] {
436        self.payload.get(1..).unwrap_or_default()
437    }
438
439    /// The message of ours the frame carries, when its delivery is
440    /// tracked.
441    pub fn message(&self) -> Option<MessageHandle> {
442        self.queued.transmission.message
443    }
444
445    /// Sends the frame, when it goes to a peer, under the session with
446    /// forward secrecy held with them, failing without one; or, without
447    /// `forward_secret`, never under one.
448    pub fn require_forward_secrecy(&mut self, forward_secret: bool) {
449        self.forward_secret = Some(forward_secret);
450    }
451
452    /// What [`Departure::require_forward_secrecy`] holds the frame to.
453    pub fn forward_secrecy(&self) -> Option<bool> {
454        self.forward_secret
455    }
456
457    /// The peer the frame is addressed to, when it goes to one alone.
458    pub fn peer(&self) -> Option<PublicKey> {
459        match self.route {
460            Route::Peer { peer, .. } => Some(peer),
461            Route::Channel { .. } | Route::ChannelPeer { .. } => None,
462        }
463    }
464}
465
466enum Route {
467    Peer {
468        peer: PublicKey,
469        options: SendOptions,
470    },
471    Channel {
472        key: ChannelKey,
473        options: SendOptions,
474    },
475    ChannelPeer {
476        key: ChannelKey,
477        peer: PublicKey,
478        options: SendOptions,
479    },
480}
481
482/// How a send went.
483pub enum Sent {
484    Ticket(SendProgressTicket),
485    /// Nothing will make this frame sendable; fail it.
486    Unsendable,
487    /// The node refused it for now; try again later.
488    Busy,
489}
490
491/// What [`TransmitQueue::departed`] made of a send.
492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub enum Departed {
494    /// It is in flight.
495    Sent,
496    /// It will never go: fail it.
497    Failed(Undeliverable),
498    /// The node is full for now: send nothing more until a ticket
499    /// advances.
500    Busy,
501}
502
503/// A frame that will not go out: an unregistrable peer, a channel no
504/// longer held.
505#[derive(Clone, Copy, Debug, PartialEq, Eq)]
506pub struct Undeliverable {
507    pub session_id: u64,
508    pub transmission_id: u32,
509}
510
511impl Undeliverable {
512    fn of(queued: &Queued) -> Self {
513        Self {
514            session_id: queued.session_id,
515            transmission_id: queued.transmission.transmission_id,
516        }
517    }
518
519    /// Tells the facade the frame failed.
520    pub fn fail<P, const SLOTS: usize, const PAGES: usize>(
521        self,
522        chat: &mut ChatFacade<P, SLOTS, PAGES>,
523        now_ms: u64,
524    ) where
525        P: TextProfile + Clone,
526    {
527        chat.transmit_update(
528            self.session_id,
529            self.transmission_id,
530            TransmitUpdate::Failed,
531            now_ms,
532        );
533    }
534}
535
536/// Sends a departed frame. Needs only the node, so a caller may hold
537/// nothing else across it.
538pub async fn transmit<M: MacBackend>(node: &LocalNode<M>, departure: &Departure) -> Sent {
539    let payload = &departure.payload[..];
540    match &departure.route {
541        Route::Peer { peer, options } => {
542            let Ok(connection) = node.peer(*peer).await else {
543                return Sent::Unsendable;
544            };
545            match departure.forward_secret {
546                None => connection.send(payload, options).await.into(),
547                Some(forward_secret) => {
548                    send_with_secrecy(node, peer, payload, options, forward_secret).await
549                }
550            }
551        }
552        Route::Channel { key, options } => send_on_channel(node, key, None, payload, options).await,
553        Route::ChannelPeer { key, peer, options } => {
554            send_on_channel(node, key, Some(peer), payload, options).await
555        }
556    }
557}
558
559/// A unicast to `peer` under a session with forward secrecy, or never
560/// under one. One meant to go under a session that is no longer held
561/// cannot go: it never goes without.
562#[cfg(feature = "software-crypto")]
563async fn send_with_secrecy<M: MacBackend>(
564    node: &LocalNode<M>,
565    peer: &PublicKey,
566    payload: &[u8],
567    options: &SendOptions,
568    forward_secret: bool,
569) -> Sent {
570    match node
571        .send_with_secrecy(peer, payload, options, forward_secret)
572        .await
573    {
574        Ok(ticket) => Sent::Ticket(ticket),
575        Err(umsh_node::NodeError::PfsSessionMissing) => Sent::Unsendable,
576        Err(_) => Sent::Busy,
577    }
578}
579
580/// Forward secrecy is the software crypto back end's alone: a frame meant
581/// to go under it cannot go.
582#[cfg(not(feature = "software-crypto"))]
583async fn send_with_secrecy<M: MacBackend>(
584    node: &LocalNode<M>,
585    peer: &PublicKey,
586    payload: &[u8],
587    options: &SendOptions,
588    forward_secret: bool,
589) -> Sent {
590    if forward_secret {
591        return Sent::Unsendable;
592    }
593    umsh_node::Transport::send(node, peer, payload, options)
594        .await
595        .into()
596}
597
598/// A multicast on the channel `key` names, or with `peer` a blind unicast
599/// to one member.
600#[cfg(feature = "software-crypto")]
601async fn send_on_channel<M: MacBackend>(
602    node: &LocalNode<M>,
603    key: &ChannelKey,
604    peer: Option<&PublicKey>,
605    payload: &[u8],
606    options: &SendOptions,
607) -> Sent {
608    let Some(bound) = node.bound_channel(&umsh_node::Channel::private(*key, "")) else {
609        return Sent::Unsendable;
610    };
611    let Some(peer) = peer else {
612        return bound.send_all(payload, options).await.into();
613    };
614    // The MAC will only address a registered peer, and a channel member is
615    // not one: nothing about being in a channel together registers anybody.
616    // Registering on the way out spends a peer slot only on the members we
617    // have to ask something of.
618    if node.peer(*peer).await.is_err() {
619        return Sent::Unsendable;
620    }
621    bound.send(peer, payload, options).await.into()
622}
623
624/// Channels are sealed by the software crypto back end alone.
625#[cfg(not(feature = "software-crypto"))]
626async fn send_on_channel<M: MacBackend>(
627    _node: &LocalNode<M>,
628    _key: &ChannelKey,
629    _peer: Option<&PublicKey>,
630    _payload: &[u8],
631    _options: &SendOptions,
632) -> Sent {
633    Sent::Unsendable
634}
635
636impl<E> From<Result<SendProgressTicket, E>> for Sent {
637    fn from(result: Result<SendProgressTicket, E>) -> Self {
638        match result {
639            Ok(ticket) => Sent::Ticket(ticket),
640            Err(_) => Sent::Busy,
641        }
642    }
643}
644
645/// Options for a frame on a channel: as far as the channel reaches, naming
646/// us by full key, and on `EMERGENCY` unencrypted.
647///
648/// An emergency message only members can read is not an emergency message.
649/// The full source key keeps it attributable without encryption. An
650/// unencrypted blind unicast still names its channel, which is what a
651/// repeater decides to forward on. The spec also asks `EMERGENCY` chat to be
652/// signed, but the text-message format has no signature yet, so none is sent.
653fn channel_options(
654    channels: &ChannelRegistry,
655    policy: &TransmitPolicy,
656    channel: &ChannelTag,
657    key: &ChannelKey,
658) -> SendOptions {
659    let reach = channel_reach(
660        channels,
661        &policy.well_known,
662        channel,
663        policy.default_flood_hops,
664    );
665    let options = with_flood_reach(SendOptions::default(), reach).with_full_source();
666    if policy.well_known.is_emergency(key) {
667        options.unencrypted()
668    } else {
669        options
670    }
671}