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_payload;
141mod app_util;
142#[cfg(feature = "software-crypto")]
143mod channel;
144mod dispatch;
145mod host;
146mod identity;
147pub mod location;
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_payload::{
160    expect_payload_type, parse_mac_command_payload, parse_node_identity_payload, split_payload_type,
161};
162#[cfg(feature = "software-crypto")]
163pub use channel::Channel;
164pub use host::{Host, HostError};
165pub use identity::{NodeCapabilities, NodeIdentityPayload, NodeRole};
166pub use mac::{MacBackend, MacBackendError};
167pub use mac_command::OwnedMacCommand;
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
179#[cfg(test)]
180mod tests {
181    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
182    use std::{
183        cell::RefCell,
184        collections::VecDeque,
185        future::Future,
186        num::NonZeroU8,
187        pin::pin,
188        rc::Rc,
189        task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
190    };
191    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
192    use umsh_core::{NodeHint, PublicKey};
193    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
194    use umsh_crypto::NodeIdentity;
195    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
196    use umsh_crypto::software::SoftwareIdentity;
197    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
198    use umsh_hal::Snr;
199    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
200    use umsh_mac::MacEventRef;
201    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
202    use umsh_mac::{CapacityError, LocalIdentityId, PeerId, SendError, SendOptions, SendReceipt};
203
204    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
205    use crate::ReceivedPacketRef;
206    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
207    use crate::{MacBackend, MacBackendError, OwnedMacCommand, SendToken};
208    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
209    use umsh_core::ChannelId;
210    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
211    use umsh_text::OwnedTextMessage;
212    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
213    #[test]
214    fn peer_receive_handlers_precede_node_receive_handlers() {
215        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
216
217        let mac = FakeMac::new(Vec::new());
218        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
219        let membership = Rc::new(RefCell::new(NodeMembership::new()));
220        let state = Rc::new(RefCell::new(LocalNodeState::new()));
221        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
222        let peer = PublicKey([0x41; 32]);
223        let peer_connection = block_on_ready(node.peer(peer)).unwrap();
224
225        let call_order = Rc::new(RefCell::new(Vec::new()));
226        let peer_call_order = call_order.clone();
227        let _peer_subscription = peer_connection.on_receive(move |_| {
228            peer_call_order.borrow_mut().push("peer");
229            true
230        });
231        let node_call_order = call_order.clone();
232        let _node_subscription = node.on_receive(move |_| {
233            node_call_order.borrow_mut().push("node");
234            true
235        });
236
237        assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
238        assert_eq!(call_order.borrow().as_slice(), ["peer"]);
239    }
240
241    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
242    #[test]
243    fn receive_callbacks_can_observe_rx_metadata() {
244        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
245
246        let mac = FakeMac::new(Vec::new());
247        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
248        let membership = Rc::new(RefCell::new(NodeMembership::new()));
249        let state = Rc::new(RefCell::new(LocalNodeState::new()));
250        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
251        let peer = PublicKey([0x44; 32]);
252
253        let observed = Rc::new(RefCell::new(None));
254        let observed_for_callback = observed.clone();
255        let _subscription = node.on_receive(move |packet| {
256            *observed_for_callback.borrow_mut() = Some((
257                packet.rssi(),
258                packet.snr(),
259                packet.lqi(),
260                packet.received_at_ms(),
261            ));
262            true
263        });
264
265        let payload = encode_text_payload("metadata");
266        let packet = test_unicast_packet_with_rx(
267            peer,
268            &payload,
269            umsh_mac::RxMetadata::new(
270                Some(-73),
271                Some(Snr::from_centibels(123)),
272                NonZeroU8::new(200),
273                Some(123_456),
274            ),
275        );
276
277        assert!(node.dispatch_received_packet(&packet));
278        assert_eq!(
279            *observed.borrow(),
280            Some((
281                Some(-73),
282                Some(Snr::from_centibels(123)),
283                NonZeroU8::new(200),
284                Some(123_456),
285            ))
286        );
287    }
288
289    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
290    #[test]
291    fn subscription_guard_unregisters_on_drop() {
292        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
293
294        let mac = FakeMac::new(Vec::new());
295        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
296        let membership = Rc::new(RefCell::new(NodeMembership::new()));
297        let state = Rc::new(RefCell::new(LocalNodeState::new()));
298        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
299        let peer = PublicKey([0x33; 32]);
300
301        let hits = Rc::new(RefCell::new(0u32));
302        {
303            let hits = hits.clone();
304            let _subscription = node.on_receive(move |_| {
305                *hits.borrow_mut() += 1;
306                true
307            });
308            assert!(node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
309        }
310
311        assert_eq!(*hits.borrow(), 1);
312        assert!(!node.dispatch_received_packet(&test_unicast_packet(peer, &[0x01, 0x02])));
313        assert_eq!(*hits.borrow(), 1);
314    }
315
316    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
317    #[test]
318    fn callbacks_observe_control_side_events_and_peer_ack_state() {
319        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
320
321        let mac = FakeMac::new(Vec::new());
322        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
323        let membership = Rc::new(RefCell::new(NodeMembership::new()));
324        let state = Rc::new(RefCell::new(LocalNodeState::new()));
325        let node = LocalNode::new(LocalIdentityId(1), mac, dispatcher, membership, state);
326
327        let peer = PublicKey([0x42; 32]);
328        let peer_connection = block_on_ready(node.peer(peer)).unwrap();
329        let node_discovery = Rc::new(RefCell::new(Vec::new()));
330        let beacons = Rc::new(RefCell::new(Vec::new()));
331        let commands = Rc::new(RefCell::new(Vec::new()));
332        let peer_acks = Rc::new(RefCell::new(Vec::new()));
333        let peer_timeouts = Rc::new(RefCell::new(Vec::new()));
334
335        let discovery_log = node_discovery.clone();
336        let _discovered_subscription = node.on_node_discovered(move |key, name| {
337            discovery_log
338                .borrow_mut()
339                .push((key, name.map(str::to_string)));
340        });
341        let beacon_log = beacons.clone();
342        let _beacon_subscription = node.on_beacon(move |from_hint, from_key| {
343            beacon_log.borrow_mut().push((from_hint, from_key));
344        });
345        let command_log = commands.clone();
346        let _command_subscription = node.on_mac_command(move |from, command| {
347            command_log.borrow_mut().push((from, command.clone()));
348        });
349        let peer_ack_log = peer_acks.clone();
350        let _ack_subscription = peer_connection.on_ack_received(move |token| {
351            peer_ack_log.borrow_mut().push(token);
352        });
353        let peer_timeout_log = peer_timeouts.clone();
354        let _timeout_subscription = peer_connection.on_ack_timeout(move |token| {
355            peer_timeout_log.borrow_mut().push(token);
356        });
357
358        let token = SendToken::new(LocalIdentityId(1), SendReceipt(12));
359        let timeout_token = SendToken::new(LocalIdentityId(1), SendReceipt(13));
360        let hint = NodeHint([1, 2, 3]);
361        let command = OwnedMacCommand::EchoRequest {
362            data: vec![9, 8, 7],
363        };
364
365        node.dispatch_node_discovered(peer, Some("alice"));
366        node.dispatch_beacon(hint, Some(peer));
367        node.dispatch_mac_command(peer, &command);
368        node.dispatch_ack_received(peer, token);
369        node.dispatch_ack_timeout(peer, timeout_token);
370
371        assert_eq!(
372            node_discovery.borrow().as_slice(),
373            &[(peer, Some(String::from("alice")))]
374        );
375        assert_eq!(beacons.borrow().as_slice(), &[(hint, Some(peer))]);
376        assert_eq!(commands.borrow().as_slice(), &[(peer, command)]);
377        assert_eq!(peer_acks.borrow().as_slice(), &[token]);
378        assert_eq!(peer_timeouts.borrow().as_slice(), &[timeout_token]);
379    }
380
381    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
382    #[test]
383    fn pfs_routed_send_tracks_ack_against_ephemeral_identity() {
384        use crate::node::{LocalNode, LocalNodeState, NodeMembership};
385
386        let mac = FakeMac::new(vec![[7u8; 32], [9u8; 32]]);
387        let dispatcher = Rc::new(RefCell::new(crate::dispatch::EventDispatcher::new()));
388        let membership = Rc::new(RefCell::new(NodeMembership::new()));
389        let state = Rc::new(RefCell::new(LocalNodeState::new()));
390        let node = LocalNode::new(
391            LocalIdentityId(1),
392            mac.clone(),
393            dispatcher.clone(),
394            membership,
395            state,
396        );
397
398        let peer = PublicKey([0x55; 32]);
399        let peer_connection = block_on_ready(node.peer(peer)).unwrap();
400        let options = SendOptions::default().with_ack_requested(true);
401
402        block_on_ready(node.request_pfs(&peer, 60, &options)).unwrap();
403        let request = mac.take_unicasts().pop().expect("request send");
404        let request_command = parse_owned_mac_command(&request.payload);
405        let request_ephemeral = match request_command {
406            OwnedMacCommand::PfsSessionRequest { ephemeral_key, .. } => ephemeral_key,
407            other => panic!("unexpected request payload: {other:?}"),
408        };
409
410        block_on_ready(node.handle_pfs_command(
411            &peer,
412            &OwnedMacCommand::PfsSessionResponse {
413                ephemeral_key: PublicKey([0x44; 32]),
414                duration_minutes: 60,
415            },
416            &options,
417        ))
418        .unwrap();
419
420        let payload = encode_text_payload("hello over pfs");
421        let ticket = block_on_ready(peer_connection.send(&payload, &options)).unwrap();
422        let sent = mac.take_unicasts().pop().expect("pfs-routed send");
423        assert_eq!(sent.from, LocalIdentityId(10));
424        assert_eq!(sent.to, PublicKey([0x44; 32]));
425
426        let pairwise_from_pfs = PublicKey([0x44; 32]);
427        let _ = request_ephemeral; // Keeps the request path explicit in the test setup.
428        dispatcher.borrow_mut().dispatch_ticket_state(
429            sent.from,
430            &MacEventRef::AckReceived {
431                peer: pairwise_from_pfs,
432                receipt: SendReceipt(42),
433            },
434        );
435        assert!(ticket.was_acked());
436        assert!(ticket.is_finished());
437    }
438
439    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
440    #[test]
441    fn pfs_session_manager_request_and_teardown() {
442        use crate::pfs::PfsSessionManager;
443
444        let mac = FakeMac::new(vec![[3u8; 32]]);
445        let peer_long_term = PublicKey([0x55; 32]);
446        let options = SendOptions::default().with_ack_requested(true);
447
448        let mut pfs = PfsSessionManager::new();
449        block_on_ready(pfs.request_session(
450            &mac,
451            LocalIdentityId(1),
452            &peer_long_term,
453            60,
454            &options,
455        ))
456        .unwrap();
457
458        let sent = mac.take_unicasts();
459        assert_eq!(sent.len(), 1);
460        assert_eq!(sent[0].from, LocalIdentityId(1));
461        assert_eq!(sent[0].to, peer_long_term);
462        assert_eq!(
463            parse_owned_mac_command(&sent[0].payload),
464            OwnedMacCommand::PfsSessionRequest {
465                ephemeral_key: *SoftwareIdentity::from_secret_bytes(&[3u8; 32]).public_key(),
466                duration_minutes: 60,
467            }
468        );
469
470        assert!(
471            block_on_ready(pfs.end_session(
472                &mac,
473                LocalIdentityId(1),
474                &peer_long_term,
475                true,
476                &options,
477            ))
478            .unwrap()
479        );
480        let sent = mac.take_unicasts();
481        assert_eq!(sent.len(), 1);
482        assert_eq!(
483            parse_owned_mac_command(&sent[0].payload),
484            OwnedMacCommand::EndPfsSession
485        );
486    }
487
488    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
489    #[test]
490    fn pfs_end_session_errors_when_missing() {
491        use crate::pfs::PfsSessionManager;
492
493        let mac = FakeMac::new(Vec::new());
494        let options = SendOptions::default();
495        let mut pfs = PfsSessionManager::new();
496
497        let error = block_on_ready(pfs.end_session(
498            &mac,
499            LocalIdentityId(1),
500            &PublicKey([0x77; 32]),
501            true,
502            &options,
503        ))
504        .unwrap_err();
505        assert!(matches!(error, crate::NodeError::PfsSessionMissing));
506    }
507
508    #[cfg(feature = "unsafe-advanced")]
509    fn encode_text_payload(text: &str) -> Vec<u8> {
510        let message = OwnedTextMessage {
511            message_type: umsh_text::MessageType::Basic,
512            sender_handle: None,
513            sequence: None,
514            sequence_reset: false,
515            regarding: None,
516            editing: None,
517            bg_color: None,
518            text_color: None,
519            body: String::from(text),
520        };
521        let mut body = [0u8; 512];
522        let len = umsh_text::text_message::encode(&message.as_borrowed(), &mut body).unwrap();
523        let mut payload = Vec::with_capacity(len + 1);
524        payload.push(umsh_core::PayloadType::TextMessage as u8);
525        payload.extend_from_slice(&body[..len]);
526        payload
527    }
528
529    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
530    fn test_unicast_packet<'a>(from: PublicKey, payload: &'a [u8]) -> ReceivedPacketRef<'a> {
531        test_unicast_packet_with_rx(from, payload, umsh_mac::RxMetadata::default())
532    }
533
534    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
535    fn test_unicast_packet_with_rx<'a>(
536        from: PublicKey,
537        payload: &'a [u8],
538        rx: umsh_mac::RxMetadata,
539    ) -> ReceivedPacketRef<'a> {
540        let wire = Box::leak(payload.to_vec().into_boxed_slice());
541        let header = umsh_core::PacketHeader {
542            fcf: umsh_core::Fcf::new(umsh_core::PacketType::Unicast, false, false),
543            options_range: 0..0,
544            flood_hops: None,
545            dst: None,
546            channel: None,
547            ack_dst: None,
548            source: umsh_core::SourceAddrRef::Hint(from.hint()),
549            sec_info: None,
550            body_range: 0..wire.len(),
551            mic_range: wire.len()..wire.len(),
552            total_len: wire.len(),
553        };
554        ReceivedPacketRef::new(
555            wire,
556            wire,
557            header,
558            umsh_core::ParsedOptions::default(),
559            Some(from),
560            Some(from.hint()),
561            true,
562            None,
563            rx,
564        )
565    }
566
567    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
568    #[derive(Clone, Default)]
569    struct FakeMac {
570        state: Rc<RefCell<FakeMacState>>,
571    }
572
573    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
574    #[derive(Default)]
575    struct FakeMacState {
576        random_blocks: VecDeque<[u8; 32]>,
577        next_peer_id: u8,
578        next_ephemeral_id: u8,
579        now_ms: u64,
580        unicasts: Vec<SentUnicast>,
581        removed_ephemerals: Vec<LocalIdentityId>,
582        peers: Vec<(PublicKey, PeerId)>,
583    }
584
585    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
586    #[derive(Clone, Debug, PartialEq, Eq)]
587    struct SentUnicast {
588        from: LocalIdentityId,
589        to: PublicKey,
590        payload: Vec<u8>,
591        options: SendOptions,
592    }
593
594    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
595    impl FakeMac {
596        fn new(random_blocks: Vec<[u8; 32]>) -> Self {
597            Self {
598                state: Rc::new(RefCell::new(FakeMacState {
599                    random_blocks: random_blocks.into(),
600                    next_peer_id: 0,
601                    next_ephemeral_id: 10,
602                    now_ms: 1_000,
603                    ..FakeMacState::default()
604                })),
605            }
606        }
607
608        fn take_unicasts(&self) -> Vec<SentUnicast> {
609            core::mem::take(&mut self.state.borrow_mut().unicasts)
610        }
611    }
612
613    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
614    impl MacBackend for FakeMac {
615        type SendError = SendError;
616        type CapacityError = CapacityError;
617
618        async fn add_peer(
619            &self,
620            key: PublicKey,
621        ) -> Result<PeerId, MacBackendError<Self::SendError, Self::CapacityError>> {
622            let mut state = self.state.borrow_mut();
623            if let Some((_, existing)) = state
624                .peers
625                .iter()
626                .find(|(existing_key, _)| *existing_key == key)
627            {
628                return Ok(*existing);
629            }
630            let peer_id = PeerId(state.next_peer_id);
631            state.next_peer_id = state.next_peer_id.wrapping_add(1);
632            state.peers.push((key, peer_id));
633            Ok(peer_id)
634        }
635
636        async fn add_private_channel(
637            &self,
638            _key: umsh_core::ChannelKey,
639        ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
640            Ok(())
641        }
642
643        async fn add_named_channel(
644            &self,
645            _name: &str,
646        ) -> Result<(), MacBackendError<Self::SendError, Self::CapacityError>> {
647            Ok(())
648        }
649
650        async fn send_broadcast(
651            &self,
652            _from: LocalIdentityId,
653            _payload: &[u8],
654            _options: &SendOptions,
655        ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
656            Ok(SendReceipt(99))
657        }
658
659        async fn send_multicast(
660            &self,
661            _from: LocalIdentityId,
662            _channel: &ChannelId,
663            _payload: &[u8],
664            _options: &SendOptions,
665        ) -> Result<SendReceipt, MacBackendError<Self::SendError, Self::CapacityError>> {
666            Ok(SendReceipt(99))
667        }
668
669        async fn send_unicast(
670            &self,
671            from: LocalIdentityId,
672            dst: &PublicKey,
673            payload: &[u8],
674            options: &SendOptions,
675        ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
676        {
677            self.state.borrow_mut().unicasts.push(SentUnicast {
678                from,
679                to: *dst,
680                payload: payload.to_vec(),
681                options: options.clone(),
682            });
683            Ok(Some(SendReceipt(42)))
684        }
685
686        async fn send_blind_unicast(
687            &self,
688            from: LocalIdentityId,
689            dst: &PublicKey,
690            _channel: &ChannelId,
691            payload: &[u8],
692            options: &SendOptions,
693        ) -> Result<Option<SendReceipt>, MacBackendError<Self::SendError, Self::CapacityError>>
694        {
695            self.send_unicast(from, dst, payload, options).await
696        }
697
698        async fn fill_random(&self, dest: &mut [u8]) {
699            let mut state = self.state.borrow_mut();
700            let next = state.random_blocks.pop_front().expect("test rng exhausted");
701            dest.copy_from_slice(&next[..dest.len()]);
702        }
703
704        async fn now_ms(&self) -> u64 {
705            self.state.borrow().now_ms
706        }
707
708        async fn register_ephemeral(
709            &self,
710            _parent: LocalIdentityId,
711            _identity: SoftwareIdentity,
712        ) -> Result<LocalIdentityId, MacBackendError<Self::SendError, Self::CapacityError>>
713        {
714            let mut state = self.state.borrow_mut();
715            let id = LocalIdentityId(state.next_ephemeral_id);
716            state.next_ephemeral_id = state.next_ephemeral_id.wrapping_add(1);
717            Ok(id)
718        }
719
720        async fn remove_ephemeral(&self, id: LocalIdentityId) -> bool {
721            self.state.borrow_mut().removed_ephemerals.push(id);
722            true
723        }
724    }
725
726    #[cfg(all(feature = "software-crypto", feature = "unsafe-advanced"))]
727    fn parse_owned_mac_command(payload: &[u8]) -> OwnedMacCommand {
728        OwnedMacCommand::from(
729            crate::parse_mac_command_payload(umsh_core::PacketType::Unicast, payload).unwrap(),
730        )
731    }
732
733    #[cfg(feature = "unsafe-advanced")]
734    fn block_on_ready<F: Future>(future: F) -> F::Output {
735        fn raw_waker() -> RawWaker {
736            fn clone(_: *const ()) -> RawWaker {
737                raw_waker()
738            }
739            fn wake(_: *const ()) {}
740            fn wake_by_ref(_: *const ()) {}
741            fn drop(_: *const ()) {}
742
743            RawWaker::new(
744                core::ptr::null(),
745                &RawWakerVTable::new(clone, wake, wake_by_ref, drop),
746            )
747        }
748
749        let waker = unsafe { Waker::from_raw(raw_waker()) };
750        let mut context = Context::from_waker(&waker);
751        let mut future = pin!(future);
752        match future.as_mut().poll(&mut context) {
753            Poll::Ready(value) => value,
754            Poll::Pending => panic!("test future unexpectedly returned Poll::Pending"),
755        }
756    }
757}