1use std::sync::Arc;
2
3use futures::{FutureExt, SinkExt, StreamExt};
4use futures_concurrency::stream::StreamExt as _;
5use hopr_api::{
6 ct::{ProbeRouting, ProbingTrafficGeneration},
7 graph::{EdgeTransportTelemetry, NetworkGraphError, NetworkGraphUpdate, NetworkGraphView},
8 types::{
9 crypto::types::OffchainPublicKey, crypto_random::Randomizable, internal::prelude::*,
10 primitive::traits::AsUnixTimestamp,
11 },
12};
13use hopr_protocol_app::{
14 prelude::{ApplicationDataIn, ApplicationDataOut, OutgoingPacketInfo, ReservedTag},
15 v1::Tag,
16};
17use hopr_transport_tag_allocator::{AllocatedTag, TagAllocator};
18use hopr_utils::{platform::time::native::current_time, runtime::AbortableList};
19
20use crate::{
21 HoprProbeProcess,
22 config::ProbeConfig,
23 content::Message,
24 ping::PingQueryReplier,
25 types::{NeighborProbe, NeighborTelemetry, PathTelemetry},
26};
27
28type CacheNeighborKey = (HoprPseudonym, NeighborProbe);
29type CacheNeighborValue = (Box<NodeId>, std::time::Duration, Option<PingQueryReplier>);
30
31pub enum ProbeDispatch {
33 Consumed,
35 Passthrough(HoprPseudonym, ApplicationDataIn),
37}
38
39#[derive(Clone)]
45pub struct ProbeClassifierState<G> {
46 active_neighbor_probes: moka::future::Cache<CacheNeighborKey, CacheNeighborValue>,
47 active_path_probes: moka::future::Cache<Tag, (PathTelemetry, Arc<AllocatedTag>)>,
48 network_graph: G,
49}
50
51impl<G> ProbeClassifierState<G>
52where
53 G: NetworkGraphUpdate + Clone + Send + Sync + 'static,
54{
55 pub async fn classify<T>(
60 &self,
61 mut push_to_network: T,
62 pseudonym: HoprPseudonym,
63 in_data: ApplicationDataIn,
64 ) -> ProbeDispatch
65 where
66 T: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Unpin + Send + 'static,
67 T::Error: Send,
68 {
69 let tag: Tag = in_data.data.application_tag;
70
71 if let Some((path_telemetry, _allocated_tag)) = self.active_path_probes.remove(&tag).await {
72 tracing::debug!(%tag, "loopback probe successfully received");
73 self.network_graph
74 .record_edge::<NeighborTelemetry, PathTelemetry>(hopr_api::graph::MeasurableEdge::Probe(Ok(
75 EdgeTransportTelemetry::Loopback(path_telemetry),
76 )));
77 } else if tag == ReservedTag::Ping.into() {
78 let message: anyhow::Result<Message> = in_data
79 .data
80 .try_into()
81 .map_err(|e| anyhow::anyhow!("failed to convert data into message: {e}"));
82
83 match message {
84 Ok(message) => match message {
85 Message::Telemetry(_) => {
86 tracing::warn!(%pseudonym, "received telemetry on reserved ping tag, ignoring");
87 }
88 Message::Probe(NeighborProbe::Ping(ping)) => {
89 tracing::debug!(%pseudonym, nonce = const_hex::encode(ping), "received ping");
90 tracing::trace!(%pseudonym, nonce = const_hex::encode(ping), "wrapping a pong in the found SURB");
91
92 let message = Message::Probe(NeighborProbe::Pong(ping));
93 if let Ok(data) = message.try_into() {
94 let routing = DestinationRouting::Return(pseudonym.into());
95 let data = ApplicationDataOut::with_no_packet_info(data);
96 if let Err(_error) = push_to_network.send((routing, data)).await {
97 tracing::error!(%pseudonym, "failed to send back a pong");
98 }
99 } else {
100 tracing::error!(%pseudonym, "failed to convert pong message into data");
101 }
102 }
103 Message::Probe(NeighborProbe::Pong(pong)) => {
104 tracing::debug!(%pseudonym, nonce = const_hex::encode(pong), "received pong");
105 if let Some((peer, start, replier)) = self
106 .active_neighbor_probes
107 .remove(&(pseudonym, NeighborProbe::Ping(pong)))
108 .await
109 {
110 let latency = current_time().as_unix_timestamp().saturating_sub(start);
111
112 if let NodeId::Offchain(opk) = peer.as_ref() {
113 tracing::debug!(%pseudonym, nonce = const_hex::encode(pong), latency_ms = latency.as_millis(), "probe successful");
114 self.network_graph.record_edge::<NeighborTelemetry, PathTelemetry>(
115 hopr_api::graph::MeasurableEdge::Probe(Ok(EdgeTransportTelemetry::Neighbor(
116 NeighborTelemetry {
117 peer: *opk,
118 rtt: latency,
119 },
120 ))),
121 )
122 } else {
123 tracing::warn!(%pseudonym, nonce = const_hex::encode(pong), latency_ms = latency.as_millis(), "probe successful to non-offchain peer");
124 }
125
126 if let Some(replier) = replier {
127 replier.notify(Ok(latency));
128 }
129 } else {
130 tracing::warn!(%pseudonym, nonce = const_hex::encode(pong), possible_reasons = "[timeout, adversary]", "received pong for unknown probe");
131 }
132 }
133 },
134 Err(error) => tracing::error!(%pseudonym, %error, "cannot deserialize message"),
135 }
136 } else {
137 return ProbeDispatch::Passthrough(pseudonym, in_data);
138 }
139
140 ProbeDispatch::Consumed
141 }
142
143 pub fn filter_stream<T, S>(
146 self,
147 push_to_network: T,
148 stream: S,
149 ) -> impl futures::Stream<Item = (HoprPseudonym, ApplicationDataIn)>
150 where
151 T: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Clone + Unpin + Send + Sync + 'static,
152 T::Error: Send,
153 S: futures::Stream<Item = (HoprPseudonym, ApplicationDataIn)>,
154 {
155 use futures::StreamExt;
156 stream.filter_map(move |(pseudonym, data)| {
157 let state = self.clone();
158 let push = push_to_network.clone();
159 async move {
160 match state.classify(push, pseudonym, data).await {
161 ProbeDispatch::Consumed => None,
162 ProbeDispatch::Passthrough(ps, d) => Some((ps, d)),
163 }
164 }
165 })
166 }
167}
168
169pub struct Probe {
174 cfg: ProbeConfig,
176 tag_allocator: Arc<dyn TagAllocator + Send + Sync>,
178}
179
180impl Probe {
181 pub fn new(cfg: ProbeConfig, tag_allocator: Arc<dyn TagAllocator + Send + Sync>) -> Self {
182 Self { cfg, tag_allocator }
183 }
184
185 pub async fn continuously_scan<T, V, Tr, G>(
191 self,
192 api_out: T, manual_events: V, probing_traffic_generator: Tr,
195 network_graph: G,
196 ) -> (AbortableList<HoprProbeProcess>, ProbeClassifierState<G>)
197 where
198 T: futures::Sink<(DestinationRouting, ApplicationDataOut)> + Clone + Send + Sync + Unpin + 'static,
199 T::Error: Send,
200 V: futures::Stream<Item = (OffchainPublicKey, PingQueryReplier)> + Send + 'static,
201 Tr: ProbingTrafficGeneration + Send + Sync + 'static,
202 G: NetworkGraphView + NetworkGraphUpdate + Clone + Send + Sync + 'static,
203 {
204 let max_parallel_probes = self.cfg.max_parallel_probes;
205
206 let probing_routes = probing_traffic_generator.build();
207
208 let network_graph_internal_neighbor = network_graph.clone();
210 let network_graph_internal_path = network_graph.clone();
211 let timeout = self.cfg.timeout;
212 let active_neighbor_probes: moka::future::Cache<CacheNeighborKey, CacheNeighborValue> =
213 moka::future::Cache::builder()
214 .time_to_live(timeout)
215 .max_capacity(100_000)
216 .async_eviction_listener(
217 move |k: Arc<CacheNeighborKey>,
218 v: CacheNeighborValue,
219 cause|
220 -> moka::notification::ListenerFuture {
221 if matches!(cause, moka::notification::RemovalCause::Expired) {
222 let store = network_graph_internal_neighbor.clone();
224 let (peer, _start, notifier) = v;
225
226 tracing::debug!(%peer, pseudonym = %k.0, probe = %k.1, reason = "timeout", "neighbor probe failed");
227 if let Some(replier) = notifier {
228 if matches!(peer.as_ref(), NodeId::Offchain(_)) {
229 replier.notify(Err(()));
230 } else {
231 tracing::warn!(
232 reason = "non-offchain peer",
233 "cannot notify timeout for non-offchain peer"
234 );
235 }
236 };
237
238 if let NodeId::Offchain(opk) = peer.as_ref() {
239 let opk: OffchainPublicKey = *opk;
240 store
241 .record_edge::<NeighborTelemetry, PathTelemetry>(
242 hopr_api::graph::MeasurableEdge::Probe(Err(
243 NetworkGraphError::ProbeNeighborTimeout(Box::new(opk)),
244 )),
245 );
246 futures::FutureExt::boxed(futures::future::ready(()))
247
248 } else {
249 futures::FutureExt::boxed(futures::future::ready(()))
250 }
251 } else {
252 futures::FutureExt::boxed(futures::future::ready(()))
254 }
255 },
256 )
257 .build();
258
259 let active_path_probes: moka::future::Cache<Tag, (PathTelemetry, Arc<AllocatedTag>)> =
260 moka::future::Cache::builder()
261 .time_to_live(timeout)
262 .max_capacity(100_000)
263 .async_eviction_listener(
264 move |tag: Arc<Tag>,
265 (path, _allocated_tag): (PathTelemetry, Arc<AllocatedTag>),
266 cause|
267 -> moka::notification::ListenerFuture {
268 if matches!(cause, moka::notification::RemovalCause::Expired) {
269 let store = network_graph_internal_path.clone();
271
272 tracing::debug!(%tag, reason = "timeout", "loopback probe failed");
273
274 store.record_edge::<NeighborTelemetry, PathTelemetry>(
275 hopr_api::graph::MeasurableEdge::Probe(Err(NetworkGraphError::ProbeLoopbackTimeout(
276 path,
277 ))),
278 );
279 futures::FutureExt::boxed(futures::future::ready(()))
280 } else {
281 futures::FutureExt::boxed(futures::future::ready(()))
283 }
284 },
285 )
286 .build();
287
288 let push_to_network = api_out.clone();
289
290 let mut processes = AbortableList::default();
291
292 let direct_neighbors =
294 probing_routes
295 .map(|peer| (peer, None))
296 .merge(manual_events.filter_map(|(peer, notifier)| async move {
297 let routing = DestinationRouting::Forward {
298 destination: Box::new(peer.into()),
299 pseudonym: Some(HoprPseudonym::random()),
300 forward_options: RoutingOptions::Hops(0.try_into().expect("0 is a valid u8")),
301 return_options: Some(RoutingOptions::Hops(0.try_into().expect("0 is a valid u8"))),
302 };
303 Some((ProbeRouting::Neighbor(routing), Some(notifier)))
304 }));
305
306 let tag_allocator = self.tag_allocator.clone();
307 let classifier_neighbor_probes = active_neighbor_probes.clone();
308 let classifier_path_probes = active_path_probes.clone();
309 let emit_diag = hopr_utils::runtime::diagnostics::ConcurrentDiagnostics::new(
310 "probe_emit_for_each_concurrent",
311 module_path!(),
312 file!(),
313 line!(),
314 );
315 processes.insert(
316 HoprProbeProcess::Emit,
317 hopr_utils::spawn_as_abortable_named!("probe_emit", async move {
318 direct_neighbors
319 .for_each_concurrent(max_parallel_probes, move |(peer, notifier)| {
320 let active_neighbor_probes = active_neighbor_probes.clone();
321 let active_path_probes = active_path_probes.clone();
322 let push_to_network = push_to_network.clone();
323 let tag_allocator = tag_allocator.clone();
324 let emit_diag = emit_diag.clone();
325
326 emit_diag.wrap(|| async move {
327 match peer {
328 ProbeRouting::Neighbor(DestinationRouting::Forward {
329 destination,
330 pseudonym,
331 forward_options,
332 return_options,
333 }) => {
334 let nonce = NeighborProbe::random_nonce();
335
336 let message = Message::Probe(nonce);
337
338 if let Ok(data) = message.try_into() {
339 let routing = DestinationRouting::Forward {
340 destination: destination.clone(),
341 pseudonym,
342 forward_options,
343 return_options,
344 };
345 let data = ApplicationDataOut {
348 data,
349 packet_info: Some(OutgoingPacketInfo {
350 max_surbs_in_packet: 1,
351 ..Default::default()
352 }),
353 };
354 let mut push_to_network = push_to_network.clone();
355
356 if let Err(_error) = push_to_network.send((routing, data)).await {
357 tracing::error!("failed to send out a ping");
358 } else {
359 active_neighbor_probes
360 .insert(
361 (
362 pseudonym
363 .expect("the pseudonym must be present in Forward routing"),
364 nonce,
365 ),
366 (destination, current_time().as_unix_timestamp(), notifier),
367 )
368 .await;
369 }
370 } else {
371 tracing::error!("failed to convert ping message into data");
372 }
373 }
374 ProbeRouting::Neighbor(DestinationRouting::Return(_surb_matcher)) => tracing::error!(
375 error = "logical error",
376 "resolved transport routing is not forward"
377 ),
378 ProbeRouting::Looping((routing, path_id)) => {
379 let message = Message::Telemetry(PathTelemetry {
380 id: hopr_api::types::crypto_random::random_bytes(),
381 path: std::array::from_fn(|i| path_id[i / 8].to_le_bytes()[i % 8]),
382 timestamp: std::time::SystemTime::now()
383 .duration_since(std::time::UNIX_EPOCH)
384 .unwrap_or_default()
385 .as_millis(),
386 });
387
388 if let Some(allocated_tag) = tag_allocator.allocate() {
389 let tag_value = allocated_tag.value();
390
391 if let Ok(packet) = hopr_protocol_app::prelude::ApplicationData::new(
392 tag_value,
393 message.to_bytes().as_ref(),
394 ) {
395 let mut push_to_network = push_to_network.clone();
396
397 if let Err(_error) = push_to_network
400 .send((
401 routing,
402 ApplicationDataOut {
403 data: packet,
404 packet_info: Some(OutgoingPacketInfo {
405 max_surbs_in_packet: 0,
406 ..Default::default()
407 }),
408 },
409 ))
410 .await
411 {
412 tracing::error!("failed to send out a ping");
413 } else {
414 if let Message::Telemetry(telemetry) = message {
416 active_path_probes
417 .insert(tag_value.into(), (telemetry, Arc::new(allocated_tag)))
418 .await;
419 }
420 }
421 } else {
422 tracing::error!("failed to construct data for path telemetry")
423 }
424 } else {
425 tracing::warn!("probing telemetry tag pool exhausted, skipping loopback probe");
426 }
427 }
428 }
429 })
430 })
431 .inspect(|_| {
432 tracing::warn!(
433 task = "transport (probe - generate outgoing)",
434 "long-running background task finished"
435 )
436 })
437 .await;
438 }),
439 );
440
441 let classifier = ProbeClassifierState {
442 active_neighbor_probes: classifier_neighbor_probes,
443 active_path_probes: classifier_path_probes,
444 network_graph,
445 };
446
447 (processes, classifier)
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use std::{collections::VecDeque, sync::RwLock, time::Duration};
454
455 use async_trait::async_trait;
456 use futures::future::BoxFuture;
457 use hopr_api::{
458 graph::{
459 EdgeLinkObservable, MeasurableEdge, NetworkGraphError,
460 traits::{EdgeNetworkObservableRead, EdgeObservableRead, EdgeObservableWrite, EdgeProtocolObservable},
461 },
462 types::crypto::keypairs::{ChainKeypair, Keypair, OffchainKeypair},
463 };
464 use hopr_protocol_app::prelude::{ApplicationData, ReservedTag, Tag};
465
466 use super::*;
467 use crate::errors::ProbeError;
468
469 lazy_static::lazy_static!(
470 static ref OFFCHAIN_KEYPAIR: OffchainKeypair = OffchainKeypair::random();
471 static ref ONCHAIN_KEYPAIR: ChainKeypair = ChainKeypair::random();
472 static ref NEIGHBOURS: Vec<OffchainPublicKey> = vec![
473 *OffchainKeypair::random().public(),
474 *OffchainKeypair::random().public(),
475 *OffchainKeypair::random().public(),
476 *OffchainKeypair::random().public(),
477 ];
478 );
479
480 #[derive(Debug, Clone, Copy, Default)]
482 pub struct TestEdgeTransportObservations;
483
484 impl EdgeLinkObservable for TestEdgeTransportObservations {
485 fn record(&mut self, _latency: std::result::Result<Duration, ()>) {}
486
487 fn average_latency(&self) -> Option<Duration> {
488 None
489 }
490
491 fn average_probe_rate(&self) -> Option<f64> {
494 Some(1.0)
495 }
496
497 fn score(&self) -> Option<f64> {
498 Some(1.0)
499 }
500 }
501
502 impl EdgeNetworkObservableRead for TestEdgeTransportObservations {
503 fn is_connected(&self) -> Option<bool> {
504 Some(true)
505 }
506 }
507
508 impl EdgeProtocolObservable for TestEdgeTransportObservations {
509 fn balance(&self) -> Option<hopr_api::graph::traits::Balance> {
510 None
511 }
512 }
513
514 impl hopr_api::graph::EdgeImmediateProtocolObservable for TestEdgeTransportObservations {
515 fn ack_rate(&self) -> Option<f64> {
516 None
517 }
518 }
519
520 #[derive(Debug, Clone, Copy, Default)]
521 pub struct TestEdgeObservations;
522
523 impl EdgeObservableWrite for TestEdgeObservations {
524 fn record(&mut self, _measurement: hopr_api::graph::traits::EdgeWeightType) {}
525 }
526
527 impl EdgeObservableRead for TestEdgeObservations {
528 type ImmediateMeasurement = TestEdgeTransportObservations;
529 type IntermediateMeasurement = TestEdgeTransportObservations;
530
531 fn last_update(&self) -> std::time::Duration {
532 std::time::SystemTime::now()
533 .duration_since(std::time::UNIX_EPOCH)
534 .unwrap_or_default()
535 }
536
537 fn immediate_qos(&self) -> Option<&Self::ImmediateMeasurement> {
538 None
539 }
540
541 fn intermediate_qos(&self) -> Option<&Self::IntermediateMeasurement> {
542 None
543 }
544
545 fn score(&self) -> Option<f64> {
546 Some(1.0)
547 }
548 }
549
550 #[derive(Debug, Clone)]
551 pub struct PeerStore {
552 me: OffchainPublicKey,
553 get_peers: Arc<RwLock<VecDeque<Vec<OffchainPublicKey>>>>,
554 #[allow(clippy::type_complexity)]
555 on_finished: Arc<RwLock<Vec<(OffchainPublicKey, crate::errors::Result<Duration>)>>>,
556 }
557
558 impl NetworkGraphUpdate for PeerStore {
559 fn record_edge<N, P>(&self, telemetry: MeasurableEdge<N, P>)
560 where
561 N: hopr_api::graph::MeasurablePeer + Send + Clone,
562 P: hopr_api::graph::MeasurablePath + Send + Clone,
563 {
564 let mut on_finished = self.on_finished.write().unwrap();
565
566 match telemetry {
567 hopr_api::graph::MeasurableEdge::Probe(Ok(EdgeTransportTelemetry::Neighbor(neighbor_telemetry))) => {
568 let peer: OffchainPublicKey = *neighbor_telemetry.peer();
569 let duration = neighbor_telemetry.rtt();
570 on_finished.push((peer, Ok(duration)));
571 }
572 hopr_api::graph::MeasurableEdge::Probe(Err(NetworkGraphError::ProbeNeighborTimeout(peer))) => {
573 on_finished.push((
574 *peer.as_ref(),
575 Err(ProbeError::TrafficError(NetworkGraphError::ProbeNeighborTimeout(peer))),
576 ));
577 }
578 _ => panic!("unexpected telemetry type, unimplemented"),
579 }
580 }
581
582 fn record_node<N>(&self, _node: N)
583 where
584 N: hopr_api::graph::MeasurableNode + Send + Clone,
585 {
586 unimplemented!()
587 }
588
589 fn set_ticket_face_value(&self, _ticket_face_value: hopr_api::graph::traits::Balance) {}
590 }
591
592 #[async_trait]
593 impl NetworkGraphView for PeerStore {
594 type NodeId = OffchainPublicKey;
595 type Observed = TestEdgeObservations;
596
597 fn identity(&self) -> &OffchainPublicKey {
598 &self.me
599 }
600
601 fn ticket_face_value(&self) -> Option<hopr_api::graph::traits::Balance> {
602 None
603 }
604
605 fn node_count(&self) -> usize {
606 self.get_peers.read().unwrap().front().map_or(0, |v| v.len())
607 }
608
609 fn contains_node(&self, _key: &OffchainPublicKey) -> bool {
610 false
611 }
612
613 fn nodes(&self) -> futures::stream::BoxStream<'static, OffchainPublicKey> {
615 let mut get_peers = self.get_peers.write().unwrap();
616 Box::pin(futures::stream::iter(get_peers.pop_front().unwrap_or_default()))
617 }
618
619 fn edge(&self, _src: &OffchainPublicKey, _dest: &OffchainPublicKey) -> Option<TestEdgeObservations> {
620 Some(TestEdgeObservations)
621 }
622
623 fn path_slot(&self, _key: &OffchainPublicKey) -> Option<u64> {
625 None
626 }
627 }
628
629 type TestClassifier = ProbeClassifierState<PeerStore>;
630
631 struct TestInterface {
632 probe_classifier: TestClassifier,
633 from_probing_to_network_rx: futures::channel::mpsc::Receiver<(DestinationRouting, ApplicationDataOut)>,
634 from_probing_to_network_tx: futures::channel::mpsc::Sender<(DestinationRouting, ApplicationDataOut)>,
635 manual_probe_tx: futures::channel::mpsc::Sender<(OffchainPublicKey, PingQueryReplier)>,
636 }
637
638 async fn test_with_probing<F, Fut>(cfg: ProbeConfig, store: PeerStore, test: F) -> anyhow::Result<()>
639 where
640 Fut: std::future::Future<Output = anyhow::Result<()>>,
641 F: Fn(TestInterface) -> Fut + Send + Sync + 'static,
642 {
643 let tag_allocators = hopr_transport_tag_allocator::create_allocators(
644 ReservedTag::range().end..u16::MAX as u64 + 1,
645 [
646 (hopr_transport_tag_allocator::Usage::Session, 2048),
647 (hopr_transport_tag_allocator::Usage::SessionTerminalTelemetry, 4000),
648 (hopr_transport_tag_allocator::Usage::ProvingTelemetry, 10000),
649 ],
650 )
651 .expect("tag allocators should be created");
652 let probing_allocator = tag_allocators
653 .into_iter()
654 .find_map(|(u, alloc)| matches!(u, hopr_transport_tag_allocator::Usage::ProvingTelemetry).then_some(alloc))
655 .expect("probing allocator should exist");
656
657 let probe = Probe::new(cfg, probing_allocator);
658
659 let (from_probing_to_network_tx, from_probing_to_network_rx) =
660 futures::channel::mpsc::channel::<(DestinationRouting, ApplicationDataOut)>(100);
661
662 let (manual_probe_tx, manual_probe_rx) =
663 futures::channel::mpsc::channel::<(OffchainPublicKey, PingQueryReplier)>(100);
664
665 let (jhs, probe_classifier) = probe
666 .continuously_scan(
667 from_probing_to_network_tx.clone(),
668 manual_probe_rx,
669 TestProbeStrategy::ImmediateNeighbor { store: store.clone() },
670 store,
671 )
672 .await;
673
674 let interface = TestInterface {
675 probe_classifier,
676 from_probing_to_network_rx,
677 from_probing_to_network_tx,
678 manual_probe_tx,
679 };
680
681 let result = test(interface).await;
682
683 jhs.abort_all();
684
685 result
686 }
687
688 const NO_PROBE_PASSES: f64 = 0.0;
689 const ALL_PROBES_PASS: f64 = 1.0;
690
691 fn concurrent_classify(
694 delay: Option<std::time::Duration>,
695 pass_rate: f64,
696 classifier: TestClassifier,
697 push_to_network: futures::channel::mpsc::Sender<(DestinationRouting, ApplicationDataOut)>,
698 ) -> impl Fn((DestinationRouting, ApplicationDataOut)) -> BoxFuture<'static, ()> {
699 debug_assert!(
700 (NO_PROBE_PASSES..=ALL_PROBES_PASS).contains(&pass_rate),
701 "Pass rate must be between {NO_PROBE_PASSES} and {ALL_PROBES_PASS}"
702 );
703
704 move |(path, data_out): (DestinationRouting, ApplicationDataOut)| -> BoxFuture<'static, ()> {
705 let classifier = classifier.clone();
706 let push_to_network = push_to_network.clone();
707
708 Box::pin(async move {
709 if let DestinationRouting::Forward { pseudonym, .. } = path {
710 let message: Message = data_out.data.try_into().expect("failed to convert data into message");
711 if let Message::Probe(NeighborProbe::Ping(ping)) = message {
712 let pong_message = Message::Probe(NeighborProbe::Pong(ping));
713
714 if let Some(delay) = delay {
715 tokio::time::sleep(delay).await;
716 }
717
718 if rand::random_range(NO_PROBE_PASSES..=ALL_PROBES_PASS) < pass_rate {
719 let pseudonym = pseudonym.expect("the pseudonym is always known from cache");
720 classifier
721 .classify(
722 push_to_network,
723 pseudonym,
724 ApplicationDataIn {
725 data: pong_message
726 .try_into()
727 .expect("failed to convert pong message into data"),
728 packet_info: Default::default(),
729 },
730 )
731 .await;
732 }
733 }
734 };
735 })
736 }
737 }
738
739 #[tokio::test]
740 async fn probe_should_record_value_for_manual_neighbor_probe() -> anyhow::Result<()> {
742 let cfg = ProbeConfig {
743 timeout: std::time::Duration::from_millis(5),
744 interval: std::time::Duration::from_secs(0),
745 ..Default::default()
746 };
747
748 let store = PeerStore {
749 me: *OFFCHAIN_KEYPAIR.public(),
750 get_peers: Arc::new(RwLock::new(VecDeque::new())),
751 on_finished: Arc::new(RwLock::new(Vec::new())),
752 };
753
754 test_with_probing(cfg, store, move |iface: TestInterface| async move {
755 let mut manual_probe_tx = iface.manual_probe_tx;
756 let from_probing_to_network_rx = iface.from_probing_to_network_rx;
757 let from_probing_to_network_tx = iface.from_probing_to_network_tx;
758 let probe_classifier = iface.probe_classifier;
759
760 let (tx, mut rx) = futures::channel::mpsc::channel::<std::result::Result<Duration, ()>>(128);
761 manual_probe_tx.send((NEIGHBOURS[0], PingQueryReplier::new(tx))).await?;
762
763 let _jh: hopr_utils::runtime::prelude::JoinHandle<()> = tokio::spawn(async move {
764 from_probing_to_network_rx
765 .for_each_concurrent(
766 cfg.max_parallel_probes + 1,
767 concurrent_classify(None, ALL_PROBES_PASS, probe_classifier, from_probing_to_network_tx),
768 )
769 .await;
770 });
771
772 let _duration = tokio::time::timeout(std::time::Duration::from_secs(1), rx.next())
773 .await?
774 .ok_or_else(|| anyhow::anyhow!("Probe did not return a result in time"))?
775 .map_err(|_| anyhow::anyhow!("Probe failed"))?;
776
777 Ok(())
778 })
779 .await
780 }
781
782 #[tokio::test]
783 async fn probe_should_record_failure_on_manual_fail() -> anyhow::Result<()> {
785 let cfg = ProbeConfig {
786 timeout: std::time::Duration::from_millis(5),
787 interval: std::time::Duration::from_secs(0),
788 ..Default::default()
789 };
790
791 let store = PeerStore {
792 me: *OFFCHAIN_KEYPAIR.public(),
793 get_peers: Arc::new(RwLock::new(VecDeque::new())),
794 on_finished: Arc::new(RwLock::new(Vec::new())),
795 };
796
797 test_with_probing(cfg, store, move |iface: TestInterface| async move {
798 let mut manual_probe_tx = iface.manual_probe_tx;
799 let from_probing_to_network_rx = iface.from_probing_to_network_rx;
800 let from_probing_to_network_tx = iface.from_probing_to_network_tx;
801 let probe_classifier = iface.probe_classifier;
802
803 let (tx, mut rx) = futures::channel::mpsc::channel::<std::result::Result<Duration, ()>>(128);
804 manual_probe_tx.send((NEIGHBOURS[0], PingQueryReplier::new(tx))).await?;
805
806 let _jh: hopr_utils::runtime::prelude::JoinHandle<()> = tokio::spawn(async move {
807 from_probing_to_network_rx
808 .for_each_concurrent(
809 cfg.max_parallel_probes + 1,
810 concurrent_classify(None, NO_PROBE_PASSES, probe_classifier, from_probing_to_network_tx),
811 )
812 .await;
813 });
814
815 assert!(tokio::time::timeout(cfg.timeout * 2, rx.next()).await.is_err());
816
817 Ok(())
818 })
819 .await
820 }
821
822 #[tokio::test]
823 async fn probe_should_record_results_of_successful_automatically_generated_probes() -> anyhow::Result<()> {
825 let cfg = ProbeConfig {
826 timeout: std::time::Duration::from_millis(20),
827 max_parallel_probes: NEIGHBOURS.len(),
828 interval: std::time::Duration::from_secs(0),
829 ..Default::default()
830 };
831
832 let store = PeerStore {
833 me: *OFFCHAIN_KEYPAIR.public(),
834 get_peers: Arc::new(RwLock::new({
835 let mut neighbors = VecDeque::new();
836 neighbors.push_back(NEIGHBOURS.clone());
837 neighbors
838 })),
839 on_finished: Arc::new(RwLock::new(Vec::new())),
840 };
841
842 test_with_probing(cfg, store.clone(), move |iface: TestInterface| async move {
843 let from_probing_to_network_rx = iface.from_probing_to_network_rx;
844 let from_probing_to_network_tx = iface.from_probing_to_network_tx;
845 let probe_classifier = iface.probe_classifier;
846
847 let _jh: hopr_utils::runtime::prelude::JoinHandle<()> = tokio::spawn(async move {
848 from_probing_to_network_rx
849 .for_each_concurrent(
850 cfg.max_parallel_probes + 1,
851 concurrent_classify(None, ALL_PROBES_PASS, probe_classifier, from_probing_to_network_tx),
852 )
853 .await;
854 });
855
856 tokio::time::sleep(cfg.timeout * 3).await;
860
861 Ok(())
862 })
863 .await?;
864
865 assert_eq!(
866 store
867 .on_finished
868 .read()
869 .expect("should be lockable")
870 .iter()
871 .filter(|(_peer, result)| result.is_ok())
872 .count(),
873 NEIGHBOURS.len()
874 );
875
876 Ok(())
877 }
878
879 #[tokio::test]
880 async fn probe_should_record_results_of_timed_out_automatically_generated_probes() -> anyhow::Result<()> {
882 let cfg = ProbeConfig {
883 timeout: std::time::Duration::from_millis(10),
884 max_parallel_probes: NEIGHBOURS.len(),
885 interval: std::time::Duration::from_secs(0),
886 ..Default::default()
887 };
888
889 let store = PeerStore {
890 me: *OFFCHAIN_KEYPAIR.public(),
891 get_peers: Arc::new(RwLock::new({
892 let mut neighbors = VecDeque::new();
893 neighbors.push_back(NEIGHBOURS.clone());
894 neighbors
895 })),
896 on_finished: Arc::new(RwLock::new(Vec::new())),
897 };
898
899 let timeout = cfg.timeout * 2;
900
901 test_with_probing(cfg, store.clone(), move |iface: TestInterface| async move {
902 let from_probing_to_network_rx = iface.from_probing_to_network_rx;
903 let from_probing_to_network_tx = iface.from_probing_to_network_tx;
904 let probe_classifier = iface.probe_classifier;
905
906 let _jh: hopr_utils::runtime::prelude::JoinHandle<()> = tokio::spawn(async move {
907 from_probing_to_network_rx
908 .for_each_concurrent(
909 cfg.max_parallel_probes + 1,
910 concurrent_classify(
911 Some(timeout),
912 ALL_PROBES_PASS,
913 probe_classifier,
914 from_probing_to_network_tx,
915 ),
916 )
917 .await;
918 });
919
920 tokio::time::sleep(timeout * 2).await;
922
923 Ok(())
924 })
925 .await?;
926
927 assert_eq!(
928 store
929 .on_finished
930 .read()
931 .expect("should be lockable")
932 .iter()
933 .filter(|(_peer, result)| result.is_err())
934 .count(),
935 NEIGHBOURS.len()
936 );
937
938 Ok(())
939 }
940
941 #[tokio::test]
942 async fn probe_should_reply_with_pong_when_receiving_ping() -> anyhow::Result<()> {
943 use anyhow::Context;
944
945 let cfg = ProbeConfig {
946 timeout: std::time::Duration::from_millis(100),
947 interval: std::time::Duration::from_secs(10),
948 ..Default::default()
949 };
950
951 let store = PeerStore {
952 me: *OFFCHAIN_KEYPAIR.public(),
953 get_peers: Arc::new(RwLock::new(VecDeque::new())),
954 on_finished: Arc::new(RwLock::new(Vec::new())),
955 };
956
957 test_with_probing(cfg, store, move |iface: TestInterface| async move {
958 let probe_classifier = iface.probe_classifier;
959 let from_probing_to_network_tx = iface.from_probing_to_network_tx;
960 let mut from_probing_to_network_rx = iface.from_probing_to_network_rx;
961
962 let ping = NeighborProbe::random_nonce();
964 let ping_nonce = match ping {
965 NeighborProbe::Ping(n) => n,
966 _ => unreachable!(),
967 };
968 let ping_msg = Message::Probe(ping);
969 let app_data: ApplicationData = ping_msg.try_into().context("converting ping to ApplicationData")?;
970
971 let result = probe_classifier
973 .classify(
974 from_probing_to_network_tx,
975 HoprPseudonym::random(),
976 ApplicationDataIn {
977 data: app_data,
978 packet_info: Default::default(),
979 },
980 )
981 .await;
982 anyhow::ensure!(matches!(result, ProbeDispatch::Consumed), "ping should be consumed");
983
984 let (routing, data_out) = tokio::time::timeout(Duration::from_secs(2), from_probing_to_network_rx.next())
986 .await
987 .context("timeout waiting for pong")?
988 .context("probe should send pong reply")?;
989
990 anyhow::ensure!(
992 matches!(routing, DestinationRouting::Return(_)),
993 "pong should use Return routing, got: {routing:?}"
994 );
995
996 let response_msg: Message = data_out.data.try_into().context("converting response to Message")?;
998 anyhow::ensure!(
999 matches!(response_msg, Message::Probe(NeighborProbe::Pong(n)) if n == ping_nonce),
1000 "response should be Pong with matching nonce"
1001 );
1002
1003 Ok(())
1004 })
1005 .await
1006 }
1007
1008 #[tokio::test]
1009 async fn probe_should_pass_through_non_associated_tags() -> anyhow::Result<()> {
1011 let cfg = ProbeConfig {
1012 timeout: std::time::Duration::from_millis(20),
1013 interval: std::time::Duration::from_secs(0),
1014 ..Default::default()
1015 };
1016
1017 let store = PeerStore {
1018 me: *OFFCHAIN_KEYPAIR.public(),
1019 get_peers: Arc::new(RwLock::new({
1020 let mut neighbors = VecDeque::new();
1021 neighbors.push_back(NEIGHBOURS.clone());
1022 neighbors
1023 })),
1024 on_finished: Arc::new(RwLock::new(Vec::new())),
1025 };
1026
1027 test_with_probing(cfg, store.clone(), move |iface: TestInterface| async move {
1028 let probe_classifier = iface.probe_classifier;
1029 let from_probing_to_network_tx = iface.from_probing_to_network_tx;
1030
1031 let expected_data = ApplicationData::new(Tag::MAX, b"Hello, this is a test message!")?;
1032
1033 let result = probe_classifier
1034 .classify(
1035 from_probing_to_network_tx,
1036 HoprPseudonym::random(),
1037 ApplicationDataIn {
1038 data: expected_data.clone(),
1039 packet_info: Default::default(),
1040 },
1041 )
1042 .await;
1043
1044 match result {
1045 ProbeDispatch::Passthrough(_, actual) => assert_eq!(actual.data, expected_data),
1046 ProbeDispatch::Consumed => anyhow::bail!("expected Passthrough, got Consumed"),
1047 }
1048
1049 Ok(())
1050 })
1051 .await
1052 }
1053
1054 #[derive(Clone)]
1058 enum TestProbeStrategy {
1059 ManualNeighbor,
1060 ImmediateNeighbor {
1061 store: PeerStore,
1062 },
1063 OneShotLoopback {
1064 routing: DestinationRouting,
1065 path_id: hopr_api::types::internal::routing::PathId,
1066 },
1067 }
1068
1069 impl hopr_api::ct::ProbingTrafficGeneration for TestProbeStrategy {
1070 fn build(&self) -> futures::stream::BoxStream<'static, hopr_api::ct::ProbeRouting> {
1071 match self {
1072 Self::ManualNeighbor => Box::pin(futures::stream::pending()),
1073 Self::ImmediateNeighbor { store } => {
1074 let peers: Vec<OffchainPublicKey> =
1075 store.get_peers.write().unwrap().pop_front().unwrap_or_default();
1076 Box::pin(futures::StreamExt::chain(
1077 futures::stream::iter(peers.into_iter().map(|peer| {
1078 ProbeRouting::Neighbor(DestinationRouting::Forward {
1079 destination: Box::new(peer.into()),
1080 pseudonym: Some(HoprPseudonym::random()),
1081 forward_options: RoutingOptions::Hops(0.try_into().expect("0 is a valid u8")),
1082 return_options: Some(RoutingOptions::Hops(0.try_into().expect("0 is a valid u8"))),
1083 })
1084 })),
1085 futures::stream::pending(),
1086 ))
1087 }
1088 Self::OneShotLoopback { routing, path_id } => {
1089 let probe = hopr_api::ct::ProbeRouting::Looping((routing.clone(), *path_id));
1090 Box::pin(futures::StreamExt::chain(
1091 futures::stream::iter(std::iter::once(probe)),
1092 futures::stream::pending(),
1093 ))
1094 }
1095 }
1096 }
1097 }
1098
1099 #[rstest::rstest]
1106 #[case::neighbor_probe_requests_one_surb(TestProbeStrategy::ManualNeighbor, 1)]
1107 #[case::loopback_probe_requests_zero_surbs(
1108 TestProbeStrategy::OneShotLoopback {
1109 routing: DestinationRouting::Forward {
1110 destination: Box::new((*OFFCHAIN_KEYPAIR.public()).into()),
1111 pseudonym: Some(HoprPseudonym::random()),
1112 forward_options: RoutingOptions::Hops(1.try_into().expect("1 is a valid u8")),
1113 return_options: None,
1114 },
1115 path_id: [1, 2, 3, 4, 5],
1116 },
1117 0,
1118 )]
1119 #[tokio::test]
1120 async fn probe_should_emit_with_expected_surb_count(
1121 #[case] strategy: TestProbeStrategy,
1122 #[case] expected_max_surbs: usize,
1123 ) -> anyhow::Result<()> {
1124 let cfg = ProbeConfig {
1125 timeout: std::time::Duration::from_secs(1),
1126 interval: std::time::Duration::from_secs(0),
1127 ..Default::default()
1128 };
1129
1130 let tag_allocators = hopr_transport_tag_allocator::create_allocators(
1133 ReservedTag::range().end..u16::MAX as u64 + 1,
1134 [
1135 (hopr_transport_tag_allocator::Usage::Session, 2048),
1136 (hopr_transport_tag_allocator::Usage::SessionTerminalTelemetry, 4000),
1137 (hopr_transport_tag_allocator::Usage::ProvingTelemetry, 10000),
1138 ],
1139 )
1140 .expect("tag allocators should be created");
1141 let probing_allocator = tag_allocators
1142 .into_iter()
1143 .find_map(|(u, alloc)| matches!(u, hopr_transport_tag_allocator::Usage::ProvingTelemetry).then_some(alloc))
1144 .expect("probing allocator should exist");
1145
1146 let probe = Probe::new(cfg, probing_allocator);
1147
1148 let (from_probing_to_network_tx, mut from_probing_to_network_rx) =
1149 futures::channel::mpsc::channel::<(DestinationRouting, ApplicationDataOut)>(100);
1150 let (mut manual_probe_tx, manual_probe_rx) =
1151 futures::channel::mpsc::channel::<(OffchainPublicKey, PingQueryReplier)>(100);
1152
1153 let store = PeerStore {
1154 me: *OFFCHAIN_KEYPAIR.public(),
1155 get_peers: Arc::new(RwLock::new(VecDeque::new())),
1156 on_finished: Arc::new(RwLock::new(Vec::new())),
1157 };
1158
1159 let is_manual = matches!(strategy, TestProbeStrategy::ManualNeighbor);
1162 let (jhs, _probe_classifier) = probe
1163 .continuously_scan(from_probing_to_network_tx, manual_probe_rx, strategy, store)
1164 .await;
1165
1166 if is_manual {
1167 let (tx, _rx) = futures::channel::mpsc::channel::<std::result::Result<Duration, ()>>(128);
1168 manual_probe_tx.send((NEIGHBOURS[0], PingQueryReplier::new(tx))).await?;
1169 }
1170
1171 let (_routing, data_out) =
1172 tokio::time::timeout(std::time::Duration::from_secs(1), from_probing_to_network_rx.next())
1173 .await?
1174 .ok_or_else(|| anyhow::anyhow!("no probe emitted"))?;
1175
1176 jhs.abort_all();
1177
1178 let packet_info = data_out
1179 .packet_info
1180 .ok_or_else(|| anyhow::anyhow!("probe must carry explicit OutgoingPacketInfo"))?;
1181 assert_eq!(
1182 packet_info.max_surbs_in_packet, expected_max_surbs,
1183 "probe must request exactly {expected_max_surbs} SURB(s)"
1184 );
1185
1186 Ok(())
1187 }
1188}