1use core::{
8 cell::RefCell,
9 marker::PhantomData,
10 pin::Pin,
11 task::{Context, Poll},
12};
13use std::{
14 collections::{BTreeMap, BTreeSet, VecDeque},
15 fmt,
16 rc::Rc,
17 sync::{
18 Arc, Mutex,
19 atomic::{AtomicBool, AtomicU64, Ordering},
20 mpsc as std_mpsc,
21 },
22 time::Duration,
23};
24
25use embedded_hal_async::delay::DelayNs;
26use tokio::sync::{mpsc, oneshot};
27use umsh_core::{ChannelKey, ChannelTag, NodeHint, PayloadType, PublicKey};
28use umsh_crypto::{
29 CryptoEngine, NodeIdentity,
30 software::{SoftwareAes, SoftwareIdentity, SoftwareSha256},
31};
32use umsh_hal::{Clock, CounterStore, KeyValueStore, Radio, RxInfo, Snr, TxError, TxOptions};
33use umsh_mac::{Mac, MacHandle, OperatingPolicy, RepeaterConfig, SendOptions};
34use umsh_node::{
35 Host, LocalNode, MacBackend, NodeCapabilities, NodeIdentityPayload, NodeIdentityProfile,
36 NodeRole, PacketFamily, SendProgressTicket, Transport,
37 location::{MAX_PRECISION, NodeLocation},
38};
39use umsh_sync::AsyncRefCell;
40use umsh_text::engine::{ArchiveResult, DeliveryState, Destination};
41use umsh_text::model::{ConversationKey, SenderScope};
42use umsh_text::validate::{DeliveryPath, Envelope};
43
44use crate::mobile_chat::{
45 ChannelRegistry, MobileChatArchiveLookupRecord, MobileChatArchiveResultKind,
46 MobileChatCheckpointRecord, MobileChatComposeBatchRecord, MobileChatDeliveryRecord,
47 MobileChatDirection, MobileChatMutationKind, MobileChatMutationRecord, MobileChatOriginalRef,
48 MobileChatPresence, MobileChatRegardingRef, MobileChatRxMetadataRecord,
49 MobileChatSenderResolutionRecord, MobileChatState,
50};
51use crate::{MobileCounterStore, MobileError, MobileIdentity};
52
53const MAX_FRAME_SIZE: usize = 256;
54const DEFAULT_FRAME_TIME_MS: u32 = 800;
55const BEACON_FLOOD_HOPS: u8 = 5;
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Error)]
60pub enum MobileMeshError {
61 InvalidPeer,
62 SessionUnavailable,
63 OperationInProgress,
64 CounterPersistenceFailed,
65 SendFailed,
66 ChatComposeFailed,
67 ChatBatchMissing,
68 InvalidChannelKey,
70 ChannelCapacity,
72 UnknownConversation,
75 InvalidLocation,
79}
80
81impl fmt::Display for MobileMeshError {
82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83 formatter.write_str(match self {
84 Self::InvalidPeer => "MESH_INVALID_PEER",
85 Self::SessionUnavailable => "MESH_SESSION_UNAVAILABLE",
86 Self::OperationInProgress => "MESH_OPERATION_IN_PROGRESS",
87 Self::CounterPersistenceFailed => "MESH_COUNTER_PERSISTENCE_FAILED",
88 Self::SendFailed => "MESH_SEND_FAILED",
89 Self::ChatComposeFailed => "MESH_CHAT_COMPOSE_FAILED",
90 Self::ChatBatchMissing => "MESH_CHAT_BATCH_MISSING",
91 Self::InvalidChannelKey => "MESH_INVALID_CHANNEL_KEY",
92 Self::ChannelCapacity => "MESH_CHANNEL_CAPACITY",
93 Self::UnknownConversation => "MESH_UNKNOWN_CONVERSATION",
94 Self::InvalidLocation => "MESH_INVALID_LOCATION",
95 })
96 }
97}
98
99impl std::error::Error for MobileMeshError {}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
102pub enum MobileMeshPingOutcome {
103 Reply,
104 TimedOut,
105 Failed,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
109pub struct MobileMeshPingEventRecord {
110 pub operation_id: u64,
111 pub outcome: MobileMeshPingOutcome,
112 pub round_trip_milliseconds: Option<u64>,
113 pub hop_count: Option<u8>,
116 pub route_hints: Vec<Vec<u8>>,
119 pub rssi_dbm: Option<i16>,
121 pub snr_centibels: Option<i16>,
122 pub lqi: Option<u8>,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq, uniffi::Enum)]
127pub enum MobileMeshRouteKind {
128 Unknown,
132 Direct,
134 Source,
136 Flood,
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
142pub struct MobileMeshRouteRecord {
143 pub kind: MobileMeshRouteKind,
144 pub hints: Vec<Vec<u8>>,
147 pub flood_hops: Option<u8>,
149 pub flood_regions: Vec<Vec<u8>>,
151}
152
153impl MobileMeshRouteRecord {
154 fn unknown() -> Self {
155 Self {
156 kind: MobileMeshRouteKind::Unknown,
157 hints: Vec::new(),
158 flood_hops: None,
159 flood_regions: Vec::new(),
160 }
161 }
162}
163
164impl From<Option<umsh_mac::CachedRoute>> for MobileMeshRouteRecord {
165 fn from(route: Option<umsh_mac::CachedRoute>) -> Self {
166 match route {
167 None => Self::unknown(),
168 Some(umsh_mac::CachedRoute::Direct) => Self {
169 kind: MobileMeshRouteKind::Direct,
170 ..Self::unknown()
171 },
172 Some(umsh_mac::CachedRoute::Source(hops)) => Self {
173 kind: MobileMeshRouteKind::Source,
174 hints: hops.iter().map(|hop| hop.0.to_vec()).collect(),
175 ..Self::unknown()
176 },
177 Some(umsh_mac::CachedRoute::Flood { hops, regions }) => Self {
178 kind: MobileMeshRouteKind::Flood,
179 flood_hops: Some(hops),
180 flood_regions: regions.iter().map(|region| region.to_vec()).collect(),
181 ..Self::unknown()
182 },
183 }
184 }
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
194pub struct MobileMeshAdvertisementRecord {
195 pub peer_address: String,
197 pub payload: Vec<u8>,
200 pub source_authenticated: bool,
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, uniffi::Record)]
218pub struct MobileMeshSharedLocationRecord {
219 pub latitude_degrees: f64,
220 pub longitude_degrees: f64,
221 pub precision_bytes: u8,
224}
225
226#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
235pub struct MobileMeshPeerHeardRecord {
236 pub peer_address: Option<String>,
241 pub node_hint: Option<Vec<u8>>,
245 pub source_authenticated: bool,
250}
251
252#[uniffi::export(with_foreign)]
259pub trait MobileMeshWakeListener: Send + Sync {
260 fn on_update_pending(&self);
261}
262
263struct WakeSignal {
267 pending: AtomicBool,
268 listener: Mutex<Option<Arc<dyn MobileMeshWakeListener>>>,
269}
270
271impl WakeSignal {
272 fn new() -> Self {
273 Self {
274 pending: AtomicBool::new(false),
275 listener: Mutex::new(None),
276 }
277 }
278
279 fn notify(&self) {
280 if self.pending.swap(true, Ordering::AcqRel) {
281 return;
282 }
283 let listener = self
284 .listener
285 .lock()
286 .ok()
287 .and_then(|slot| slot.as_ref().cloned());
288 if let Some(listener) = listener {
289 listener.on_update_pending();
290 }
291 }
292
293 fn drained(&self) {
294 self.pending.store(false, Ordering::Release);
295 }
296
297 fn set_listener(&self, listener: Option<Arc<dyn MobileMeshWakeListener>>) {
298 let already_pending = {
299 let Ok(mut slot) = self.listener.lock() else {
300 return;
301 };
302 *slot = listener.clone();
303 self.pending.load(Ordering::Acquire)
304 };
305 if already_pending && let Some(listener) = listener {
308 listener.on_update_pending();
309 }
310 }
311}
312
313struct NotifyingSender<T> {
315 tx: std_mpsc::Sender<T>,
316 wake: Arc<WakeSignal>,
317}
318
319impl<T> Clone for NotifyingSender<T> {
320 fn clone(&self) -> Self {
321 Self {
322 tx: self.tx.clone(),
323 wake: self.wake.clone(),
324 }
325 }
326}
327
328impl<T> NotifyingSender<T> {
329 fn send(&self, value: T) -> Result<(), std_mpsc::SendError<T>> {
330 self.tx.send(value)?;
331 self.wake.notify();
332 Ok(())
333 }
334}
335
336#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
337pub struct MobileMeshSessionUpdateRecord {
338 pub outbound_frames: Vec<MobileMeshOutboundFrameRecord>,
342 pub ping_events: Vec<MobileMeshPingEventRecord>,
343 pub advertisement_events: Vec<MobileMeshAdvertisementRecord>,
344 pub peer_heard_events: Vec<MobileMeshPeerHeardRecord>,
345 pub chat_batch_id: Option<u64>,
348 pub chat_mutations: Vec<MobileChatMutationRecord>,
349 pub chat_deliveries: Vec<MobileChatDeliveryRecord>,
350 pub chat_archive_lookups: Vec<MobileChatArchiveLookupRecord>,
351 pub chat_sender_resolutions: Vec<MobileChatSenderResolutionRecord>,
354 pub chat_diagnostics: Vec<String>,
355}
356
357#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
358pub struct MobileMeshOutboundFrameRecord {
359 pub id: u64,
360 pub data: Vec<u8>,
361 pub nocca: bool,
366}
367
368#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
369pub struct MobileMeshRxRecord {
370 pub data: Vec<u8>,
371 pub rssi_dbm: Option<i16>,
372 pub lqi: Option<u8>,
373 pub snr_cb: Option<i16>,
374}
375
376enum WorkerCommand {
377 RegisterPeers {
378 peers: Vec<PublicKey>,
379 response: oneshot::Sender<Result<(), MobileMeshError>>,
380 },
381 RemovePeers {
382 peers: Vec<PublicKey>,
383 response: oneshot::Sender<Result<(), MobileMeshError>>,
384 },
385 RegisterChannels {
386 keys: Vec<ChannelKey>,
387 response: oneshot::Sender<Result<(), MobileMeshError>>,
388 },
389 RemoveChannels {
390 keys: Vec<ChannelKey>,
391 response: oneshot::Sender<Result<(), MobileMeshError>>,
392 },
393 Ping {
394 operation_id: u64,
395 peer: PublicKey,
396 timeout_ms: u64,
397 },
398 RestoreChat {
399 checkpoints: Vec<MobileChatCheckpointRecord>,
400 response: oneshot::Sender<()>,
401 },
402 ComposeChat {
403 conversation_address: String,
404 client_token: u32,
405 request: ChatComposeRequest,
406 response: oneshot::Sender<Result<MobileChatComposeBatchRecord, MobileMeshError>>,
407 },
408 CommitChatBatch {
409 batch_id: u64,
410 response: oneshot::Sender<Result<(), MobileMeshError>>,
411 },
412 RejectChatBatch {
413 batch_id: u64,
414 checkpoints: Vec<MobileChatCheckpointRecord>,
415 response: oneshot::Sender<Result<(), MobileMeshError>>,
416 },
417 ChatArchiveResult {
418 request_id: u32,
419 kind: MobileChatArchiveResultKind,
420 payload: Vec<u8>,
421 },
422 Advertise {
423 name: Option<String>,
424 timestamp: Option<u32>,
425 scheduled: bool,
429 response: oneshot::Sender<Result<(), MobileMeshError>>,
430 },
431 Beacon {
432 response: oneshot::Sender<Result<(), MobileMeshError>>,
433 },
434 SignIdentityBundle {
435 name: Option<String>,
436 timestamp: Option<u32>,
437 response: oneshot::Sender<Result<Vec<u8>, MobileMeshError>>,
438 },
439 RequestIdentity {
440 peer: PublicKey,
441 response: oneshot::Sender<Result<(), MobileMeshError>>,
442 },
443 DiscoverIdentities {
444 role_code: Option<u8>,
445 capability_bits: Option<u8>,
446 response: oneshot::Sender<Result<(), MobileMeshError>>,
447 },
448 RequestIdentityByHint {
449 conversation_address: String,
450 hint: NodeHint,
451 response: oneshot::Sender<Result<(), MobileMeshError>>,
452 },
453 SetDiscoverable {
454 enabled: bool,
455 name: Option<String>,
456 response: oneshot::Sender<()>,
457 },
458 SetAdvertisedLocation {
460 location: Option<NodeLocation>,
461 response: oneshot::Sender<()>,
462 },
463 SetChatDisplayName {
464 name: String,
465 response: oneshot::Sender<()>,
466 },
467 PeerRoute {
468 peer: PublicKey,
469 response: oneshot::Sender<MobileMeshRouteRecord>,
470 },
471 ClearPeerRoute {
472 peer: PublicKey,
473 response: oneshot::Sender<bool>,
474 },
475 FailOutboundTransmissions,
476 Receive(MobileMeshRxRecord),
477 Shutdown,
478}
479
480enum ChatComposeRequest {
481 Text {
482 body: String,
483 },
484 Edit {
485 original: MobileChatOriginalRef,
486 body: String,
487 },
488 Delete {
489 original: MobileChatOriginalRef,
490 },
491 Reaction {
492 target: MobileChatRegardingRef,
493 body: String,
494 },
495}
496
497struct InboundFrame {
498 record: MobileMeshRxRecord,
499}
500
501enum InboundTextSource {
503 Direct { peer: PublicKey },
505 ChannelGroup {
508 channel: ChannelTag,
509 hint: NodeHint,
510 full_key: Option<PublicKey>,
511 },
512 ChannelDirect {
514 channel: ChannelTag,
515 peer: PublicKey,
516 },
517}
518
519struct InboundText {
520 source: InboundTextSource,
521 payload: Vec<u8>,
522 received_at_ms: Option<u64>,
523 rx: MobileChatRxMetadataRecord,
524}
525
526struct InFlightChatTransmission {
527 transmission_id: u32,
528 gate_peer: Option<PublicKey>,
532 ticket: SendProgressTicket,
533 sent_reported: bool,
534 non_ack: bool,
537 queued_at_ms: u64,
542 stall_reported: bool,
543}
544
545const CHAT_TRANSMISSION_STALL_MS: u64 = 60_000;
549
550#[derive(Clone)]
551enum MobileChatWorkerEvent {
552 Mutation(MobileChatMutationRecord),
553 SenderResolution(MobileChatSenderResolutionRecord),
554 Delivery(MobileChatDeliveryRecord),
555 ArchiveLookup(MobileChatArchiveLookupRecord),
556 Diagnostic(String),
557}
558
559struct PendingChatEventBatch {
560 id: u64,
561 events: Vec<MobileChatWorkerEvent>,
562}
563
564#[derive(Debug)]
565enum BridgeRadioError {
566 Closed,
567 FrameTooLarge,
568}
569
570struct BridgeTransmitCompletions {
571 next_id: AtomicU64,
572 failure_generation: AtomicU64,
573 poisoned: AtomicBool,
581 pending: Mutex<BTreeMap<u64, oneshot::Sender<bool>>>,
582}
583
584impl BridgeTransmitCompletions {
585 fn new() -> Self {
586 Self {
587 next_id: AtomicU64::new(1),
588 failure_generation: AtomicU64::new(0),
589 poisoned: AtomicBool::new(false),
590 pending: Mutex::new(BTreeMap::new()),
591 }
592 }
593
594 fn generation(&self) -> u64 {
595 self.failure_generation.load(Ordering::SeqCst)
596 }
597
598 fn allocate(
599 &self,
600 generation: u64,
601 completion: oneshot::Sender<bool>,
602 ) -> Result<Option<u64>, BridgeRadioError> {
603 let id = self.next_id.fetch_add(1, Ordering::Relaxed).max(1);
604 let mut pending = self.pending.lock().map_err(|_| BridgeRadioError::Closed)?;
605 if self.poisoned.load(Ordering::SeqCst)
606 || generation != self.failure_generation.load(Ordering::SeqCst)
607 {
608 return Ok(None);
609 }
610 pending.insert(id, completion);
611 Ok(Some(id))
612 }
613
614 fn poison(&self) {
617 self.poisoned.store(true, Ordering::SeqCst);
618 self.failure_generation.fetch_add(1, Ordering::SeqCst);
619 }
620
621 fn clear_poison(&self) {
622 self.poisoned.store(false, Ordering::SeqCst);
623 }
624
625 fn complete(&self, id: u64, transmitted: bool) -> bool {
626 self.pending
627 .lock()
628 .ok()
629 .and_then(|mut pending| pending.remove(&id))
630 .is_some_and(|completion| completion.send(transmitted).is_ok())
631 }
632
633 fn fail_all(&self) {
634 self.failure_generation.fetch_add(1, Ordering::SeqCst);
635 let completions = self
636 .pending
637 .lock()
638 .map(|mut pending| core::mem::take(&mut *pending))
639 .unwrap_or_default();
640 for completion in completions.into_values() {
641 let _ = completion.send(false);
642 }
643 }
644}
645
646struct BridgeRadio {
647 inbound: mpsc::UnboundedReceiver<InboundFrame>,
648 outbound: NotifyingSender<MobileMeshOutboundFrameRecord>,
649 completions: Arc<BridgeTransmitCompletions>,
650}
651
652impl Radio for BridgeRadio {
653 type Error = BridgeRadioError;
654
655 async fn transmit(
656 &mut self,
657 data: &[u8],
658 options: TxOptions,
659 ) -> Result<(), TxError<Self::Error>> {
660 if data.len() > MAX_FRAME_SIZE {
661 return Err(TxError::Io(BridgeRadioError::FrameTooLarge));
662 }
663 let nocca = matches!(options.cad, umsh_hal::CadPolicy::Skip);
667 let (completion_tx, completion_rx) = oneshot::channel();
668 let generation = self.completions.generation();
669 let Some(id) = self
670 .completions
671 .allocate(generation, completion_tx)
672 .map_err(TxError::Io)?
673 else {
674 return Ok(());
677 };
678 if self
679 .outbound
680 .send(MobileMeshOutboundFrameRecord {
681 id,
682 data: data.to_vec(),
683 nocca,
684 })
685 .is_err()
686 {
687 let _ = self.completions.complete(id, false);
688 return Err(TxError::Io(BridgeRadioError::Closed));
689 }
690
691 match completion_rx.await {
699 Ok(true) => Ok(()),
700 Ok(false) => Ok(()),
706 Err(_) => Err(TxError::Io(BridgeRadioError::Closed)),
707 }
708 }
709
710 fn poll_receive(
711 &mut self,
712 cx: &mut Context<'_>,
713 buf: &mut [u8],
714 ) -> Poll<Result<RxInfo, Self::Error>> {
715 match self.inbound.poll_recv(cx) {
716 Poll::Ready(Some(frame)) => {
717 if frame.record.data.len() > buf.len() {
718 return Poll::Ready(Err(BridgeRadioError::FrameTooLarge));
719 }
720 let len = frame.record.data.len();
721 buf[..len].copy_from_slice(&frame.record.data);
722 Poll::Ready(Ok(RxInfo {
723 len,
724 rssi: frame.record.rssi_dbm.unwrap_or(0),
725 snr: Snr::from_centibels(frame.record.snr_cb.unwrap_or(0)),
726 lqi: frame.record.lqi.and_then(core::num::NonZeroU8::new),
727 }))
728 }
729 Poll::Ready(None) => Poll::Ready(Err(BridgeRadioError::Closed)),
730 Poll::Pending => Poll::Pending,
731 }
732 }
733
734 fn max_frame_size(&self) -> usize {
735 MAX_FRAME_SIZE
736 }
737 fn t_frame_ms(&self) -> u32 {
738 DEFAULT_FRAME_TIME_MS
739 }
740}
741
742#[derive(Clone)]
743struct SharedCounterStore(Arc<MobileCounterStore>);
744
745impl CounterStore for SharedCounterStore {
746 type Error = crate::CounterStoreError;
747
748 async fn load(&self, context: &[u8]) -> Result<u32, Self::Error> {
749 self.0.load_boundary(context.to_vec())
750 }
751
752 async fn store(&self, context: &[u8], value: u32) -> Result<(), Self::Error> {
753 self.0.commit_boundary(context.to_vec(), value)
754 }
755
756 async fn flush(&self) -> Result<(), Self::Error> {
757 CounterStore::flush(self.0.as_ref()).await
758 }
759}
760
761#[derive(Clone, Default)]
762struct MemoryKeyValueStore(Arc<Mutex<BTreeMap<Vec<u8>, Vec<u8>>>>);
763
764impl KeyValueStore for MemoryKeyValueStore {
765 type Error = MobileMeshError;
766
767 async fn load(&self, key: &[u8], out: &mut [u8]) -> Result<Option<usize>, Self::Error> {
768 let values = self
769 .0
770 .lock()
771 .map_err(|_| MobileMeshError::SessionUnavailable)?;
772 let Some(value) = values.get(key) else {
773 return Ok(None);
774 };
775 if value.len() > out.len() {
776 return Err(MobileMeshError::SessionUnavailable);
777 }
778 out[..value.len()].copy_from_slice(value);
779 Ok(Some(value.len()))
780 }
781
782 async fn store(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
783 self.0
784 .lock()
785 .map_err(|_| MobileMeshError::SessionUnavailable)?
786 .insert(key.to_vec(), value.to_vec());
787 Ok(())
788 }
789
790 async fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
791 self.0
792 .lock()
793 .map_err(|_| MobileMeshError::SessionUnavailable)?
794 .remove(key);
795 Ok(())
796 }
797}
798
799#[derive(Clone)]
804struct MobileClock {
805 origin: tokio::time::Instant,
806 sleep: Rc<RefCell<Option<Pin<Box<tokio::time::Sleep>>>>>,
807}
808
809impl MobileClock {
810 fn new() -> Self {
811 Self {
812 origin: tokio::time::Instant::now(),
813 sleep: Rc::new(RefCell::new(None)),
814 }
815 }
816}
817
818impl Clock for MobileClock {
819 fn now_ms(&self) -> u64 {
820 self.origin.elapsed().as_millis() as u64
821 }
822
823 fn poll_delay_until(&self, cx: &mut Context<'_>, deadline_ms: u64) -> Poll<()> {
824 let deadline = self.origin + Duration::from_millis(deadline_ms);
825 if tokio::time::Instant::now() >= deadline {
826 return Poll::Ready(());
827 }
828 let mut slot = self.sleep.borrow_mut();
829 let sleep = slot.get_or_insert_with(|| Box::pin(tokio::time::sleep_until(deadline)));
830 sleep.as_mut().reset(deadline);
831 sleep.as_mut().poll(cx)
832 }
833}
834
835#[derive(Clone, Copy, Default)]
836struct MobileDelay;
837
838impl DelayNs for MobileDelay {
839 async fn delay_ns(&mut self, ns: u32) {
840 tokio::time::sleep(Duration::from_nanos(u64::from(ns))).await;
841 }
842}
843
844struct MobilePlatform(PhantomData<()>);
845
846impl umsh_mac::Platform for MobilePlatform {
847 type Identity = SoftwareIdentity;
848 type Aes = SoftwareAes;
849 type Sha = SoftwareSha256;
850 type Radio = BridgeRadio;
851 type Delay = MobileDelay;
852 type Clock = MobileClock;
853 type Rng = rand::rngs::ThreadRng;
854 type CounterStore = SharedCounterStore;
855 type KeyValueStore = MemoryKeyValueStore;
856}
857
858const MOBILE_MAC_PEERS: usize = 64;
863
864const MOBILE_MAC_CHANNELS: usize = 32;
869
870type MobileMac =
871 Mac<MobilePlatform, { umsh_mac::DEFAULT_IDENTITIES }, MOBILE_MAC_PEERS, MOBILE_MAC_CHANNELS>;
872const MOBILE_CHAT_TRANSMIT_WINDOW: usize = 8;
873
874#[derive(uniffi::Object)]
880pub struct MobileMeshSession {
881 commands: mpsc::UnboundedSender<WorkerCommand>,
882 outbound: Mutex<std_mpsc::Receiver<MobileMeshOutboundFrameRecord>>,
883 transmit_completions: Arc<BridgeTransmitCompletions>,
884 events: Mutex<std_mpsc::Receiver<MobileMeshPingEventRecord>>,
885 advertisements: Mutex<std_mpsc::Receiver<MobileMeshAdvertisementRecord>>,
886 peer_heard: Mutex<std_mpsc::Receiver<MobileMeshPeerHeardRecord>>,
887 chat_events: Mutex<std_mpsc::Receiver<MobileChatWorkerEvent>>,
888 pending_chat_events: Mutex<Option<PendingChatEventBatch>>,
889 next_chat_batch_id: Mutex<u64>,
890 next_operation_id: Mutex<u64>,
891 wake: Arc<WakeSignal>,
892}
893
894#[uniffi::export]
895impl MobileMeshSession {
896 #[uniffi::constructor]
897 pub async fn new(
898 identity: Arc<MobileIdentity>,
899 counter_store: Arc<MobileCounterStore>,
900 ) -> Result<Arc<Self>, MobileMeshError> {
901 Self::build(identity, counter_store, false).await
902 }
903
904 pub fn ping(&self, peer_address: String, timeout_ms: u64) -> Result<u64, MobileMeshError> {
905 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
906 let operation_id = {
907 let mut next = self
908 .next_operation_id
909 .lock()
910 .map_err(|_| MobileMeshError::SessionUnavailable)?;
911 let current = *next;
912 *next = next.wrapping_add(1).max(1);
913 current
914 };
915 self.commands
916 .send(WorkerCommand::Ping {
917 operation_id,
918 peer,
919 timeout_ms,
920 })
921 .map_err(|_| MobileMeshError::SessionUnavailable)?;
922 Ok(operation_id)
923 }
924
925 pub async fn advertise_identity(
930 &self,
931 name: Option<String>,
932 timestamp: Option<u32>,
933 ) -> Result<(), MobileMeshError> {
934 self.send_advertisement(name, timestamp, false).await
935 }
936
937 pub async fn advertise_identity_scheduled(
944 &self,
945 name: Option<String>,
946 timestamp: Option<u32>,
947 ) -> Result<(), MobileMeshError> {
948 self.send_advertisement(name, timestamp, true).await
949 }
950
951 async fn send_advertisement(
952 &self,
953 name: Option<String>,
954 timestamp: Option<u32>,
955 scheduled: bool,
956 ) -> Result<(), MobileMeshError> {
957 let (response, result) = oneshot::channel();
958 self.commands
959 .send(WorkerCommand::Advertise {
960 name,
961 timestamp,
962 scheduled,
963 response,
964 })
965 .map_err(|_| MobileMeshError::SessionUnavailable)?;
966 result
967 .await
968 .map_err(|_| MobileMeshError::SessionUnavailable)?
969 }
970
971 pub async fn send_beacon(&self) -> Result<(), MobileMeshError> {
975 let (response, result) = oneshot::channel();
976 self.commands
977 .send(WorkerCommand::Beacon { response })
978 .map_err(|_| MobileMeshError::SessionUnavailable)?;
979 result
980 .await
981 .map_err(|_| MobileMeshError::SessionUnavailable)?
982 }
983
984 pub async fn request_identity(&self, peer_address: String) -> Result<(), MobileMeshError> {
990 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
991 let (response, result) = oneshot::channel();
992 self.commands
993 .send(WorkerCommand::RequestIdentity { peer, response })
994 .map_err(|_| MobileMeshError::SessionUnavailable)?;
995 result
996 .await
997 .map_err(|_| MobileMeshError::SessionUnavailable)?
998 }
999
1000 pub async fn discover_identities(
1012 &self,
1013 role_code: Option<u8>,
1014 capability_bits: Option<u8>,
1015 ) -> Result<(), MobileMeshError> {
1016 let (response, result) = oneshot::channel();
1017 self.commands
1018 .send(WorkerCommand::DiscoverIdentities {
1019 role_code,
1020 capability_bits,
1021 response,
1022 })
1023 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1024 result
1025 .await
1026 .map_err(|_| MobileMeshError::SessionUnavailable)?
1027 }
1028
1029 pub async fn request_identity_by_hint(
1041 &self,
1042 conversation_address: String,
1043 hint: Vec<u8>,
1044 ) -> Result<(), MobileMeshError> {
1045 let hint: [u8; 3] = hint
1046 .try_into()
1047 .map_err(|_| MobileMeshError::UnknownConversation)?;
1048 let (response, result) = oneshot::channel();
1049 self.commands
1050 .send(WorkerCommand::RequestIdentityByHint {
1051 conversation_address,
1052 hint: NodeHint(hint),
1053 response,
1054 })
1055 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1056 result
1057 .await
1058 .map_err(|_| MobileMeshError::SessionUnavailable)?
1059 }
1060
1061 pub async fn set_chat_display_name(&self, name: String) -> Result<(), MobileMeshError> {
1076 let (response, result) = oneshot::channel();
1077 self.commands
1078 .send(WorkerCommand::SetChatDisplayName { name, response })
1079 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1080 result
1081 .await
1082 .map_err(|_| MobileMeshError::SessionUnavailable)
1083 }
1084
1085 pub async fn set_discoverable(
1086 &self,
1087 enabled: bool,
1088 name: Option<String>,
1089 ) -> Result<(), MobileMeshError> {
1090 let (response, result) = oneshot::channel();
1091 self.commands
1092 .send(WorkerCommand::SetDiscoverable {
1093 enabled,
1094 name,
1095 response,
1096 })
1097 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1098 result
1099 .await
1100 .map_err(|_| MobileMeshError::SessionUnavailable)
1101 }
1102
1103 pub async fn set_advertised_location(
1114 &self,
1115 location: Option<MobileMeshSharedLocationRecord>,
1116 ) -> Result<(), MobileMeshError> {
1117 let location = location.map(disclosed_cell).transpose()?;
1118 let (response, result) = oneshot::channel();
1119 self.commands
1120 .send(WorkerCommand::SetAdvertisedLocation { location, response })
1121 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1122 result
1123 .await
1124 .map_err(|_| MobileMeshError::SessionUnavailable)
1125 }
1126
1127 pub async fn peer_route(
1132 &self,
1133 peer_address: String,
1134 ) -> Result<MobileMeshRouteRecord, MobileMeshError> {
1135 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
1136 let (response, result) = oneshot::channel();
1137 self.commands
1138 .send(WorkerCommand::PeerRoute { peer, response })
1139 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1140 result
1141 .await
1142 .map_err(|_| MobileMeshError::SessionUnavailable)
1143 }
1144
1145 pub async fn clear_peer_route(&self, peer_address: String) -> Result<bool, MobileMeshError> {
1150 let peer = decode_peer(&peer_address).map_err(|_| MobileMeshError::InvalidPeer)?;
1151 let (response, result) = oneshot::channel();
1152 self.commands
1153 .send(WorkerCommand::ClearPeerRoute { peer, response })
1154 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1155 result
1156 .await
1157 .map_err(|_| MobileMeshError::SessionUnavailable)
1158 }
1159
1160 pub async fn sign_identity_bundle(
1163 &self,
1164 name: Option<String>,
1165 timestamp: Option<u32>,
1166 ) -> Result<Vec<u8>, MobileMeshError> {
1167 let (response, result) = oneshot::channel();
1168 self.commands
1169 .send(WorkerCommand::SignIdentityBundle {
1170 name,
1171 timestamp,
1172 response,
1173 })
1174 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1175 result
1176 .await
1177 .map_err(|_| MobileMeshError::SessionUnavailable)?
1178 }
1179
1180 pub async fn register_peers(&self, peer_addresses: Vec<String>) -> Result<(), MobileMeshError> {
1181 let peers = peer_addresses
1182 .iter()
1183 .map(|address| decode_peer(address).map_err(|_| MobileMeshError::InvalidPeer))
1184 .collect::<Result<Vec<_>, _>>()?;
1185 let (response, result) = oneshot::channel();
1186 self.commands
1187 .send(WorkerCommand::RegisterPeers { peers, response })
1188 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1189 result
1190 .await
1191 .map_err(|_| MobileMeshError::SessionUnavailable)?
1192 }
1193
1194 pub async fn remove_peers(&self, peer_addresses: Vec<String>) -> Result<(), MobileMeshError> {
1200 let peers = peer_addresses
1201 .iter()
1202 .map(|address| decode_peer(address).map_err(|_| MobileMeshError::InvalidPeer))
1203 .collect::<Result<Vec<_>, _>>()?;
1204 let (response, result) = oneshot::channel();
1205 self.commands
1206 .send(WorkerCommand::RemovePeers { peers, response })
1207 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1208 result
1209 .await
1210 .map_err(|_| MobileMeshError::SessionUnavailable)?
1211 }
1212
1213 pub async fn register_channels(&self, keys: Vec<Vec<u8>>) -> Result<(), MobileMeshError> {
1219 let keys = decode_channel_keys(keys)?;
1220 let (response, result) = oneshot::channel();
1221 self.commands
1222 .send(WorkerCommand::RegisterChannels { keys, response })
1223 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1224 result
1225 .await
1226 .map_err(|_| MobileMeshError::SessionUnavailable)?
1227 }
1228
1229 pub async fn remove_channels(&self, keys: Vec<Vec<u8>>) -> Result<(), MobileMeshError> {
1232 let keys = decode_channel_keys(keys)?;
1233 let (response, result) = oneshot::channel();
1234 self.commands
1235 .send(WorkerCommand::RemoveChannels { keys, response })
1236 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1237 result
1238 .await
1239 .map_err(|_| MobileMeshError::SessionUnavailable)?
1240 }
1241
1242 pub fn receive(&self, frame: MobileMeshRxRecord) -> Result<(), MobileMeshError> {
1243 if frame.data.is_empty() || frame.data.len() > MAX_FRAME_SIZE {
1244 return Err(MobileMeshError::SessionUnavailable);
1245 }
1246 self.commands
1247 .send(WorkerCommand::Receive(frame))
1248 .map_err(|_| MobileMeshError::SessionUnavailable)
1249 }
1250
1251 pub fn complete_outbound_frame(
1255 &self,
1256 frame_id: u64,
1257 transmitted: bool,
1258 ) -> Result<(), MobileMeshError> {
1259 if !transmitted {
1260 self.transmit_completions.poison();
1264 self.commands
1265 .send(WorkerCommand::FailOutboundTransmissions)
1266 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1267 }
1268 self.transmit_completions
1269 .complete(frame_id, transmitted)
1270 .then_some(())
1271 .ok_or(MobileMeshError::SessionUnavailable)
1272 }
1273
1274 pub async fn restore_chat(
1275 &self,
1276 checkpoints: Vec<MobileChatCheckpointRecord>,
1277 ) -> Result<(), MobileMeshError> {
1278 let (response, result) = oneshot::channel();
1279 self.commands
1280 .send(WorkerCommand::RestoreChat {
1281 checkpoints,
1282 response,
1283 })
1284 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1285 result
1286 .await
1287 .map_err(|_| MobileMeshError::SessionUnavailable)
1288 }
1289
1290 pub async fn compose_text(
1293 &self,
1294 conversation_address: String,
1295 client_token: u32,
1296 body: String,
1297 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1298 self.compose_chat(
1299 conversation_address,
1300 client_token,
1301 ChatComposeRequest::Text { body },
1302 )
1303 .await
1304 }
1305
1306 pub async fn compose_edit(
1311 &self,
1312 conversation_address: String,
1313 client_token: u32,
1314 original: MobileChatOriginalRef,
1315 body: String,
1316 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1317 self.compose_chat(
1318 conversation_address,
1319 client_token,
1320 ChatComposeRequest::Edit { original, body },
1321 )
1322 .await
1323 }
1324
1325 pub async fn compose_delete(
1328 &self,
1329 conversation_address: String,
1330 client_token: u32,
1331 original: MobileChatOriginalRef,
1332 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1333 self.compose_chat(
1334 conversation_address,
1335 client_token,
1336 ChatComposeRequest::Delete { original },
1337 )
1338 .await
1339 }
1340
1341 pub async fn compose_reaction(
1350 &self,
1351 conversation_address: String,
1352 client_token: u32,
1353 target: MobileChatRegardingRef,
1354 body: String,
1355 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1356 self.compose_chat(
1357 conversation_address,
1358 client_token,
1359 ChatComposeRequest::Reaction { target, body },
1360 )
1361 .await
1362 }
1363
1364 pub async fn commit_chat_batch(&self, batch_id: u64) -> Result<(), MobileMeshError> {
1365 let (response, result) = oneshot::channel();
1366 self.commands
1367 .send(WorkerCommand::CommitChatBatch { batch_id, response })
1368 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1369 result
1370 .await
1371 .map_err(|_| MobileMeshError::SessionUnavailable)?
1372 }
1373
1374 pub async fn reject_chat_batch(
1375 &self,
1376 batch_id: u64,
1377 checkpoints: Vec<MobileChatCheckpointRecord>,
1378 ) -> Result<(), MobileMeshError> {
1379 let (response, result) = oneshot::channel();
1380 self.commands
1381 .send(WorkerCommand::RejectChatBatch {
1382 batch_id,
1383 checkpoints,
1384 response,
1385 })
1386 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1387 result
1388 .await
1389 .map_err(|_| MobileMeshError::SessionUnavailable)?
1390 }
1391
1392 pub fn apply_chat_archive_result(
1393 &self,
1394 request_id: u32,
1395 kind: MobileChatArchiveResultKind,
1396 payload: Vec<u8>,
1397 ) -> Result<(), MobileMeshError> {
1398 self.commands
1399 .send(WorkerCommand::ChatArchiveResult {
1400 request_id,
1401 kind,
1402 payload,
1403 })
1404 .map_err(|_| MobileMeshError::SessionUnavailable)
1405 }
1406
1407 pub fn acknowledge_chat_batch(&self, batch_id: u64) -> Result<(), MobileMeshError> {
1408 let mut pending = self
1409 .pending_chat_events
1410 .lock()
1411 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1412 if pending.as_ref().is_some_and(|batch| batch.id == batch_id) {
1413 *pending = None;
1414 self.wake.notify();
1418 }
1419 Ok(())
1420 }
1421
1422 pub fn fail_outbound_transmissions(&self) -> Result<(), MobileMeshError> {
1427 self.transmit_completions.poison();
1434 self.commands
1435 .send(WorkerCommand::FailOutboundTransmissions)
1436 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1437 self.transmit_completions.fail_all();
1440 Ok(())
1441 }
1442
1443 pub fn set_wake_listener(&self, listener: Arc<dyn MobileMeshWakeListener>) {
1447 self.wake.set_listener(Some(listener));
1448 }
1449
1450 pub fn clear_wake_listener(&self) {
1451 self.wake.set_listener(None);
1452 }
1453
1454 pub fn poll_update(&self) -> MobileMeshSessionUpdateRecord {
1455 self.wake.drained();
1458 let mut outbound_frames = Vec::new();
1459 if let Ok(receiver) = self.outbound.lock() {
1460 outbound_frames.extend(receiver.try_iter());
1461 }
1462 let mut ping_events = Vec::new();
1463 if let Ok(receiver) = self.events.lock() {
1464 ping_events.extend(receiver.try_iter());
1465 }
1466 let mut advertisement_events = Vec::new();
1467 if let Ok(receiver) = self.advertisements.lock() {
1468 advertisement_events.extend(receiver.try_iter());
1469 }
1470 let mut peer_heard_events = Vec::new();
1471 if let Ok(receiver) = self.peer_heard.lock() {
1472 peer_heard_events.extend(receiver.try_iter());
1473 }
1474 let mut chat_mutations = Vec::new();
1475 let mut chat_deliveries = Vec::new();
1476 let mut chat_archive_lookups = Vec::new();
1477 let mut chat_sender_resolutions = Vec::new();
1478 let mut chat_diagnostics = Vec::new();
1479 let mut chat_batch_id = None;
1480 if let Ok(mut pending) = self.pending_chat_events.lock() {
1481 if pending.is_none()
1482 && let Ok(receiver) = self.chat_events.lock()
1483 {
1484 let events = receiver.try_iter().collect::<Vec<_>>();
1485 if !events.is_empty()
1486 && let Ok(mut next) = self.next_chat_batch_id.lock()
1487 {
1488 let id = *next;
1489 *next = next.wrapping_add(1).max(1);
1490 *pending = Some(PendingChatEventBatch { id, events });
1491 }
1492 }
1493 if let Some(batch) = pending.as_ref() {
1494 chat_batch_id = Some(batch.id);
1495 for event in batch.events.iter().cloned() {
1496 match event {
1497 MobileChatWorkerEvent::Mutation(record) => chat_mutations.push(record),
1498 MobileChatWorkerEvent::Delivery(record) => chat_deliveries.push(record),
1499 MobileChatWorkerEvent::ArchiveLookup(record) => {
1500 chat_archive_lookups.push(record);
1501 }
1502 MobileChatWorkerEvent::SenderResolution(record) => {
1503 chat_sender_resolutions.push(record);
1504 }
1505 MobileChatWorkerEvent::Diagnostic(record) => chat_diagnostics.push(record),
1506 }
1507 }
1508 }
1509 }
1510 MobileMeshSessionUpdateRecord {
1511 outbound_frames,
1512 ping_events,
1513 advertisement_events,
1514 peer_heard_events,
1515 chat_batch_id,
1516 chat_mutations,
1517 chat_deliveries,
1518 chat_archive_lookups,
1519 chat_sender_resolutions,
1520 chat_diagnostics,
1521 }
1522 }
1523}
1524
1525impl MobileMeshSession {
1526 async fn compose_chat(
1527 &self,
1528 conversation_address: String,
1529 client_token: u32,
1530 request: ChatComposeRequest,
1531 ) -> Result<MobileChatComposeBatchRecord, MobileMeshError> {
1532 let (response, result) = oneshot::channel();
1535 self.commands
1536 .send(WorkerCommand::ComposeChat {
1537 conversation_address,
1538 client_token,
1539 request,
1540 response,
1541 })
1542 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1543 result
1544 .await
1545 .map_err(|_| MobileMeshError::SessionUnavailable)?
1546 }
1547
1548 #[cfg(test)]
1554 async fn new_with_virtual_time(
1555 identity: Arc<MobileIdentity>,
1556 counter_store: Arc<MobileCounterStore>,
1557 ) -> Result<Arc<Self>, MobileMeshError> {
1558 Self::build(identity, counter_store, true).await
1559 }
1560
1561 async fn build(
1562 identity: Arc<MobileIdentity>,
1563 counter_store: Arc<MobileCounterStore>,
1564 virtual_time: bool,
1565 ) -> Result<Arc<Self>, MobileMeshError> {
1566 let (commands, command_rx) = mpsc::unbounded_channel();
1567 let wake = Arc::new(WakeSignal::new());
1568 let (outbound_tx, outbound) = std_mpsc::channel();
1569 let (event_tx, events) = std_mpsc::channel();
1570 let (advertisement_tx, advertisements) = std_mpsc::channel();
1571 let (peer_heard_tx, peer_heard) = std_mpsc::channel();
1572 let (chat_event_tx, chat_events) = std_mpsc::channel();
1573 let outbound_tx = NotifyingSender {
1574 tx: outbound_tx,
1575 wake: wake.clone(),
1576 };
1577 let event_tx = NotifyingSender {
1578 tx: event_tx,
1579 wake: wake.clone(),
1580 };
1581 let advertisement_tx = NotifyingSender {
1582 tx: advertisement_tx,
1583 wake: wake.clone(),
1584 };
1585 let peer_heard_tx = NotifyingSender {
1586 tx: peer_heard_tx,
1587 wake: wake.clone(),
1588 };
1589 let chat_event_tx = NotifyingSender {
1590 tx: chat_event_tx,
1591 wake: wake.clone(),
1592 };
1593 let (ready_tx, ready_rx) = oneshot::channel();
1594 let worker_identity = identity.take_for_session()?;
1595 let transmit_completions = Arc::new(BridgeTransmitCompletions::new());
1596 let worker_transmit_completions = transmit_completions.clone();
1597
1598 std::thread::Builder::new()
1599 .name("umsh-mobile-mesh".to_owned())
1600 .stack_size(16 * 1024 * 1024)
1605 .spawn(move || {
1606 let mut builder = tokio::runtime::Builder::new_current_thread();
1607 builder.enable_time();
1608 #[cfg(test)]
1609 if virtual_time {
1610 builder.start_paused(true);
1611 }
1612 #[cfg(not(test))]
1613 let _ = virtual_time;
1614 let runtime = match builder.build() {
1615 Ok(runtime) => runtime,
1616 Err(_) => {
1617 let _ = ready_tx.send(Err(MobileMeshError::SessionUnavailable));
1618 return;
1619 }
1620 };
1621 let local = tokio::task::LocalSet::new();
1622 local.block_on(
1626 &runtime,
1627 Box::pin(run_worker(
1628 worker_identity,
1629 SharedCounterStore(counter_store),
1630 command_rx,
1631 outbound_tx,
1632 worker_transmit_completions,
1633 event_tx,
1634 advertisement_tx,
1635 peer_heard_tx,
1636 chat_event_tx,
1637 ready_tx,
1638 )),
1639 );
1640 })
1641 .map_err(|_| MobileMeshError::SessionUnavailable)?;
1642
1643 ready_rx
1644 .await
1645 .map_err(|_| MobileMeshError::SessionUnavailable)??;
1646 Ok(Arc::new(Self {
1647 commands,
1648 outbound: Mutex::new(outbound),
1649 transmit_completions,
1650 events: Mutex::new(events),
1651 advertisements: Mutex::new(advertisements),
1652 peer_heard: Mutex::new(peer_heard),
1653 chat_events: Mutex::new(chat_events),
1654 pending_chat_events: Mutex::new(None),
1655 next_chat_batch_id: Mutex::new(1),
1656 next_operation_id: Mutex::new(1),
1657 wake,
1658 }))
1659 }
1660}
1661
1662impl Drop for MobileMeshSession {
1663 fn drop(&mut self) {
1664 self.transmit_completions.fail_all();
1665 let _ = self.commands.send(WorkerCommand::Shutdown);
1666 }
1667}
1668
1669fn disclosed_cell(record: MobileMeshSharedLocationRecord) -> Result<NodeLocation, MobileMeshError> {
1675 if !(1..=MAX_PRECISION).contains(&record.precision_bytes)
1676 || !record.latitude_degrees.is_finite()
1677 || record.latitude_degrees.abs() > 90.0
1678 || !record.longitude_degrees.is_finite()
1679 || record.longitude_degrees.abs() > 180.0
1680 {
1681 return Err(MobileMeshError::InvalidLocation);
1682 }
1683 Ok(NodeLocation::from_e7(
1684 (record.latitude_degrees * 1e7).round() as i32,
1685 (record.longitude_degrees * 1e7).round() as i32,
1686 record.precision_bytes,
1687 ))
1688}
1689
1690fn phone_identity_profile(
1696 public_key: PublicKey,
1697 name: Option<&str>,
1698 location: Option<NodeLocation>,
1699) -> NodeIdentityProfile {
1700 let mut profile = NodeIdentityProfile::new(
1701 public_key,
1702 NodeRole::Chat,
1703 NodeCapabilities::MOBILE | NodeCapabilities::TEXT_MESSAGES,
1704 );
1705 profile.name = name
1706 .map(|name| {
1707 let mut end = name.len().min(24);
1708 while !name.is_char_boundary(end) {
1709 end -= 1;
1710 }
1711 name[..end].to_owned()
1712 })
1713 .filter(|name| !name.is_empty());
1714 profile.location = location;
1715 profile
1716}
1717
1718async fn build_signed_identity_bundle(
1724 signer: &SoftwareIdentity,
1725 name: Option<&str>,
1726 timestamp: Option<u32>,
1727 location: Option<NodeLocation>,
1728) -> Result<Vec<u8>, MobileMeshError> {
1729 let name = name
1730 .map(|name| {
1731 let mut end = name.len().min(24);
1732 while !name.is_char_boundary(end) {
1733 end -= 1;
1734 }
1735 name[..end].to_owned()
1736 })
1737 .filter(|name| !name.is_empty());
1738 let payload = NodeIdentityPayload {
1739 role: NodeRole::Chat,
1740 capabilities: NodeCapabilities::MOBILE | NodeCapabilities::TEXT_MESSAGES,
1741 name,
1742 location,
1743 altitude_m: None,
1744 timestamp,
1745 supported_regions: None,
1746 nonce: None,
1747 signature: None,
1748 };
1749 let mut buf = [0u8; 192];
1750 let len = payload
1751 .encode_for_signing(&mut buf)
1752 .map_err(|_| MobileMeshError::SendFailed)?;
1753 let signature = signer
1754 .sign(&buf[..len])
1755 .await
1756 .map_err(|_| MobileMeshError::SendFailed)?;
1757 let mut bundle = buf[..len].to_vec();
1758 bundle.extend_from_slice(&signature);
1759 Ok(bundle)
1760}
1761
1762async fn run_worker(
1763 identity: SoftwareIdentity,
1764 counter_store: SharedCounterStore,
1765 mut commands: mpsc::UnboundedReceiver<WorkerCommand>,
1766 outbound: NotifyingSender<MobileMeshOutboundFrameRecord>,
1767 transmit_completions: Arc<BridgeTransmitCompletions>,
1768 events: NotifyingSender<MobileMeshPingEventRecord>,
1769 advertisements: NotifyingSender<MobileMeshAdvertisementRecord>,
1770 peer_heard: NotifyingSender<MobileMeshPeerHeardRecord>,
1771 chat_events: NotifyingSender<MobileChatWorkerEvent>,
1772 ready: oneshot::Sender<Result<(), MobileMeshError>>,
1773) {
1774 let local_key = *identity.public_key();
1775 let signer = identity.clone();
1778 let (inbound_tx, inbound_rx) = mpsc::unbounded_channel();
1779 let worker_completions = transmit_completions.clone();
1780 let radio = BridgeRadio {
1781 inbound: inbound_rx,
1782 outbound,
1783 completions: transmit_completions,
1784 };
1785 let mac = MobileMac::new(
1786 radio,
1787 CryptoEngine::new(SoftwareAes, SoftwareSha256),
1788 MobileClock::new(),
1789 rand::rng(),
1790 counter_store,
1791 RepeaterConfig::default(),
1792 OperatingPolicy::default(),
1793 );
1794 let cell = AsyncRefCell::new(mac);
1795 let handle = MacHandle::new(&cell);
1796 let identity_id = match handle.add_identity(identity).await {
1797 Ok(id) => id,
1798 Err(_) => {
1799 let _ = ready.send(Err(MobileMeshError::SessionUnavailable));
1800 return;
1801 }
1802 };
1803 if handle.load_persisted_counter(identity_id).await.is_err() {
1804 let _ = ready.send(Err(MobileMeshError::CounterPersistenceFailed));
1805 return;
1806 }
1807 handle.set_auto_register_full_key_peers(true).await;
1814
1815 let mut host = Host::new(handle);
1816 let node = host.add_node(identity_id);
1817 let mut discoverable = true;
1824 let mut responder_name: Option<String> = None;
1825 let mut advertised_location: Option<NodeLocation> = None;
1826 node.enable_identity_responder_default(phone_identity_profile(
1827 local_key,
1828 responder_name.as_deref(),
1829 advertised_location,
1830 ));
1831 let channel_registry = Rc::new(RefCell::new(ChannelRegistry::default()));
1834 let mut chat = MobileChatState::new(local_key, channel_registry.clone());
1835 let peer_heard_events = peer_heard.clone();
1839 let peer_heard_subscription = node.on_receive(move |packet| {
1840 let _ = peer_heard_events.send(MobileMeshPeerHeardRecord {
1841 peer_address: packet.from_key().map(|peer| encode_peer_address(&peer)),
1842 node_hint: packet.from_hint().map(|hint| hint.0.to_vec()),
1843 source_authenticated: packet.source_authenticated(),
1844 });
1845 false
1846 });
1847 let inbound_text = Rc::new(RefCell::new(Vec::<InboundText>::new()));
1848 let inbound_text_callback = inbound_text.clone();
1849 let text_channels = channel_registry.clone();
1850 let echo_events = chat_events.clone();
1851 let text_subscription = node.on_receive(move |packet| {
1852 if packet.payload_type() != PayloadType::TextMessage {
1853 return false;
1854 }
1855 if packet.from_key() == Some(local_key) {
1865 let hops = packet
1866 .flood_hops()
1867 .map(|hops| hops.accumulated())
1868 .unwrap_or(0);
1869 let _ = echo_events.send(MobileChatWorkerEvent::Diagnostic(format!(
1870 "own multicast relayed back after {hops} hop(s)"
1871 )));
1872 return true;
1873 }
1874 let channel_tag = packet.channel().map(|channel| {
1879 (
1880 crate::channel_tag(channel.key()),
1881 text_channels
1882 .borrow()
1883 .contains(&crate::channel_tag(channel.key())),
1884 )
1885 });
1886 if let Some((tag, _)) = channel_tag
1897 && tag == crate::emergency_channel_tag()
1898 && (packet.encrypted() || packet.from_key().is_none())
1899 {
1900 let reason = if packet.encrypted() {
1901 "encrypted"
1902 } else {
1903 "missing its full source key"
1904 };
1905 let _ = echo_events.send(MobileChatWorkerEvent::Diagnostic(format!(
1906 "dropped an emergency-channel text frame: {reason}"
1907 )));
1908 return false;
1909 }
1910 let source = match (packet.packet_family(), channel_tag) {
1911 (PacketFamily::Unicast, _) => match packet.from_key() {
1912 Some(peer) => InboundTextSource::Direct { peer },
1913 None => return false,
1914 },
1915 (PacketFamily::Multicast, Some((channel, true))) => match packet.from_hint() {
1918 Some(hint) => InboundTextSource::ChannelGroup {
1919 channel,
1920 hint,
1921 full_key: packet.from_key(),
1922 },
1923 None => return false,
1924 },
1925 (PacketFamily::BlindUnicast, Some((channel, true))) => match packet.from_key() {
1928 Some(peer) => InboundTextSource::ChannelDirect { channel, peer },
1929 None => return false,
1930 },
1931 _ => return false,
1932 };
1933 inbound_text_callback.borrow_mut().push(InboundText {
1934 source,
1935 payload: packet.payload().to_vec(),
1936 received_at_ms: packet.received_at_ms(),
1937 rx: MobileChatRxMetadataRecord {
1938 rssi_dbm: packet.rssi(),
1939 snr_centibels: packet.snr().map(|snr| snr.as_centibels()),
1940 lqi: packet.lqi().map(|lqi| lqi.get()),
1941 hop_count: packet.flood_hops().map(|hops| hops.accumulated()),
1942 route_hints: packet
1943 .trace_route_hops()
1944 .map(|hop| hop.0.to_vec())
1945 .collect(),
1946 source_authenticated: packet.source_authenticated(),
1947 },
1948 });
1949 true
1950 });
1951 let advertisement_events = advertisements.clone();
1952 let advertisement_subscription = node.on_receive(move |packet| {
1953 if packet.payload_type() != PayloadType::NodeIdentity {
1954 return false;
1955 }
1956 let Some(peer) = packet.from_key() else {
1959 return false;
1960 };
1961 let _ = advertisement_events.send(MobileMeshAdvertisementRecord {
1962 peer_address: encode_peer_address(&peer),
1963 payload: packet.payload().to_vec(),
1964 source_authenticated: packet.source_authenticated(),
1965 });
1966 true
1967 });
1968 let mut in_flight_chat = Vec::<InFlightChatTransmission>::new();
1969 let mut member_routes = BTreeMap::<(ChannelTag, [u8; 3]), MemberRoute>::new();
1972 let mut chat_pipeline_ready = BTreeSet::<[u8; 32]>::new();
1973 let mut pending_chat_transmissions = VecDeque::<umsh_text::engine::Transmission>::new();
1974 let pending = Rc::new(RefCell::new(BTreeMap::<[u8; 32], u64>::new()));
1975 let pong_pending = pending.clone();
1976 let pong_events = events.clone();
1977 let pong_subscription = node.on_pong_with_metadata(move |peer, metadata| {
1978 if let Some(operation_id) = pong_pending.borrow_mut().remove(&peer.0) {
1979 let _ = pong_events.send(MobileMeshPingEventRecord {
1980 operation_id,
1981 outcome: MobileMeshPingOutcome::Reply,
1982 round_trip_milliseconds: Some(metadata.round_trip_ms),
1983 hop_count: metadata.hop_count,
1984 route_hints: metadata
1985 .route_hints
1986 .iter()
1987 .map(|hint| hint.0.to_vec())
1988 .collect(),
1989 rssi_dbm: metadata.rssi_dbm,
1990 snr_centibels: metadata.snr_centibels,
1991 lqi: metadata.lqi,
1992 });
1993 }
1994 });
1995 let timeout_pending = pending.clone();
1996 let timeout_events = events.clone();
1997 let timeout_subscription = node.on_ping_timeout(move |peer| {
1998 if let Some(operation_id) = timeout_pending.borrow_mut().remove(&peer.0) {
1999 let _ = timeout_events.send(MobileMeshPingEventRecord {
2000 operation_id,
2001 outcome: MobileMeshPingOutcome::TimedOut,
2002 round_trip_milliseconds: None,
2003 hop_count: None,
2004 route_hints: Vec::new(),
2005 rssi_dbm: None,
2006 snr_centibels: None,
2007 lqi: None,
2008 });
2009 }
2010 });
2011 let _subscriptions = (
2012 pong_subscription,
2013 timeout_subscription,
2014 peer_heard_subscription,
2015 text_subscription,
2016 advertisement_subscription,
2017 );
2018 let _ = ready.send(Ok(()));
2019 let mut protocol_timeout_tick = tokio::time::interval(Duration::from_millis(50));
2020 protocol_timeout_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
2021
2022 let inbound_ready = tokio::sync::Notify::new();
2032 let timeout_servicer = host.protocol_timeout_servicer();
2033
2034 let pump_loop = async {
2035 loop {
2036 if host.pump_once().await.is_err() {
2037 return;
2038 }
2039 if !inbound_text.borrow().is_empty() {
2040 inbound_ready.notify_one();
2041 }
2042 }
2043 };
2044
2045 let command_loop = async {
2046 loop {
2047 tokio::select! {
2048 biased;
2049 command = commands.recv() => {
2050 match command {
2051 Some(WorkerCommand::RegisterPeers { peers, response }) => {
2052 let mut result = Ok(());
2053 for peer in peers {
2054 if node.peer(peer).await.is_err() {
2055 result = Err(MobileMeshError::SendFailed);
2056 break;
2057 }
2058 }
2059 let _ = response.send(result);
2060 }
2061 Some(WorkerCommand::RemovePeers { peers, response }) => {
2062 for peer in peers {
2063 let _ = node.remove_peer(&peer).await;
2066 }
2067 let _ = response.send(Ok(()));
2068 }
2069 Some(WorkerCommand::RegisterChannels { keys, response }) => {
2070 let mut result = Ok(());
2071 for key in keys {
2072 if node.join(&umsh_node::Channel::private(key, "")).await.is_err() {
2076 result = Err(MobileMeshError::ChannelCapacity);
2077 break;
2078 }
2079 channel_registry
2080 .borrow_mut()
2081 .register(crate::channel_tag(&key), key);
2082 }
2083 let _ = response.send(result);
2084 }
2085 Some(WorkerCommand::RemoveChannels { keys, response }) => {
2086 for key in keys {
2087 let _ = node.leave(&umsh_node::Channel::private(key, "")).await;
2089 channel_registry
2090 .borrow_mut()
2091 .remove(&crate::channel_tag(&key));
2092 }
2093 let _ = response.send(Ok(()));
2094 }
2095 Some(WorkerCommand::Ping { operation_id, peer, timeout_ms }) => {
2096 if pending.borrow().contains_key(&peer.0) {
2097 emit_ping_failure(&events, operation_id);
2098 continue;
2099 }
2100 let result = match node.peer(peer).await {
2101 Ok(connection) => connection
2102 .ping(
2103 6,
2104 &SendOptions::default()
2105 .with_flood_hops(5)
2106 .with_trace_route()
2107 .with_mic_size(umsh_node::PING_MIC_SIZE),
2108 timeout_ms,
2109 )
2110 .await
2111 .map(|_| ())
2112 .map_err(|_| MobileMeshError::SendFailed),
2113 Err(_) => Err(MobileMeshError::SendFailed),
2114 };
2115 if result.is_ok() {
2116 pending.borrow_mut().insert(peer.0, operation_id);
2117 if handle.service_counter_persistence().await.is_err() {
2121 emit_ping_failure(&events, operation_id);
2122 return;
2123 }
2124 }
2125 if result.is_err() {
2126 emit_ping_failure(&events, operation_id);
2127 }
2128 }
2129 Some(WorkerCommand::Advertise { name, timestamp, scheduled, response }) => {
2130 let result = match build_signed_identity_bundle(
2131 &signer,
2132 name.as_deref(),
2133 timestamp,
2134 advertised_location,
2135 )
2136 .await
2137 {
2138 Ok(bundle) => {
2139 let mut frame = Vec::with_capacity(bundle.len() + 1);
2140 frame.push(PayloadType::NodeIdentity as u8);
2141 frame.extend_from_slice(&bundle);
2142 let options = SendOptions::default().with_full_source();
2145 let options = if scheduled {
2146 options.no_flood()
2150 } else {
2151 options.with_trace_route()
2155 };
2156 node.send_all(&frame, &options)
2157 .await
2158 .map(|_| ())
2159 .map_err(|_| MobileMeshError::SendFailed)
2160 }
2161 Err(error) => Err(error),
2162 };
2163 if result.is_ok()
2164 && handle.service_counter_persistence().await.is_err()
2165 {
2166 let _ = response.send(Err(MobileMeshError::SendFailed));
2167 return;
2168 }
2169 let _ = response.send(result);
2170 }
2171 Some(WorkerCommand::Beacon { response }) => {
2172 let result = node
2175 .send_all(
2176 &[],
2177 &SendOptions::default()
2178 .with_flood_hops(BEACON_FLOOD_HOPS)
2179 .with_trace_route()
2180 .with_trace_signal(),
2181 )
2182 .await
2183 .map(|_| ())
2184 .map_err(|_| MobileMeshError::SendFailed);
2185 if result.is_ok()
2186 && handle.service_counter_persistence().await.is_err()
2187 {
2188 let _ = response.send(Err(MobileMeshError::SendFailed));
2189 return;
2190 }
2191 let _ = response.send(result);
2192 }
2193 Some(WorkerCommand::SignIdentityBundle { name, timestamp, response }) => {
2194 let result = build_signed_identity_bundle(
2199 &signer,
2200 name.as_deref(),
2201 timestamp,
2202 None,
2203 )
2204 .await;
2205 let _ = response.send(result);
2206 }
2207 Some(WorkerCommand::RequestIdentity { peer, response }) => {
2208 let result = match node.peer(peer).await {
2209 Ok(connection) => connection
2210 .request_identity(
2211 &SendOptions::default()
2212 .with_flood_hops(5)
2213 .with_ack_requested(false),
2214 )
2215 .await
2216 .map(|_| ())
2217 .map_err(|_| MobileMeshError::SendFailed),
2218 Err(_) => Err(MobileMeshError::SendFailed),
2219 };
2220 if result.is_ok()
2224 && handle.service_counter_persistence().await.is_err()
2225 {
2226 let _ = response.send(Err(MobileMeshError::SendFailed));
2227 return;
2228 }
2229 let _ = response.send(result);
2230 }
2231 Some(WorkerCommand::RequestIdentityByHint {
2232 conversation_address,
2233 hint,
2234 response,
2235 }) => {
2236 let channel = chat
2237 .parse_conversation_address(&conversation_address)
2238 .and_then(|conversation| match conversation {
2239 ConversationKey::ChannelGroup { channel } => Some(channel),
2240 _ => None,
2241 });
2242 let result = match channel {
2243 Some(channel) => {
2244 let route = member_routes.get(&(channel, hint.0)).cloned();
2245 let mut nonce_bytes = [0u8; 4];
2246 handle.fill_random(&mut nonce_bytes).await;
2247 request_identity_over_channel(
2248 &node,
2249 &channel_registry,
2250 channel,
2251 hint,
2252 u32::from_be_bytes(nonce_bytes),
2253 route,
2254 )
2255 .await
2256 }
2257 None => Err(MobileMeshError::UnknownConversation),
2258 };
2259 if result.is_ok()
2260 && handle.service_counter_persistence().await.is_err()
2261 {
2262 let _ = response.send(Err(MobileMeshError::SendFailed));
2263 return;
2264 }
2265 let _ = response.send(result);
2266 }
2267 Some(WorkerCommand::SetChatDisplayName { name, response }) => {
2268 chat.engine.set_local_handle(&name);
2269 let _ = response.send(());
2270 }
2271 Some(WorkerCommand::SetDiscoverable { enabled, name, response }) => {
2272 discoverable = enabled;
2273 responder_name = name;
2274 if discoverable {
2275 node.enable_identity_responder_default(phone_identity_profile(
2276 local_key,
2277 responder_name.as_deref(),
2278 advertised_location,
2279 ));
2280 } else {
2281 node.disable_identity_responder();
2282 }
2283 let _ = response.send(());
2284 }
2285 Some(WorkerCommand::SetAdvertisedLocation { location, response }) => {
2286 advertised_location = location;
2287 if discoverable {
2292 node.enable_identity_responder_default(phone_identity_profile(
2293 local_key,
2294 responder_name.as_deref(),
2295 advertised_location,
2296 ));
2297 }
2298 let _ = response.send(());
2299 }
2300 Some(WorkerCommand::DiscoverIdentities {
2301 role_code,
2302 capability_bits,
2303 response,
2304 }) => {
2305 let result = async {
2306 let mut builder = umsh_node::mac_command::IdentityRequestBuilder::new();
2307 let mut nonce_bytes = [0u8; 4];
2308 handle.fill_random(&mut nonce_bytes).await;
2309 builder = builder
2310 .nonce(u32::from_be_bytes(nonce_bytes))
2311 .map_err(|_| MobileMeshError::SendFailed)?;
2312 if let Some(role) = role_code {
2313 builder = builder
2314 .filter_role(NodeRole::from_byte(role))
2315 .map_err(|_| MobileMeshError::SendFailed)?;
2316 }
2317 let capability_bits = capability_bits
2322 .or(if role_code.is_none() { Some(0) } else { None });
2323 if let Some(bits) = capability_bits {
2324 builder = builder
2325 .filter_caps(NodeCapabilities::from_bits_truncate(bits))
2326 .map_err(|_| MobileMeshError::SendFailed)?;
2327 }
2328 let options_block = builder.build();
2329 let cmd = umsh_node::MacCommand::IdentityRequest {
2330 options: &options_block,
2331 };
2332 let mut frame = [0u8; 128];
2333 frame[0] = PayloadType::MacCommand as u8;
2334 let length = umsh_node::mac_command::encode(&cmd, &mut frame[1..])
2335 .map_err(|_| MobileMeshError::SendFailed)?
2336 + 1;
2337 node.send_all(
2341 &frame[..length],
2342 &SendOptions::default().with_full_source().no_flood(),
2343 )
2344 .await
2345 .map(|_| ())
2346 .map_err(|_| MobileMeshError::SendFailed)
2347 }
2348 .await;
2349 if result.is_ok()
2350 && handle.service_counter_persistence().await.is_err()
2351 {
2352 let _ = response.send(Err(MobileMeshError::SendFailed));
2353 return;
2354 }
2355 let _ = response.send(result);
2356 }
2357 Some(WorkerCommand::PeerRoute { peer, response }) => {
2358 let _ = response.send(node.peer_route(&peer).await.into());
2359 }
2360 Some(WorkerCommand::ClearPeerRoute { peer, response }) => {
2361 let _ = response.send(node.clear_peer_route(&peer).await);
2362 }
2363 Some(WorkerCommand::RestoreChat { checkpoints, response }) => {
2364 chat.restore(&checkpoints, handle.now_ms().await);
2365 let _ = response.send(());
2366 }
2367 Some(WorkerCommand::ComposeChat {
2368 conversation_address,
2369 client_token,
2370 request,
2371 response,
2372 }) => {
2373 let conversation =
2377 chat.parse_conversation_address(&conversation_address);
2378 let result = if !chat.pending_batches.is_empty() {
2379 Err(MobileMeshError::OperationInProgress)
2380 } else if let Some(conversation) = conversation {
2381 let now_ms = handle.now_ms().await;
2382 let composed = match &request {
2383 ChatComposeRequest::Text { body } => {
2384 chat.compose_text(conversation, client_token, body, now_ms)
2385 }
2386 ChatComposeRequest::Edit { original, body } => chat.compose_edit(
2387 conversation,
2388 client_token,
2389 original,
2390 body,
2391 now_ms,
2392 ),
2393 ChatComposeRequest::Delete { original } => chat.compose_delete(
2394 conversation,
2395 client_token,
2396 original,
2397 now_ms,
2398 ),
2399 ChatComposeRequest::Reaction { target, body } => chat
2400 .compose_reaction(
2401 conversation,
2402 client_token,
2403 target,
2404 body,
2405 now_ms,
2406 ),
2407 };
2408 match composed {
2409 Ok(composed) => {
2410 for delivery in composed.deliveries {
2411 let _ = chat_events.send(
2412 MobileChatWorkerEvent::Delivery(delivery),
2413 );
2414 }
2415 for diagnostic in composed.diagnostics {
2416 let _ = chat_events.send(
2417 MobileChatWorkerEvent::Diagnostic(diagnostic),
2418 );
2419 }
2420 Ok(composed.record)
2421 }
2422 Err(()) => Err(MobileMeshError::ChatComposeFailed),
2423 }
2424 } else {
2425 Err(MobileMeshError::UnknownConversation)
2429 };
2430 let _ = response.send(result);
2431 }
2432 Some(WorkerCommand::CommitChatBatch { batch_id, response }) => {
2433 let result = match chat.pending_batches.remove(&batch_id) {
2434 Some(batch) => {
2435 let now_ms = handle.now_ms().await;
2436 let sent = queue_chat_transmissions(
2437 &node,
2438 batch.transmissions,
2439 &mut pending_chat_transmissions,
2440 &mut in_flight_chat,
2441 &chat_pipeline_ready,
2442 &channel_registry,
2443 &mut chat,
2444 now_ms,
2445 )
2446 .await;
2447 publish_chat_drain(chat.drain(), &chat_events);
2448 if sent > 0
2449 && handle.service_counter_persistence().await.is_err()
2450 {
2451 Err(MobileMeshError::CounterPersistenceFailed)
2452 } else {
2453 Ok(())
2454 }
2455 }
2456 None => Err(MobileMeshError::ChatBatchMissing),
2457 };
2458 let fatal = result == Err(MobileMeshError::CounterPersistenceFailed);
2459 let _ = response.send(result);
2460 if fatal {
2461 return;
2462 }
2463 }
2464 Some(WorkerCommand::RejectChatBatch {
2465 batch_id,
2466 checkpoints,
2467 response,
2468 }) => {
2469 let result = match chat.pending_batches.remove(&batch_id) {
2470 Some(batch) => {
2471 for transmission in batch.transmissions {
2472 chat.engine.transmit_update(
2473 transmission.transmission_id,
2474 DeliveryState::Failed,
2475 handle.now_ms().await,
2476 );
2477 }
2478 publish_chat_drain(chat.drain(), &chat_events);
2479 chat = MobileChatState::new(
2480 local_key,
2481 channel_registry.clone(),
2482 );
2483 for diagnostic in
2484 chat.restore(&checkpoints, handle.now_ms().await)
2485 {
2486 let _ = chat_events.send(
2487 MobileChatWorkerEvent::Diagnostic(diagnostic),
2488 );
2489 }
2490 Ok(())
2491 }
2492 None => Err(MobileMeshError::ChatBatchMissing),
2493 };
2494 let _ = response.send(result);
2495 }
2496 Some(WorkerCommand::ChatArchiveResult {
2497 request_id,
2498 kind,
2499 payload,
2500 }) => {
2501 let now_ms = handle.now_ms().await;
2502 match kind {
2503 MobileChatArchiveResultKind::Found => chat.engine.archive_result(
2504 request_id,
2505 ArchiveResult::Found { payload: &payload },
2506 now_ms,
2507 ),
2508 MobileChatArchiveResultKind::Deleted => chat.engine.archive_result(
2509 request_id,
2510 ArchiveResult::Deleted,
2511 now_ms,
2512 ),
2513 MobileChatArchiveResultKind::Evicted => chat.engine.archive_result(
2514 request_id,
2515 ArchiveResult::Evicted,
2516 now_ms,
2517 ),
2518 MobileChatArchiveResultKind::Unknown => chat.engine.archive_result(
2519 request_id,
2520 ArchiveResult::Unknown,
2521 now_ms,
2522 ),
2523 }
2524 let drain = chat.drain();
2525 let transmissions = drain.transmissions.clone();
2526 publish_chat_drain(drain, &chat_events);
2527 if !transmissions.is_empty() || !pending_chat_transmissions.is_empty() {
2528 let sent = queue_chat_transmissions(
2529 &node,
2530 transmissions,
2531 &mut pending_chat_transmissions,
2532 &mut in_flight_chat,
2533 &chat_pipeline_ready,
2534 &channel_registry,
2535 &mut chat,
2536 now_ms,
2537 )
2538 .await;
2539 publish_chat_drain(chat.drain(), &chat_events);
2540 if sent > 0 && handle.service_counter_persistence().await.is_err() {
2541 return;
2542 }
2543 }
2544 }
2545 Some(WorkerCommand::FailOutboundTransmissions) => {
2546 let now_ms = handle.now_ms().await;
2547 for transmission in pending_chat_transmissions.drain(..) {
2548 chat.engine.transmit_update(
2549 transmission.transmission_id,
2550 DeliveryState::Failed,
2551 now_ms,
2552 );
2553 }
2554 for transmission in in_flight_chat.drain(..) {
2555 if let Some(receipt) = transmission.ticket.receipt() {
2556 let _ = handle.cancel_pending_ack(identity_id, receipt).await;
2557 }
2558 chat.engine.transmit_update(
2559 transmission.transmission_id,
2560 DeliveryState::Failed,
2561 now_ms,
2562 );
2563 }
2564 publish_chat_drain(chat.drain(), &chat_events);
2565 worker_completions.clear_poison();
2568 }
2569 Some(WorkerCommand::Receive(record)) => {
2570 let _ = inbound_tx.send(InboundFrame { record });
2571 }
2572 Some(WorkerCommand::Shutdown) | None => return,
2573 }
2574 }
2575 _ = inbound_ready.notified() => {
2576 let received = inbound_text.borrow_mut().drain(..).collect::<Vec<_>>();
2577 for text in received {
2578 let received_at_ms = match text.received_at_ms {
2579 Some(value) => value,
2580 None => handle.now_ms().await,
2581 };
2582 let (envelope, sender_full_key) = match text.source {
2588 InboundTextSource::Direct { peer } => (
2589 Envelope {
2590 path: DeliveryPath::Unicast,
2591 conversation: ConversationKey::Direct { peer },
2592 sender: SenderScope::Peer(peer),
2593 },
2594 Some(peer),
2595 ),
2596 InboundTextSource::ChannelGroup {
2597 channel,
2598 hint,
2599 full_key,
2600 } => {
2601 if let Some(peer) = full_key {
2602 if let Some(resolution) =
2603 chat.resolve_member(channel, hint, peer)
2604 {
2605 let _ = chat_events.send(
2606 MobileChatWorkerEvent::SenderResolution(resolution),
2607 );
2608 }
2609 }
2610 remember_member_route(
2611 &mut member_routes,
2612 channel,
2613 hint,
2614 &text.rx,
2615 );
2616 (
2617 Envelope {
2618 path: DeliveryPath::Multicast,
2619 conversation: ConversationKey::ChannelGroup { channel },
2620 sender: SenderScope::ClaimedMember(hint),
2621 },
2622 full_key,
2623 )
2624 }
2625 InboundTextSource::ChannelDirect { channel, peer } => (
2626 Envelope {
2627 path: DeliveryPath::BlindUnicast,
2628 conversation: ConversationKey::ChannelDirect { channel, peer },
2629 sender: SenderScope::Peer(peer),
2630 },
2631 Some(peer),
2632 ),
2633 };
2634 let _ = chat.engine.receive(
2635 &envelope,
2636 sender_full_key,
2637 &text.payload,
2638 received_at_ms,
2639 );
2640 let mut drain = chat.drain();
2641 attach_rx_metadata(&mut drain.mutations, &text.rx);
2644 let transmissions = drain.transmissions.clone();
2645 publish_chat_drain(drain, &chat_events);
2646 if !transmissions.is_empty() || !pending_chat_transmissions.is_empty() {
2647 let sent = queue_chat_transmissions(
2648 &node,
2649 transmissions,
2650 &mut pending_chat_transmissions,
2651 &mut in_flight_chat,
2652 &chat_pipeline_ready,
2653 &channel_registry,
2654 &mut chat,
2655 received_at_ms,
2656 )
2657 .await;
2658 publish_chat_drain(chat.drain(), &chat_events);
2659 if sent > 0 && handle.service_counter_persistence().await.is_err() {
2660 return;
2661 }
2662 }
2663 }
2664 }
2665 _ = protocol_timeout_tick.tick() => {
2666 timeout_servicer.service().await;
2667 let now_ms = handle.now_ms().await;
2668 chat.engine.tick(now_ms);
2669 service_chat_tickets(
2670 &mut chat,
2671 &mut in_flight_chat,
2672 &mut chat_pipeline_ready,
2673 &chat_events,
2674 pending_chat_transmissions.len(),
2675 now_ms,
2676 );
2677 let drain = chat.drain();
2678 let transmissions = drain.transmissions.clone();
2679 publish_chat_drain(drain, &chat_events);
2680 if !transmissions.is_empty() || !pending_chat_transmissions.is_empty() {
2681 let sent = queue_chat_transmissions(
2682 &node,
2683 transmissions,
2684 &mut pending_chat_transmissions,
2685 &mut in_flight_chat,
2686 &chat_pipeline_ready,
2687 &channel_registry,
2688 &mut chat,
2689 now_ms,
2690 )
2691 .await;
2692 publish_chat_drain(chat.drain(), &chat_events);
2693 if sent > 0 && handle.service_counter_persistence().await.is_err() {
2694 return;
2695 }
2696 }
2697 }
2698 }
2699 }
2700 };
2701
2702 tokio::select! {
2705 _ = pump_loop => {}
2706 _ = command_loop => {}
2707 }
2708}
2709
2710async fn queue_chat_transmissions<M: MacBackend>(
2711 node: &LocalNode<M>,
2712 transmissions: Vec<umsh_text::engine::Transmission>,
2713 pending: &mut VecDeque<umsh_text::engine::Transmission>,
2714 in_flight: &mut Vec<InFlightChatTransmission>,
2715 pipeline_ready: &BTreeSet<[u8; 32]>,
2716 channels: &Rc<RefCell<ChannelRegistry>>,
2717 chat: &mut MobileChatState,
2718 now_ms: u64,
2719) -> usize {
2720 pending.extend(transmissions);
2721 if in_flight.len() >= MOBILE_CHAT_TRANSMIT_WINDOW {
2725 return 0;
2726 }
2727 let mut queued = 0;
2728 while let Some(transmission) = pending.pop_front() {
2729 let gate_peer = match transmission.destination {
2730 Destination::Peer(peer) => Some(peer),
2731 Destination::Channel(_) | Destination::ChannelPeer { .. } => None,
2734 };
2735 if let Some(peer) = gate_peer {
2736 if !pipeline_ready.contains(&peer.0)
2737 && in_flight.iter().any(|entry| entry.gate_peer == Some(peer))
2738 {
2739 pending.push_front(transmission);
2743 break;
2744 }
2745 }
2746 let mut payload = Vec::with_capacity(transmission.payload.len() + 1);
2747 payload.push(PayloadType::TextMessage as u8);
2748 payload.extend_from_slice(transmission.payload.as_slice());
2749 let sent = match transmission.destination {
2750 Destination::Peer(peer) => match node.peer(peer).await {
2751 Ok(connection) => {
2752 connection
2753 .send(&payload, &SendOptions::default().with_ack_requested(true))
2754 .await
2755 }
2756 Err(_) => {
2757 chat.engine.transmit_update(
2758 transmission.transmission_id,
2759 DeliveryState::Failed,
2760 now_ms,
2761 );
2762 continue;
2763 }
2764 },
2765 Destination::Channel(channel) => {
2766 let Some(bound) = bound_channel(node, channels, &channel) else {
2767 chat.engine.transmit_update(
2768 transmission.transmission_id,
2769 DeliveryState::Failed,
2770 now_ms,
2771 );
2772 continue;
2773 };
2774 let mut options = SendOptions::default().with_full_source();
2778 if channel == crate::emergency_channel_tag() {
2784 options = options.unencrypted();
2785 }
2786 bound.send_all(&payload, &options).await
2787 }
2788 Destination::ChannelPeer { channel, peer } => {
2789 let Some(bound) = bound_channel(node, channels, &channel) else {
2790 chat.engine.transmit_update(
2791 transmission.transmission_id,
2792 DeliveryState::Failed,
2793 now_ms,
2794 );
2795 continue;
2796 };
2797 if node.peer(peer).await.is_err() {
2804 chat.engine.transmit_update(
2805 transmission.transmission_id,
2806 DeliveryState::Failed,
2807 now_ms,
2808 );
2809 continue;
2810 }
2811 let mut options = SendOptions::default().with_full_source();
2814 if channel == crate::emergency_channel_tag() {
2822 options = options.unencrypted();
2823 }
2824 let r = bound.send(&peer, &payload, &options).await;
2825 r
2826 }
2827 };
2828 let ticket = match sent {
2829 Ok(ticket) => ticket,
2830 Err(_) => {
2831 pending.push_front(transmission);
2835 break;
2836 }
2837 };
2838 in_flight.push(InFlightChatTransmission {
2839 transmission_id: transmission.transmission_id,
2840 gate_peer,
2841 ticket,
2842 sent_reported: false,
2843 non_ack: gate_peer.is_none(),
2844 queued_at_ms: now_ms,
2845 stall_reported: false,
2846 });
2847 queued += 1;
2848 if in_flight.len() >= MOBILE_CHAT_TRANSMIT_WINDOW {
2849 break;
2850 }
2851 }
2852 queued
2853}
2854
2855async fn request_identity_over_channel<M: MacBackend>(
2862 node: &LocalNode<M>,
2863 channels: &Rc<RefCell<ChannelRegistry>>,
2864 channel: ChannelTag,
2865 hint: NodeHint,
2866 nonce: u32,
2867 route: Option<MemberRoute>,
2868) -> Result<(), MobileMeshError> {
2869 let Some(bound) = bound_channel(node, channels, &channel) else {
2870 return Err(MobileMeshError::UnknownConversation);
2871 };
2872 let options_block = umsh_node::mac_command::IdentityRequestBuilder::new()
2873 .nonce(nonce)
2874 .and_then(|builder| builder.filter_hint(&hint))
2875 .map_err(|_| MobileMeshError::SendFailed)?
2876 .build();
2877 let cmd = umsh_node::MacCommand::IdentityRequest {
2878 options: &options_block,
2879 };
2880 let mut frame = [0u8; 128];
2881 frame[0] = PayloadType::MacCommand as u8;
2882 let length = umsh_node::mac_command::encode(&cmd, &mut frame[1..])
2883 .map_err(|_| MobileMeshError::SendFailed)?
2884 + 1;
2885 let mut options = SendOptions::default().with_full_source();
2888 match route.as_ref() {
2889 Some(route) if !route.route_hints.is_empty() => {
2890 let hops = route
2891 .route_hints
2892 .iter()
2893 .filter_map(|hint| <[u8; 2]>::try_from(hint.as_slice()).ok())
2894 .map(umsh_core::RouterHint)
2895 .collect::<Vec<_>>();
2896 options = match options.try_with_source_route(&hops) {
2899 Ok(options) => options,
2900 Err(_) => SendOptions::default()
2901 .with_full_source()
2902 .with_flood_hops(route.hop_count.unwrap_or(5).max(1)),
2903 };
2904 }
2905 Some(MemberRoute {
2906 hop_count: Some(hops),
2907 ..
2908 }) => {
2909 options = options.with_flood_hops((*hops).max(1));
2910 }
2911 _ => {}
2912 }
2913 bound
2914 .send_all(&frame[..length], &options)
2915 .await
2916 .map(|_| ())
2917 .map_err(|_| MobileMeshError::SendFailed)
2918}
2919
2920fn bound_channel<M: MacBackend>(
2922 node: &LocalNode<M>,
2923 channels: &Rc<RefCell<ChannelRegistry>>,
2924 channel: &ChannelTag,
2925) -> Option<umsh_node::BoundChannel<M>> {
2926 let key = channels.borrow().key(channel)?;
2927 node.bound_channel(&umsh_node::Channel::private(key, ""))
2930}
2931
2932fn service_chat_tickets(
2933 chat: &mut MobileChatState,
2934 in_flight: &mut Vec<InFlightChatTransmission>,
2935 pipeline_ready: &mut BTreeSet<[u8; 32]>,
2936 events: &NotifyingSender<MobileChatWorkerEvent>,
2937 pending_depth: usize,
2938 now_ms: u64,
2939) {
2940 let occupied = in_flight.len();
2941 let mut index = 0;
2942 while index < in_flight.len() {
2943 let entry = &mut in_flight[index];
2944 if !entry.stall_reported
2947 && !entry.ticket.was_transmitted()
2948 && now_ms.saturating_sub(entry.queued_at_ms) >= CHAT_TRANSMISSION_STALL_MS
2949 {
2950 entry.stall_reported = true;
2951 let waited = now_ms.saturating_sub(entry.queued_at_ms) / 1000;
2952 let transmission_id = entry.transmission_id;
2953 let _ = events.send(MobileChatWorkerEvent::Diagnostic(format!(
2954 "transmission {transmission_id} has not left the radio after {waited}s \
2955 ({occupied}/{MOBILE_CHAT_TRANSMIT_WINDOW} window slots used, \
2956 {pending_depth} more waiting)"
2957 )));
2958 }
2959 let entry = &mut in_flight[index];
2960 if entry.ticket.was_transmitted() && !entry.sent_reported {
2961 chat.engine
2962 .transmit_update(entry.transmission_id, DeliveryState::Sent, now_ms);
2963 entry.sent_reported = true;
2964 }
2965 if entry.non_ack && entry.sent_reported {
2966 in_flight.swap_remove(index);
2970 } else if entry.ticket.was_acked() {
2971 if let Some(peer) = entry.gate_peer {
2972 pipeline_ready.insert(peer.0);
2973 }
2974 chat.engine
2975 .transmit_update(entry.transmission_id, DeliveryState::Acked, now_ms);
2976 in_flight.swap_remove(index);
2977 } else if entry.ticket.has_failed() {
2978 chat.engine
2979 .transmit_update(entry.transmission_id, DeliveryState::Failed, now_ms);
2980 in_flight.swap_remove(index);
2981 } else {
2982 index += 1;
2983 }
2984 }
2985}
2986
2987fn publish_chat_drain(
2988 drain: crate::mobile_chat::ChatDrain,
2989 events: &NotifyingSender<MobileChatWorkerEvent>,
2990) {
2991 for mutation in drain.mutations {
2992 let _ = events.send(MobileChatWorkerEvent::Mutation(mutation));
2993 }
2994 for delivery in drain.deliveries {
2995 let _ = events.send(MobileChatWorkerEvent::Delivery(delivery));
2996 }
2997 for lookup in drain.lookups {
2998 let _ = events.send(MobileChatWorkerEvent::ArchiveLookup(lookup));
2999 }
3000 for resolution in drain.resolutions {
3001 let _ = events.send(MobileChatWorkerEvent::SenderResolution(resolution));
3002 }
3003 for diagnostic in drain.diagnostics {
3004 let _ = events.send(MobileChatWorkerEvent::Diagnostic(diagnostic));
3005 }
3006}
3007
3008fn attach_rx_metadata(mutations: &mut [MobileChatMutationRecord], rx: &MobileChatRxMetadataRecord) {
3014 for mutation in mutations {
3015 let describes_receipt = match mutation.kind {
3016 MobileChatMutationKind::Insert => {
3017 mutation.direction == Some(MobileChatDirection::Inbound)
3018 && mutation.presence == MobileChatPresence::Present
3019 }
3020 MobileChatMutationKind::UpdateBody => true,
3021 MobileChatMutationKind::Edit | MobileChatMutationKind::Delete => false,
3022 };
3023 if describes_receipt {
3024 mutation.rx = Some(rx.clone());
3025 }
3026 }
3027}
3028
3029fn remember_member_route(
3032 routes: &mut BTreeMap<(ChannelTag, [u8; 3]), MemberRoute>,
3033 channel: ChannelTag,
3034 hint: NodeHint,
3035 rx: &MobileChatRxMetadataRecord,
3036) {
3037 routes.insert(
3038 (channel, hint.0),
3039 MemberRoute {
3040 hop_count: rx.hop_count,
3041 route_hints: rx.route_hints.clone(),
3042 },
3043 );
3044}
3045
3046#[derive(Clone)]
3048struct MemberRoute {
3049 hop_count: Option<u8>,
3050 route_hints: Vec<Vec<u8>>,
3051}
3052
3053fn decode_peer(address: &str) -> Result<PublicKey, MobileError> {
3054 let bytes = umsh_core::base58::decode(address.as_bytes())?;
3055 Ok(PublicKey(bytes))
3056}
3057
3058fn decode_channel_keys(keys: Vec<Vec<u8>>) -> Result<Vec<ChannelKey>, MobileMeshError> {
3059 keys.into_iter()
3060 .map(|key| {
3061 <[u8; 32]>::try_from(key.as_slice())
3062 .map(ChannelKey)
3063 .map_err(|_| MobileMeshError::InvalidChannelKey)
3064 })
3065 .collect()
3066}
3067
3068fn encode_peer_address(peer: &PublicKey) -> String {
3071 umsh_core::base58::encode(&peer.0)
3072 .into_iter()
3073 .map(char::from)
3074 .collect()
3075}
3076
3077fn emit_ping_failure(events: &NotifyingSender<MobileMeshPingEventRecord>, operation_id: u64) {
3078 let _ = events.send(MobileMeshPingEventRecord {
3079 operation_id,
3080 outcome: MobileMeshPingOutcome::Failed,
3081 round_trip_milliseconds: None,
3082 hop_count: None,
3083 route_hints: Vec::new(),
3084 rssi_dbm: None,
3085 snr_centibels: None,
3086 lqi: None,
3087 });
3088}
3089
3090#[cfg(test)]
3091mod tests {
3092 use super::*;
3093 use crate::MobileChatDeliveryState;
3094 use std::time::Instant;
3095 use umsh_crypto::NodeIdentity;
3096
3097 fn identity(seed: u8) -> Arc<MobileIdentity> {
3098 let identity = SoftwareIdentity::from_secret_bytes(&[seed; 32]);
3099 let public_identity = crate::public_identity_record(identity.public_key());
3100 Arc::new(MobileIdentity {
3101 identity: Mutex::new(Some(identity)),
3102 public_identity,
3103 })
3104 }
3105
3106 fn address(identity: &MobileIdentity) -> String {
3107 identity.public_identity.canonical_address.clone()
3108 }
3109
3110 fn hint_of(identity: &MobileIdentity) -> Vec<u8> {
3113 decode_peer(&address(identity)).unwrap().0[..3].to_vec()
3114 }
3115
3116 async fn channel_session(name: &str) -> Arc<MobileMeshSession> {
3117 let directory = tempfile::tempdir().unwrap();
3118 let store =
3119 MobileCounterStore::new(directory.path().join(name).display().to_string()).unwrap();
3120 std::mem::forget(directory);
3122 MobileMeshSession::new(identity(31), store).await.unwrap()
3123 }
3124
3125 #[tokio::test]
3126 async fn channel_registration_is_idempotent_and_reversible() {
3127 let session = channel_session("channels").await;
3128 let key = vec![0x5au8; 32];
3129
3130 session.register_channels(vec![key.clone()]).await.unwrap();
3131 session.register_channels(vec![key.clone()]).await.unwrap();
3134 session.remove_channels(vec![key.clone()]).await.unwrap();
3135 session.remove_channels(vec![key.clone()]).await.unwrap();
3137 session.register_channels(vec![key]).await.unwrap();
3139 }
3140
3141 #[tokio::test]
3142 async fn channel_keys_must_be_full_length() {
3143 let session = channel_session("shortkey").await;
3144 assert_eq!(
3145 session.register_channels(vec![vec![0x01; 31]]).await,
3146 Err(MobileMeshError::InvalidChannelKey)
3147 );
3148 assert_eq!(
3149 session.remove_channels(vec![Vec::new()]).await,
3150 Err(MobileMeshError::InvalidChannelKey)
3151 );
3152 }
3153
3154 #[tokio::test]
3155 async fn the_phone_mac_holds_more_channels_than_the_embedded_default() {
3156 let session = channel_session("capacity").await;
3157 let keys: Vec<Vec<u8>> = (0..MOBILE_MAC_CHANNELS)
3159 .map(|index| {
3160 let mut key = vec![0u8; 32];
3161 key[0] = index as u8;
3162 key[1] = 0xA5;
3163 key
3164 })
3165 .collect();
3166 assert!(keys.len() > umsh_mac::DEFAULT_CHANNELS);
3167 session.register_channels(keys).await.unwrap();
3168
3169 let overflow = vec![vec![0xFFu8; 32]];
3170 assert_eq!(
3171 session.register_channels(overflow).await,
3172 Err(MobileMeshError::ChannelCapacity)
3173 );
3174 }
3175
3176 #[tokio::test]
3177 async fn two_rust_sessions_complete_an_authenticated_ping() {
3178 let directory = tempfile::tempdir().unwrap();
3179 let alice_identity = identity(7);
3180 let bob_identity = identity(9);
3181 let alice_root = directory.path().join("alice");
3182 let bob_root = directory.path().join("bob");
3183 let alice_store = MobileCounterStore::new(alice_root.display().to_string()).unwrap();
3184 let bob_store = MobileCounterStore::new(bob_root.display().to_string()).unwrap();
3185 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3186 .await
3187 .unwrap();
3188 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
3189 .await
3190 .unwrap();
3191 assert!(!alice_root.exists());
3195 assert!(!bob_root.exists());
3196
3197 let operation = alice.ping(address(&bob_identity), 2_000).unwrap();
3201 let _ = bob.ping(address(&alice_identity), 2_000).unwrap();
3202 let deadline = Instant::now() + Duration::from_secs(10);
3203 loop {
3204 let alice_update = alice.poll_update();
3205 for frame in alice_update.outbound_frames {
3206 assert!(
3207 alice_root.exists(),
3208 "Alice released a frame before persisting its reservation"
3209 );
3210 alice.complete_outbound_frame(frame.id, true).unwrap();
3211 bob.receive(MobileMeshRxRecord {
3212 data: frame.data,
3213 rssi_dbm: Some(-40),
3214 lqi: None,
3215 snr_cb: Some(100),
3216 })
3217 .unwrap();
3218 }
3219 if let Some(event) = alice_update.ping_events.into_iter().next() {
3220 assert_eq!(event.operation_id, operation);
3221 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
3222 assert!(event.round_trip_milliseconds.is_some());
3223 assert_eq!(event.hop_count, Some(1));
3224 assert!(event.route_hints.is_empty());
3225 assert_eq!(event.rssi_dbm, Some(-42));
3226 assert_eq!(event.snr_centibels, Some(90));
3227 assert_eq!(event.lqi, None);
3228 break;
3229 }
3230
3231 let bob_update = bob.poll_update();
3232 for frame in bob_update.outbound_frames {
3233 assert!(
3234 bob_root.exists(),
3235 "Bob released a frame before persisting its reservation"
3236 );
3237 bob.complete_outbound_frame(frame.id, true).unwrap();
3238 alice
3239 .receive(MobileMeshRxRecord {
3240 data: frame.data,
3241 rssi_dbm: Some(-42),
3242 lqi: None,
3243 snr_cb: Some(90),
3244 })
3245 .unwrap();
3246 }
3247 assert!(Instant::now() < deadline, "ping did not complete");
3248 std::thread::sleep(Duration::from_millis(5));
3249 }
3250 }
3251
3252 async fn complete_ping(alice: &MobileMeshSession, bob: &MobileMeshSession, target: String) {
3255 let operation = alice.ping(target, 2_000).unwrap();
3256 let deadline = Instant::now() + Duration::from_secs(10);
3257 loop {
3258 let alice_update = alice.poll_update();
3259 for frame in alice_update.outbound_frames {
3260 alice.complete_outbound_frame(frame.id, true).unwrap();
3261 bob.receive(MobileMeshRxRecord {
3262 data: frame.data,
3263 rssi_dbm: Some(-40),
3264 lqi: None,
3265 snr_cb: Some(100),
3266 })
3267 .unwrap();
3268 }
3269 if let Some(event) = alice_update.ping_events.into_iter().next() {
3270 assert_eq!(event.operation_id, operation);
3271 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
3272 break;
3273 }
3274 let bob_update = bob.poll_update();
3275 for frame in bob_update.outbound_frames {
3276 bob.complete_outbound_frame(frame.id, true).unwrap();
3277 alice
3278 .receive(MobileMeshRxRecord {
3279 data: frame.data,
3280 rssi_dbm: Some(-42),
3281 lqi: None,
3282 snr_cb: Some(90),
3283 })
3284 .unwrap();
3285 }
3286 assert!(Instant::now() < deadline, "ping did not complete");
3287 std::thread::sleep(Duration::from_millis(5));
3288 }
3289 }
3290
3291 #[tokio::test]
3292 async fn removed_peer_re_registers_cleanly_and_traffic_still_flows() {
3293 let directory = tempfile::tempdir().unwrap();
3294 let alice_identity = identity(21);
3295 let bob_identity = identity(23);
3296 let alice_store =
3297 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3298 let bob_store =
3299 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3300 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3301 .await
3302 .unwrap();
3303 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
3304 .await
3305 .unwrap();
3306
3307 alice
3308 .register_peers(vec![address(&bob_identity)])
3309 .await
3310 .unwrap();
3311 bob.register_peers(vec![address(&alice_identity)])
3312 .await
3313 .unwrap();
3314 complete_ping(&alice, &bob, address(&bob_identity)).await;
3315
3316 alice
3319 .remove_peers(vec![address(&bob_identity)])
3320 .await
3321 .unwrap();
3322 alice
3323 .remove_peers(vec![address(&bob_identity)])
3324 .await
3325 .unwrap();
3326 alice
3327 .remove_peers(vec![address(&alice_identity)])
3328 .await
3329 .unwrap();
3330
3331 alice
3335 .register_peers(vec![address(&bob_identity)])
3336 .await
3337 .unwrap();
3338 complete_ping(&alice, &bob, address(&bob_identity)).await;
3339 }
3340
3341 #[tokio::test]
3342 async fn discover_identities_emits_one_acceptable_zero_hop_broadcast() {
3343 let directory = tempfile::tempdir().unwrap();
3344 let alice_identity = identity(31);
3345 let bob_identity = identity(33);
3346 let alice_store =
3347 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3348 let bob_store =
3349 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3350 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3351 .await
3352 .unwrap();
3353 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
3354 .await
3355 .unwrap();
3356
3357 alice.discover_identities(None, Some(0x02)).await.unwrap();
3358
3359 let deadline = Instant::now() + Duration::from_secs(10);
3360 let frames = loop {
3361 let update = alice.poll_update();
3362 if !update.outbound_frames.is_empty() {
3363 break update.outbound_frames;
3364 }
3365 assert!(Instant::now() < deadline, "solicitation never went out");
3366 std::thread::sleep(Duration::from_millis(5));
3367 };
3368 assert_eq!(frames.len(), 1);
3370 let frame = frames.into_iter().next().unwrap();
3371 alice.complete_outbound_frame(frame.id, true).unwrap();
3372 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
3373 assert_eq!(header.packet_type(), umsh_core::PacketType::Broadcast);
3374 assert!(header.flood_hops.is_none());
3376 assert!(header.fcf.full_source());
3378
3379 bob.receive(MobileMeshRxRecord {
3383 data: frame.data,
3384 rssi_dbm: Some(-40),
3385 lqi: None,
3386 snr_cb: Some(100),
3387 })
3388 .unwrap();
3389
3390 alice.discover_identities(None, None).await.unwrap();
3394 let deadline = Instant::now() + Duration::from_secs(10);
3395 let frames = loop {
3396 let update = alice.poll_update();
3397 if !update.outbound_frames.is_empty() {
3398 break update.outbound_frames;
3399 }
3400 assert!(Instant::now() < deadline, "solicitation never went out");
3401 std::thread::sleep(Duration::from_millis(5));
3402 };
3403 assert_eq!(frames.len(), 1);
3404 let frame = frames.into_iter().next().unwrap();
3405 alice.complete_outbound_frame(frame.id, true).unwrap();
3406 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
3407 let body = &frame.data[header.body_range.clone()];
3408 assert_eq!(body[0], umsh_core::PayloadType::MacCommand as u8);
3409 let umsh_node::MacCommand::IdentityRequest { options } =
3410 umsh_node::mac_command::parse(&body[1..]).unwrap()
3411 else {
3412 panic!("expected an identity request");
3413 };
3414 let has_vacuous_caps_filter = umsh_core::options::OptionDecoder::new(options)
3415 .filter_map(Result::ok)
3416 .any(|(number, value)| {
3417 number == umsh_node::mac_command::identity_filter::FILTER_NODE_CAPS && value == [0]
3418 });
3419 assert!(has_vacuous_caps_filter);
3420 }
3421
3422 #[tokio::test]
3430 async fn discover_solicitation_earns_a_stranger_reply_end_to_end() {
3431 let directory = tempfile::tempdir().unwrap();
3432 let alice_identity = identity(21);
3433 let bob_identity = identity(23);
3434 let alice_store =
3435 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3436 let bob_store =
3437 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3438 let alice = MobileMeshSession::new_with_virtual_time(alice_identity.clone(), alice_store)
3443 .await
3444 .unwrap();
3445 let bob = MobileMeshSession::new_with_virtual_time(bob_identity.clone(), bob_store)
3446 .await
3447 .unwrap();
3448 bob.set_discoverable(true, Some("Bob's phone".into()))
3450 .await
3451 .unwrap();
3452 bob.set_advertised_location(Some(MobileMeshSharedLocationRecord {
3455 latitude_degrees: 48.1173,
3456 longitude_degrees: 11.5167,
3457 precision_bytes: 5,
3458 }))
3459 .await
3460 .unwrap();
3461
3462 alice.discover_identities(None, None).await.unwrap();
3463
3464 let bob_address = address(&bob_identity);
3466 let deadline = Instant::now() + Duration::from_secs(15);
3467 let event = 'outer: loop {
3468 let alice_update = alice.poll_update();
3469 for frame in alice_update.outbound_frames {
3470 alice.complete_outbound_frame(frame.id, true).unwrap();
3471 bob.receive(MobileMeshRxRecord {
3472 data: frame.data,
3473 rssi_dbm: Some(-40),
3474 lqi: None,
3475 snr_cb: Some(100),
3476 })
3477 .unwrap();
3478 }
3479 for event in alice_update.advertisement_events {
3480 if event.peer_address == bob_address {
3481 break 'outer event;
3482 }
3483 }
3484 let bob_update = bob.poll_update();
3485 for frame in bob_update.outbound_frames {
3486 bob.complete_outbound_frame(frame.id, true).unwrap();
3487 alice
3488 .receive(MobileMeshRxRecord {
3489 data: frame.data,
3490 rssi_dbm: Some(-42),
3491 lqi: None,
3492 snr_cb: Some(90),
3493 })
3494 .unwrap();
3495 }
3496 assert!(Instant::now() < deadline, "no identity reply reached Alice");
3497 std::thread::sleep(Duration::from_millis(5));
3498 };
3499 assert!(event.source_authenticated);
3502 let payload = umsh_node::NodeIdentityPayload::from_bytes(&event.payload).unwrap();
3503 assert_eq!(payload.name.as_deref(), Some("Bob's phone"));
3504 let cell = payload
3505 .location
3506 .expect("the reply serves the shared location");
3507 assert_eq!(cell.precision(), 5);
3508 let (lat, lon) = cell.center();
3509 assert!((f64::from(lat) - 48.1173).abs() < 0.01);
3510 assert!((f64::from(lon) - 11.5167).abs() < 0.01);
3511
3512 bob.set_discoverable(false, None).await.unwrap();
3514 alice.discover_identities(None, None).await.unwrap();
3515 let quiet_until = Instant::now() + Duration::from_secs(6);
3516 while Instant::now() < quiet_until {
3517 let alice_update = alice.poll_update();
3518 for frame in alice_update.outbound_frames {
3519 alice.complete_outbound_frame(frame.id, true).unwrap();
3520 bob.receive(MobileMeshRxRecord {
3521 data: frame.data,
3522 rssi_dbm: Some(-40),
3523 lqi: None,
3524 snr_cb: Some(100),
3525 })
3526 .unwrap();
3527 }
3528 let bob_update = bob.poll_update();
3529 assert!(
3530 bob_update.outbound_frames.is_empty(),
3531 "Bob answered while not discoverable"
3532 );
3533 std::thread::sleep(Duration::from_millis(20));
3534 }
3535 }
3536
3537 #[tokio::test]
3538 async fn peer_route_is_visible_and_resettable() {
3539 let directory = tempfile::tempdir().unwrap();
3540 let alice_identity = identity(11);
3541 let bob_identity = identity(13);
3542 let alice_store =
3543 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3544 let bob_store =
3545 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3546 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3547 .await
3548 .unwrap();
3549 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
3550 .await
3551 .unwrap();
3552
3553 assert_eq!(
3556 alice.peer_route(address(&bob_identity)).await.unwrap(),
3557 MobileMeshRouteRecord::unknown()
3558 );
3559 assert!(
3560 !alice
3561 .clear_peer_route(address(&bob_identity))
3562 .await
3563 .unwrap()
3564 );
3565
3566 let operation = alice.ping(address(&bob_identity), 2_000).unwrap();
3567 let _ = bob.ping(address(&alice_identity), 2_000).unwrap();
3568 let deadline = Instant::now() + Duration::from_secs(10);
3569 loop {
3570 let alice_update = alice.poll_update();
3571 for frame in alice_update.outbound_frames {
3572 alice.complete_outbound_frame(frame.id, true).unwrap();
3573 bob.receive(MobileMeshRxRecord {
3574 data: frame.data,
3575 rssi_dbm: Some(-40),
3576 lqi: None,
3577 snr_cb: Some(100),
3578 })
3579 .unwrap();
3580 }
3581 if let Some(event) = alice_update.ping_events.into_iter().next() {
3582 assert_eq!(event.operation_id, operation);
3583 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
3584 break;
3585 }
3586
3587 let bob_update = bob.poll_update();
3588 for frame in bob_update.outbound_frames {
3589 bob.complete_outbound_frame(frame.id, true).unwrap();
3590 alice
3591 .receive(MobileMeshRxRecord {
3592 data: frame.data,
3593 rssi_dbm: Some(-42),
3594 lqi: None,
3595 snr_cb: Some(90),
3596 })
3597 .unwrap();
3598 }
3599 assert!(Instant::now() < deadline, "ping did not complete");
3600 std::thread::sleep(Duration::from_millis(5));
3601 }
3602
3603 let route = alice.peer_route(address(&bob_identity)).await.unwrap();
3608 assert_eq!(route.kind, MobileMeshRouteKind::Direct);
3609 assert!(route.hints.is_empty());
3610 assert_eq!(route.flood_hops, None);
3611
3612 assert!(
3615 alice
3616 .clear_peer_route(address(&bob_identity))
3617 .await
3618 .unwrap()
3619 );
3620 assert_eq!(
3621 alice.peer_route(address(&bob_identity)).await.unwrap(),
3622 MobileMeshRouteRecord::unknown()
3623 );
3624 assert!(
3625 !alice
3626 .clear_peer_route(address(&bob_identity))
3627 .await
3628 .unwrap()
3629 );
3630
3631 let operation = alice.ping(address(&bob_identity), 2_000).unwrap();
3634 let deadline = Instant::now() + Duration::from_secs(10);
3635 loop {
3636 let alice_update = alice.poll_update();
3637 for frame in alice_update.outbound_frames {
3638 alice.complete_outbound_frame(frame.id, true).unwrap();
3639 bob.receive(MobileMeshRxRecord {
3640 data: frame.data,
3641 rssi_dbm: Some(-40),
3642 lqi: None,
3643 snr_cb: Some(100),
3644 })
3645 .unwrap();
3646 }
3647 if let Some(event) = alice_update.ping_events.into_iter().next() {
3648 assert_eq!(event.operation_id, operation);
3649 assert_eq!(event.outcome, MobileMeshPingOutcome::Reply);
3650 break;
3651 }
3652
3653 let bob_update = bob.poll_update();
3654 for frame in bob_update.outbound_frames {
3655 bob.complete_outbound_frame(frame.id, true).unwrap();
3656 alice
3657 .receive(MobileMeshRxRecord {
3658 data: frame.data,
3659 rssi_dbm: Some(-42),
3660 lqi: None,
3661 snr_cb: Some(90),
3662 })
3663 .unwrap();
3664 }
3665 assert!(
3666 Instant::now() < deadline,
3667 "ping after reset did not complete"
3668 );
3669 std::thread::sleep(Duration::from_millis(5));
3670 }
3671 assert_eq!(
3672 alice.peer_route(address(&bob_identity)).await.unwrap().kind,
3673 MobileMeshRouteKind::Direct
3674 );
3675 }
3676
3677 #[tokio::test]
3678 async fn broadcast_advertisement_reaches_peer_with_valid_signature() {
3679 let directory = tempfile::tempdir().unwrap();
3680 let alice_identity = identity(21);
3681 let bob_identity = identity(23);
3682 let alice_store =
3683 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3684 let bob_store =
3685 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3686 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3687 .await
3688 .unwrap();
3689 let bob = MobileMeshSession::new(bob_identity, bob_store)
3690 .await
3691 .unwrap();
3692
3693 let bundle = alice
3695 .sign_identity_bundle(Some("Alice's Phone".to_owned()), Some(1_760_000_000))
3696 .await
3697 .unwrap();
3698 let record = crate::decode_node_identity(address(&alice_identity), bundle.clone()).unwrap();
3699 assert_eq!(record.signature, crate::IdentitySignatureState::Valid);
3700 assert_eq!(record.name.as_deref(), Some("Alice's Phone"));
3701 assert_eq!(record.role_label, "Chat");
3702 let uri = crate::node_uri_with_identity(address(&alice_identity), bundle).unwrap();
3703 assert!(
3704 crate::inspect_node_uri(uri)
3705 .unwrap()
3706 .identity_payload
3707 .is_some()
3708 );
3709
3710 alice
3711 .advertise_identity(Some("Alice's Phone".to_owned()), None)
3712 .await
3713 .unwrap();
3714
3715 let deadline = Instant::now() + Duration::from_secs(10);
3716 loop {
3717 for frame in alice.poll_update().outbound_frames {
3718 alice.complete_outbound_frame(frame.id, true).unwrap();
3719 bob.receive(MobileMeshRxRecord {
3720 data: frame.data,
3721 rssi_dbm: Some(-50),
3722 lqi: None,
3723 snr_cb: None,
3724 })
3725 .unwrap();
3726 }
3727 let bob_update = bob.poll_update();
3728 let heard = bob_update.peer_heard_events;
3732 if let Some(event) = bob_update.advertisement_events.into_iter().next() {
3733 assert_eq!(
3734 heard.iter().find_map(|record| record.peer_address.clone()),
3735 Some(address(&alice_identity)),
3736 "the frame that carried the advertisement also reported presence"
3737 );
3738 assert_eq!(event.peer_address, address(&alice_identity));
3739 assert!(!event.source_authenticated);
3743 let received =
3744 crate::decode_node_identity(event.peer_address, event.payload).unwrap();
3745 assert_eq!(received.signature, crate::IdentitySignatureState::Valid);
3746 assert_eq!(received.name.as_deref(), Some("Alice's Phone"));
3747 break;
3748 }
3749 assert!(Instant::now() < deadline, "advertisement not received");
3750 std::thread::sleep(Duration::from_millis(5));
3751 }
3752 }
3753
3754 #[tokio::test]
3758 async fn a_shared_location_rides_adverts_but_never_the_durable_bundle() {
3759 let directory = tempfile::tempdir().unwrap();
3760 let alice_identity = identity(51);
3761 let bob_identity = identity(53);
3762 let alice_store =
3763 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3764 let bob_store =
3765 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3766 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3767 .await
3768 .unwrap();
3769 let bob = MobileMeshSession::new(bob_identity, bob_store)
3770 .await
3771 .unwrap();
3772
3773 alice
3774 .set_advertised_location(Some(MobileMeshSharedLocationRecord {
3775 latitude_degrees: 37.774_929,
3776 longitude_degrees: -122.419_416,
3777 precision_bytes: 5,
3778 }))
3779 .await
3780 .unwrap();
3781
3782 let bundle = alice
3785 .sign_identity_bundle(Some("Alice's Phone".to_owned()), None)
3786 .await
3787 .unwrap();
3788 let record = crate::decode_node_identity(address(&alice_identity), bundle).unwrap();
3789 assert_eq!(record.signature, crate::IdentitySignatureState::Valid);
3790 assert!(
3791 record.latitude.is_none(),
3792 "a QR bundle never places its owner"
3793 );
3794
3795 let mut received = Vec::new();
3797 for share in [true, false] {
3798 if !share {
3799 alice.set_advertised_location(None).await.unwrap();
3800 }
3801 alice.advertise_identity(None, None).await.unwrap();
3802 let deadline = Instant::now() + Duration::from_secs(10);
3803 'advert: loop {
3804 for frame in alice.poll_update().outbound_frames {
3805 alice.complete_outbound_frame(frame.id, true).unwrap();
3806 bob.receive(MobileMeshRxRecord {
3807 data: frame.data,
3808 rssi_dbm: Some(-50),
3809 lqi: None,
3810 snr_cb: None,
3811 })
3812 .unwrap();
3813 }
3814 for event in bob.poll_update().advertisement_events {
3815 received.push(
3816 crate::decode_node_identity(event.peer_address, event.payload).unwrap(),
3817 );
3818 break 'advert;
3819 }
3820 assert!(Instant::now() < deadline, "advertisement not received");
3821 std::thread::sleep(Duration::from_millis(5));
3822 }
3823 }
3824
3825 let shared = &received[0];
3826 assert_eq!(shared.location_precision, Some(5));
3827 assert!((shared.latitude.unwrap() - 37.774_929).abs() < 0.01);
3828 assert!((shared.longitude.unwrap() + 122.419_416).abs() < 0.01);
3829 assert!(received[1].latitude.is_none());
3831 }
3832
3833 #[test]
3836 fn a_location_that_names_no_place_is_refused() {
3837 let valid = MobileMeshSharedLocationRecord {
3838 latitude_degrees: 37.774_929,
3839 longitude_degrees: -122.419_416,
3840 precision_bytes: 5,
3841 };
3842 for broken in [
3843 MobileMeshSharedLocationRecord {
3846 precision_bytes: 0,
3847 ..valid
3848 },
3849 MobileMeshSharedLocationRecord {
3850 precision_bytes: MAX_PRECISION + 1,
3851 ..valid
3852 },
3853 MobileMeshSharedLocationRecord {
3854 latitude_degrees: 90.1,
3855 ..valid
3856 },
3857 MobileMeshSharedLocationRecord {
3858 longitude_degrees: -180.1,
3859 ..valid
3860 },
3861 MobileMeshSharedLocationRecord {
3862 latitude_degrees: f64::NAN,
3863 ..valid
3864 },
3865 ] {
3866 assert!(matches!(
3867 disclosed_cell(broken),
3868 Err(MobileMeshError::InvalidLocation)
3869 ));
3870 }
3871 let cell = disclosed_cell(valid).unwrap();
3872 assert_eq!(cell.precision(), 5);
3873 }
3874
3875 #[tokio::test]
3878 async fn a_beacon_reports_presence_and_carries_nothing() {
3879 let directory = tempfile::tempdir().unwrap();
3880 let alice_identity = identity(31);
3881 let bob_identity = identity(33);
3882 let alice_store =
3883 MobileCounterStore::new(directory.path().join("alice").display().to_string()).unwrap();
3884 let bob_store =
3885 MobileCounterStore::new(directory.path().join("bob").display().to_string()).unwrap();
3886 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
3887 .await
3888 .unwrap();
3889 let bob = MobileMeshSession::new(bob_identity, bob_store)
3890 .await
3891 .unwrap();
3892
3893 alice.send_beacon().await.unwrap();
3894
3895 let deadline = Instant::now() + Duration::from_secs(10);
3896 loop {
3897 for frame in alice.poll_update().outbound_frames {
3898 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
3899 assert!(header.is_beacon(), "a beacon carries no body");
3900 let options =
3901 umsh_core::ParsedOptions::extract(&frame.data, header.options_range.clone())
3902 .unwrap();
3903 assert!(options.trace_route.is_some());
3904 assert!(
3905 options.trace_signal.is_some(),
3906 "the pair is what makes the trace worth collecting"
3907 );
3908 alice.complete_outbound_frame(frame.id, true).unwrap();
3909 bob.receive(MobileMeshRxRecord {
3910 data: frame.data,
3911 rssi_dbm: Some(-50),
3912 lqi: None,
3913 snr_cb: None,
3914 })
3915 .unwrap();
3916 }
3917 let bob_update = bob.poll_update();
3918 assert!(
3919 bob_update.advertisement_events.is_empty(),
3920 "an empty beacon identifies nobody"
3921 );
3922 if let Some(heard) = bob_update.peer_heard_events.into_iter().next() {
3923 assert_eq!(heard.peer_address, None);
3927 assert_eq!(heard.node_hint, Some(hint_of(&alice_identity)));
3928 assert!(!heard.source_authenticated);
3929 break;
3930 }
3931 assert!(Instant::now() < deadline, "beacon not received");
3932 std::thread::sleep(Duration::from_millis(5));
3933 }
3934 }
3935
3936 #[tokio::test]
3940 async fn a_scheduled_advertisement_stays_with_the_neighbours() {
3941 let directory = tempfile::tempdir().unwrap();
3942 let local_identity = identity(41);
3943 let store =
3944 MobileCounterStore::new(directory.path().join("local").display().to_string()).unwrap();
3945 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
3946
3947 session
3948 .advertise_identity_scheduled(Some("Phone".to_owned()), None)
3949 .await
3950 .unwrap();
3951
3952 let deadline = Instant::now() + Duration::from_secs(10);
3953 loop {
3954 let frames = session.poll_update().outbound_frames;
3955 if let Some(frame) = frames.into_iter().next() {
3956 let header = umsh_core::PacketHeader::parse(&frame.data).unwrap();
3957 assert!(
3958 header.flood_hops.is_none(),
3959 "a scheduled advertisement is not flooded"
3960 );
3961 let options =
3962 umsh_core::ParsedOptions::extract(&frame.data, header.options_range.clone())
3963 .unwrap();
3964 assert!(options.trace_route.is_none());
3965 assert!(
3966 header.fcf.full_source(),
3967 "the detached signature is only checkable against the key"
3968 );
3969 session.complete_outbound_frame(frame.id, true).unwrap();
3970 break;
3971 }
3972 assert!(Instant::now() < deadline, "advertisement never queued");
3973 std::thread::sleep(Duration::from_millis(5));
3974 }
3975 }
3976
3977 #[tokio::test]
3982 async fn virtual_time_fast_forwards_protocol_timeouts() {
3983 let directory = tempfile::tempdir().unwrap();
3984 let local_identity = identity(61);
3985 let silent_peer = identity(62);
3986 let store = MobileCounterStore::new(directory.path().join("virtual").display().to_string())
3987 .unwrap();
3988 let session = MobileMeshSession::new_with_virtual_time(local_identity, store)
3989 .await
3990 .unwrap();
3991 let started = Instant::now();
3992 let operation = session.ping(address(&silent_peer), 30_000).unwrap();
3993
3994 let deadline = started + Duration::from_secs(5);
3995 loop {
3996 let update = session.poll_update();
3997 for frame in update.outbound_frames {
3998 session.complete_outbound_frame(frame.id, true).unwrap();
3999 }
4000 if let Some(event) = update.ping_events.into_iter().next() {
4001 assert_eq!(event.operation_id, operation);
4002 assert_eq!(event.outcome, MobileMeshPingOutcome::TimedOut);
4003 break;
4004 }
4005 assert!(
4006 Instant::now() < deadline,
4007 "virtual-time ping timeout never fired"
4008 );
4009 std::thread::sleep(Duration::from_millis(2));
4010 }
4011 assert!(
4012 started.elapsed() < Duration::from_secs(5),
4013 "a 30s virtual timeout must not take real-time seconds"
4014 );
4015 }
4016
4017 #[tokio::test]
4018 async fn silent_peer_completes_with_timeout_event() {
4019 let directory = tempfile::tempdir().unwrap();
4020 let local_identity = identity(11);
4021 let silent_peer = identity(13);
4022 let store =
4023 MobileCounterStore::new(directory.path().join("local").display().to_string()).unwrap();
4024 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
4025 let operation = session.ping(address(&silent_peer), 100).unwrap();
4026 let deadline = Instant::now() + Duration::from_secs(2);
4027
4028 loop {
4029 let update = session.poll_update();
4030 for frame in update.outbound_frames {
4031 session.complete_outbound_frame(frame.id, true).unwrap();
4032 }
4033 if let Some(event) = update.ping_events.into_iter().next() {
4034 assert_eq!(event.operation_id, operation);
4035 assert_eq!(event.outcome, MobileMeshPingOutcome::TimedOut);
4036 assert_eq!(event.round_trip_milliseconds, None);
4037 assert_eq!(event.hop_count, None);
4038 assert!(event.route_hints.is_empty());
4039 assert_eq!(event.rssi_dbm, None);
4040 break;
4041 }
4042 assert!(Instant::now() < deadline, "silent ping never timed out");
4043 std::thread::sleep(Duration::from_millis(10));
4044 }
4045 }
4046
4047 struct TestWakeListener {
4048 signal: std_mpsc::Sender<()>,
4049 }
4050
4051 impl MobileMeshWakeListener for TestWakeListener {
4052 fn on_update_pending(&self) {
4053 let _ = self.signal.send(());
4054 }
4055 }
4056
4057 #[tokio::test]
4061 async fn wake_listener_fires_on_pending_data_and_rearms_after_drain() {
4062 let directory = tempfile::tempdir().unwrap();
4063 let local_identity = identity(63);
4064 let silent_peer = identity(64);
4065 let store =
4066 MobileCounterStore::new(directory.path().join("wake").display().to_string()).unwrap();
4067 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
4068 let (signal, wakes) = std_mpsc::channel();
4069 session.set_wake_listener(Arc::new(TestWakeListener { signal }));
4070
4071 let operation = session.ping(address(&silent_peer), 100).unwrap();
4073 wakes
4074 .recv_timeout(Duration::from_secs(5))
4075 .expect("no wake for the outbound ping frame");
4076
4077 let update = session.poll_update();
4078 assert!(
4079 !update.outbound_frames.is_empty(),
4080 "wake fired but nothing was pending"
4081 );
4082 for frame in update.outbound_frames {
4083 session.complete_outbound_frame(frame.id, true).unwrap();
4084 }
4085
4086 wakes
4089 .recv_timeout(Duration::from_secs(5))
4090 .expect("no wake for the ping timeout event");
4091 let deadline = Instant::now() + Duration::from_secs(2);
4092 loop {
4093 let update = session.poll_update();
4094 if let Some(event) = update.ping_events.into_iter().next() {
4095 assert_eq!(event.operation_id, operation);
4096 assert_eq!(event.outcome, MobileMeshPingOutcome::TimedOut);
4097 break;
4098 }
4099 assert!(Instant::now() < deadline, "timeout event never surfaced");
4100 std::thread::sleep(Duration::from_millis(5));
4101 }
4102 }
4103
4104 #[tokio::test]
4107 async fn wake_listener_registered_late_fires_for_already_pending_data() {
4108 let directory = tempfile::tempdir().unwrap();
4109 let local_identity = identity(65);
4110 let silent_peer = identity(66);
4111 let store =
4112 MobileCounterStore::new(directory.path().join("wake-late").display().to_string())
4113 .unwrap();
4114 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
4115
4116 session.ping(address(&silent_peer), 5_000).unwrap();
4117 std::thread::sleep(Duration::from_millis(200));
4121
4122 let (signal, wakes) = std_mpsc::channel();
4123 session.set_wake_listener(Arc::new(TestWakeListener { signal }));
4124 wakes
4125 .recv_timeout(Duration::from_secs(5))
4126 .expect("late-registered listener never fired");
4127 assert!(!session.poll_update().outbound_frames.is_empty());
4128 }
4129
4130 #[tokio::test]
4131 async fn chat_checkpoint_batch_gates_transmission_and_delivers_owned_mutation() {
4132 let directory = tempfile::tempdir().unwrap();
4133 let alice_identity = identity(21);
4134 let bob_identity = identity(22);
4135 let alice_root = directory.path().join("chat-alice");
4136 let alice_store = MobileCounterStore::new(alice_root.display().to_string()).unwrap();
4137 let bob_store =
4138 MobileCounterStore::new(directory.path().join("chat-bob").display().to_string())
4139 .unwrap();
4140 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
4141 .await
4142 .unwrap();
4143 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
4144 .await
4145 .unwrap();
4146 let alice_address = address(&alice_identity);
4147 alice
4148 .register_peers(vec![address(&bob_identity)])
4149 .await
4150 .unwrap();
4151 bob.register_peers(vec![alice_address.clone()])
4152 .await
4153 .unwrap();
4154
4155 let batch = alice
4156 .compose_text(address(&bob_identity), 77, "hello from Rust".to_owned())
4157 .await
4158 .unwrap();
4159 assert_eq!(
4160 batch.checkpoint.conversation_address,
4161 address(&bob_identity)
4162 );
4163 assert!(!batch.archives.is_empty());
4164 assert_eq!(batch.mutations.len(), 1);
4165 assert_eq!(batch.mutations[0].body.as_deref(), Some("hello from Rust"));
4166 assert_eq!(batch.mutations[0].fragment_count, Some(1));
4167 assert_eq!(
4168 alice
4169 .compose_text(address(&bob_identity), 78, "second".to_owned())
4170 .await,
4171 Err(MobileMeshError::OperationInProgress)
4172 );
4173 assert!(alice.poll_update().outbound_frames.is_empty());
4174 assert!(
4175 !alice_root.exists(),
4176 "compose alone must not touch counters"
4177 );
4178
4179 alice.commit_chat_batch(batch.batch_id).await.unwrap();
4180 assert!(alice_root.exists());
4181
4182 let deadline = Instant::now() + Duration::from_secs(10);
4185 loop {
4186 let alice_update = alice.poll_update();
4187 for frame in alice_update.outbound_frames {
4188 alice.complete_outbound_frame(frame.id, true).unwrap();
4189 bob.receive(MobileMeshRxRecord {
4190 data: frame.data,
4191 rssi_dbm: Some(-55),
4192 lqi: Some(200),
4193 snr_cb: Some(70),
4194 })
4195 .unwrap();
4196 }
4197 let bob_update = bob.poll_update();
4198 for frame in bob_update.outbound_frames.iter().cloned() {
4199 bob.complete_outbound_frame(frame.id, true).unwrap();
4200 alice
4201 .receive(MobileMeshRxRecord {
4202 data: frame.data,
4203 rssi_dbm: Some(-55),
4204 lqi: Some(200),
4205 snr_cb: Some(70),
4206 })
4207 .unwrap();
4208 }
4209 if let Some(mutation) = bob_update.chat_mutations.first() {
4210 assert_eq!(mutation.body.as_deref(), Some("hello from Rust"));
4211 assert_eq!(
4212 mutation.sender_address.as_deref(),
4213 Some(alice_address.as_str())
4214 );
4215 assert_eq!(
4216 mutation.direction,
4217 Some(crate::MobileChatDirection::Inbound)
4218 );
4219 let batch_id = bob_update.chat_batch_id.expect("owned chat batch");
4220 assert_eq!(
4221 bob.poll_update().chat_batch_id,
4222 Some(batch_id),
4223 "unacknowledged chat effects must be replayed"
4224 );
4225 bob.acknowledge_chat_batch(batch_id).unwrap();
4226 assert!(bob.poll_update().chat_mutations.is_empty());
4227 break;
4228 }
4229 assert!(Instant::now() < deadline, "chat frame did not arrive");
4230 std::thread::sleep(Duration::from_millis(5));
4231 }
4232 }
4233
4234 #[tokio::test]
4235 async fn fragmented_chat_message_crosses_mobile_radio_bridge() {
4236 let directory = tempfile::tempdir().unwrap();
4237 let alice_identity = identity(31);
4238 let bob_identity = identity(32);
4239 let alice_store =
4240 MobileCounterStore::new(directory.path().join("long-alice").display().to_string())
4241 .unwrap();
4242 let bob_store =
4243 MobileCounterStore::new(directory.path().join("long-bob").display().to_string())
4244 .unwrap();
4245 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
4246 .await
4247 .unwrap();
4248 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
4249 .await
4250 .unwrap();
4251 let alice_address = address(&alice_identity);
4252 let bob_address = address(&bob_identity);
4253 alice
4254 .register_peers(vec![bob_address.clone()])
4255 .await
4256 .unwrap();
4257 bob.register_peers(vec![alice_address.clone()])
4258 .await
4259 .unwrap();
4260
4261 let body = "fragmented mobile message ".repeat(16);
4262 let batch = alice
4263 .compose_text(bob_address, 91, body.clone())
4264 .await
4265 .unwrap();
4266 let fragment_count = usize::from(batch.mutations[0].fragment_count.unwrap_or(1));
4267 assert!(fragment_count > 1);
4268 alice.commit_chat_batch(batch.batch_id).await.unwrap();
4269
4270 let deadline = Instant::now() + Duration::from_secs(10);
4273 let mut outbound_lengths = Vec::new();
4274 let mut return_lengths = Vec::new();
4275 let mut receiver_complete = false;
4276 let mut sender_delivered = false;
4277 loop {
4278 let alice_update = alice.poll_update();
4279 let alice_frames = alice_update.outbound_frames;
4280 assert!(
4281 alice_frames.len() <= 1,
4282 "the mobile bridge must wait for physical TX completion"
4283 );
4284 for frame in alice_frames {
4285 outbound_lengths.push(frame.data.len());
4286 alice.complete_outbound_frame(frame.id, true).unwrap();
4287 bob.receive(MobileMeshRxRecord {
4288 data: frame.data,
4289 rssi_dbm: Some(-55),
4290 lqi: Some(200),
4291 snr_cb: Some(70),
4292 })
4293 .unwrap();
4294 }
4295 sender_delivered |= alice_update
4296 .chat_deliveries
4297 .iter()
4298 .any(|delivery| delivery.state == MobileChatDeliveryState::Acknowledged);
4299 if let Some(batch_id) = alice_update.chat_batch_id {
4300 alice.acknowledge_chat_batch(batch_id).unwrap();
4301 }
4302 let bob_update = bob.poll_update();
4303 for frame in bob_update.outbound_frames.iter().cloned() {
4304 return_lengths.push(frame.data.len());
4305 bob.complete_outbound_frame(frame.id, true).unwrap();
4306 alice
4307 .receive(MobileMeshRxRecord {
4308 data: frame.data,
4309 rssi_dbm: Some(-55),
4310 lqi: Some(200),
4311 snr_cb: Some(70),
4312 })
4313 .unwrap();
4314 }
4315 if let Some(mutation) = bob_update
4316 .chat_mutations
4317 .iter()
4318 .find(|mutation| mutation.complete == Some(true))
4319 {
4320 assert_eq!(mutation.body.as_deref(), Some(body.as_str()));
4321 receiver_complete = true;
4322 }
4323 if let Some(batch_id) = bob_update.chat_batch_id {
4324 bob.acknowledge_chat_batch(batch_id).unwrap();
4325 }
4326 if receiver_complete && sender_delivered {
4327 assert!(
4328 outbound_lengths.len() <= fragment_count * 2 + 4,
4329 "fragment delivery was unexpectedly amplified: {outbound_lengths:?}"
4330 );
4331 break;
4332 }
4333 assert!(
4334 Instant::now() < deadline,
4335 "fragmented chat did not complete at both endpoints; receiver_complete={receiver_complete}, sender_delivered={sender_delivered}, outbound lengths: {outbound_lengths:?}; return lengths: {return_lengths:?}"
4336 );
4337 std::thread::sleep(Duration::from_millis(5));
4338 }
4339 }
4340
4341 #[tokio::test]
4342 async fn ulcp_link_failure_terminates_pending_chat_delivery() {
4343 let directory = tempfile::tempdir().unwrap();
4344 let local_identity = identity(41);
4345 let peer_identity = identity(42);
4346 let store =
4347 MobileCounterStore::new(directory.path().join("failed-send").display().to_string())
4348 .unwrap();
4349 let session = MobileMeshSession::new(local_identity, store).await.unwrap();
4350 session
4351 .register_peers(vec![address(&peer_identity)])
4352 .await
4353 .unwrap();
4354 let batch = session
4355 .compose_text(address(&peer_identity), 17, "will fail".into())
4356 .await
4357 .unwrap();
4358 session.commit_chat_batch(batch.batch_id).await.unwrap();
4359 session.fail_outbound_transmissions().unwrap();
4360
4361 let deadline = Instant::now() + Duration::from_secs(2);
4362 loop {
4363 let update = session.poll_update();
4364 if update
4365 .chat_deliveries
4366 .iter()
4367 .any(|delivery| delivery.state == MobileChatDeliveryState::Failed)
4368 {
4369 break;
4370 }
4371 if let Some(batch_id) = update.chat_batch_id {
4372 session.acknowledge_chat_batch(batch_id).unwrap();
4373 }
4374 assert!(
4375 Instant::now() < deadline,
4376 "link failure did not terminate chat delivery"
4377 );
4378 std::thread::sleep(Duration::from_millis(5));
4379 }
4380 }
4381
4382 #[tokio::test]
4388 async fn mid_batch_failure_suppresses_fragments_queued_behind_the_blocked_one() {
4389 let directory = tempfile::tempdir().unwrap();
4390 let alice_identity = identity(51);
4391 let bob_identity = identity(52);
4392 let alice_store =
4393 MobileCounterStore::new(directory.path().join("mid-alice").display().to_string())
4394 .unwrap();
4395 let bob_store =
4396 MobileCounterStore::new(directory.path().join("mid-bob").display().to_string())
4397 .unwrap();
4398 let alice = MobileMeshSession::new(alice_identity.clone(), alice_store)
4399 .await
4400 .unwrap();
4401 let bob = MobileMeshSession::new(bob_identity.clone(), bob_store)
4402 .await
4403 .unwrap();
4404 let bob_address = address(&bob_identity);
4405 alice
4406 .register_peers(vec![bob_address.clone()])
4407 .await
4408 .unwrap();
4409 bob.register_peers(vec![address(&alice_identity)])
4410 .await
4411 .unwrap();
4412
4413 let warmup = alice
4416 .compose_text(bob_address.clone(), 1, "warmup".to_owned())
4417 .await
4418 .unwrap();
4419 alice.commit_chat_batch(warmup.batch_id).await.unwrap();
4420 let deadline = Instant::now() + Duration::from_secs(10);
4421 loop {
4422 let alice_update = alice.poll_update();
4423 for frame in alice_update.outbound_frames {
4424 alice.complete_outbound_frame(frame.id, true).unwrap();
4425 bob.receive(MobileMeshRxRecord {
4426 data: frame.data,
4427 rssi_dbm: Some(-50),
4428 lqi: None,
4429 snr_cb: Some(80),
4430 })
4431 .unwrap();
4432 }
4433 let acked = alice_update
4434 .chat_deliveries
4435 .iter()
4436 .any(|delivery| delivery.state == MobileChatDeliveryState::Acknowledged);
4437 if let Some(batch_id) = alice_update.chat_batch_id {
4438 alice.acknowledge_chat_batch(batch_id).unwrap();
4439 }
4440 let bob_update = bob.poll_update();
4441 for frame in bob_update.outbound_frames.iter().cloned() {
4442 bob.complete_outbound_frame(frame.id, true).unwrap();
4443 alice
4444 .receive(MobileMeshRxRecord {
4445 data: frame.data,
4446 rssi_dbm: Some(-50),
4447 lqi: None,
4448 snr_cb: Some(80),
4449 })
4450 .unwrap();
4451 }
4452 if let Some(batch_id) = bob_update.chat_batch_id {
4453 bob.acknowledge_chat_batch(batch_id).unwrap();
4454 }
4455 if acked {
4456 break;
4457 }
4458 assert!(Instant::now() < deadline, "warmup exchange never acked");
4459 std::thread::sleep(Duration::from_millis(5));
4460 }
4461
4462 let body = "storm test payload ".repeat(24);
4465 let batch = alice
4466 .compose_text(bob_address, 2, body.clone())
4467 .await
4468 .unwrap();
4469 assert!(batch.mutations[0].fragment_count.unwrap_or(1) > 1);
4470 alice.commit_chat_batch(batch.batch_id).await.unwrap();
4471
4472 let deadline = Instant::now() + Duration::from_secs(10);
4476 loop {
4477 let update = alice.poll_update();
4478 if let Some(batch_id) = update.chat_batch_id {
4479 alice.acknowledge_chat_batch(batch_id).unwrap();
4480 }
4481 if !update.outbound_frames.is_empty() {
4482 break;
4483 }
4484 assert!(
4485 Instant::now() < deadline,
4486 "first fragment never reached the platform"
4487 );
4488 std::thread::sleep(Duration::from_millis(5));
4489 }
4490 let fail_at = Instant::now();
4491 alice.fail_outbound_transmissions().unwrap();
4492
4493 let mut saw_failed = false;
4497 let quiet_deadline = fail_at + Duration::from_millis(1_000);
4498 while Instant::now() < quiet_deadline {
4499 let update = alice.poll_update();
4500 assert!(
4501 update.outbound_frames.is_empty(),
4502 "fragments queued behind a failed batch were still dispatched"
4503 );
4504 saw_failed |= update
4505 .chat_deliveries
4506 .iter()
4507 .any(|delivery| delivery.state == MobileChatDeliveryState::Failed);
4508 if let Some(batch_id) = update.chat_batch_id {
4509 alice.acknowledge_chat_batch(batch_id).unwrap();
4510 }
4511 std::thread::sleep(Duration::from_millis(10));
4512 }
4513 assert!(saw_failed, "batch failure never reported to the transcript");
4514
4515 let retry = alice
4517 .compose_text(address(&bob_identity), 3, "after failure".to_owned())
4518 .await
4519 .unwrap();
4520 alice.commit_chat_batch(retry.batch_id).await.unwrap();
4521 let deadline = Instant::now() + Duration::from_secs(10);
4522 loop {
4523 let update = alice.poll_update();
4524 if let Some(batch_id) = update.chat_batch_id {
4525 alice.acknowledge_chat_batch(batch_id).unwrap();
4526 }
4527 if !update.outbound_frames.is_empty() {
4528 break;
4529 }
4530 assert!(
4531 Instant::now() < deadline,
4532 "transmissions never resumed after failure recovery"
4533 );
4534 std::thread::sleep(Duration::from_millis(5));
4535 }
4536 }
4537
4538 #[tokio::test]
4545 async fn a_channel_group_message_crosses_two_sessions() {
4546 let directory = tempfile::tempdir().unwrap();
4547 let alice_identity = identity(61);
4548 let bob_identity = identity(62);
4549 let alice = MobileMeshSession::new(
4550 alice_identity.clone(),
4551 MobileCounterStore::new(directory.path().join("ch-alice").display().to_string())
4552 .unwrap(),
4553 )
4554 .await
4555 .unwrap();
4556 let bob = MobileMeshSession::new(
4557 bob_identity.clone(),
4558 MobileCounterStore::new(directory.path().join("ch-bob").display().to_string()).unwrap(),
4559 )
4560 .await
4561 .unwrap();
4562
4563 let key = vec![0x5Cu8; 32];
4564 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
4565 assert!(conversation.starts_with("ch:"));
4566 alice.register_channels(vec![key.clone()]).await.unwrap();
4567 bob.register_channels(vec![key]).await.unwrap();
4568
4569 let batch = alice
4570 .compose_text(conversation.clone(), 1, "regroup at the ridge".to_owned())
4571 .await
4572 .unwrap();
4573 assert_eq!(batch.checkpoint.conversation_address, conversation);
4574 alice.commit_chat_batch(batch.batch_id).await.unwrap();
4575
4576 let mut alice_states = Vec::new();
4577 let mut received: Option<MobileChatMutationRecord> = None;
4578 let mut resolution: Option<MobileChatSenderResolutionRecord> = None;
4579 let deadline = Instant::now() + Duration::from_secs(10);
4582 while received.is_none()
4583 || resolution.is_none()
4584 || !alice_states.contains(&MobileChatDeliveryState::Sent)
4585 {
4586 let alice_update = alice.poll_update();
4587 for frame in alice_update.outbound_frames {
4588 alice.complete_outbound_frame(frame.id, true).unwrap();
4589 bob.receive(MobileMeshRxRecord {
4590 data: frame.data,
4591 rssi_dbm: Some(-70),
4592 lqi: None,
4593 snr_cb: Some(60),
4594 })
4595 .unwrap();
4596 }
4597 alice_states.extend(
4598 alice_update
4599 .chat_deliveries
4600 .iter()
4601 .map(|delivery| delivery.state),
4602 );
4603 if let Some(batch_id) = alice_update.chat_batch_id {
4604 alice.acknowledge_chat_batch(batch_id).unwrap();
4605 }
4606
4607 let bob_update = bob.poll_update();
4608 for frame in bob_update.outbound_frames {
4609 bob.complete_outbound_frame(frame.id, true).unwrap();
4610 }
4611 if let Some(record) = bob_update
4612 .chat_mutations
4613 .iter()
4614 .find(|mutation| mutation.body.as_deref() == Some("regroup at the ridge"))
4615 {
4616 received = Some(record.clone());
4617 }
4618 if let Some(record) = bob_update.chat_sender_resolutions.first() {
4619 resolution = Some(record.clone());
4620 }
4621 if let Some(batch_id) = bob_update.chat_batch_id {
4622 bob.acknowledge_chat_batch(batch_id).unwrap();
4623 }
4624 assert!(
4625 Instant::now() < deadline,
4626 "group message incomplete (mutation: {}, resolution: {}, states: {alice_states:?})",
4627 received.is_some(),
4628 resolution.is_some()
4629 );
4630 std::thread::sleep(Duration::from_millis(5));
4631 }
4632
4633 let received = received.unwrap();
4634 assert_eq!(
4635 received.conversation_address.as_deref(),
4636 Some(&conversation[..])
4637 );
4638 assert_eq!(received.direction, Some(MobileChatDirection::Inbound));
4639 assert_eq!(
4642 received.sender_hint.as_deref(),
4643 Some(&hint_of(&alice_identity)[..])
4644 );
4645 assert_eq!(
4646 received.sender_address.as_deref(),
4647 Some(&address(&alice_identity)[..])
4648 );
4649 let rx = received
4650 .rx
4651 .expect("a received frame carries radio metadata");
4652 assert_eq!(rx.rssi_dbm, Some(-70));
4653 assert_eq!(rx.snr_centibels, Some(60));
4654 assert_eq!(rx.hop_count, Some(0));
4657
4658 let resolution = resolution.unwrap();
4659 assert_eq!(resolution.conversation_address, conversation);
4660 assert_eq!(resolution.sender_hint, hint_of(&alice_identity));
4661 assert_eq!(resolution.sender_address, address(&alice_identity));
4662
4663 assert!(alice_states.contains(&MobileChatDeliveryState::Sent));
4665 assert!(!alice_states.contains(&MobileChatDeliveryState::Acknowledged));
4666 }
4667
4668 #[tokio::test]
4678 async fn emergency_chat_is_sent_readable_and_encrypted_copies_are_refused() {
4679 use umsh_core::{MicSize, PacketBuilder, PacketHeader};
4680 use umsh_crypto::{
4681 CryptoEngine, PairwiseKeys,
4682 software::{SoftwareAes, SoftwareSha256},
4683 };
4684
4685 let directory = tempfile::tempdir().unwrap();
4686 let alice_identity = identity(71);
4687 let alice = MobileMeshSession::new(
4688 alice_identity.clone(),
4689 MobileCounterStore::new(directory.path().join("sos-alice").display().to_string())
4690 .unwrap(),
4691 )
4692 .await
4693 .unwrap();
4694 let bob = MobileMeshSession::new(
4695 identity(72),
4696 MobileCounterStore::new(directory.path().join("sos-bob").display().to_string())
4697 .unwrap(),
4698 )
4699 .await
4700 .unwrap();
4701
4702 let key = crate::inspect_channel_name(crate::EMERGENCY_CHANNEL_NAME.to_owned())
4703 .unwrap()
4704 .key;
4705 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
4706 alice.register_channels(vec![key.clone()]).await.unwrap();
4707 bob.register_channels(vec![key.clone()]).await.unwrap();
4708
4709 let body = "tower down at mile 14";
4710 let batch = alice
4711 .compose_text(conversation.clone(), 1, body.to_owned())
4712 .await
4713 .unwrap();
4714 alice.commit_chat_batch(batch.batch_id).await.unwrap();
4715
4716 let engine = CryptoEngine::new(SoftwareAes, SoftwareSha256);
4720 let channel_keys =
4721 engine.derive_channel_keys(&crate::channel_key_from_bytes(&key).unwrap());
4722 let deadline = Instant::now() + Duration::from_secs(10);
4723 let mut frame = None;
4724 while frame.is_none() {
4725 for outbound in alice.poll_update().outbound_frames {
4726 alice.complete_outbound_frame(outbound.id, true).unwrap();
4727 let header = match PacketHeader::parse(&outbound.data) {
4728 Ok(header) => header,
4729 Err(_) => continue,
4730 };
4731 if header.channel == Some(channel_keys.channel_id)
4732 && header.packet_type() == umsh_core::PacketType::Multicast
4733 {
4734 frame = Some((outbound.data, header));
4735 }
4736 }
4737 assert!(Instant::now() < deadline, "no emergency frame was sent");
4738 std::thread::sleep(Duration::from_millis(5));
4739 }
4740 let (frame, header) = frame.unwrap();
4741
4742 let sec_info = header.sec_info.expect("a multicast frame carries SECINFO");
4744 assert!(
4745 !sec_info.scf.encrypted(),
4746 "emergency chat must be readable by any node in range"
4747 );
4748 assert!(
4749 matches!(header.source, umsh_core::SourceAddrRef::FullKeyAt { .. }),
4750 "emergency chat must name its sender outright"
4751 );
4752
4753 let payload = {
4759 let mut opened = frame.clone();
4760 let range = engine
4761 .open_packet(
4762 &mut opened,
4763 &header,
4764 &PairwiseKeys {
4765 k_enc: channel_keys.k_enc,
4766 k_mic: channel_keys.k_mic,
4767 },
4768 )
4769 .unwrap();
4770 opened[range].to_vec()
4771 };
4772 assert!(
4773 sec_info.frame_counter > 0,
4774 "the forged copy needs a lower counter than the genuine one"
4775 );
4776 let alice_key = decode_peer(&address(&alice_identity)).unwrap();
4777 let mut buf = [0u8; 256];
4778 let mut forged = PacketBuilder::new(&mut buf)
4779 .multicast(channel_keys.channel_id)
4780 .source_full(&alice_key)
4781 .frame_counter(sec_info.frame_counter - 1)
4782 .encrypted()
4783 .mic_size(MicSize::Mic16)
4784 .payload(&payload)
4785 .build()
4786 .unwrap();
4787 engine
4788 .seal_packet(
4789 &mut forged,
4790 &PairwiseKeys {
4791 k_enc: channel_keys.k_enc,
4792 k_mic: channel_keys.k_mic,
4793 },
4794 )
4795 .unwrap();
4796 bob.receive(MobileMeshRxRecord {
4797 data: forged.as_bytes().to_vec(),
4798 rssi_dbm: Some(-70),
4799 lqi: None,
4800 snr_cb: Some(60),
4801 })
4802 .unwrap();
4803
4804 let mut refusal = None;
4805 let deadline = Instant::now() + Duration::from_secs(10);
4806 while refusal.is_none() {
4807 let update = bob.poll_update();
4808 assert!(
4809 !update
4810 .chat_mutations
4811 .iter()
4812 .any(|mutation| mutation.body.as_deref() == Some(body)),
4813 "an encrypted emergency frame reached the transcript"
4814 );
4815 refusal = update
4816 .chat_diagnostics
4817 .iter()
4818 .find(|line| line.contains("emergency-channel"))
4819 .cloned();
4820 if let Some(batch_id) = update.chat_batch_id {
4821 bob.acknowledge_chat_batch(batch_id).unwrap();
4822 }
4823 assert!(
4824 Instant::now() < deadline,
4825 "the encrypted copy was not refused"
4826 );
4827 std::thread::sleep(Duration::from_millis(5));
4828 }
4829 assert!(refusal.unwrap().contains("encrypted"));
4830
4831 bob.receive(MobileMeshRxRecord {
4833 data: frame,
4834 rssi_dbm: Some(-70),
4835 lqi: None,
4836 snr_cb: Some(60),
4837 })
4838 .unwrap();
4839 let deadline = Instant::now() + Duration::from_secs(10);
4840 let mut received = false;
4841 while !received {
4842 let update = bob.poll_update();
4843 received = update
4844 .chat_mutations
4845 .iter()
4846 .any(|mutation| mutation.body.as_deref() == Some(body));
4847 if let Some(batch_id) = update.chat_batch_id {
4848 bob.acknowledge_chat_batch(batch_id).unwrap();
4849 }
4850 assert!(Instant::now() < deadline, "the readable copy never arrived");
4851 std::thread::sleep(Duration::from_millis(5));
4852 }
4853 }
4854
4855 #[tokio::test]
4876 async fn a_dropped_emergency_fragment_is_repaired() {
4877 let directory = tempfile::tempdir().unwrap();
4878 let alice = MobileMeshSession::new(
4879 identity(73),
4880 MobileCounterStore::new(directory.path().join("sos-frag-a").display().to_string())
4881 .unwrap(),
4882 )
4883 .await
4884 .unwrap();
4885 let bob = MobileMeshSession::new(
4886 identity(74),
4887 MobileCounterStore::new(directory.path().join("sos-frag-b").display().to_string())
4888 .unwrap(),
4889 )
4890 .await
4891 .unwrap();
4892
4893 let key = crate::inspect_channel_name(crate::EMERGENCY_CHANNEL_NAME.to_owned())
4894 .unwrap()
4895 .key;
4896 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
4897 alice.register_channels(vec![key.clone()]).await.unwrap();
4898 bob.register_channels(vec![key]).await.unwrap();
4899
4900 let body: String = (0..600)
4901 .map(|index| char::from(b'a' + (index % 26) as u8))
4902 .collect();
4903 let batch = alice
4904 .compose_text(conversation.clone(), 1, body.clone())
4905 .await
4906 .unwrap();
4907 assert!(batch.mutations[0].fragment_count.unwrap() > 1);
4908 let archives: std::collections::HashMap<(u8, Option<u8>), Vec<u8>> = batch
4911 .archives
4912 .iter()
4913 .map(|archive| {
4914 (
4915 (archive.message_id, archive.fragment_index),
4916 archive.payload.clone(),
4917 )
4918 })
4919 .collect();
4920 alice.commit_chat_batch(batch.batch_id).await.unwrap();
4921
4922 let mut sent = 0;
4923 let mut repairs = 0;
4924 let mut assembled: Option<String> = None;
4925 let deadline = Instant::now() + Duration::from_secs(40);
4926 while assembled.as_deref() != Some(body.as_str()) {
4927 let alice_update = alice.poll_update();
4928 for frame in alice_update.outbound_frames {
4929 alice.complete_outbound_frame(frame.id, true).unwrap();
4930 sent += 1;
4931 if sent == 2 {
4934 continue;
4935 }
4936 bob.receive(MobileMeshRxRecord {
4937 data: frame.data,
4938 rssi_dbm: Some(-70),
4939 lqi: None,
4940 snr_cb: Some(60),
4941 })
4942 .unwrap();
4943 }
4944 for lookup in &alice_update.chat_archive_lookups {
4945 match archives.get(&(lookup.message_id, lookup.fragment_index)) {
4946 Some(payload) => alice
4947 .apply_chat_archive_result(
4948 lookup.request_id,
4949 MobileChatArchiveResultKind::Found,
4950 payload.clone(),
4951 )
4952 .unwrap(),
4953 None => alice
4954 .apply_chat_archive_result(
4955 lookup.request_id,
4956 MobileChatArchiveResultKind::Unknown,
4957 Vec::new(),
4958 )
4959 .unwrap(),
4960 }
4961 }
4962 if let Some(batch_id) = alice_update.chat_batch_id {
4963 alice.acknowledge_chat_batch(batch_id).unwrap();
4964 }
4965
4966 let bob_update = bob.poll_update();
4967 for frame in bob_update.outbound_frames {
4968 bob.complete_outbound_frame(frame.id, true).unwrap();
4969 repairs += 1;
4970 alice
4971 .receive(MobileMeshRxRecord {
4972 data: frame.data,
4973 rssi_dbm: Some(-70),
4974 lqi: None,
4975 snr_cb: Some(60),
4976 })
4977 .unwrap();
4978 }
4979 for mutation in &bob_update.chat_mutations {
4980 if let Some(text) = mutation.body.as_deref() {
4981 assembled = Some(text.to_owned());
4982 }
4983 }
4984 if let Some(batch_id) = bob_update.chat_batch_id {
4985 bob.acknowledge_chat_batch(batch_id).unwrap();
4986 }
4987 assert!(
4988 Instant::now() < deadline,
4989 "a dropped emergency fragment was never repaired \
4990 ({repairs} repair frame(s), assembled {:?})",
4991 assembled.as_ref().map(|text| text.len())
4992 );
4993 std::thread::sleep(Duration::from_millis(5));
4994 }
4995 assert!(repairs > 0, "the message assembled without any repair");
4996 }
4997
4998 #[tokio::test]
5006 async fn a_relayed_copy_of_our_own_group_message_is_not_transcribed() {
5007 let directory = tempfile::tempdir().unwrap();
5008 let identity = identity(67);
5009 let session = MobileMeshSession::new(
5010 identity.clone(),
5011 MobileCounterStore::new(directory.path().join("echo").display().to_string()).unwrap(),
5012 )
5013 .await
5014 .unwrap();
5015
5016 let key = vec![0x3Eu8; 32];
5017 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
5018 session.register_channels(vec![key]).await.unwrap();
5019
5020 let batch = session
5021 .compose_text(conversation.clone(), 1, "anyone out there".to_owned())
5022 .await
5023 .unwrap();
5024 session.commit_chat_batch(batch.batch_id).await.unwrap();
5025
5026 let deadline = Instant::now() + Duration::from_secs(10);
5029 let mut echoed = 0;
5030 let mut inbound = Vec::new();
5031 while echoed == 0
5032 || Instant::now() < deadline.min(Instant::now() + Duration::from_millis(1))
5033 {
5034 let update = session.poll_update();
5035 for frame in update.outbound_frames {
5036 session.complete_outbound_frame(frame.id, true).unwrap();
5037 session
5038 .receive(MobileMeshRxRecord {
5039 data: frame.data,
5040 rssi_dbm: Some(-60),
5041 lqi: None,
5042 snr_cb: Some(70),
5043 })
5044 .unwrap();
5045 echoed += 1;
5046 }
5047 inbound.extend(
5048 update
5049 .chat_mutations
5050 .iter()
5051 .filter(|mutation| mutation.direction == Some(MobileChatDirection::Inbound))
5052 .cloned(),
5053 );
5054 if let Some(batch_id) = update.chat_batch_id {
5055 session.acknowledge_chat_batch(batch_id).unwrap();
5056 }
5057 if echoed > 0 && Instant::now() > deadline {
5058 break;
5059 }
5060 std::thread::sleep(Duration::from_millis(5));
5061 if echoed > 0 {
5062 for _ in 0..20 {
5064 let update = session.poll_update();
5065 inbound.extend(
5066 update
5067 .chat_mutations
5068 .iter()
5069 .filter(|mutation| {
5070 mutation.direction == Some(MobileChatDirection::Inbound)
5071 })
5072 .cloned(),
5073 );
5074 if let Some(batch_id) = update.chat_batch_id {
5075 session.acknowledge_chat_batch(batch_id).unwrap();
5076 }
5077 std::thread::sleep(Duration::from_millis(5));
5078 }
5079 break;
5080 }
5081 }
5082
5083 assert!(echoed > 0, "the session never transmitted the message");
5084 assert!(
5085 inbound.is_empty(),
5086 "our own relayed message was transcribed as inbound: {inbound:?}"
5087 );
5088 }
5089
5090 #[tokio::test]
5094 async fn composing_to_an_unheld_channel_is_refused() {
5095 let directory = tempfile::tempdir().unwrap();
5096 let session = MobileMeshSession::new(
5097 identity(63),
5098 MobileCounterStore::new(directory.path().join("unheld").display().to_string()).unwrap(),
5099 )
5100 .await
5101 .unwrap();
5102
5103 let key = vec![0x77u8; 32];
5104 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
5105 assert_eq!(
5106 session
5107 .compose_text(conversation.clone(), 1, "hello".to_owned())
5108 .await,
5109 Err(MobileMeshError::UnknownConversation)
5110 );
5111
5112 session.register_channels(vec![key.clone()]).await.unwrap();
5113 let batch = session
5114 .compose_text(conversation.clone(), 2, "hello".to_owned())
5115 .await
5116 .expect("a joined channel composes");
5117 session
5120 .reject_chat_batch(batch.batch_id, Vec::new())
5121 .await
5122 .unwrap();
5123
5124 session.remove_channels(vec![key]).await.unwrap();
5125 assert_eq!(
5126 session
5127 .compose_text(conversation, 3, "hello".to_owned())
5128 .await,
5129 Err(MobileMeshError::UnknownConversation)
5130 );
5131 }
5132
5133 #[tokio::test]
5136 async fn a_malformed_conversation_address_is_refused() {
5137 let directory = tempfile::tempdir().unwrap();
5138 let session = MobileMeshSession::new(
5139 identity(64),
5140 MobileCounterStore::new(directory.path().join("malformed").display().to_string())
5141 .unwrap(),
5142 )
5143 .await
5144 .unwrap();
5145 for address in ["ch:not-hex", "ch:0011", "definitely not base58 !!"] {
5146 assert_eq!(
5147 session
5148 .compose_text(address.to_owned(), 1, "hello".to_owned())
5149 .await,
5150 Err(MobileMeshError::UnknownConversation),
5151 "{address} should not resolve to a conversation"
5152 );
5153 }
5154 }
5155
5156 #[tokio::test]
5159 async fn direct_chat_still_delivers_and_now_carries_radio_metadata() {
5160 let directory = tempfile::tempdir().unwrap();
5161 let alice_identity = identity(65);
5162 let bob_identity = identity(66);
5163 let alice = MobileMeshSession::new(
5164 alice_identity.clone(),
5165 MobileCounterStore::new(directory.path().join("dm-alice").display().to_string())
5166 .unwrap(),
5167 )
5168 .await
5169 .unwrap();
5170 let bob = MobileMeshSession::new(
5171 bob_identity.clone(),
5172 MobileCounterStore::new(directory.path().join("dm-bob").display().to_string()).unwrap(),
5173 )
5174 .await
5175 .unwrap();
5176 let bob_address = address(&bob_identity);
5177 alice
5178 .register_peers(vec![bob_address.clone()])
5179 .await
5180 .unwrap();
5181 bob.register_peers(vec![address(&alice_identity)])
5182 .await
5183 .unwrap();
5184
5185 let batch = alice
5186 .compose_text(bob_address.clone(), 1, "still here".to_owned())
5187 .await
5188 .unwrap();
5189 assert_eq!(batch.checkpoint.conversation_address, bob_address);
5190 alice.commit_chat_batch(batch.batch_id).await.unwrap();
5191
5192 let deadline = Instant::now() + Duration::from_secs(10);
5193 let received = loop {
5194 let alice_update = alice.poll_update();
5195 for frame in alice_update.outbound_frames {
5196 alice.complete_outbound_frame(frame.id, true).unwrap();
5197 bob.receive(MobileMeshRxRecord {
5198 data: frame.data,
5199 rssi_dbm: Some(-55),
5200 lqi: None,
5201 snr_cb: Some(75),
5202 })
5203 .unwrap();
5204 }
5205 if let Some(batch_id) = alice_update.chat_batch_id {
5206 alice.acknowledge_chat_batch(batch_id).unwrap();
5207 }
5208 let bob_update = bob.poll_update();
5209 for frame in bob_update.outbound_frames {
5210 bob.complete_outbound_frame(frame.id, true).unwrap();
5211 alice
5212 .receive(MobileMeshRxRecord {
5213 data: frame.data,
5214 rssi_dbm: Some(-55),
5215 lqi: None,
5216 snr_cb: Some(75),
5217 })
5218 .unwrap();
5219 }
5220 let found = bob_update
5221 .chat_mutations
5222 .iter()
5223 .find(|mutation| mutation.body.as_deref() == Some("still here"))
5224 .cloned();
5225 if let Some(batch_id) = bob_update.chat_batch_id {
5226 bob.acknowledge_chat_batch(batch_id).unwrap();
5227 }
5228 if let Some(found) = found {
5229 break found;
5230 }
5231 assert!(
5232 Instant::now() < deadline,
5233 "the direct message never arrived"
5234 );
5235 std::thread::sleep(Duration::from_millis(5));
5236 };
5237
5238 assert_eq!(
5239 received.conversation_address.as_deref(),
5240 Some(&address(&alice_identity)[..])
5241 );
5242 assert_eq!(received.sender_hint, None);
5245 assert_eq!(
5246 received.sender_address.as_deref(),
5247 Some(&address(&alice_identity)[..])
5248 );
5249 let rx = received
5250 .rx
5251 .expect("a received frame carries radio metadata");
5252 assert_eq!(rx.rssi_dbm, Some(-55));
5253 assert_eq!(rx.snr_centibels, Some(75));
5254 assert!(rx.source_authenticated);
5255 }
5256
5257 fn assert_batch_id_matches_events(update: &MobileMeshSessionUpdateRecord) {
5265 let has_events = !update.chat_mutations.is_empty()
5266 || !update.chat_deliveries.is_empty()
5267 || !update.chat_archive_lookups.is_empty()
5268 || !update.chat_sender_resolutions.is_empty()
5269 || !update.chat_diagnostics.is_empty();
5270 assert_eq!(
5271 update.chat_batch_id.is_some(),
5272 has_events,
5273 "batch id {:?} disagrees with the batch's contents",
5274 update.chat_batch_id
5275 );
5276 }
5277
5278 #[tokio::test]
5284 async fn a_fragmented_channel_group_message_arrives_whole() {
5285 let directory = tempfile::tempdir().unwrap();
5286 let alice_identity = identity(71);
5287 let bob_identity = identity(72);
5288 let alice = MobileMeshSession::new(
5289 alice_identity.clone(),
5290 MobileCounterStore::new(directory.path().join("frag-alice").display().to_string())
5291 .unwrap(),
5292 )
5293 .await
5294 .unwrap();
5295 let bob = MobileMeshSession::new(
5296 bob_identity.clone(),
5297 MobileCounterStore::new(directory.path().join("frag-bob").display().to_string())
5298 .unwrap(),
5299 )
5300 .await
5301 .unwrap();
5302
5303 let key = vec![0x9Au8; 32];
5304 let conversation = crate::channel_conversation_address(key.clone()).unwrap();
5305 alice.register_channels(vec![key.clone()]).await.unwrap();
5306 bob.register_channels(vec![key]).await.unwrap();
5307
5308 let body: String = (0..600)
5310 .map(|index| char::from(b'a' + (index % 26) as u8))
5311 .collect();
5312 let batch = alice
5313 .compose_text(conversation.clone(), 1, body.clone())
5314 .await
5315 .unwrap();
5316 let fragments = batch.mutations[0].fragment_count.unwrap();
5317 assert!(
5318 fragments > 1,
5319 "the test body must fragment, got {fragments}"
5320 );
5321 alice.commit_chat_batch(batch.batch_id).await.unwrap();
5322
5323 let mut transmitted = 0;
5324 let mut repairs = 0;
5325 let mut assembled: Option<String> = None;
5326 let deadline = Instant::now() + Duration::from_secs(15);
5327 while assembled.as_deref() != Some(body.as_str()) {
5328 let alice_update = alice.poll_update();
5329 assert_batch_id_matches_events(&alice_update);
5330 for frame in alice_update.outbound_frames {
5331 alice.complete_outbound_frame(frame.id, true).unwrap();
5332 transmitted += 1;
5333 bob.receive(MobileMeshRxRecord {
5334 data: frame.data,
5335 rssi_dbm: Some(-70),
5336 lqi: None,
5337 snr_cb: Some(60),
5338 })
5339 .unwrap();
5340 }
5341 if let Some(batch_id) = alice_update.chat_batch_id {
5342 alice.acknowledge_chat_batch(batch_id).unwrap();
5343 }
5344
5345 let bob_update = bob.poll_update();
5346 assert_batch_id_matches_events(&bob_update);
5347 for frame in bob_update.outbound_frames {
5348 bob.complete_outbound_frame(frame.id, true).unwrap();
5349 repairs += 1;
5352 alice
5353 .receive(MobileMeshRxRecord {
5354 data: frame.data,
5355 rssi_dbm: Some(-70),
5356 lqi: None,
5357 snr_cb: Some(60),
5358 })
5359 .unwrap();
5360 }
5361 for mutation in &bob_update.chat_mutations {
5362 if let Some(text) = mutation.body.as_deref() {
5363 assembled = Some(text.to_owned());
5364 }
5365 }
5366 if let Some(batch_id) = bob_update.chat_batch_id {
5367 bob.acknowledge_chat_batch(batch_id).unwrap();
5368 }
5369 assert!(
5370 Instant::now() < deadline,
5371 "fragmented group message never completed \
5372 ({transmitted} frame(s) transmitted of {fragments}, \
5373 assembled {:?})",
5374 assembled.as_ref().map(|text| text.len())
5375 );
5376 std::thread::sleep(Duration::from_millis(5));
5377 }
5378
5379 assert!(
5385 transmitted >= usize::from(fragments),
5386 "only {transmitted} of {fragments} fragment(s) were transmitted"
5387 );
5388 assert_eq!(repairs, 0, "the message needed {repairs} repair request(s)");
5389 }
5390}