umsh_node/
lib.rs

1#![allow(async_fn_in_trait)]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4//! Application-facing node layer built on top of [`umsh-mac`](umsh_mac).
5//!
6//! > Note: This reference implementation is a work in progress and was developed
7//! > with the assistance of an LLM. It should be considered experimental.
8//!
9//! `umsh-node` sits between the radio-facing MAC coordinator in `umsh-mac` and the
10//! application. Where `umsh-mac` thinks in raw frames, keys, replay windows, and transmit
11//! queues, `umsh-node` provides composable abstractions for sending and receiving messages,
12//! tracking in-flight sends, and managing channel membership.
13//!
14//! The receive boundary is intentionally low-level: raw subscriptions get a
15//! [`ReceivedPacketRef`] that stays close to the accepted on-wire packet. Payload-specific
16//! helpers such as those in the `umsh-text` crate live one layer up and are built on top of
17//! those raw packet callbacks.
18//!
19//! This crate requires `alloc` (heap allocation for `String`, `Vec`, etc.). It is
20//! otherwise `no_std` compatible.
21//!
22//! # Architecture overview
23//!
24//! ```text
25//! ┌──────────────────────────────────────────────────────────────┐
26//! │  Application                                                 │
27//! │  Host · LocalNode · PeerConnection · BoundChannel            │
28//! ┌──────────────────────┴───────────────────────────────────────┐
29//! │  Host                                                        │
30//! │    ├── drives the shared MAC/runtime event loop              │
31//! │    └── owns multiple LocalNode handles                       │
32//! └──────────────────────┬───────────────────────────────────────┘
33//!                        │
34//! ┌──────────────────────┴───────────────────────────────────────┐
35//! │  LocalNode<M>                                                │
36//! │    ├── sends down through MacBackend                         │
37//! │    ├── owns per-identity PFS state                           │
38//! │    └── dispatches node/peer callback subscriptions           │
39//! └──────────────────────┬───────────────────────────────────────┘
40//!                        │  MacBackend trait
41//! ┌──────────────────────┴───────────────────────────────────────┐
42//! │  MacHandle → Mac<P>  (no_std, heapless)                      │
43//! └──────────────────────────────────────────────────────────────┘
44//! ```
45//!
46//! # Key types
47//!
48//! - [`Host`] — preferred multi-identity driver. Owns the shared MAC event loop and routes
49//!   inbound traffic to the right [`LocalNode`].
50//! - [`LocalNode`] — per-identity application handle. Implements [`Transport`] (unicast /
51//!   broadcast), owns PFS state, and exposes raw packet plus control-side subscriptions.
52//! - [`BoundChannel`] — a channel bound to a `LocalNode`. Implements [`Transport`]
53//!   (blind unicast / multicast). Available with the `software-crypto` feature.
54//! - [`PeerConnection`] — relationship with one remote peer, generic over transport context,
55//!   with peer-scoped callback subscriptions.
56//! - [`Transport`] — shared send interface (`send` / `send_all`).
57//! - [`SendProgressTicket`] — lightweight polling handle for observing in-flight send
58//!   progress (`was_transmitted`, `was_acked`, `is_finished`).
59//! - [`Subscription`] — owned callback registration that auto-unsubscribes on drop.
60//! - [`ReceivedPacketRef`] — borrowed receive view passed into low-level `on_receive(...)`
61//!   handlers and wrappers, including local RX observations such as RSSI, SNR, LQI, and
62//!   receive timestamp.
63//! - [`MacBackend`] — pluggable MAC backend trait for testability.
64//!
65//! # Control payload types
66//!
67//! [`umsh_text::OwnedTextMessage`], [`OwnedNodeIdentityPayload`], and [`OwnedMacCommand`] are
68//! optional heap-allocated conveniences for callers that need
69//! to retain parsed payloads across task boundaries. Most receive-side code should prefer the
70//! borrowed views from the payload crates and [`ReceivedPacketRef`].
71//!
72//! # MAC abstraction
73//!
74//! [`MacBackend`] exposes the public send/configure surface of the MAC coordinator.
75//! Safe PFS session management is available with `software-crypto` and builds on
76//! that public surface directly.
77//!
78//! [`MacHandle`](umsh_mac::MacHandle) implements `MacBackend`, and test code can provide
79//! a fake implementation to drive the node layer deterministically.
80//!
81//! # Typical usage
82//!
83//! For most applications, register callbacks and then let [`Host::run`] own the shared
84//! MAC event loop:
85//!
86//! ```rust,ignore
87//! let mut host = Host::new(mac_handle);
88//! let node = host.add_node(identity_id);
89//! let peer = node.peer(peer_key)?;
90//! let chat = umsh_text::UnicastTextChatWrapper::from_peer(&peer);
91//!
92//! let _messages = chat.on_text(|packet, text| {
93//!     println!(
94//!         "peer says: {} (hops={})",
95//!         text.body,
96//!         packet.flood_hops().map(|h| h.remaining()).unwrap_or(0),
97//!     );
98//! });
99//!
100//! let _ticket = chat.send_text("hello", &SendOptions::default()).await?;
101//! host.run().await?;
102//! ```
103//!
104//! If you need to multiplex UMSH progress with another async source such as user input, use
105//! [`Host::pump_once`] as a single wake-driven step. It already waits on radio activity and
106//! protocol deadlines; you should not add a manual poll/sleep loop around it.
107//!
108//! ```rust,ignore
109//! loop {
110//!     tokio::select! {
111//!         line = stdin.next_line() => { /* handle input */ }
112//!         result = host.pump_once() => result?,
113//!     }
114//! }
115//! ```
116//!
117//! If you need protocol fidelity instead of a payload wrapper, subscribe directly on the node
118//! or peer and inspect the raw packet view:
119//!
120//! ```rust,ignore
121//! let _raw = peer.on_receive(|packet| {
122//!     if packet.packet_family() == umsh::mac::PacketFamily::Unicast {
123//!         println!(
124//!             "from={:?} encrypted={} mic_len={}",
125//!             packet.from_key(),
126//!             packet.encrypted(),
127//!             packet.mic_len(),
128//!         );
129//!     }
130//!     false
131//! });
132//! ```
133
134#[cfg(not(feature = "alloc"))]
135compile_error!("umsh-node currently requires the alloc feature");
136
137extern crate alloc;
138
139mod app_error;
140mod app_owned;
141mod app_payload;
142mod app_util;
143#[cfg(feature = "software-crypto")]
144mod channel;
145mod dispatch;
146mod host;
147mod identity;
148mod mac;
149pub mod mac_command;
150mod node;
151mod peer;
152#[cfg(feature = "software-crypto")]
153mod pfs;
154mod receive;
155mod ticket;
156mod transport;
157
158pub use app_error::{AppEncodeError, AppParseError};
159pub use app_owned::{OwnedMacCommand, OwnedNodeIdentityPayload};
160pub use app_payload::{
161    expect_payload_type, parse_mac_command_payload, parse_node_identity_payload, split_payload_type,
162};
163#[cfg(feature = "software-crypto")]
164pub use channel::Channel;
165pub use host::{Host, HostError};
166pub use identity::{Capabilities, NodeIdentityPayload, NodeRole};
167pub use mac::{MacBackend, MacBackendError};
168pub use mac_command::{CommandId, MacCommand};
169#[cfg(feature = "software-crypto")]
170pub use node::BoundChannel;
171#[cfg(feature = "software-crypto")]
172pub use node::PfsStatus;
173pub use node::{LocalNode, NodeError, Subscription};
174pub use peer::PeerConnection;
175pub use receive::{ChannelInfoRef, PacketFamily, ReceivedPacketRef, RouteHops, RxMetadata, Snr};
176pub use ticket::{SendProgressTicket, SendToken};
177pub use transport::Transport;
178
179pub mod identity_payload {
180    pub use crate::identity::{encode, parse};
181}
182
183#[cfg(test)]
184mod tests {
185    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
186    use std::{
187        cell::RefCell,
188        collections::VecDeque,
189        future::Future,
190        num::NonZeroU8,
191        pin::pin,
192        rc::Rc,
193        task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
194    };
195    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
196    use umsh_core::{NodeHint, PublicKey};
197    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
198    use umsh_crypto::NodeIdentity;
199    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
200    use umsh_crypto::software::SoftwareIdentity;
201    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
202    use umsh_hal::Snr;
203    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
204    use umsh_mac::MacEventRef;
205    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
206    use umsh_mac::{CapacityError, LocalIdentityId, PeerId, SendError, SendOptions, SendReceipt};
207
208    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
209    use crate::ReceivedPacketRef;
210    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
211    use crate::{MacBackend, MacBackendError, OwnedMacCommand, SendToken};
212    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
213    use umsh_core::ChannelId;
214    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
215    use umsh_text::OwnedTextMessage;
216    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
217    #[test]
218    fn peer_receive_handlers_precede_node_receive_handlers() {
219        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
220
221        let mac = FakeMac::new(Vec::new());
222        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
223        let membership = Rc::new(RefCell::new(NodeMembership::new()));
224        let state = Rc::new(RefCell::new(LocalNodeState::new()));
225        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
226        let peer = PublicKey([0x41; 32]);
227        let peer_connection = node.peer(peer).unwrap();
228
229        let call_order = Rc::new(RefCell::new(Vec::new()));
230        let peer_call_order = call_order.clone();
231        let _peer_subscription = peer_connection.on_receive(move |_| {
232            peer_call_order.borrow_mut().push("peer");
233            true
234        });
235        let node_call_order = call_order.clone();
236        let _node_subscription = node.on_receive(move |_| {
237            node_call_order.borrow_mut().push("node");
238            true
239        });
240
241        assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
242        assert_eq!(call_order.borrow().as_slice(), ["peer"]);
243    }
244
245    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
246    #[test]
247    fn receive_callbacks_can_observe_rx_metadata() {
248        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
249
250        let mac = FakeMac::new(Vec::new());
251        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
252        let membership = Rc::new(RefCell::new(NodeMembership::new()));
253        let state = Rc::new(RefCell::new(LocalNodeState::new()));
254        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
255        let peer = PublicKey([0x44; 32]);
256
257        let observed = Rc::new(RefCell::new(None));
258        let observed_for_callback = observed.clone();
259        let _subscription = node.on_receive(move |packet| {
260            *observed_for_callback.borrow_mut() = Some((
261                packet.rssi(),
262                packet.snr(),
263                packet.lqi(),
264                packet.received_at_ms(),
265            ));
266            true
267        });
268
269        let payload = encode_text_payload("metadata");
270        let packet = test_unicast_packet_with_rx(
271            peer,
272            &payload,
273            umsh_mac::RxMetadata::new(
274                Some(-73),
275                Some(Snr::from_centibels(123)),
276                NonZeroU8::new(200),
277                Some(123_456),
278            ),
279        );
280
281        assert!(node.dispatch_received_packet(&packet));
282        assert_eq!(
283            *observed.borrow(),
284            Some((
285                Some(-73),
286                Some(Snr::from_centibels(123)),
287                NonZeroU8::new(200),
288                Some(123_456),
289            ))
290        );
291    }
292
293    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
294    #[test]
295    fn subscription_guard_unregisters_on_drop() {
296        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
297
298        let mac = FakeMac::new(Vec::new());
299        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
300        let membership = Rc::new(RefCell::new(NodeMembership::new()));
301        let state = Rc::new(RefCell::new(LocalNodeState::new()));
302        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
303        let peer = PublicKey([0x33; 32]);
304
305        let hits = Rc::new(RefCell::new(0u32));
306        {
307            let hits = hits.clone();
308            let _subscription = node.on_receive(move |_| {
309                *hits.borrow_mut() += 1;
310                true
311            });
312            assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
313        }
314
315        assert_eq!(*hits.borrow(), 1);
316        assert!(!node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
317        assert_eq!(*hits.borrow(), 1);
318    }
319
320    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
321    #[test]
322    fn callbacks_observe_control_side_events_and_peer_ack_state() {
323        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
324
325        let mac = FakeMac::new(Vec::new());
326        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
327        let membership = Rc::new(RefCell::new(NodeMembership::new()));
328        let state = Rc::new(RefCell::new(LocalNodeState::new()));
329        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
330
331        let peer = PublicKey([0x42; 32]);
332        let peer_connection = node.peer(peer).unwrap();
333        let node_discovery = Rc::new(RefCell::new(Vec::new()));
334        let beacons = Rc::new(RefCell::new(Vec::new()));
335        let commands = Rc::new(RefCell::new(Vec::new()));
336        let peer_acks = Rc::new(RefCell::new(Vec::new()));
337        let peer_timeouts = Rc::new(RefCell::new(Vec::new()));
338
339        let discovery_log = node_discovery.clone();
340        let _discovered_subscription = node.on_node_discovered(move |key, name| {
341            discovery_log
342                .borrow_mut()
343                .push((key, name.map(str::to_string)));
344        });
345        let beacon_log = beacons.clone();
346        let _beacon_subscription = node.on_beacon(move |from_hint, from_key| {
347            beacon_log.borrow_mut().push((from_hint, from_key));
348        });
349        let command_log = commands.clone();
350        let _command_subscription = node.on_mac_command(move |from, command| {
351            command_log.borrow_mut().push((from, command.clone()));
352        });
353        let peer_ack_log = peer_acks.clone();
354        let _ack_subscription = peer_connection.on_ack_received(move |token| {
355            peer_ack_log.borrow_mut().push(token);
356        });
357        let peer_timeout_log = peer_timeouts.clone();
358        let _timeout_subscription = peer_connection.on_ack_timeout(move |token| {
359            peer_timeout_log.borrow_mut().push(token);
360        });
361
362        let token = SendToken::new(LocalIdentityId(1), SendReceipt(12));
363        let timeout_token = SendToken::new(LocalIdentityId(1), SendReceipt(13));
364        let hint = NodeHint([1, 2, 3]);
365        let command = OwnedMacCommand::EchoRequest {
366            data: vec![9, 8, 7],
367        };
368
369        node.dispatch_node_discovered(peer, Some("alice"));
370        node.dispatch_beacon(hint, Some(peer));
371        node.dispatch_mac_command(peer, &command);
372        node.dispatch_ack_received(peer, token);
373        node.dispatch_ack_timeout(peer, timeout_token);
374
375        assert_eq!(
376            node_discovery.borrow().as_slice(),
377            &[(peer, Some(String::from("alice")))]
378        );
379        assert_eq!(beacons.borrow().as_slice(), &[(hint, Some(peer))]);
380        assert_eq!(commands.borrow().as_slice(), &[(peer, command)]);
381        assert_eq!(peer_acks.borrow().as_slice(), &[token]);
382        assert_eq!(peer_timeouts.borrow().as_slice(), &[timeout_token]);
383    }
384
385    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
386    #[test]
387    fn pfs_routed_send_tracks_ack_against_ephemeral_identity() {
388        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
389
390        let mac = FakeMac::new(vec![[7u8; 32], [9u8; 32]]);
391        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
392        let membership = Rc::new(RefCell::new(NodeMembership::new()));
393        let state = Rc::new(RefCell::new(LocalNodeState::new()));
394        let node = LocalNode::new(
395            LocalIdentityId(1),
396            mac.clone(),
397            dispatcher.clone(),
398            membership,
399            state,
400        );
401
402        let peer = PublicKey([0x55; 32]);
403        let peer_connection = node.peer(peer).unwrap();
404        let options = SendOptions::default().with_ack_requested(true);
405
406        block_on_ready(node.request_pfs(&peer, 60, &options)).unwrap();
407        let request = mac.take_unicasts().pop().expect("request send");
408        let request_command = parse_owned_mac_command(&request.payload);
409        let request_ephemeral = match request_command {
410            OwnedMacCommand::PfsSessionRequest { ephemeral_key, .. } => ephemeral_key,
411            other => panic!("unexpected request payload: {other:?}"),
412        };
413
414        block_on_ready(node.handle_pfs_command(
415            &peer,
416            &OwnedMacCommand::PfsSessionResponse {
417                ephemeral_key: PublicKey([0x44; 32]),
418                duration_minutes: 60,
419            },
420            &options,
421        ))
422        .unwrap();
423
424        let payload = encode_text_payload("hello over pfs");
425        let ticket = block_on_ready(peer_connection.send(&payload, &options)).unwrap();
426        let sent = mac.take_unicasts().pop().expect("pfs-routed send");
427        assert_eq!(sent.from, LocalIdentityId(10));
428        assert_eq!(sent.to, PublicKey([0x44; 32]));
429
430        let pairwise_from_pfs = PublicKey([0x44; 32]);
431        let _ = request_ephemeral; // Keeps the request path explicit in the test setup.
432        dispatcher.borrow_mut().dispatch_ticket_state(
433            sent.from,
434            &MacEventRef::AckReceived {
435                peer: pairwise_from_pfs,
436                receipt: SendReceipt(42),
437            },
438        );
439        assert!(ticket.was_acked());
440        assert!(ticket.is_finished());
441    }
442
443    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
444    #[test]
445    fn pfs_session_manager_request_and_teardown() {
446        use crate::pfs::PfsSessionManager;
447
448        let mac = FakeMac::new(vec![[3u8; 32]]);
449        let peer_long_term = PublicKey([0x55; 32]);
450        let options = SendOptions::default().with_ack_requested(true);
451
452        let mut pfs = PfsSessionManager::new();
453        block_on_ready(pfs.request_session(
454            &mac,
455            LocalIdentityId(1),
456            &peer_long_term,
457            60,
458            &options,
459        ))
460        .unwrap();
461
462        let sent = mac.take_unicasts();
463        assert_eq!(sent.len(), 1);
464        assert_eq!(sent[0].from, LocalIdentityId(1));
465        assert_eq!(sent[0].to, peer_long_term);
466        assert_eq!(
467            parse_owned_mac_command(&sent[0].payload),
468            OwnedMacCommand::PfsSessionRequest {
469                ephemeral_key: *SoftwareIdentity::from_secret_bytes(&[3u8; 32]).public_key(),
470                duration_minutes: 60,
471            }
472        );
473
474        assert!(
475            block_on_ready(pfs.end_session(
476                &mac,
477                LocalIdentityId(1),
478                &peer_long_term,
479                true,
480                &options,
481            ))
482            .unwrap()
483        );
484        let sent = mac.take_unicasts();
485        assert_eq!(sent.len(), 1);
486        assert_eq!(
487            parse_owned_mac_command(&sent[0].payload),
488            OwnedMacCommand::EndPfsSession
489        );
490    }
491
492    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
493    #[test]
494    fn pfs_end_session_errors_when_missing() {
495        use crate::pfs::PfsSessionManager;
496
497        let mac = FakeMac::new(Vec::new());
498        let options = SendOptions::default();
499        let mut pfs = PfsSessionManager::new();
500
501        let error = block_on_ready(pfs.end_session(
502            &mac,
503            LocalIdentityId(1),
504            &PublicKey([0x77; 32]),
505            true,
506            &options,
507        ))
508        .unwrap_err();
509        assert!(matches!(error, crate::NodeError::PfsSessionMissing));
510    }
511
512    #[cfg(feature = "unsafe-advanced")]
513    fn encode_text_payload(text: &str) -> Vec<u8> {
514        let message = OwnedTextMessage {
515            message_type: umsh_text::MessageType::Basic,
516            sender_handle: None,
517            sequence: None,
518            sequence_reset: false,
519            regarding: None,
520            editing: None,
521            bg_color: None,
522            text_color: None,
523            body: String::from(text),
524        };
525        let mut body = [0u8; 512];
526        let len = umsh_text::text_message::encode(&message.as_borrowed(), &mut body).unwrap();
527        let mut payload = Vec::with_capacity(len + 1);
528        payload.push(umsh_core::PayloadType::TextMessage as u8);
529        payload.extend_from_slice(&body[..len]);
530        payload
531    }
532
533    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
534    fn test_unicast_packet<'a>(from: PublicKey, payload: &'a [u8]) -> ReceivedPacketRef<'a> {
535        test_unicast_packet_with_rx(from, payload, umsh_mac::RxMetadata::default())
536    }
537
538    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
539    fn test_unicast_packet_with_rx<'a>(
540        from: PublicKey,
541        payload: &'a [u8],
542        rx: umsh_mac::RxMetadata,
543    ) -> ReceivedPacketRef<'a> {
544        let wire = Box::leak(payload.to_vec().into_boxed_slice());
545        let header = umsh_core::PacketHeader {
546            fcf: umsh_core::Fcf::new(umsh_core::PacketType::Unicast, false, false, false),
547            options_range: 0..0,
548            flood_hops: None,
549            dst: None,
550            channel: None,
551            ack_dst: None,
552            source: umsh_core::SourceAddrRef::Hint(from.hint()),
553            sec_info: None,
554            body_range: 0..wire.len(),
555            mic_range: wire.len()..wire.len(),
556            total_len: wire.len(),
557        };
558        ReceivedPacketRef::new(
559            wire,
560            wire,
561            header,
562            umsh_core::ParsedOptions::default(),
563            Some(from),
564            Some(from.hint()),
565            true,
566            None,
567            rx,
568        )
569    }
570
571    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
572    #[derive(Clone, Default)]
573    struct FakeMac {
574        state: Rc<RefCell<FakeMacState>>,
575    }
576
577    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
578    #[derive(Default)]
579    struct FakeMacState {
580        random_blocks: VecDeque<[u8; 32]>,
581        next_peer_id: u8,
582        next_ephemeral_id: u8,
583        now_ms: u64,
584        unicasts: Vec<SentUnicast>,
585        removed_ephemerals: Vec<LocalIdentityId>,
586        peers: Vec<(PublicKey, PeerId)>,
587    }
588
589    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
590    #[derive(Clone, Debug, PartialEq, Eq)]
591    struct SentUnicast {
592        from: LocalIdentityId,
593        to: PublicKey,
594        payload: Vec<u8>,
595        options: SendOptions,
596    }
597
598    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
599    impl FakeMac {
600        fn new(random_blocks: Vec<[u8; 32]>) -> Self {
601            Self {
602                state: Rc::new(RefCell::new(FakeMacState {
603                    random_blocks: random_blocks.into(),
604                    next_peer_id: 0,
605                    next_ephemeral_id: 10,
606                    now_ms: 1_000,
607                    ..FakeMacState::default()
608                })),
609            }
610        }
611
612        fn take_unicasts(&self) -> Vec<SentUnicast> {
613            core::mem::take(&mut self.state.borrow_mut().unicasts)
614        }
615    }
616
617    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
618    impl MacBackend for FakeMac {
619        type SendError = SendError;
620        type CapacityError = CapacityError;
621
622        fn add_peer(
623            &self,
624            key: PublicKey,
625        ) -> Result<PeerId, MacBackendError<Self::SendError, Self::CapacityError>> {
626            let mut state = self.state.borrow_mut();
627            if let Some((_, existing)) = state
628                .peers
629                .iter()
630                .find(|(existing_key, _)| *existing_key == key)
631            {
632                return Ok(*existing);
633            }
634            let peer_id = PeerId(state.next_peer_id);
635            state.next_peer_id = state.next_peer_id.wrapping_add(1);
636            state.peers.push((key, peer_id));
637            Ok(peer_id)
638        }
639
640        fn add_private_channel(
641            &self,
642            _key: umsh_core::ChannelKey,
643        ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
644            Ok(())
645        }
646
647        fn add_named_channel(
648            &self,
649            _name: &str,
650        ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
651            Ok(())
652        }
653
654        async fn send_broadcast(
655            &self,
656            _from: LocalIdentityId,
657            _payload: &[u8],
658            _options: &SendOptions,
659        ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
660            Ok(SendReceipt(99))
661        }
662
663        async fn send_multicast(
664            &self,
665            _from: LocalIdentityId,
666            _channel: &ChannelId,
667            _payload: &[u8],
668            _options: &SendOptions,
669        ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
670            Ok(SendReceipt(99))
671        }
672
673        async fn send_unicast(
674            &self,
675            from: LocalIdentityId,
676            dst: &PublicKey,
677            payload: &[u8],
678            options: &SendOptions,
679        ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
680        {
681            self.state.borrow_mut().unicasts.push(SentUnicast {
682                from,
683                to: *dst,
684                payload: payload.to_vec(),
685                options: options.clone(),
686            });
687            Ok(Some(SendReceipt(42)))
688        }
689
690        async fn send_blind_unicast(
691            &self,
692            from: LocalIdentityId,
693            dst: &PublicKey,
694            _channel: &ChannelId,
695            payload: &[u8],
696            options: &SendOptions,
697        ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
698        {
699            self.send_unicast(from, dst, payload, options).await
700        }
701
702        fn fill_random(
703            &self,
704            dest: &mut [u8],
705        ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
706            let mut state = self.state.borrow_mut();
707            let next = state.random_blocks.pop_front().expect("test rng exhausted");
708            dest.copy_from_slice(&next[..dest.len()]);
709            Ok(())
710        }
711
712        fn now_ms(&self) -> Result<u64, MacBackendError<Self::SendError, Self::CapacityError>> {
713            Ok(self.state.borrow().now_ms)
714        }
715
716        fn register_ephemeral(
717            &self,
718            _parent: LocalIdentityId,
719            _identity: SoftwareIdentity,
720        ) -> Result<LocalIdentityId, MacBackendError<Self::SendError, Self::CapacityError>>
721        {
722            let mut state = self.state.borrow_mut();
723            let id = LocalIdentityId(state.next_ephemeral_id);
724            state.next_ephemeral_id = state.next_ephemeral_id.wrapping_add(1);
725            Ok(id)
726        }
727
728        fn remove_ephemeral(
729            &self,
730            id: LocalIdentityId,
731        ) -> Result<bool, MacBackendError<Self::SendError, Self::CapacityError>> {
732            self.state.borrow_mut().removed_ephemerals.push(id);
733            Ok(true)
734        }
735    }
736
737    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
738    fn parse_owned_mac_command(payload: &[u8]) -> OwnedMacCommand {
739        OwnedMacCommand::from(
740            crate::parse_mac_command_payload(umsh_core::PacketType::Unicast, payload).unwrap(),
741        )
742    }
743
744    #[cfg(feature = "unsafe-advanced")]
745    fn block_on_ready<F: Future>(future: F) -> F::Output {
746        fn raw_waker() -> RawWaker {
747            fn clone(_: *const ()) -> RawWaker {
748                raw_waker()
749            }
750            fn wake(_: *const ()) {}
751            fn wake_by_ref(_: *const ()) {}
752            fn drop(_: *const ()) {}
753
754            RawWaker::new(
755                core::ptr::null(),
756                &RawWakerVTable::new(clone, wake, wake_by_ref, drop),
757            )
758        }
759
760        let waker = unsafe { Waker::from_raw(raw_waker()) };
761        let mut context = Context::from_waker(&waker);
762        let mut future = pin!(future);
763        match future.as_mut().poll(&mut context) {
764            Poll::Ready(value) => value,
765            Poll::Pending => panic!("test future unexpectedly returned Poll::Pending"),
766        }
767    }
768}