umsh_mac/
test_support.rs

1//! Std-only simulated radio network and dummy platform components for tests.
2//!
3//! This module is intended for development, simulation, and examples. The
4//! simulated network types are useful outside unit tests, but the dummy crypto
5//! and RNG implementations are deliberately insecure and must not be used for
6//! real deployments.
7
8use core::convert::Infallible;
9use std::{
10    cell::{Cell, RefCell},
11    collections::VecDeque,
12    rc::Rc,
13    vec::Vec,
14};
15
16use core::task::{Context, Poll};
17use embedded_hal_async::delay::DelayNs;
18use rand::{Rng, TryCryptoRng, TryRng};
19use umsh_core::PublicKey;
20use umsh_crypto::{
21    AesCipher, AesProvider, CryptoEngine, NodeIdentity, Sha256Provider, SharedSecret,
22};
23use umsh_hal::{
24    Clock, CounterStore, KeyValueStore, Radio, RxInfo, RxOrigin, Snr, TxError, TxOptions,
25};
26
27use crate::{
28    DEFAULT_ACKS, DEFAULT_CHANNELS, DEFAULT_DUP, DEFAULT_FRAME, DEFAULT_IDENTITIES, DEFAULT_PEERS,
29    DEFAULT_TX, Mac, OperatingPolicy, Platform, RepeaterConfig,
30};
31
32const DEFAULT_RSSI: i16 = -40;
33const DEFAULT_SNR: Snr = Snr::from_decibels(10);
34
35/// Convenience alias for a `Mac` instantiated with the simulated test components.
36pub type TestMac<
37    const IDENTITIES: usize = DEFAULT_IDENTITIES,
38    const PEERS: usize = DEFAULT_PEERS,
39    const CHANNELS: usize = DEFAULT_CHANNELS,
40    const ACKS: usize = DEFAULT_ACKS,
41    const TX: usize = DEFAULT_TX,
42    const FRAME: usize = DEFAULT_FRAME,
43    const DUP: usize = DEFAULT_DUP,
44> = Mac<TestPlatform, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP>;
45
46/// Convenience alias for a `Mac` instantiated with the modeled simulated components.
47pub type ModeledTestMac<
48    const IDENTITIES: usize = DEFAULT_IDENTITIES,
49    const PEERS: usize = DEFAULT_PEERS,
50    const CHANNELS: usize = DEFAULT_CHANNELS,
51    const ACKS: usize = DEFAULT_ACKS,
52    const TX: usize = DEFAULT_TX,
53    const FRAME: usize = DEFAULT_FRAME,
54    const DUP: usize = DEFAULT_DUP,
55> = Mac<ModeledTestPlatform, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP>;
56
57/// Platform bundle for the simulated test components.
58pub struct TestPlatform;
59
60impl Platform for TestPlatform {
61    type Identity = DummyIdentity;
62    type Aes = DummyAes;
63    type Sha = DummySha;
64    type Radio = SimulatedRadio;
65    type Delay = DummyDelay;
66    type Clock = DummyClock;
67    type Rng = DummyRng;
68    type CounterStore = DummyCounterStore;
69    type KeyValueStore = DummyKeyValueStore;
70}
71
72/// Platform bundle for the modeled simulated components.
73pub struct ModeledTestPlatform;
74
75impl Platform for ModeledTestPlatform {
76    type Identity = DummyIdentity;
77    type Aes = DummyAes;
78    type Sha = DummySha;
79    type Radio = ModeledRadio;
80    type Delay = DummyDelay;
81    type Clock = DummyClock;
82    type Rng = DummyRng;
83    type CounterStore = DummyCounterStore;
84    type KeyValueStore = DummyKeyValueStore;
85}
86
87/// Create a test MAC coordinator using the simulated components.
88pub fn make_test_mac<
89    const IDENTITIES: usize,
90    const PEERS: usize,
91    const CHANNELS: usize,
92    const ACKS: usize,
93    const TX: usize,
94    const FRAME: usize,
95    const DUP: usize,
96>(
97    radio: SimulatedRadio,
98    clock: DummyClock,
99) -> TestMac<IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP> {
100    Mac::new(
101        radio,
102        CryptoEngine::new(DummyAes, DummySha),
103        clock,
104        DummyRng::default(),
105        DummyCounterStore,
106        RepeaterConfig::default(),
107        OperatingPolicy::default(),
108    )
109}
110
111/// Create a test MAC coordinator using the modeled simulated components.
112pub fn make_modeled_test_mac<
113    const IDENTITIES: usize,
114    const PEERS: usize,
115    const CHANNELS: usize,
116    const ACKS: usize,
117    const TX: usize,
118    const FRAME: usize,
119    const DUP: usize,
120>(
121    radio: ModeledRadio,
122    clock: DummyClock,
123) -> ModeledTestMac<IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP> {
124    Mac::new(
125        radio,
126        CryptoEngine::new(DummyAes, DummySha),
127        clock,
128        DummyRng::default(),
129        DummyCounterStore,
130        RepeaterConfig::default(),
131        OperatingPolicy::default(),
132    )
133}
134
135/// Shared simulated radio topology and frame queues.
136#[derive(Clone)]
137pub struct SimulatedNetwork {
138    inner: Rc<RefCell<NetworkState>>,
139}
140
141struct NetworkState {
142    inboxes: Vec<VecDeque<QueuedFrame>>,
143    links: Vec<Vec<LinkProfile>>,
144}
145
146struct QueuedFrame {
147    data: Vec<u8>,
148    rssi: i16,
149    snr: Snr,
150}
151
152#[derive(Clone, Copy)]
153struct LinkProfile {
154    connected: bool,
155    rssi: i16,
156    snr: Snr,
157}
158
159impl Default for LinkProfile {
160    fn default() -> Self {
161        Self {
162            connected: false,
163            rssi: DEFAULT_RSSI,
164            snr: DEFAULT_SNR,
165        }
166    }
167}
168
169impl Default for SimulatedNetwork {
170    fn default() -> Self {
171        Self::new()
172    }
173}
174
175impl SimulatedNetwork {
176    /// Create an empty simulated network.
177    pub fn new() -> Self {
178        Self {
179            inner: Rc::new(RefCell::new(NetworkState {
180                inboxes: Vec::new(),
181                links: Vec::new(),
182            })),
183        }
184    }
185
186    /// Add a radio with default limits.
187    pub fn add_radio(&self) -> SimulatedRadio {
188        self.add_radio_with_config(256, 10)
189    }
190
191    /// Add a radio with explicit frame-size and airtime limits.
192    pub fn add_radio_with_config(&self, max_frame_size: usize, t_frame_ms: u32) -> SimulatedRadio {
193        let mut state = self.inner.borrow_mut();
194        let id = state.inboxes.len();
195        for row in &mut state.links {
196            row.push(LinkProfile::default());
197        }
198        state.inboxes.push(VecDeque::new());
199        state.links.push(vec![LinkProfile::default(); id + 1]);
200        SimulatedRadio {
201            network: self.clone(),
202            id,
203            max_frame_size,
204            t_frame_ms,
205        }
206    }
207
208    /// Connect `from` to `to` with default signal values.
209    pub fn connect(&self, from: usize, to: usize) {
210        self.set_link(from, to, true, DEFAULT_RSSI, DEFAULT_SNR);
211    }
212
213    /// Connect two radios in both directions.
214    pub fn connect_bidirectional(&self, a: usize, b: usize) {
215        self.connect(a, b);
216        self.connect(b, a);
217    }
218
219    /// Remove the directed link from `from` to `to`.
220    pub fn disconnect(&self, from: usize, to: usize) {
221        self.set_link(from, to, false, DEFAULT_RSSI, DEFAULT_SNR);
222    }
223
224    /// Configure one directed link with explicit signal values.
225    pub fn set_link(&self, from: usize, to: usize, connected: bool, rssi: i16, snr: Snr) {
226        let mut state = self.inner.borrow_mut();
227        let Some(row) = state.links.get_mut(from) else {
228            panic!("unknown simulated radio id {from}");
229        };
230        let Some(link) = row.get_mut(to) else {
231            panic!("unknown simulated radio id {to}");
232        };
233        *link = LinkProfile {
234            connected,
235            rssi,
236            snr,
237        };
238    }
239
240    /// Inject a frame directly into one radio's receive queue.
241    pub fn inject_frame(&self, to: usize, frame: &[u8]) {
242        self.inject_frame_with_info(to, frame, DEFAULT_RSSI, DEFAULT_SNR);
243    }
244
245    /// Inject a frame directly into one radio's receive queue with explicit metadata.
246    pub fn inject_frame_with_info(&self, to: usize, frame: &[u8], rssi: i16, snr: Snr) {
247        let mut state = self.inner.borrow_mut();
248        let Some(queue) = state.inboxes.get_mut(to) else {
249            panic!("unknown simulated radio id {to}");
250        };
251        queue.push_back(QueuedFrame {
252            data: frame.to_vec(),
253            rssi,
254            snr,
255        });
256    }
257
258    fn transmit(&self, from: usize, frame: &[u8]) {
259        let mut state = self.inner.borrow_mut();
260        let Some(row) = state.links.get(from) else {
261            panic!("unknown simulated radio id {from}");
262        };
263        let deliveries: Vec<(usize, i16, Snr)> = row
264            .iter()
265            .enumerate()
266            .filter_map(|(to, link)| link.connected.then_some((to, link.rssi, link.snr)))
267            .collect();
268        for (to, rssi, snr) in deliveries {
269            state.inboxes[to].push_back(QueuedFrame {
270                data: frame.to_vec(),
271                rssi,
272                snr,
273            });
274        }
275    }
276
277    fn receive(&self, id: usize, buf: &mut [u8]) -> RxInfo {
278        let mut state = self.inner.borrow_mut();
279        let Some(queue) = state.inboxes.get_mut(id) else {
280            panic!("unknown simulated radio id {id}");
281        };
282        let Some(frame) = queue.pop_front() else {
283            return RxInfo {
284                len: 0,
285                rssi: 0,
286                snr: Snr::from_decibels(0),
287                lqi: None,
288                origin: RxOrigin::Air,
289            };
290        };
291        let len = frame.data.len().min(buf.len());
292        buf[..len].copy_from_slice(&frame.data[..len]);
293        RxInfo {
294            len,
295            rssi: frame.rssi,
296            snr: frame.snr,
297            lqi: None,
298            origin: RxOrigin::Air,
299        }
300    }
301}
302
303/// Radio implementation backed by a [`SimulatedNetwork`].
304#[derive(Clone)]
305pub struct SimulatedRadio {
306    network: SimulatedNetwork,
307    id: usize,
308    max_frame_size: usize,
309    t_frame_ms: u32,
310}
311
312impl SimulatedRadio {
313    /// Return the radio's stable identifier within the simulated network.
314    pub fn id(&self) -> usize {
315        self.id
316    }
317}
318
319impl Radio for SimulatedRadio {
320    type Error = ();
321
322    async fn transmit(
323        &mut self,
324        data: &[u8],
325        _options: TxOptions,
326    ) -> Result<(), TxError<Self::Error>> {
327        self.network.transmit(self.id, data);
328        Ok(())
329    }
330
331    fn poll_receive(
332        &mut self,
333        _cx: &mut Context<'_>,
334        buf: &mut [u8],
335    ) -> Poll<Result<RxInfo, Self::Error>> {
336        let rx = self.network.receive(self.id, buf);
337        if rx.len == 0 {
338            Poll::Pending
339        } else {
340            Poll::Ready(Ok(rx))
341        }
342    }
343
344    fn max_frame_size(&self) -> usize {
345        self.max_frame_size
346    }
347
348    fn t_frame_ms(&self) -> u32 {
349        self.t_frame_ms
350    }
351}
352
353/// Link model used by [`ModeledNetwork`] to derive receive metadata and loss.
354#[derive(Clone, Copy, Debug, PartialEq, Eq)]
355pub struct ModeledLinkProfile {
356    pub connected: bool,
357    pub base_rssi: i16,
358    pub base_snr: Snr,
359    pub rssi_jitter_dbm: i16,
360    pub snr_jitter_centibels: i16,
361    pub propagation_delay_ms: u32,
362    pub drop_per_thousand: u16,
363}
364
365impl Default for ModeledLinkProfile {
366    fn default() -> Self {
367        Self {
368            connected: false,
369            base_rssi: DEFAULT_RSSI,
370            base_snr: DEFAULT_SNR,
371            rssi_jitter_dbm: 2,
372            snr_jitter_centibels: 10,
373            propagation_delay_ms: 0,
374            drop_per_thousand: 0,
375        }
376    }
377}
378
379impl ModeledLinkProfile {
380    /// Return a connected profile with the default modeled signal characteristics.
381    pub fn connected() -> Self {
382        Self {
383            connected: true,
384            ..Self::default()
385        }
386    }
387}
388
389/// Shared simulated network with scheduled delivery, jitter, packet loss, and coarse collision modeling.
390#[derive(Clone)]
391pub struct ModeledNetwork {
392    inner: Rc<RefCell<ModeledNetworkState>>,
393    clock: DummyClock,
394}
395
396struct ModeledNetworkState {
397    inboxes: Vec<VecDeque<QueuedFrame>>,
398    links: Vec<Vec<ModeledLinkProfile>>,
399    in_flight: Vec<InFlightTransmission>,
400    scheduled: Vec<ScheduledDelivery>,
401    rng: ModeledRng,
402}
403
404struct InFlightTransmission {
405    from: usize,
406    start_ms: u64,
407    end_ms: u64,
408}
409
410struct ScheduledDelivery {
411    to: usize,
412    available_at_ms: u64,
413    start_ms: u64,
414    end_ms: u64,
415    data: Vec<u8>,
416    rssi: i16,
417    snr: Snr,
418    collided: bool,
419}
420
421impl ModeledNetwork {
422    /// Create an empty modeled network with a shared clock starting at 0 ms.
423    pub fn new() -> Self {
424        Self::with_clock(DummyClock::new(0))
425    }
426
427    /// Create an empty modeled network using a caller-supplied shared clock.
428    pub fn with_clock(clock: DummyClock) -> Self {
429        Self {
430            inner: Rc::new(RefCell::new(ModeledNetworkState {
431                inboxes: Vec::new(),
432                links: Vec::new(),
433                in_flight: Vec::new(),
434                scheduled: Vec::new(),
435                rng: ModeledRng::new(0x554d_5348),
436            })),
437            clock,
438        }
439    }
440
441    /// Return the shared modeled clock.
442    pub fn clock(&self) -> DummyClock {
443        self.clock.clone()
444    }
445
446    /// Advance the shared simulation time.
447    pub fn advance_ms(&self, delta_ms: u64) {
448        self.clock.advance_ms(delta_ms);
449        self.promote_due_frames();
450    }
451
452    /// Override the deterministic RNG seed used for loss/jitter sampling.
453    pub fn reseed(&self, seed: u64) {
454        self.inner.borrow_mut().rng = ModeledRng::new(seed);
455    }
456
457    /// Add a modeled radio with default limits.
458    pub fn add_radio(&self) -> ModeledRadio {
459        self.add_radio_with_config(256, 100)
460    }
461
462    /// Add a modeled radio with explicit frame-size and airtime limits.
463    pub fn add_radio_with_config(&self, max_frame_size: usize, t_frame_ms: u32) -> ModeledRadio {
464        let mut state = self.inner.borrow_mut();
465        let id = state.inboxes.len();
466        for row in &mut state.links {
467            row.push(ModeledLinkProfile::default());
468        }
469        state.inboxes.push(VecDeque::new());
470        state
471            .links
472            .push(vec![ModeledLinkProfile::default(); id + 1]);
473        ModeledRadio {
474            network: self.clone(),
475            id,
476            max_frame_size,
477            t_frame_ms,
478        }
479    }
480
481    /// Connect `from` to `to` using the default modeled link profile.
482    pub fn connect(&self, from: usize, to: usize) {
483        self.set_link_profile(from, to, ModeledLinkProfile::connected());
484    }
485
486    /// Connect two radios in both directions using the default modeled link profile.
487    pub fn connect_bidirectional(&self, a: usize, b: usize) {
488        self.connect(a, b);
489        self.connect(b, a);
490    }
491
492    /// Remove the directed link from `from` to `to`.
493    pub fn disconnect(&self, from: usize, to: usize) {
494        self.set_link_profile(from, to, ModeledLinkProfile::default());
495    }
496
497    /// Configure one directed link with an explicit modeled profile.
498    pub fn set_link_profile(&self, from: usize, to: usize, profile: ModeledLinkProfile) {
499        let mut state = self.inner.borrow_mut();
500        let Some(row) = state.links.get_mut(from) else {
501            panic!("unknown modeled radio id {from}");
502        };
503        let Some(link) = row.get_mut(to) else {
504            panic!("unknown modeled radio id {to}");
505        };
506        *link = profile;
507    }
508
509    /// Return whether any future deliveries are still pending.
510    pub fn has_pending_deliveries(&self) -> bool {
511        !self.inner.borrow().scheduled.is_empty()
512    }
513
514    fn promote_due_frames(&self) {
515        let now_ms = self.clock.now_ms();
516        let mut state = self.inner.borrow_mut();
517        state.in_flight.retain(|tx| tx.end_ms > now_ms);
518        let mut index = 0usize;
519        while index < state.scheduled.len() {
520            if state.scheduled[index].available_at_ms > now_ms {
521                index += 1;
522                continue;
523            }
524            let delivery = state.scheduled.swap_remove(index);
525            if delivery.collided {
526                continue;
527            }
528            state.inboxes[delivery.to].push_back(QueuedFrame {
529                data: delivery.data,
530                rssi: delivery.rssi,
531                snr: delivery.snr,
532            });
533        }
534    }
535
536    fn channel_busy(&self, from: usize, now_ms: u64) -> bool {
537        let state = self.inner.borrow();
538        state.in_flight.iter().any(|tx| {
539            tx.from != from
540                && tx.start_ms <= now_ms
541                && now_ms < tx.end_ms
542                && state
543                    .links
544                    .get(tx.from)
545                    .and_then(|row| row.get(from))
546                    .map(|profile| profile.connected)
547                    .unwrap_or(false)
548        })
549    }
550
551    fn transmit(
552        &self,
553        from: usize,
554        frame: &[u8],
555        t_frame_ms: u32,
556        options: TxOptions,
557    ) -> Result<(), TxError<()>> {
558        self.promote_due_frames();
559        let now_ms = self.clock.now_ms();
560        if !matches!(options.cad, umsh_hal::CadPolicy::Skip) && self.channel_busy(from, now_ms) {
561            return Err(TxError::CadTimeout);
562        }
563
564        let mut state = self.inner.borrow_mut();
565        let Some(row) = state.links.get(from) else {
566            panic!("unknown modeled radio id {from}");
567        };
568        // A half-duplex radio cannot start a frame while it is still sending
569        // one. A real driver's transmit blocks for the airtime; the model has
570        // no way to block a caller, so the new frame queues behind whatever
571        // this radio still has on the air. Without this a node polled twice
572        // inside one frame time transmits over itself, and every neighbour
573        // discards both copies as a collision.
574        let busy_until_ms = state
575            .in_flight
576            .iter()
577            .filter(|tx| tx.from == from)
578            .map(|tx| tx.end_ms)
579            .max()
580            .unwrap_or(0);
581        let start_ms = now_ms.max(busy_until_ms);
582        let end_ms = start_ms.saturating_add(u64::from(t_frame_ms));
583        let deliveries: Vec<(usize, ModeledLinkProfile)> = row
584            .iter()
585            .enumerate()
586            .filter_map(|(to, link)| link.connected.then_some((to, *link)))
587            .collect();
588
589        for (to, profile) in deliveries {
590            if profile.drop_per_thousand > 0
591                && state.rng.random_u16(1000) < profile.drop_per_thousand
592            {
593                continue;
594            }
595
596            let rssi_jitter = if profile.rssi_jitter_dbm > 0 {
597                state
598                    .rng
599                    .random_i16_inclusive(-profile.rssi_jitter_dbm, profile.rssi_jitter_dbm)
600            } else {
601                0
602            };
603            let snr_jitter = if profile.snr_jitter_centibels > 0 {
604                state.rng.random_i16_inclusive(
605                    -profile.snr_jitter_centibels,
606                    profile.snr_jitter_centibels,
607                )
608            } else {
609                0
610            };
611            let propagation_delay_ms = u64::from(profile.propagation_delay_ms);
612            let available_at_ms = end_ms.saturating_add(propagation_delay_ms);
613            let mut delivery = ScheduledDelivery {
614                to,
615                available_at_ms,
616                start_ms,
617                end_ms,
618                data: frame.to_vec(),
619                rssi: profile.base_rssi.saturating_add(rssi_jitter),
620                snr: Snr::from_centibels(
621                    profile.base_snr.as_centibels().saturating_add(snr_jitter),
622                ),
623                collided: false,
624            };
625
626            for existing in &mut state.scheduled {
627                if existing.to != to {
628                    continue;
629                }
630                if existing.start_ms < end_ms && start_ms < existing.end_ms {
631                    existing.collided = true;
632                    delivery.collided = true;
633                }
634            }
635
636            state.scheduled.push(delivery);
637        }
638
639        state.in_flight.push(InFlightTransmission {
640            from,
641            start_ms,
642            end_ms,
643        });
644        Ok(())
645    }
646
647    fn receive(&self, id: usize, buf: &mut [u8]) -> RxInfo {
648        self.promote_due_frames();
649        let mut state = self.inner.borrow_mut();
650        let Some(queue) = state.inboxes.get_mut(id) else {
651            panic!("unknown modeled radio id {id}");
652        };
653        let Some(frame) = queue.pop_front() else {
654            return RxInfo {
655                len: 0,
656                rssi: 0,
657                snr: Snr::from_decibels(0),
658                lqi: None,
659                origin: RxOrigin::Air,
660            };
661        };
662        let len = frame.data.len().min(buf.len());
663        buf[..len].copy_from_slice(&frame.data[..len]);
664        RxInfo {
665            len,
666            rssi: frame.rssi,
667            snr: frame.snr,
668            lqi: None,
669            origin: RxOrigin::Air,
670        }
671    }
672}
673
674impl Default for ModeledNetwork {
675    fn default() -> Self {
676        Self::new()
677    }
678}
679
680/// Radio implementation backed by a [`ModeledNetwork`].
681#[derive(Clone)]
682pub struct ModeledRadio {
683    network: ModeledNetwork,
684    id: usize,
685    max_frame_size: usize,
686    t_frame_ms: u32,
687}
688
689#[derive(Clone, Copy)]
690struct ModeledRng(u64);
691
692impl ModeledRng {
693    fn new(seed: u64) -> Self {
694        Self(seed.max(1))
695    }
696
697    fn next_u32(&mut self) -> u32 {
698        let mut x = self.0;
699        x ^= x << 13;
700        x ^= x >> 7;
701        x ^= x << 17;
702        self.0 = x.max(1);
703        x as u32
704    }
705
706    fn random_u16(&mut self, upper_exclusive: u16) -> u16 {
707        if upper_exclusive == 0 {
708            0
709        } else {
710            (self.next_u32() % u32::from(upper_exclusive)) as u16
711        }
712    }
713
714    fn random_i16_inclusive(&mut self, min: i16, max: i16) -> i16 {
715        if min >= max {
716            min
717        } else {
718            let span = (i32::from(max) - i32::from(min) + 1) as u32;
719            (i32::from(min) + (self.next_u32() % span) as i32) as i16
720        }
721    }
722}
723
724impl ModeledRadio {
725    /// Return the radio's stable identifier within the modeled network.
726    pub fn id(&self) -> usize {
727        self.id
728    }
729}
730
731impl Radio for ModeledRadio {
732    type Error = ();
733
734    async fn transmit(
735        &mut self,
736        data: &[u8],
737        options: TxOptions,
738    ) -> Result<(), TxError<Self::Error>> {
739        self.network
740            .transmit(self.id, data, self.t_frame_ms, options)
741    }
742
743    fn poll_receive(
744        &mut self,
745        _cx: &mut Context<'_>,
746        buf: &mut [u8],
747    ) -> Poll<Result<RxInfo, Self::Error>> {
748        let rx = self.network.receive(self.id, buf);
749        if rx.len == 0 {
750            Poll::Pending
751        } else {
752            Poll::Ready(Ok(rx))
753        }
754    }
755
756    fn max_frame_size(&self) -> usize {
757        self.max_frame_size
758    }
759
760    fn t_frame_ms(&self) -> u32 {
761        self.t_frame_ms
762    }
763}
764
765/// Minimal `NodeIdentity` implementation used by tests and simulations.
766///
767/// This type is not suitable for production use.
768#[derive(Clone)]
769pub struct DummyIdentity {
770    public_key: PublicKey,
771}
772
773impl DummyIdentity {
774    /// Construct a dummy identity from fixed public-key bytes.
775    ///
776    /// When the `software-crypto` feature is enabled the supplied bytes
777    /// are treated as a *seed* and run through Ed25519 key derivation so
778    /// that the resulting public key is always a valid point on the
779    /// curve. This keeps test code source-compatible (callers still pass
780    /// distinguishable byte patterns like `[0xAB; 32]`) while letting
781    /// the MAC layer enforce real curve validation in `add_peer`.
782    ///
783    /// Without `software-crypto` the bytes are used verbatim — useful
784    /// for `no_std` test scenarios where Ed25519 derivation is not
785    /// available, and where the validator is also compiled out.
786    pub fn new(bytes: [u8; 32]) -> Self {
787        #[cfg(feature = "software-crypto")]
788        let public_key = {
789            use umsh_crypto::NodeIdentity;
790            *umsh_crypto::software::SoftwareIdentity::from_secret_bytes(&bytes).public_key()
791        };
792        #[cfg(not(feature = "software-crypto"))]
793        let public_key = PublicKey(bytes);
794        Self { public_key }
795    }
796
797    /// Construct a dummy identity from a raw `PublicKey` without
798    /// running it through Ed25519 derivation. Intended for callers that
799    /// need exact control over the test public-key bytes (e.g. to
800    /// construct a key that the validator should reject).
801    pub fn from_public_key(public_key: PublicKey) -> Self {
802        Self { public_key }
803    }
804}
805
806impl NodeIdentity for DummyIdentity {
807    type Error = ();
808
809    fn public_key(&self) -> &PublicKey {
810        &self.public_key
811    }
812
813    async fn sign(&self, _message: &[u8]) -> Result<[u8; 64], Self::Error> {
814        Ok([0u8; 64])
815    }
816
817    async fn agree(&self, peer: &PublicKey) -> Result<SharedSecret, Self::Error> {
818        let mut out = [0u8; 32];
819        for (index, byte) in out.iter_mut().enumerate() {
820            *byte = self.public_key.0[index] ^ peer.0[index];
821        }
822        Ok(SharedSecret(out))
823    }
824}
825
826/// Dummy XOR-based cipher used by tests.
827///
828/// This type is intentionally insecure.
829pub struct DummyCipher {
830    key: [u8; 32],
831}
832
833impl AesCipher for DummyCipher {
834    fn encrypt_block(&self, block: &mut [u8; 16]) {
835        for (byte, key) in block.iter_mut().zip(self.key.iter()) {
836            *byte ^= *key;
837        }
838    }
839
840    fn decrypt_block(&self, block: &mut [u8; 16]) {
841        self.encrypt_block(block);
842    }
843}
844
845/// Dummy AES provider used by tests.
846///
847/// This type is intentionally insecure.
848#[derive(Clone, Copy)]
849pub struct DummyAes;
850
851impl AesProvider for DummyAes {
852    type Cipher = DummyCipher;
853
854    fn new_cipher(&self, key: &[u8; 32]) -> Self::Cipher {
855        DummyCipher { key: *key }
856    }
857}
858
859/// Dummy SHA/HMAC provider used by tests.
860///
861/// This type is intentionally insecure.
862#[derive(Clone, Copy)]
863pub struct DummySha;
864
865impl Sha256Provider for DummySha {
866    fn hash(&self, data: &[&[u8]]) -> [u8; 32] {
867        let mut out = [0u8; 32];
868        for chunk in data {
869            for (index, byte) in chunk.iter().enumerate() {
870                out[index % 32] ^= *byte;
871            }
872        }
873        out
874    }
875
876    fn hmac(&self, key: &[u8], data: &[&[u8]]) -> [u8; 32] {
877        let mut out = [0u8; 32];
878        for (index, byte) in key.iter().enumerate() {
879            out[index % 32] ^= *byte;
880        }
881        for chunk in data {
882            for (index, byte) in chunk.iter().enumerate() {
883                out[index % 32] ^= *byte;
884            }
885        }
886        out
887    }
888}
889
890/// Mutable monotonic test clock.
891#[derive(Clone, Default)]
892pub struct DummyClock {
893    now_ms: Rc<Cell<u64>>,
894}
895
896impl DummyClock {
897    /// Create the clock starting at `now_ms`.
898    pub fn new(now_ms: u64) -> Self {
899        Self {
900            now_ms: Rc::new(Cell::new(now_ms)),
901        }
902    }
903
904    /// Advance the clock by `delta_ms`.
905    pub fn advance_ms(&self, delta_ms: u64) {
906        self.now_ms.set(self.now_ms.get().saturating_add(delta_ms));
907    }
908
909    /// Set the current clock value.
910    pub fn set_ms(&self, now_ms: u64) {
911        self.now_ms.set(now_ms);
912    }
913}
914
915impl Clock for DummyClock {
916    fn now_ms(&self) -> u64 {
917        self.now_ms.get()
918    }
919}
920
921/// No-op async delay used only to satisfy the platform bundle in tests.
922#[derive(Clone, Copy, Default)]
923pub struct DummyDelay;
924
925impl DelayNs for DummyDelay {
926    async fn delay_ns(&mut self, _ns: u32) {}
927}
928
929/// Deterministic byte-filling RNG used by tests.
930///
931/// This type is intentionally predictable.
932#[derive(Clone, Default)]
933pub struct DummyRng(pub u8);
934
935impl TryRng for DummyRng {
936    type Error = Infallible;
937
938    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
939        let mut bytes = [0u8; 4];
940        self.fill_bytes(&mut bytes);
941        Ok(u32::from_le_bytes(bytes))
942    }
943
944    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
945        let mut bytes = [0u8; 8];
946        self.fill_bytes(&mut bytes);
947        Ok(u64::from_le_bytes(bytes))
948    }
949
950    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Infallible> {
951        for byte in dest.iter_mut() {
952            *byte = self.0;
953            self.0 = self.0.wrapping_add(1);
954        }
955        Ok(())
956    }
957}
958
959impl TryCryptoRng for DummyRng {}
960
961/// No-op counter store used by tests.
962#[derive(Clone, Copy, Default)]
963pub struct DummyCounterStore;
964
965impl CounterStore for DummyCounterStore {
966    type Error = ();
967
968    async fn load(&self, _context: &[u8]) -> Result<u32, Self::Error> {
969        Ok(0)
970    }
971
972    async fn store(&self, _context: &[u8], _value: u32) -> Result<(), Self::Error> {
973        Ok(())
974    }
975
976    async fn flush(&self) -> Result<(), Self::Error> {
977        Ok(())
978    }
979}
980
981/// No-op key-value store used only to satisfy the platform bundle in tests.
982#[derive(Clone, Copy, Default)]
983pub struct DummyKeyValueStore;
984
985impl KeyValueStore for DummyKeyValueStore {
986    type Error = ();
987
988    async fn load(&self, _key: &[u8], _buf: &mut [u8]) -> Result<Option<usize>, Self::Error> {
989        Ok(None)
990    }
991
992    async fn store(&self, _key: &[u8], _value: &[u8]) -> Result<(), Self::Error> {
993        Ok(())
994    }
995
996    async fn delete(&self, _key: &[u8]) -> Result<(), Self::Error> {
997        Ok(())
998    }
999}