1use std::{
17 sync::{
18 Arc,
19 atomic::{AtomicU64, Ordering},
20 },
21 time::Duration,
22};
23
24use bytes::Bytes;
25use dashmap::DashMap;
26use hopr_api::{
27 graph::{ForwardAndReturnPath, MeasurableEdge, MeasurablePath, MeasurablePeer, NetworkGraphView, SurbTelemetry},
28 types::{
29 crypto::{prelude::*, types::OffchainPublicKey},
30 internal::prelude::*,
31 },
32};
33use hopr_crypto_packet::{HoprSurb, prelude::PacketSignals};
34use hopr_protocol_hopr::{IncomingPacket, IncomingPacketError, OutgoingPacket, PacketDecoder, PacketEncoder};
35
36const PATH_ID_SLOTS: usize = 5;
38
39const PENDING_SURB_TTL: Duration = Duration::from_secs(60);
45
46#[derive(Debug, Default)]
51pub struct SurbRoundTripCounters {
52 expected: AtomicU64,
53 observed: AtomicU64,
54}
55
56impl SurbRoundTripCounters {
57 fn record_expected(&self, count: u64) {
58 self.expected.fetch_add(count, Ordering::Relaxed);
59 }
60
61 fn record_observed(&self, count: u64) {
62 self.observed.fetch_add(count, Ordering::Relaxed);
63 }
64
65 fn peek(&self) -> (u64, u64) {
67 (
68 self.expected.load(Ordering::Relaxed),
69 self.observed.load(Ordering::Relaxed),
70 )
71 }
72
73 fn take(&self) -> (u64, u64) {
75 (
76 self.expected.swap(0, Ordering::Relaxed),
77 self.observed.swap(0, Ordering::Relaxed),
78 )
79 }
80}
81
82#[derive(Debug, Clone)]
88pub struct SurbRoundTripRegistry {
89 inner: Arc<DashMap<ForwardAndReturnPath, Arc<SurbRoundTripCounters>>>,
90 destinations: Arc<DashMap<ForwardAndReturnPath, OffchainPublicKey>>,
92 silence: Arc<DashMap<ForwardAndReturnPath, Silence>>,
94 replanned: Arc<DashMap<OffchainPublicKey, u32>>,
96 me_slot: Arc<AtomicU64>,
102}
103
104const ME_SLOT_UNKNOWN: u64 = u64::MAX;
110
111impl Default for SurbRoundTripRegistry {
112 fn default() -> Self {
113 Self {
114 inner: Default::default(),
115 destinations: Default::default(),
116 silence: Default::default(),
117 replanned: Default::default(),
118 me_slot: Arc::new(AtomicU64::new(ME_SLOT_UNKNOWN)),
119 }
120 }
121}
122
123fn return_relayers(reply: &PathId, me_slot: u64) -> impl Iterator<Item = u64> + '_ {
125 reply.iter().skip(1).take_while(move |&&slot| slot != me_slot).copied()
126}
127
128#[derive(Debug, Default, Clone, Copy)]
130struct Silence {
131 runs: u32,
133 delivered: bool,
139}
140
141const MIN_EXPECTED_FOR_SILENCE: u64 = 20;
146
147const SILENT_FLUSHES_BEFORE_DEGRADED: u32 = 5;
154
155const FLUSHES_BETWEEN_REPLANS: u32 = 8;
167
168impl SurbRoundTripRegistry {
169 fn counters(&self, paths: ForwardAndReturnPath) -> Arc<SurbRoundTripCounters> {
170 self.inner.entry(paths).or_default().value().clone()
171 }
172
173 pub fn note_me_slot(&self, slot: u64) {
175 self.me_slot.store(slot, Ordering::Relaxed);
176 }
177
178 pub fn record_expected(&self, paths: ForwardAndReturnPath, count: u64, destination: OffchainPublicKey) {
180 self.destinations.insert(paths, destination);
181 self.counters(paths).record_expected(count);
182 }
183
184 pub fn degraded_destinations(&self) -> Vec<OffchainPublicKey> {
195 let mut degraded = Vec::new();
196
197 self.replanned.retain(|_, since| {
199 *since += 1;
200 *since < FLUSHES_BETWEEN_REPLANS
201 });
202
203 let delivering: std::collections::HashSet<OffchainPublicKey> = self
211 .inner
212 .iter()
213 .filter(|entry| entry.value().peek().1 > 0)
214 .filter_map(|entry| self.destinations.get(entry.key()).map(|d| *d))
215 .collect();
216
217 let me_slot = self.me_slot.load(Ordering::Relaxed);
227 if me_slot == ME_SLOT_UNKNOWN {
228 return degraded;
229 }
230
231 let delivering_relayers: std::collections::HashSet<u64> = self
232 .inner
233 .iter()
234 .filter(|entry| entry.value().peek().1 > 0)
235 .flat_map(|entry| return_relayers(&entry.key().reply, me_slot).collect::<Vec<_>>())
236 .collect();
237
238 let mut blind_spot: std::collections::HashSet<OffchainPublicKey> = std::collections::HashSet::new();
244
245 for entry in self.inner.iter() {
246 let paths = *entry.key();
247 let (expected, observed) = entry.value().peek();
248 let mut state = self.silence.entry(paths).or_default();
249
250 if observed > 0 {
251 state.delivered = true;
253 state.runs = 0;
254 continue;
255 }
256
257 if expected < MIN_EXPECTED_FOR_SILENCE {
258 state.runs = 0;
260 continue;
261 }
262
263 if !state.delivered {
264 continue;
267 }
268
269 let Some(dest) = self.destinations.get(&paths).map(|d| *d) else {
270 continue;
271 };
272
273 if !delivering.contains(&dest) {
279 blind_spot.insert(dest);
282 state.runs = 0;
283 continue;
284 }
285
286 if return_relayers(&paths.reply, me_slot).all(|r| delivering_relayers.contains(&r)) {
290 state.runs = 0;
291 continue;
292 }
293
294 state.runs += 1;
295 tracing::debug!(
299 destination = %dest,
300 runs = state.runs,
301 threshold = SILENT_FLUSHES_BEFORE_DEGRADED,
302 expected,
303 "return pair silent while a sibling delivers; silence run climbing",
304 );
305 if state.runs >= SILENT_FLUSHES_BEFORE_DEGRADED {
306 state.runs = 0;
310 if !self.replanned.contains_key(&dest) {
314 tracing::debug!(
315 destination = %dest,
316 runs = SILENT_FLUSHES_BEFORE_DEGRADED,
317 "return path degraded: sustained silence corroborated by a delivering sibling; \
318 marking destination for re-plan",
319 );
320 self.replanned.insert(dest, 0);
321 degraded.push(dest);
322 }
323 }
324 }
325
326 if !degraded.is_empty() || !blind_spot.is_empty() {
330 tracing::debug!(
331 degraded = degraded.len(),
332 blind_spot = blind_spot.len(),
333 blind_spot_destinations = ?blind_spot,
334 delivering_destinations = delivering.len(),
335 delivering_relayers = delivering_relayers.len(),
336 "surb return-path degradation scan",
337 );
338 }
339
340 degraded
341 }
342
343 pub fn record_observed(&self, paths: ForwardAndReturnPath, count: u64) {
345 self.counters(paths).record_observed(count);
346 }
347
348 pub fn drain(&self) -> Vec<(ForwardAndReturnPath, u64, u64)> {
350 self.inner
351 .iter()
352 .filter_map(|entry| {
353 let (expected, observed) = entry.value().take();
354 (expected > 0 || observed > 0).then(|| (*entry.key(), expected, observed))
355 })
356 .collect()
357 }
358}
359
360pub type PathSlotResolver = Arc<dyn Fn(&OffchainPublicKey) -> Option<u64> + Send + Sync>;
366
367pub fn no_path_slots() -> PathSlotResolver {
372 Arc::new(|_| None)
373}
374
375pub type PendingLegs = moka::sync::Cache<HoprSurbId, ForwardAndReturnPath>;
381
382pub fn pending_legs(max_pending: u64) -> PendingLegs {
384 moka::sync::Cache::builder()
385 .max_capacity(max_pending)
386 .time_to_live(PENDING_SURB_TTL)
387 .build()
388}
389
390pub fn path_slots_of<G>(graph: G) -> PathSlotResolver
392where
393 G: NetworkGraphView<NodeId = OffchainPublicKey> + Send + Sync + 'static,
394{
395 Arc::new(move |key| graph.path_slot(key))
396}
397
398fn path_id(slots: &PathSlotResolver, nodes: impl IntoIterator<Item = OffchainPublicKey>) -> Option<PathId> {
404 let mut id = [0u64; PATH_ID_SLOTS];
405 let mut len = 0;
406
407 for node in nodes {
408 if len == PATH_ID_SLOTS {
409 return None;
410 }
411 id[len] = slots(&node)?;
412 len += 1;
413 }
414
415 (len > 1).then_some(id)
416}
417
418fn round_trip_paths(
424 slots: &PathSlotResolver,
425 me: &OffchainPublicKey,
426 forward: &[OffchainPublicKey],
427 reply: &[OffchainPublicKey],
428) -> Option<ForwardAndReturnPath> {
429 let destination = *forward.last()?;
430
431 Some(ForwardAndReturnPath {
432 forward: path_id(slots, std::iter::once(*me).chain(forward.iter().copied()))?,
433 reply: path_id(slots, std::iter::once(destination).chain(reply.iter().copied()))?,
434 })
435}
436
437#[derive(Debug, Clone)]
442pub enum NoPeerTelemetry {}
443
444impl MeasurablePeer for NoPeerTelemetry {
445 fn peer(&self) -> &OffchainPublicKey {
446 match *self {}
447 }
448
449 fn rtt(&self) -> Duration {
450 match *self {}
451 }
452}
453
454#[derive(Debug, Clone)]
456pub enum NoPathTelemetry {}
457
458impl MeasurablePath for NoPathTelemetry {
459 fn id(&self) -> &[u8] {
460 match *self {}
461 }
462
463 fn path(&self) -> &[u8] {
464 match *self {}
465 }
466
467 fn timestamp(&self) -> u128 {
468 match *self {}
469 }
470}
471
472pub fn flush_into<G>(registry: &SurbRoundTripRegistry, graph: &G, timestamp: u128)
476where
477 G: hopr_api::graph::NetworkGraphUpdate,
478{
479 let (mut legs, mut total_expected, mut total_observed) = (0usize, 0u64, 0u64);
480
481 for (paths, expected, observed) in registry.drain() {
482 legs += 1;
483 total_expected += expected;
484 total_observed += observed;
485 tracing::debug!(
488 forward = ?paths.forward,
489 reply = ?paths.reply,
490 expected,
491 observed,
492 "surb round-trip pair"
493 );
494 graph.record_edge::<NoPeerTelemetry, NoPathTelemetry>(MeasurableEdge::Surb(SurbTelemetry {
495 paths,
496 timestamp,
497 expected,
498 observed,
499 }));
500 }
501
502 {
503 tracing::info!(
510 legs,
511 expected = total_expected,
512 observed = total_observed,
513 "surb round-trip flush tick"
514 );
515 }
516}
517
518#[derive(Clone)]
523pub struct SurbTelemetryCodec<C> {
524 inner: C,
525 me: OffchainPublicKey,
526 slots: PathSlotResolver,
527 registry: SurbRoundTripRegistry,
528 pending: PendingLegs,
533}
534
535impl<C: std::fmt::Debug> std::fmt::Debug for SurbTelemetryCodec<C> {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 f.debug_struct("SurbTelemetryCodec")
539 .field("inner", &self.inner)
540 .field("me", &self.me)
541 .finish_non_exhaustive()
542 }
543}
544
545impl<C> SurbTelemetryCodec<C> {
546 pub fn new(
548 inner: C,
549 me: OffchainPublicKey,
550 slots: PathSlotResolver,
551 registry: SurbRoundTripRegistry,
552 pending: PendingLegs,
553 ) -> Self {
554 Self {
555 inner,
556 me,
557 slots,
558 registry,
559 pending,
560 }
561 }
562
563 fn on_minted(&self, routing: &ResolvedTransportRouting<HoprSurb>, minted: &[HoprSurbId]) {
568 let ResolvedTransportRouting::Forward {
569 forward_path,
570 return_paths,
571 ..
572 } = routing
573 else {
574 return;
575 };
576
577 let forward: Vec<_> = forward_path.transport_path().iter().copied().collect();
578 let Some(destination) = forward.last().copied() else {
579 return;
580 };
581
582 if let Some(slot) = (self.slots)(&self.me) {
583 self.registry.note_me_slot(slot);
584 }
585
586 tracing::debug!(
587 minted = minted.len(),
588 return_paths = return_paths.len(),
589 forward_hops = forward.len(),
590 "surb mint seen by telemetry"
591 );
592
593 for (surb_id, return_path) in minted.iter().zip(return_paths.iter()) {
594 let reply: Vec<_> = return_path.transport_path().iter().copied().collect();
595 let Some(paths) = round_trip_paths(&self.slots, &self.me, &forward, &reply) else {
596 tracing::debug!(reply_hops = reply.len(), "surb round-trip legs did not resolve");
597 continue;
598 };
599
600 self.registry.record_expected(paths, 1, destination);
601 self.pending.insert(*surb_id, paths);
602 }
603 }
604
605 fn on_replied(&self, surb_id: &HoprSurbId) {
607 match self.pending.remove(surb_id) {
609 Some(paths) => self.registry.record_observed(paths, 1),
610 None => tracing::debug!("reply on a surb with no pending legs"),
611 }
612 }
613}
614
615impl<C> PacketEncoder for SurbTelemetryCodec<C>
616where
617 C: PacketEncoder,
618{
619 type Error = C::Error;
620
621 fn encode_packet<T: AsRef<[u8]> + Send + 'static, S: Into<PacketSignals> + Send + 'static>(
622 &self,
623 data: T,
624 routing: ResolvedTransportRouting<HoprSurb>,
625 signals: S,
626 ) -> Result<OutgoingPacket, <Self as PacketEncoder>::Error> {
627 let packet = self.inner.encode_packet(data, routing.clone(), signals)?;
628 self.on_minted(&routing, &packet.minted_surbs);
629 Ok(packet)
630 }
631
632 fn encode_acknowledgements(
633 &self,
634 acks: &[VerifiedAcknowledgement],
635 destination: &OffchainPublicKey,
636 ) -> Result<OutgoingPacket, <Self as PacketEncoder>::Error> {
637 self.inner.encode_acknowledgements(acks, destination)
638 }
639}
640
641impl<C> PacketDecoder for SurbTelemetryCodec<C>
642where
643 C: PacketDecoder,
644{
645 type Error = C::Error;
646
647 fn decode(
648 &self,
649 sender: PeerId,
650 data: Bytes,
651 ) -> Result<IncomingPacket, IncomingPacketError<<Self as PacketDecoder>::Error>> {
652 let packet = self.inner.decode(sender, data)?;
653
654 if let IncomingPacket::Final(f) = &packet {
655 tracing::debug!(on_surb = f.replied_on_surb.is_some(), "final packet decoded");
656 }
657
658 if let IncomingPacket::Final(final_packet) = &packet
659 && let Some(surb_id) = final_packet.replied_on_surb
660 {
661 self.on_replied(&surb_id);
662 }
663
664 Ok(packet)
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use anyhow::Context;
671 use hopr_api::{
672 graph::NetworkGraphWrite,
673 types::{
674 crypto::{
675 crypto_traits::Randomizable,
676 prelude::{ChainKeypair, Keypair, OffchainKeypair},
677 },
678 primitive::primitives::Address,
679 },
680 };
681 use hopr_network_graph::petgraph::ChannelGraph;
682
683 use super::*;
684
685 fn recorder(graph: ChannelGraph) -> SurbTelemetryCodec<()> {
687 let me = *graph.identity();
688 SurbTelemetryCodec::new(
689 (),
690 me,
691 path_slots_of(graph),
692 SurbRoundTripRegistry::default(),
693 pending_legs(128),
694 )
695 }
696
697 fn surb_id(seed: u8) -> HoprSurbId {
698 [seed; SURB_ID_SIZE]
699 }
700
701 fn address() -> Address {
703 ChainKeypair::random().public().to_address()
704 }
705
706 fn graph_with(count: usize) -> (ChannelGraph, OffchainPublicKey, Vec<OffchainPublicKey>) {
707 let me = *OffchainKeypair::random().public();
708 let graph = ChannelGraph::new(me);
709
710 let peers = (0..count)
711 .map(|_| {
712 let key = *OffchainKeypair::random().public();
713 graph.add_node(key);
714 key
715 })
716 .collect();
717
718 (graph, me, peers)
719 }
720
721 #[test]
722 fn drain_should_report_nothing_before_anything_is_recorded() {
723 assert!(SurbRoundTripRegistry::default().drain().is_empty());
724 }
725
726 #[test]
727 fn drain_should_take_the_counts_and_leave_the_entry_empty() {
728 let (_graph, _me, peers) = graph_with(1);
729 let registry = registry_at_slot_zero();
730 let paths = ForwardAndReturnPath {
731 forward: [0, 1, 0, 0, 0],
732 reply: [1, 0, 0, 0, 0],
733 };
734
735 registry.record_expected(paths, 3, peers[0]);
736 registry.record_observed(paths, 2);
737
738 assert_eq!(vec![(paths, 3, 2)], registry.drain());
739 assert!(registry.drain().is_empty());
741 }
742
743 #[test]
744 fn round_trip_paths_should_join_the_legs_at_the_destination() -> anyhow::Result<()> {
745 let (graph, me, peers) = graph_with(1);
746 let destination = peers[0];
747
748 let paths = round_trip_paths(&path_slots_of(graph.clone()), &me, &[destination], &[me])
749 .context("both nodes are in the graph")?;
750
751 let me_slot = graph.path_slot(&me).context("self is in the graph")?;
752 let dest_slot = graph.path_slot(&destination).context("destination is in the graph")?;
753
754 assert_eq!([me_slot, dest_slot, 0, 0, 0], paths.forward);
757 assert_eq!([dest_slot, me_slot, 0, 0, 0], paths.reply);
758 Ok(())
759 }
760
761 #[test]
762 fn round_trip_paths_should_be_none_when_a_node_is_unknown_to_the_graph() {
763 let (graph, me, _) = graph_with(0);
764 let stranger = *OffchainKeypair::random().public();
765
766 assert!(round_trip_paths(&path_slots_of(graph), &me, &[stranger], &[me]).is_none());
769 }
770
771 #[test]
772 fn round_trip_paths_should_be_none_for_a_leg_too_long_to_identify() {
773 let (graph, me, peers) = graph_with(5);
774 let forward: Vec<_> = peers.clone();
775
776 assert!(round_trip_paths(&path_slots_of(graph), &me, &forward, &[me]).is_none());
778 }
779
780 #[test]
781 fn minting_a_surb_should_raise_an_expectation_over_the_legs_it_was_minted_on() -> anyhow::Result<()> {
782 let (graph, me, peers) = graph_with(1);
783 let destination = peers[0];
784 let recorder = recorder(graph);
785
786 let routing = ResolvedTransportRouting::Forward {
787 pseudonym: HoprPseudonym::random(),
788 forward_path: ValidatedPath::direct(destination, address()),
789 return_paths: vec![ValidatedPath::direct(me, address())],
790 };
791
792 recorder.on_minted(&routing, &[surb_id(1)]);
793
794 let drained = recorder.registry.drain();
795 assert_eq!(1, drained.len());
796 let (_, expected, observed) = drained[0];
797 assert_eq!(1, expected);
798 assert_eq!(0, observed, "nothing has come back yet");
799 Ok(())
800 }
801
802 #[test]
803 fn a_reply_should_be_credited_to_the_legs_its_surb_was_minted_on() -> anyhow::Result<()> {
804 let (graph, me, peers) = graph_with(1);
805 let destination = peers[0];
806 let recorder = recorder(graph);
807
808 let routing = ResolvedTransportRouting::Forward {
809 pseudonym: HoprPseudonym::random(),
810 forward_path: ValidatedPath::direct(destination, address()),
811 return_paths: vec![ValidatedPath::direct(me, address())],
812 };
813
814 recorder.on_minted(&routing, &[surb_id(1)]);
815 recorder.on_replied(&surb_id(1));
816
817 let drained = recorder.registry.drain();
818 assert_eq!(1, drained.len());
819 let (_, expected, observed) = drained[0];
820 assert_eq!((1, 1), (expected, observed));
821 Ok(())
822 }
823
824 #[test]
825 fn a_surb_should_only_be_credited_once() -> anyhow::Result<()> {
826 let (graph, me, peers) = graph_with(1);
827 let destination = peers[0];
828 let recorder = recorder(graph);
829
830 let routing = ResolvedTransportRouting::Forward {
831 pseudonym: HoprPseudonym::random(),
832 forward_path: ValidatedPath::direct(destination, address()),
833 return_paths: vec![ValidatedPath::direct(me, address())],
834 };
835
836 recorder.on_minted(&routing, &[surb_id(1)]);
837 recorder.on_replied(&surb_id(1));
838 recorder.on_replied(&surb_id(1));
841
842 let (_, expected, observed) = recorder.registry.drain()[0];
843 assert_eq!((1, 1), (expected, observed));
844 Ok(())
845 }
846
847 #[test]
848 fn flushing_should_leave_nothing_behind_to_report_twice() -> anyhow::Result<()> {
849 let (graph, me, peers) = graph_with(1);
850 let destination = peers[0];
851 let recorder = recorder(graph.clone());
852
853 let routing = ResolvedTransportRouting::Forward {
854 pseudonym: HoprPseudonym::random(),
855 forward_path: ValidatedPath::direct(destination, address()),
856 return_paths: vec![ValidatedPath::direct(me, address())],
857 };
858 recorder.on_minted(&routing, &[surb_id(1)]);
859 recorder.on_replied(&surb_id(1));
860
861 flush_into(&recorder.registry, &graph, 0);
862
863 assert!(recorder.registry.drain().is_empty());
866 Ok(())
867 }
868
869 #[test]
870 fn a_reply_should_be_credited_when_the_halves_are_separate_instances() -> anyhow::Result<()> {
871 let (graph, me, peers) = graph_with(1);
875 let destination = peers[0];
876 let registry = registry_at_slot_zero();
877 let pending = pending_legs(128);
878
879 let minting = SurbTelemetryCodec::new((), me, path_slots_of(graph.clone()), registry.clone(), pending.clone());
880 let observing = SurbTelemetryCodec::new((), me, path_slots_of(graph), registry.clone(), pending);
881
882 minting.on_minted(
883 &ResolvedTransportRouting::Forward {
884 pseudonym: HoprPseudonym::random(),
885 forward_path: ValidatedPath::direct(destination, address()),
886 return_paths: vec![ValidatedPath::direct(me, address())],
887 },
888 &[surb_id(1)],
889 );
890 observing.on_replied(&surb_id(1));
891
892 let (_, expected, observed) = registry.drain()[0];
893 assert_eq!(
894 (1, 1),
895 (expected, observed),
896 "the reply must reach the legs the mint recorded"
897 );
898 Ok(())
899 }
900
901 fn flush(registry: &SurbRoundTripRegistry) -> Vec<OffchainPublicKey> {
906 let degraded = registry.degraded_destinations();
907 registry.drain();
908 degraded
909 }
910
911 fn registry_at_slot_zero() -> SurbRoundTripRegistry {
917 let registry = SurbRoundTripRegistry::default();
918 registry.note_me_slot(0);
919 registry
920 }
921
922 fn deliver(registry: &SurbRoundTripRegistry, paths: ForwardAndReturnPath, destination: OffchainPublicKey) {
924 registry.record_expected(paths, MIN_EXPECTED_FOR_SILENCE, destination);
925 registry.record_observed(paths, 1);
926 }
927
928 fn mint_only(registry: &SurbRoundTripRegistry, paths: ForwardAndReturnPath, destination: OffchainPublicKey) {
930 registry.record_expected(paths, MIN_EXPECTED_FOR_SILENCE, destination);
931 }
932
933 fn heartbeat() -> ForwardAndReturnPath {
938 ForwardAndReturnPath {
939 forward: [0, 3, 2, 0, 0],
940 reply: [2, 3, 0, 0, 0],
941 }
942 }
943
944 fn legs() -> ForwardAndReturnPath {
949 ForwardAndReturnPath {
950 forward: [0, 1, 2, 0, 0],
951 reply: [2, 1, 0, 0, 0],
952 }
953 }
954
955 #[test]
963 fn silence_should_not_be_attributed_before_our_own_slot_is_known() {
964 let (graph, me, peers) = graph_with(1);
965 let destination = peers[0];
966 let registry = SurbRoundTripRegistry::default();
967 let paths = legs();
968
969 deliver(®istry, paths, destination);
972 assert!(flush(®istry).is_empty());
973 for _ in 0..SILENT_FLUSHES_BEFORE_DEGRADED + 2 {
974 mint_only(®istry, paths, destination);
975 deliver(®istry, heartbeat(), destination);
976 assert!(
977 flush(®istry).is_empty(),
978 "no relayer can be identified without our own slot, so nothing may be blamed"
979 );
980 }
981
982 registry.note_me_slot(0);
984 deliver(®istry, paths, destination);
985 assert!(flush(®istry).is_empty());
986 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
987 mint_only(®istry, paths, destination);
988 deliver(®istry, heartbeat(), destination);
989 assert!(flush(®istry).is_empty());
990 }
991 mint_only(®istry, paths, destination);
992 deliver(®istry, heartbeat(), destination);
993 assert_eq!(vec![destination], flush(®istry));
994
995 let _ = (graph, me);
996 }
997
998 #[test]
999 fn sustained_silence_should_name_the_destination_to_replan() {
1000 let (graph, me, peers) = graph_with(1);
1001 let destination = peers[0];
1002 let registry = registry_at_slot_zero();
1003 let paths = legs();
1004
1005 deliver(®istry, paths, destination);
1007 assert!(flush(®istry).is_empty());
1008
1009 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
1012 mint_only(®istry, paths, destination);
1013 deliver(®istry, heartbeat(), destination);
1014 assert!(flush(®istry).is_empty());
1015 }
1016 mint_only(®istry, paths, destination);
1017 deliver(®istry, heartbeat(), destination);
1018 assert_eq!(vec![destination], flush(®istry));
1019 let _ = (graph, me);
1020 }
1021
1022 #[tokio::test]
1032 async fn a_silent_return_path_drives_replan_then_refill_and_marks_the_balancer_stale() {
1033 use hopr_transport_session::{BalancerStateValues, SurbBalancerConfig};
1034
1035 use crate::protocol::return_path_recovery::{RecoveryStep, ReturnPathEpisodes, run_flush_tick};
1036
1037 let (graph, _me, peers) = graph_with(1);
1038 let destination = peers[0];
1039 let registry = registry_at_slot_zero();
1040 let paths = legs();
1041 let grace = Duration::from_secs(10);
1042 let mut episodes = ReturnPathEpisodes::new(grace);
1043
1044 let balancer = BalancerStateValues::new(SurbBalancerConfig {
1047 target_surb_buffer_size: 1000,
1048 sustain_on_return_path_loss: true,
1049 ..Default::default()
1050 });
1051 balancer.buffer_level.store(1000, Ordering::Relaxed);
1052 assert!(!balancer.is_disabled(), "precondition: SURB supply is enabled");
1053 assert!(
1054 !balancer.return_path_estimate_is_stale(),
1055 "precondition: nothing has marked the return path yet"
1056 );
1057
1058 let replans = std::cell::Cell::new(0usize);
1059 let refills = std::cell::Cell::new(0usize);
1060
1061 deliver(®istry, paths, destination);
1063 let first = run_flush_tick(
1064 ®istry,
1065 &graph,
1066 0,
1067 &mut episodes,
1068 |_d| async {
1069 replans.set(replans.get() + 1);
1070 1usize
1071 },
1072 |_d| async {
1073 refills.set(refills.get() + 1);
1074 balancer.mark_return_path_degraded(grace);
1075 1usize
1076 },
1077 )
1078 .await;
1079 assert!(first.is_empty(), "a freshly delivering pair is not degraded");
1080 assert_eq!((replans.get(), refills.get()), (0, 0), "nothing to recover yet");
1081
1082 let mut steps = Vec::new();
1084 for _ in 0..SILENT_FLUSHES_BEFORE_DEGRADED {
1085 mint_only(®istry, paths, destination);
1086 deliver(®istry, heartbeat(), destination);
1087 steps = run_flush_tick(
1088 ®istry,
1089 &graph,
1090 0,
1091 &mut episodes,
1092 |_d| async {
1093 replans.set(replans.get() + 1);
1094 1usize
1095 },
1096 |_d| async {
1097 refills.set(refills.get() + 1);
1098 balancer.mark_return_path_degraded(grace);
1099 1usize
1100 },
1101 )
1102 .await;
1103 if !steps.is_empty() {
1104 break;
1105 }
1106 }
1107
1108 assert!(
1110 matches!(
1111 steps.as_slice(),
1112 [
1113 RecoveryStep::Replanned { destination: d1, moved: 1 },
1114 RecoveryStep::Refilled { destination: d2, sessions: 1 },
1115 ] if *d1 == destination && *d2 == destination
1116 ),
1117 "expected re-plan then refill for the silent destination, got {steps:?}"
1118 );
1119 assert_eq!(refills.get(), 1, "exactly one refill, behind the re-plan");
1120
1121 assert!(
1123 balancer.return_path_estimate_is_stale(),
1124 "the refill must mark the return path degraded"
1125 );
1126 assert!(
1127 !balancer.is_disabled(),
1128 "supply-vs-delivery: SURB supply is still healthy…"
1129 );
1130 assert_eq!(
1131 balancer.buffer_level.load(Ordering::Relaxed),
1132 1000,
1133 "…the buffer never left its target — the signal is delivery, not distress"
1134 );
1135
1136 deliver(®istry, paths, destination);
1139 let recovered = run_flush_tick(
1140 ®istry,
1141 &graph,
1142 0,
1143 &mut episodes,
1144 |_d| async {
1145 replans.set(replans.get() + 1);
1146 1usize
1147 },
1148 |_d| async {
1149 refills.set(refills.get() + 1);
1150 balancer.mark_return_path_degraded(grace);
1151 1usize
1152 },
1153 )
1154 .await;
1155 assert!(
1156 recovered.is_empty(),
1157 "once a reply arrives the return path is no longer named silent, got {recovered:?}"
1158 );
1159 assert_eq!(refills.get(), 1, "no second refill after the path recovered");
1160 }
1161
1162 #[test]
1172 fn one_destination_should_not_be_replanned_again_while_a_new_path_is_settling() {
1173 let (graph, me, peers) = graph_with(2);
1174 let destination = peers[0];
1175 let registry = registry_at_slot_zero();
1176
1177 let first = legs();
1178 let second = ForwardAndReturnPath {
1179 forward: [0, 4, 2, 0, 0],
1180 reply: [2, 4, 0, 0, 0],
1181 };
1182 assert_ne!(
1183 first, second,
1184 "the two pairs must be distinct for this to test anything"
1185 );
1186
1187 deliver(®istry, first, destination);
1188 deliver(®istry, second, destination);
1189 assert!(flush(®istry).is_empty());
1190
1191 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
1194 mint_only(®istry, first, destination);
1195 deliver(®istry, second, destination);
1196 assert!(flush(®istry).is_empty());
1197 }
1198 mint_only(®istry, first, destination);
1199 deliver(®istry, second, destination);
1200 assert_eq!(
1201 vec![destination],
1202 flush(®istry),
1203 "the first silence must be acted on"
1204 );
1205
1206 const WINDOW: u32 = 4 * SILENT_FLUSHES_BEFORE_DEGRADED;
1209 let mut reports = 0;
1210 for _ in 0..WINDOW {
1211 mint_only(®istry, first, destination);
1212 mint_only(®istry, second, destination);
1213 deliver(®istry, heartbeat(), destination);
1214 reports += flush(®istry).len();
1215 }
1216
1217 assert!(
1225 reports <= 3,
1226 "a destination must not be re-planned faster than a new path can settle: {reports} re-plans in {WINDOW} \
1227 flushes"
1228 );
1229
1230 let _ = (graph, me);
1231 }
1232
1233 #[test]
1239 fn a_relayer_still_delivering_elsewhere_should_not_be_blamed() {
1240 let (graph, me, peers) = graph_with(3);
1241 let destination = peers[0];
1242 let registry = registry_at_slot_zero();
1243
1244 let delivering = legs();
1246 let silent = ForwardAndReturnPath {
1247 forward: [0, 4, 2, 0, 0],
1248 reply: [2, 1, 0, 0, 0],
1249 };
1250 assert_ne!(delivering, silent, "the pairs must be distinct keys");
1251
1252 deliver(®istry, silent, destination);
1253 assert!(flush(®istry).is_empty());
1254
1255 for _ in 0..(3 * SILENT_FLUSHES_BEFORE_DEGRADED) {
1257 mint_only(®istry, silent, destination);
1258 deliver(®istry, delivering, destination);
1259 assert!(
1260 flush(®istry).is_empty(),
1261 "a relayer that is delivering elsewhere must not be blamed for this pair's silence"
1262 );
1263 }
1264
1265 let _ = (graph, me);
1266 }
1267
1268 #[test]
1270 fn silence_on_every_relayer_should_blame_none_of_them() {
1271 let (graph, me, peers) = graph_with(3);
1272 let destination = peers[0];
1273 let registry = registry_at_slot_zero();
1274
1275 let first = legs();
1276 let second = heartbeat();
1277
1278 for _ in 0..5 {
1279 deliver(®istry, first, destination);
1280 deliver(®istry, second, destination);
1281 assert!(flush(®istry).is_empty());
1282 }
1283
1284 for _ in 0..(3 * SILENT_FLUSHES_BEFORE_DEGRADED) {
1286 mint_only(®istry, first, destination);
1287 mint_only(®istry, second, destination);
1288 assert!(
1289 flush(®istry).is_empty(),
1290 "silence shared by every relayer names none of them"
1291 );
1292 }
1293
1294 let _ = (graph, me);
1295 }
1296
1297 #[test]
1305 fn a_peer_that_goes_quiet_should_not_be_mistaken_for_a_dead_return_path() {
1306 let (graph, me, peers) = graph_with(2);
1307 let destination = peers[0];
1308 let registry = registry_at_slot_zero();
1309
1310 let first = legs();
1311 let second = heartbeat();
1312
1313 for _ in 0..10 {
1315 deliver(®istry, first, destination);
1316 deliver(®istry, second, destination);
1317 assert!(flush(®istry).is_empty());
1318 }
1319
1320 for _ in 0..(4 * SILENT_FLUSHES_BEFORE_DEGRADED) {
1323 mint_only(®istry, first, destination);
1324 mint_only(®istry, second, destination);
1325 assert!(
1326 flush(®istry).is_empty(),
1327 "a quiet peer is not a dead return path, however long it stays quiet"
1328 );
1329 }
1330
1331 let _ = (graph, me);
1332 }
1333
1334 #[test]
1335 fn a_pair_that_never_delivered_should_never_be_called_degraded() {
1336 let (graph, me, peers) = graph_with(1);
1337 let destination = peers[0];
1338 let registry = registry_at_slot_zero();
1339 let paths = legs();
1340
1341 for _ in 0..SILENT_FLUSHES_BEFORE_DEGRADED + 3 {
1345 registry.record_expected(paths, MIN_EXPECTED_FOR_SILENCE * 10, destination);
1346 assert!(
1347 flush(®istry).is_empty(),
1348 "a pair that has never returned a reply is too young to call dead"
1349 );
1350 }
1351 let _ = (graph, me);
1352 }
1353
1354 #[test]
1355 fn a_single_reply_should_clear_the_silence() {
1356 let (graph, me, peers) = graph_with(1);
1357 let destination = peers[0];
1358 let registry = registry_at_slot_zero();
1359 let paths = legs();
1360
1361 deliver(®istry, paths, destination);
1362 assert!(flush(®istry).is_empty());
1363
1364 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
1365 mint_only(®istry, paths, destination);
1366 assert!(flush(®istry).is_empty());
1367 }
1368
1369 deliver(®istry, paths, destination);
1372 assert!(flush(®istry).is_empty());
1373 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
1374 mint_only(®istry, paths, destination);
1375 assert!(flush(®istry).is_empty());
1376 }
1377 let _ = (graph, me);
1378 }
1379
1380 #[test]
1381 fn a_degraded_pair_should_not_fire_again_on_the_next_flush() {
1382 let (graph, me, peers) = graph_with(1);
1383 let destination = peers[0];
1384 let registry = registry_at_slot_zero();
1385 let paths = legs();
1386
1387 deliver(®istry, paths, destination);
1388 flush(®istry);
1389 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
1390 mint_only(®istry, paths, destination);
1391 deliver(®istry, heartbeat(), destination);
1392 flush(®istry);
1393 }
1394 mint_only(®istry, paths, destination);
1395 deliver(®istry, heartbeat(), destination);
1396 assert_eq!(vec![destination], flush(®istry));
1397
1398 for _ in 1..SILENT_FLUSHES_BEFORE_DEGRADED {
1401 mint_only(®istry, paths, destination);
1402 deliver(®istry, heartbeat(), destination);
1403 assert!(flush(®istry).is_empty());
1404 }
1405 let _ = (graph, me);
1406 }
1407
1408 #[test]
1409 fn an_idle_path_should_never_be_called_degraded() {
1410 let (graph, me, peers) = graph_with(1);
1411 let destination = peers[0];
1412 let registry = registry_at_slot_zero();
1413 let paths = legs();
1414
1415 deliver(®istry, paths, destination);
1416 assert!(flush(®istry).is_empty());
1417
1418 for _ in 0..SILENT_FLUSHES_BEFORE_DEGRADED + 2 {
1421 registry.record_expected(paths, MIN_EXPECTED_FOR_SILENCE - 1, destination);
1422 assert!(flush(®istry).is_empty());
1423 }
1424 let _ = (graph, me);
1425 }
1426
1427 #[test]
1428 fn a_reply_on_a_surb_we_never_minted_should_be_ignored() {
1429 let (graph, ..) = graph_with(1);
1430 let recorder = recorder(graph);
1431
1432 recorder.on_replied(&surb_id(9));
1433
1434 assert!(recorder.registry.drain().is_empty());
1435 }
1436}