1use hopr_api::graph::{MeasurableEdge, MeasurableNode, NetworkGraphWrite, traits::EdgeObservableWrite};
2#[cfg(all(feature = "telemetry", not(test)))]
3use hopr_api::graph::{NetworkGraphView, traits::EdgeObservableRead};
4use petgraph::graph::{EdgeIndex, NodeIndex};
5
6use crate::{ChannelGraph, Observations, graph::InnerGraph};
7
8#[cfg(all(feature = "telemetry", not(test)))]
9lazy_static::lazy_static! {
10 static ref METRIC_PEERS_BY_QUALITY: hopr_api::types::telemetry::SimpleHistogram =
11 hopr_api::types::telemetry::SimpleHistogram::new(
12 "hopr_peers_by_quality",
13 "Distribution of the quality score of the node's directly-probed neighbors",
14 vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
15 )
16 .unwrap();
17}
18
19#[cfg(all(feature = "telemetry", not(test)))]
23fn observe_neighbor_quality(graph: &ChannelGraph, peer: &hopr_api::OffchainPublicKey) {
24 if let Some(obs) = graph.edge(graph.me(), peer) {
25 METRIC_PEERS_BY_QUALITY.observe(obs.score());
26 }
27}
28
29fn resolve_loopback_edges(inner: &InnerGraph, me_idx: NodeIndex, path_bytes: &[u8]) -> Option<Vec<EdgeIndex>> {
39 if path_bytes.len() != size_of::<hopr_api::ct::PathId>() {
40 tracing::warn!(
41 path_len = path_bytes.len(),
42 expected = size_of::<hopr_api::ct::PathId>(),
43 "invalid loopback path byte length"
44 );
45 return None;
46 }
47
48 let mut path_id = [0u64; 5];
49 for (i, chunk) in path_bytes.chunks_exact(8).enumerate() {
50 path_id[i] = u64::from_le_bytes(chunk.try_into().expect("chunk is 8 bytes"));
51 }
52
53 let me_val = me_idx.index() as u64;
54
55 if path_id[0] != me_val {
57 tracing::warn!("loopback path does not start at self");
58 return None;
59 }
60
61 let Some(end_pos) = path_id[1..].iter().position(|&v| v == me_val).map(|p| p + 1) else {
63 tracing::warn!("loopback path does not close back to self");
64 return None;
65 };
66
67 let mut edges = Vec::new();
69
70 for pair in path_id[..=end_pos].windows(2) {
71 let from = NodeIndex::new(pair[0] as usize);
72 let to = NodeIndex::new(pair[1] as usize);
73 let Some(edge) = inner.graph.find_edge(from, to) else {
74 break;
75 };
76 edges.push(edge);
77 }
78
79 if edges.len() < 2 {
80 tracing::warn!(
81 edge_count = edges.len(),
82 "loopback path too short to attribute intermediate measurement"
83 );
84 return None;
85 }
86
87 Some(edges)
88}
89
90impl hopr_api::graph::NetworkGraphUpdate for ChannelGraph {
91 #[tracing::instrument(level = "debug", skip(self, update))]
92 fn record_edge<N, P>(&self, update: MeasurableEdge<N, P>)
93 where
94 N: hopr_api::graph::MeasurablePeer + Send + Clone,
95 P: hopr_api::graph::MeasurablePath + Send + Clone,
96 {
97 use hopr_api::graph::{
98 EdgeLinkObservable,
99 traits::{EdgeObservableRead, EdgeWeightType},
100 };
101
102 match update {
103 MeasurableEdge::Probe(Ok(hopr_api::graph::EdgeTransportTelemetry::Neighbor(ref telemetry))) => {
104 tracing::trace!(
105 peer = %telemetry.peer(),
106 latency_ms = telemetry.rtt().as_millis(),
107 "neighbor probe successful"
108 );
109
110 self.upsert_edge(&self.me, telemetry.peer(), |obs| {
113 obs.record(EdgeWeightType::Connected(true));
114 obs.record(EdgeWeightType::Immediate(Ok(telemetry.rtt() / 2)));
115 });
116 self.upsert_edge(telemetry.peer(), &self.me, |obs| {
117 obs.record(EdgeWeightType::Connected(true));
118 obs.record(EdgeWeightType::Immediate(Ok(telemetry.rtt() / 2)));
119 });
120
121 #[cfg(all(feature = "telemetry", not(test)))]
122 observe_neighbor_quality(self, telemetry.peer());
123 }
124 MeasurableEdge::Probe(Ok(hopr_api::graph::EdgeTransportTelemetry::Loopback(telemetry))) => {
125 tracing::trace!("loopback probe successful");
126
127 let mut inner = self.inner.write();
128 let Some(me_idx) = inner.indices.get_by_left(&self.me).copied() else {
129 tracing::debug!("failed to resolve index of myself for loopback probe attribution");
130 return;
131 };
132 let Some(edges) = resolve_loopback_edges(&inner, me_idx, telemetry.path()) else {
133 tracing::debug!("failed to resolve loopback path for probe attribution");
134 return;
135 };
136
137 let target_idx = edges.len() - 2;
138
139 let now_ms = std::time::SystemTime::now()
150 .duration_since(std::time::UNIX_EPOCH)
151 .unwrap_or_default()
152 .as_millis();
153 let Some(elapsed_ms) = now_ms.checked_sub(telemetry.timestamp()) else {
154 tracing::debug!("loopback probe timestamp in the future, skipping attribution");
155 return;
156 };
157 let total_rtt = std::time::Duration::from_millis(elapsed_ms as u64);
158 if total_rtt > self.max_plausible_loopback_rtt {
159 tracing::debug!(
160 rtt_ms = total_rtt.as_millis(),
161 "implausible loopback probe RTT, skipping attribution"
162 );
163 return;
164 }
165 let mut known_latency = std::time::Duration::ZERO;
166
167 for &edge in &edges {
168 if let Some(weight) = inner.graph.edge_weight(edge) {
169 let lat = weight
170 .intermediate_qos()
171 .and_then(|q| q.average_latency())
172 .or_else(|| weight.immediate_qos().and_then(|q| q.average_latency()));
173 if let Some(lat) = lat {
174 known_latency += lat;
175 }
176 } else {
177 tracing::debug!("failed to find edge for loopback probe attribution");
178 }
179 }
180
181 let attributed_duration = total_rtt.saturating_sub(known_latency);
182
183 tracing::trace!(
184 target_edge = edges[target_idx].index(),
185 attributed_ms = attributed_duration.as_millis(),
186 total_rtt_ms = total_rtt.as_millis(),
187 path_edges = edges.len(),
188 "loopback probe attributed to intermediate edge"
189 );
190
191 if let Some(weight) = inner.graph.edge_weight_mut(edges[target_idx]) {
192 weight.record(EdgeWeightType::Intermediate(Ok(attributed_duration)));
193 } else {
194 tracing::debug!("failed to find target edge for loopback probe attribution");
195 }
196 }
197 MeasurableEdge::Probe(Err(hopr_api::graph::NetworkGraphError::ProbeNeighborTimeout(ref peer))) => {
198 tracing::trace!(
199 peer = %peer,
200 reason = "probe timeout",
201 "neighbor probe failed"
202 );
203
204 self.upsert_edge(&self.me, peer, |obs| {
207 obs.record(EdgeWeightType::Immediate(Err(())));
208 });
209 self.upsert_edge(peer, &self.me, |obs| {
210 obs.record(EdgeWeightType::Immediate(Err(())));
211 });
212
213 #[cfg(all(feature = "telemetry", not(test)))]
214 observe_neighbor_quality(self, peer);
215 }
216 MeasurableEdge::Probe(Err(hopr_api::graph::NetworkGraphError::ProbeLoopbackTimeout(telemetry))) => {
217 tracing::trace!("loopback probe failed");
218
219 let mut inner = self.inner.write();
220 let Some(me_idx) = inner.indices.get_by_left(&self.me).copied() else {
221 tracing::debug!("failed to resolve index of myself");
222 return;
223 };
224 let Some(edges) = resolve_loopback_edges(&inner, me_idx, telemetry.path()) else {
225 tracing::debug!("failed to resolve loopback path for probe timeout, cannot attribute");
226 return;
227 };
228
229 let target_idx = edges.len() - 2;
230
231 tracing::trace!(
232 target_edge = edges[target_idx].index(),
233 path_edges = edges.len(),
234 "loopback probe timeout attributed to intermediate edge"
235 );
236
237 if let Some(weight) = inner.graph.edge_weight_mut(edges[target_idx]) {
238 weight.record(EdgeWeightType::Intermediate(Err(())));
239 }
240 }
241 MeasurableEdge::Capacity(update) => {
242 self.upsert_edge(&update.src, &update.dest, |obs: &mut Observations| {
243 obs.record(EdgeWeightType::Capacity(update.capacity));
244 });
245 }
246 MeasurableEdge::ConnectionStatus { peer, connected } => {
247 tracing::trace!(
248 peer = %peer,
249 connected = connected,
250 "recording connection status update"
251 );
252
253 self.upsert_edge(&self.me, &peer, |obs| {
254 obs.record(EdgeWeightType::Connected(connected));
255 });
256 self.upsert_edge(&peer, &self.me, |obs| {
257 obs.record(EdgeWeightType::Connected(connected));
258 });
259 }
260 }
261 }
262
263 #[tracing::instrument(level = "debug", skip(self, update))]
264 fn record_node<N>(&self, update: N)
265 where
266 N: MeasurableNode + Clone + Send + Sync + 'static,
267 {
268 hopr_api::graph::NetworkGraphWrite::add_node(self, update.into());
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use anyhow::Context;
275 use assertables::assert_in_delta;
276 use hex_literal::hex;
277 use hopr_api::{
278 OffchainPublicKey,
279 graph::{
280 EdgeLinkObservable, EdgeTransportTelemetry, MeasurablePath, MeasurablePeer, NetworkGraphError,
281 NetworkGraphUpdate, NetworkGraphView, NetworkGraphWrite,
282 traits::{EdgeObservableRead, EdgeProtocolObservable},
283 },
284 types::crypto::prelude::{Keypair, OffchainKeypair},
285 };
286
287 use super::*;
288
289 const SECRET_0: [u8; 32] = hex!("60741b83b99e36aa0c1331578156e16b8e21166d01834abb6c64b103f885734d");
291 const SECRET_1: [u8; 32] = hex!("71bf1f42ebbfcd89c3e197a3fd7cda79b92499e509b6fefa0fe44d02821d146a");
292 const SECRET_2: [u8; 32] = hex!("c24bd833704dd2abdae3933fcc9962c2ac404f84132224c474147382d4db2299");
293 const SECRET_3: [u8; 32] = hex!("e0bf93e9c916104da00b1850adc4608bd7e9087bbd3f805451f4556aa6b3fd6e");
294
295 fn pubkey_from(secret: &[u8; 32]) -> OffchainPublicKey {
297 *OffchainKeypair::from_secret(secret).expect("valid secret key").public()
298 }
299
300 #[derive(Debug, Clone)]
301 struct TestNeighbor {
302 peer: OffchainPublicKey,
303 rtt: std::time::Duration,
304 }
305
306 impl MeasurablePeer for TestNeighbor {
307 fn peer(&self) -> &OffchainPublicKey {
308 &self.peer
309 }
310
311 fn rtt(&self) -> std::time::Duration {
312 self.rtt
313 }
314 }
315
316 #[derive(Debug, Clone)]
317 struct TestPath;
318
319 impl MeasurablePath for TestPath {
320 fn id(&self) -> &[u8] {
321 &[]
322 }
323
324 fn path(&self) -> &[u8] {
325 &[]
326 }
327
328 fn timestamp(&self) -> u128 {
329 0
330 }
331 }
332
333 #[tokio::test]
334 async fn neighbor_probe_should_update_edge_observation() -> anyhow::Result<()> {
335 let me_kp = OffchainKeypair::from_secret(&SECRET_0)?;
336 let me = *me_kp.public();
337 let peer_kp = OffchainKeypair::from_secret(&SECRET_1)?;
338 let peer_key = *peer_kp.public();
339
340 let graph = ChannelGraph::new(me);
341 graph.add_node(peer_key);
342 graph.add_edge(&me, &peer_key)?;
343
344 let rtt = std::time::Duration::from_millis(100);
345 let telemetry: Result<EdgeTransportTelemetry<TestNeighbor, TestPath>, NetworkGraphError<TestPath>> =
346 Ok(EdgeTransportTelemetry::Neighbor(TestNeighbor { peer: peer_key, rtt }));
347 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
348
349 let obs = graph.edge(&me, &peer_key).context("edge observation should exist")?;
350 let immediate = obs
351 .immediate_qos()
352 .context("immediate QoS should be present after probe")?;
353 assert_eq!(immediate.average_latency().context("latency should be set")?, rtt / 2,);
354 Ok(())
355 }
356
357 #[tokio::test]
358 async fn neighbor_probe_should_create_symmetric_edges() -> anyhow::Result<()> {
359 let me = pubkey_from(&SECRET_0);
360 let peer = pubkey_from(&SECRET_1);
361
362 let graph = ChannelGraph::new(me);
363 graph.add_node(peer);
364 let rtt = std::time::Duration::from_millis(100);
367 let telemetry: Result<EdgeTransportTelemetry<TestNeighbor, TestPath>, NetworkGraphError<TestPath>> =
368 Ok(EdgeTransportTelemetry::Neighbor(TestNeighbor { peer, rtt }));
369 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
370
371 let obs_fwd = graph.edge(&me, &peer).context("edge me→peer should exist")?;
373 let imm_fwd = obs_fwd.immediate_qos().context("me→peer should have immediate QoS")?;
374 assert_eq!(
375 imm_fwd.average_latency().context("me→peer latency should be set")?,
376 rtt / 2
377 );
378
379 let obs_rev = graph.edge(&peer, &me).context("edge peer→me should exist")?;
381 let imm_rev = obs_rev.immediate_qos().context("peer→me should have immediate QoS")?;
382 assert_eq!(
383 imm_rev.average_latency().context("peer→me latency should be set")?,
384 rtt / 2
385 );
386
387 Ok(())
388 }
389
390 #[tokio::test]
391 async fn neighbor_probe_timeout_should_create_symmetric_edges() -> anyhow::Result<()> {
392 let me = pubkey_from(&SECRET_0);
393 let peer = pubkey_from(&SECRET_1);
394
395 let graph = ChannelGraph::new(me);
396 graph.add_node(peer);
397 let telemetry: Result<EdgeTransportTelemetry<TestNeighbor, TestPath>, NetworkGraphError<TestPath>> =
400 Err(NetworkGraphError::ProbeNeighborTimeout(Box::new(peer)));
401 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
402
403 let obs_fwd = graph
405 .edge(&me, &peer)
406 .context("edge me→peer should exist after timeout")?;
407 let imm_fwd = obs_fwd.immediate_qos().context("me→peer should have immediate QoS")?;
408 assert!(
409 imm_fwd.average_latency().is_none(),
410 "failed probe should not set latency"
411 );
412 assert!(
413 imm_fwd.average_probe_rate() < 1.0,
414 "failed probe should lower success rate"
415 );
416
417 let obs_rev = graph
419 .edge(&peer, &me)
420 .context("edge peer→me should exist after timeout")?;
421 let imm_rev = obs_rev.immediate_qos().context("peer→me should have immediate QoS")?;
422 assert!(
423 imm_rev.average_latency().is_none(),
424 "failed probe should not set latency on reverse"
425 );
426 assert!(
427 imm_rev.average_probe_rate() < 1.0,
428 "failed probe should lower success rate on reverse"
429 );
430
431 Ok(())
432 }
433
434 #[tokio::test]
435 async fn probe_timeout_should_record_as_failed_probe() -> anyhow::Result<()> {
436 let me_kp = OffchainKeypair::from_secret(&SECRET_0)?;
437 let me = *me_kp.public();
438 let peer_kp = OffchainKeypair::from_secret(&SECRET_1)?;
439 let peer_key = *peer_kp.public();
440
441 let graph = ChannelGraph::new(me);
442 graph.add_node(peer_key);
443 graph.add_edge(&me, &peer_key)?;
444
445 let telemetry: Result<EdgeTransportTelemetry<TestNeighbor, TestPath>, NetworkGraphError<TestPath>> =
446 Err(NetworkGraphError::ProbeNeighborTimeout(Box::new(peer_key)));
447 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
448
449 let obs = graph.edge(&me, &peer_key).context("edge observation should exist")?;
450 let immediate = obs
451 .immediate_qos()
452 .context("immediate QoS should be present after failed probe")?;
453 assert!(immediate.average_latency().is_none());
454 assert!(immediate.average_probe_rate() < 1.0);
455 Ok(())
456 }
457
458 #[tokio::test]
459 async fn capacity_update_should_set_edge_capacity() -> anyhow::Result<()> {
460 let me = pubkey_from(&SECRET_0);
461 let peer = pubkey_from(&SECRET_1);
462 let graph = ChannelGraph::new(me);
463 graph.add_node(peer);
464 graph.add_edge(&me, &peer)?;
465
466 let capacity_update = hopr_api::graph::EdgeCapacityUpdate {
467 src: me,
468 dest: peer,
469 capacity: Some(1000),
470 };
471 graph.record_edge::<TestNeighbor, TestPath>(hopr_api::graph::MeasurableEdge::Capacity(Box::new(
472 capacity_update,
473 )));
474
475 let obs = graph.edge(&me, &peer).context("edge should exist")?;
476 let intermediate = obs
477 .intermediate_qos()
478 .context("intermediate QoS should be present after capacity update")?;
479 assert_eq!(intermediate.capacity(), Some(1000));
480 Ok(())
481 }
482
483 #[tokio::test]
484 async fn capacity_update_should_accept_none_value() -> anyhow::Result<()> {
485 let me = pubkey_from(&SECRET_0);
486 let peer = pubkey_from(&SECRET_1);
487 let graph = ChannelGraph::new(me);
488 graph.add_node(peer);
489 graph.add_edge(&me, &peer)?;
490
491 let capacity_update = hopr_api::graph::EdgeCapacityUpdate {
492 src: me,
493 dest: peer,
494 capacity: None,
495 };
496 graph.record_edge::<TestNeighbor, TestPath>(hopr_api::graph::MeasurableEdge::Capacity(Box::new(
497 capacity_update,
498 )));
499
500 let obs = graph.edge(&me, &peer).context("edge should exist")?;
501 let intermediate = obs.intermediate_qos().context("intermediate QoS should be present")?;
502 assert_eq!(intermediate.capacity(), None);
503 Ok(())
504 }
505
506 #[tokio::test]
507 async fn record_node_should_add_node_to_graph() {
508 let me = pubkey_from(&SECRET_0);
509 let peer = pubkey_from(&SECRET_1);
510 let graph = ChannelGraph::new(me);
511
512 assert!(!graph.contains_node(&peer));
513 graph.record_node(peer);
514 assert!(graph.contains_node(&peer));
515 }
516
517 #[tokio::test]
518 async fn probe_should_create_edge_if_absent() -> anyhow::Result<()> {
519 let me = pubkey_from(&SECRET_0);
520 let peer = pubkey_from(&SECRET_1);
521 let graph = ChannelGraph::new(me);
522 graph.add_node(peer);
523 let rtt = std::time::Duration::from_millis(80);
526 let telemetry: Result<EdgeTransportTelemetry<TestNeighbor, TestPath>, NetworkGraphError<TestPath>> =
527 Ok(EdgeTransportTelemetry::Neighbor(TestNeighbor { peer, rtt }));
528 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
529
530 assert!(graph.has_edge(&me, &peer), "probe should create edge via upsert");
531 let obs = graph.edge(&me, &peer).context("edge should exist")?;
532 assert!(obs.immediate_qos().is_some());
533 Ok(())
534 }
535
536 #[tokio::test]
537 async fn multiple_probes_should_accumulate_in_observations() -> anyhow::Result<()> {
538 let me = pubkey_from(&SECRET_0);
539 let peer = pubkey_from(&SECRET_1);
540 let graph = ChannelGraph::new(me);
541 graph.add_node(peer);
542 graph.add_edge(&me, &peer)?;
543
544 for _ in 0..5 {
546 let telemetry: Result<EdgeTransportTelemetry<TestNeighbor, TestPath>, NetworkGraphError<TestPath>> =
547 Ok(EdgeTransportTelemetry::Neighbor(TestNeighbor {
548 peer,
549 rtt: std::time::Duration::from_millis(60),
550 }));
551 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
552 }
553
554 let obs = graph.edge(&me, &peer).context("edge should exist")?;
555 let qos = obs.immediate_qos().context("immediate QoS should exist")?;
556 assert_eq!(
557 qos.average_latency().context("latency should be set")?,
558 std::time::Duration::from_millis(30), );
560 assert!(qos.average_probe_rate() > 0.9, "all probes succeeded");
561 Ok(())
562 }
563
564 #[derive(Debug, Clone)]
567 struct LoopbackTestPath {
568 path_bytes: Vec<u8>,
569 timestamp_ms: u128,
570 }
571
572 impl LoopbackTestPath {
573 fn new(path_id: [u64; 5], timestamp_ms: u128) -> Self {
574 let path_bytes = path_id.iter().flat_map(|v| v.to_le_bytes()).collect();
575 Self {
576 path_bytes,
577 timestamp_ms,
578 }
579 }
580 }
581
582 impl MeasurablePath for LoopbackTestPath {
583 fn id(&self) -> &[u8] {
584 &[]
585 }
586
587 fn path(&self) -> &[u8] {
588 &self.path_bytes
589 }
590
591 fn timestamp(&self) -> u128 {
592 self.timestamp_ms
593 }
594 }
595
596 fn now_unix_ms() -> u128 {
598 std::time::SystemTime::now()
599 .duration_since(std::time::UNIX_EPOCH)
600 .unwrap_or_default()
601 .as_millis()
602 }
603
604 fn send_loopback(graph: &ChannelGraph, path_id: [u64; 5], rtt_ms: u128) {
609 let telemetry: Result<
610 EdgeTransportTelemetry<TestNeighbor, LoopbackTestPath>,
611 NetworkGraphError<LoopbackTestPath>,
612 > = Ok(EdgeTransportTelemetry::Loopback(LoopbackTestPath::new(
613 path_id,
614 now_unix_ms() - rtt_ms,
615 )));
616 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
617 }
618
619 fn send_loopback_timeout(graph: &ChannelGraph, path_id: [u64; 5]) {
621 let telemetry: Result<
622 EdgeTransportTelemetry<TestNeighbor, LoopbackTestPath>,
623 NetworkGraphError<LoopbackTestPath>,
624 > = Err(NetworkGraphError::ProbeLoopbackTimeout(LoopbackTestPath::new(
625 path_id, 0,
626 )));
627 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
628 }
629
630 #[tokio::test]
631 async fn loopback_three_hop_should_attribute_to_penultimate_edge() -> anyhow::Result<()> {
632 let me = pubkey_from(&SECRET_0);
637 let a = pubkey_from(&SECRET_1);
638 let b = pubkey_from(&SECRET_2);
639
640 let graph = ChannelGraph::new(me);
641 graph.add_node(a);
642 graph.add_node(b);
643 graph.add_edge(&me, &a)?;
644 graph.add_edge(&a, &b)?;
645 graph.add_edge(&b, &me)?; send_loopback(&graph, [0, 1, 2, 0, 0], 200);
648
649 let obs = graph.edge(&a, &b).context("edge a→b should exist")?;
650 let qos = obs
651 .intermediate_qos()
652 .context("intermediate QoS should be present on a→b")?;
653 assert_in_delta!(
654 qos.average_latency().context("latency should be set")?.as_millis(),
655 200,
656 25
657 );
658
659 let obs_me_a = graph.edge(&me, &a).context("edge me→a should exist")?;
661 assert!(obs_me_a.intermediate_qos().is_none());
662
663 Ok(())
664 }
665
666 #[tokio::test]
667 async fn loopback_four_hop_should_attribute_to_penultimate_edge() -> anyhow::Result<()> {
668 let me = pubkey_from(&SECRET_0);
673 let a = pubkey_from(&SECRET_1);
674 let b = pubkey_from(&SECRET_2);
675 let c = pubkey_from(&SECRET_3);
676
677 let graph = ChannelGraph::new(me);
678 graph.add_node(a);
679 graph.add_node(b);
680 graph.add_node(c);
681 graph.add_edge(&me, &a)?;
682 graph.add_edge(&a, &b)?;
683 graph.add_edge(&b, &c)?;
684 graph.add_edge(&c, &me)?; send_loopback(&graph, [0, 1, 2, 3, 0], 300);
687
688 let obs = graph.edge(&b, &c).context("edge b→c should exist")?;
690 let qos = obs
691 .intermediate_qos()
692 .context("intermediate QoS should be present on b→c")?;
693 assert_in_delta!(
694 qos.average_latency().context("latency should be set")?.as_millis(),
695 300,
696 25
697 ); let obs_me_a = graph.edge(&me, &a).context("edge me→a should exist")?;
701 assert!(obs_me_a.intermediate_qos().is_none());
702 let obs_a_b = graph.edge(&a, &b).context("edge a→b should exist")?;
703 assert!(obs_a_b.intermediate_qos().is_none());
704
705 Ok(())
706 }
707
708 #[tokio::test]
709 async fn loopback_should_subtract_known_preceding_latencies() -> anyhow::Result<()> {
710 let me = pubkey_from(&SECRET_0);
716 let a = pubkey_from(&SECRET_1);
717 let b = pubkey_from(&SECRET_2);
718 let c = pubkey_from(&SECRET_3);
719
720 let graph = ChannelGraph::new(me);
721 graph.add_node(a);
722 graph.add_node(b);
723 graph.add_node(c);
724 graph.add_edge(&me, &a)?;
725 graph.add_edge(&a, &b)?;
726 graph.add_edge(&b, &c)?;
727 graph.add_edge(&c, &me)?; graph.upsert_edge(&me, &a, |obs| {
731 use hopr_api::graph::traits::EdgeObservableWrite;
732 obs.record(hopr_api::graph::traits::EdgeWeightType::Intermediate(Ok(
733 std::time::Duration::from_millis(80),
734 )));
735 });
736 graph.upsert_edge(&a, &b, |obs| {
737 use hopr_api::graph::traits::EdgeObservableWrite;
738 obs.record(hopr_api::graph::traits::EdgeWeightType::Intermediate(Ok(
739 std::time::Duration::from_millis(40),
740 )));
741 });
742
743 send_loopback(&graph, [0, 1, 2, 3, 0], 300);
744
745 let obs = graph.edge(&b, &c).context("edge b→c should exist")?;
746 let qos = obs
747 .intermediate_qos()
748 .context("intermediate QoS should be present on b→c")?;
749 assert_in_delta!(
750 qos.average_latency().context("latency should be set")?.as_millis(),
751 180,
752 25
753 ); Ok(())
756 }
757
758 #[tokio::test]
759 async fn loopback_should_subtract_immediate_latency_on_first_edge() -> anyhow::Result<()> {
760 let me = pubkey_from(&SECRET_0);
765 let a = pubkey_from(&SECRET_1);
766 let b = pubkey_from(&SECRET_2);
767
768 let graph = ChannelGraph::new(me);
769 graph.add_node(a);
770 graph.add_node(b);
771 graph.add_edge(&me, &a)?;
772 graph.add_edge(&a, &b)?;
773 graph.add_edge(&b, &me)?;
774
775 graph.upsert_edge(&me, &a, |obs| {
777 use hopr_api::graph::traits::EdgeObservableWrite;
778 obs.record(hopr_api::graph::traits::EdgeWeightType::Immediate(Ok(
779 std::time::Duration::from_millis(60),
780 )));
781 });
782
783 send_loopback(&graph, [0, 1, 2, 0, 0], 200);
784
785 let obs = graph.edge(&a, &b).context("edge a→b should exist")?;
786 let qos = obs
787 .intermediate_qos()
788 .context("intermediate QoS should be present on a→b")?;
789 assert_in_delta!(
790 qos.average_latency().context("latency should be set")?.as_millis(),
791 140,
792 25
793 ); Ok(())
796 }
797
798 #[tokio::test]
799 async fn loopback_invalid_path_length_should_be_ignored() -> anyhow::Result<()> {
800 let me = pubkey_from(&SECRET_0);
801 let a = pubkey_from(&SECRET_1);
802
803 let graph = ChannelGraph::new(me);
804 graph.add_node(a);
805 graph.add_edge(&me, &a)?;
806
807 let telemetry: Result<
809 EdgeTransportTelemetry<TestNeighbor, LoopbackTestPath>,
810 NetworkGraphError<LoopbackTestPath>,
811 > = Ok(EdgeTransportTelemetry::Loopback(LoopbackTestPath {
812 path_bytes: vec![0u8; 16], timestamp_ms: 100,
814 }));
815 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
816
817 let obs = graph.edge(&me, &a).context("edge should exist")?;
819 assert!(
820 obs.intermediate_qos().is_none(),
821 "invalid path bytes should not produce any intermediate measurement"
822 );
823
824 Ok(())
825 }
826
827 #[tokio::test]
828 async fn loopback_implausible_rtt_should_be_ignored() -> anyhow::Result<()> {
829 let me = pubkey_from(&SECRET_0);
833 let a = pubkey_from(&SECRET_1);
834 let b = pubkey_from(&SECRET_2);
835
836 let graph = ChannelGraph::new(me);
837 graph.add_node(a);
838 graph.add_node(b);
839 graph.add_edge(&me, &a)?;
840 graph.add_edge(&a, &b)?;
841 graph.add_edge(&b, &me)?; send_loopback(&graph, [0, 1, 2, 0, 0], 90_000);
845
846 let obs = graph.edge(&a, &b).context("edge a→b should exist")?;
847 assert!(
848 obs.intermediate_qos().is_none(),
849 "implausible RTT must not produce an intermediate measurement"
850 );
851
852 Ok(())
853 }
854
855 #[tokio::test]
856 async fn loopback_future_timestamp_should_be_ignored() -> anyhow::Result<()> {
857 let me = pubkey_from(&SECRET_0);
861 let a = pubkey_from(&SECRET_1);
862 let b = pubkey_from(&SECRET_2);
863
864 let graph = ChannelGraph::new(me);
865 graph.add_node(a);
866 graph.add_node(b);
867 graph.add_edge(&me, &a)?;
868 graph.add_edge(&a, &b)?;
869 graph.add_edge(&b, &me)?; let telemetry: Result<
872 EdgeTransportTelemetry<TestNeighbor, LoopbackTestPath>,
873 NetworkGraphError<LoopbackTestPath>,
874 > = Ok(EdgeTransportTelemetry::Loopback(LoopbackTestPath::new(
875 [0, 1, 2, 0, 0],
876 now_unix_ms() + 5_000, )));
878 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
879
880 let obs = graph.edge(&a, &b).context("edge a→b should exist")?;
881 assert!(
882 obs.intermediate_qos().is_none(),
883 "future timestamp must not produce an intermediate measurement"
884 );
885
886 Ok(())
887 }
888
889 #[tokio::test]
890 async fn loopback_single_edge_path_should_be_ignored_if_no_immediate_or_intermediate_result_exists_for_the_edge()
891 -> anyhow::Result<()> {
892 let me = pubkey_from(&SECRET_0);
897 let a = pubkey_from(&SECRET_1);
898
899 let graph = ChannelGraph::new(me);
900 graph.add_node(a);
901 graph.add_edge(&me, &a)?;
902
903 send_loopback(&graph, [0, 1, 0, 0, 0], 100);
904
905 let obs = graph.edge(&me, &a).context("edge should exist")?;
906 assert!(
907 obs.intermediate_qos().is_none(),
908 "single-edge path should not produce intermediate measurement"
909 );
910
911 Ok(())
912 }
913
914 #[tokio::test]
915 async fn loopback_two_edge_path_should_attribute_when_return_edge_exists() -> anyhow::Result<()> {
916 let me = pubkey_from(&SECRET_0);
922 let a = pubkey_from(&SECRET_1);
923
924 let graph = ChannelGraph::new(me);
925 graph.add_node(a);
926 graph.add_edge(&me, &a)?;
927 graph.add_edge(&a, &me)?; graph.upsert_edge(&me, &a, |obs| {
931 use hopr_api::graph::traits::EdgeObservableWrite;
932 obs.record(hopr_api::graph::traits::EdgeWeightType::Immediate(Ok(
933 std::time::Duration::from_millis(50),
934 )));
935 });
936
937 send_loopback(&graph, [0, 1, 0, 0, 0], 100);
938
939 let obs = graph.edge(&me, &a).context("edge me→a should exist")?;
940 let qos = obs
941 .intermediate_qos()
942 .context("intermediate QoS should be present on me→a")?;
943 assert_in_delta!(
944 qos.average_latency().context("latency should be set")?.as_millis(),
945 50,
946 25
947 ); let imm = obs
951 .immediate_qos()
952 .context("immediate QoS should still be present on me→a")?;
953 assert_eq!(
954 imm.average_latency().context("immediate latency should be set")?,
955 std::time::Duration::from_millis(50),
956 );
957
958 Ok(())
959 }
960
961 #[tokio::test]
962 async fn loopback_broken_chain_should_be_ignored() -> anyhow::Result<()> {
963 let me = pubkey_from(&SECRET_0);
968 let a = pubkey_from(&SECRET_1);
969 let b = pubkey_from(&SECRET_2);
970 let c = pubkey_from(&SECRET_3);
971
972 let graph = ChannelGraph::new(me);
973 graph.add_node(a);
974 graph.add_node(b);
975 graph.add_node(c);
976 graph.add_edge(&me, &a)?;
977 graph.add_edge(&b, &c)?; send_loopback(&graph, [0, 1, 3, 0, 0], 200);
980
981 let obs_me_a = graph.edge(&me, &a).context("edge me→a should exist")?;
982 assert!(
983 obs_me_a.intermediate_qos().is_none(),
984 "broken chain should not attribute any intermediate measurement"
985 );
986
987 Ok(())
988 }
989
990 #[tokio::test]
991 async fn loopback_wrong_start_node_should_be_ignored() -> anyhow::Result<()> {
992 let me = pubkey_from(&SECRET_0);
994 let a = pubkey_from(&SECRET_1);
995
996 let graph = ChannelGraph::new(me);
997 graph.add_node(a);
998 graph.add_edge(&me, &a)?;
999
1000 send_loopback(&graph, [99, 1, 0, 0, 0], 200);
1001
1002 let obs = graph.edge(&me, &a).context("edge should exist")?;
1003 assert!(
1004 obs.intermediate_qos().is_none(),
1005 "wrong start node should not produce any measurement"
1006 );
1007
1008 Ok(())
1009 }
1010
1011 #[tokio::test]
1012 async fn loopback_probes_should_accumulate_on_target_edge() -> anyhow::Result<()> {
1013 let me = pubkey_from(&SECRET_0);
1016 let a = pubkey_from(&SECRET_1);
1017 let b = pubkey_from(&SECRET_2);
1018
1019 let graph = ChannelGraph::new(me);
1020 graph.add_node(a);
1021 graph.add_node(b);
1022 graph.add_edge(&me, &a)?;
1023 graph.add_edge(&a, &b)?;
1024 graph.add_edge(&b, &me)?; for _ in 0..5 {
1030 send_loopback(&graph, [0, 1, 2, 0, 0], 100);
1031 }
1032
1033 let obs = graph.edge(&a, &b).context("edge a→b should exist")?;
1034 let qos = obs.intermediate_qos().context("intermediate QoS should be present")?;
1035 assert!(
1036 qos.average_latency().is_some(),
1037 "latency should be set after multiple probes"
1038 );
1039 assert!(
1040 qos.average_probe_rate() > 0.9,
1041 "all probes succeeded, rate should be high"
1042 );
1043
1044 Ok(())
1045 }
1046
1047 #[tokio::test]
1049 async fn loopback_saturating_sub_should_not_underflow() -> anyhow::Result<()> {
1050 let me = pubkey_from(&SECRET_0);
1054 let a = pubkey_from(&SECRET_1);
1055 let b = pubkey_from(&SECRET_2);
1056 let c = pubkey_from(&SECRET_3);
1057
1058 let graph = ChannelGraph::new(me);
1059 graph.add_node(a);
1060 graph.add_node(b);
1061 graph.add_node(c);
1062 graph.add_edge(&me, &a)?;
1063 graph.add_edge(&a, &b)?;
1064 graph.add_edge(&b, &c)?;
1065 graph.add_edge(&c, &me)?; graph.upsert_edge(&me, &a, |obs| {
1069 use hopr_api::graph::traits::EdgeObservableWrite;
1070 obs.record(hopr_api::graph::traits::EdgeWeightType::Intermediate(Ok(
1071 std::time::Duration::from_millis(500),
1072 )));
1073 });
1074
1075 send_loopback(&graph, [0, 1, 2, 3, 0], 100);
1077
1078 let obs = graph.edge(&b, &c).context("edge b→c should exist")?;
1079 let qos = obs.intermediate_qos().context("intermediate QoS should be present")?;
1080 assert!(
1084 qos.average_probe_rate() > 0.0,
1085 "probe should still be recorded even with saturated duration"
1086 );
1087
1088 Ok(())
1089 }
1090
1091 #[tokio::test]
1092 async fn loopback_timeout_should_record_failed_intermediate_on_target_edge() -> anyhow::Result<()> {
1093 let me = pubkey_from(&SECRET_0);
1097 let a = pubkey_from(&SECRET_1);
1098 let b = pubkey_from(&SECRET_2);
1099
1100 let graph = ChannelGraph::new(me);
1101 graph.add_node(a);
1102 graph.add_node(b);
1103 graph.add_edge(&me, &a)?;
1104 graph.add_edge(&a, &b)?;
1105 graph.add_edge(&b, &me)?; send_loopback_timeout(&graph, [0, 1, 2, 0, 0]);
1108
1109 let obs = graph.edge(&a, &b).context("edge a→b should exist")?;
1110 let qos = obs
1111 .intermediate_qos()
1112 .context("intermediate QoS should be present on a→b after timeout")?;
1113 assert!(qos.average_latency().is_none(), "failed probe should not set latency");
1114 assert!(qos.average_probe_rate() < 1.0, "failed probe should lower success rate");
1115
1116 let obs_me_a = graph.edge(&me, &a).context("edge me→a should exist")?;
1118 assert!(obs_me_a.intermediate_qos().is_none());
1119
1120 Ok(())
1121 }
1122
1123 #[tokio::test]
1124 async fn loopback_timeout_four_hop_should_attribute_to_penultimate_edge() -> anyhow::Result<()> {
1125 let me = pubkey_from(&SECRET_0);
1128 let a = pubkey_from(&SECRET_1);
1129 let b = pubkey_from(&SECRET_2);
1130 let c = pubkey_from(&SECRET_3);
1131
1132 let graph = ChannelGraph::new(me);
1133 graph.add_node(a);
1134 graph.add_node(b);
1135 graph.add_node(c);
1136 graph.add_edge(&me, &a)?;
1137 graph.add_edge(&a, &b)?;
1138 graph.add_edge(&b, &c)?;
1139 graph.add_edge(&c, &me)?;
1140
1141 send_loopback_timeout(&graph, [0, 1, 2, 3, 0]);
1142
1143 let obs = graph.edge(&b, &c).context("edge b→c should exist")?;
1145 let qos = obs
1146 .intermediate_qos()
1147 .context("intermediate QoS should be present on b→c")?;
1148 assert!(qos.average_latency().is_none());
1149 assert!(qos.average_probe_rate() < 1.0);
1150
1151 let obs_me_a = graph.edge(&me, &a).context("edge me→a should exist")?;
1153 assert!(obs_me_a.intermediate_qos().is_none());
1154 let obs_a_b = graph.edge(&a, &b).context("edge a→b should exist")?;
1155 assert!(obs_a_b.intermediate_qos().is_none());
1156
1157 Ok(())
1158 }
1159
1160 #[tokio::test]
1161 async fn loopback_timeout_invalid_path_should_be_ignored() -> anyhow::Result<()> {
1162 let me = pubkey_from(&SECRET_0);
1163 let a = pubkey_from(&SECRET_1);
1164
1165 let graph = ChannelGraph::new(me);
1166 graph.add_node(a);
1167 graph.add_edge(&me, &a)?;
1168
1169 let telemetry: Result<
1171 EdgeTransportTelemetry<TestNeighbor, LoopbackTestPath>,
1172 NetworkGraphError<LoopbackTestPath>,
1173 > = Err(NetworkGraphError::ProbeLoopbackTimeout(LoopbackTestPath {
1174 path_bytes: vec![0u8; 8],
1175 timestamp_ms: 0,
1176 }));
1177 graph.record_edge(hopr_api::graph::MeasurableEdge::Probe(telemetry));
1178
1179 let obs = graph.edge(&me, &a).context("edge should exist")?;
1180 assert!(obs.intermediate_qos().is_none());
1181
1182 Ok(())
1183 }
1184
1185 #[tokio::test]
1186 async fn loopback_timeout_single_edge_should_be_ignored() -> anyhow::Result<()> {
1187 let me = pubkey_from(&SECRET_0);
1189 let a = pubkey_from(&SECRET_1);
1190
1191 let graph = ChannelGraph::new(me);
1192 graph.add_node(a);
1193 graph.add_edge(&me, &a)?;
1194
1195 send_loopback_timeout(&graph, [0, 1, 0, 0, 0]);
1196
1197 let obs = graph.edge(&me, &a).context("edge should exist")?;
1198 assert!(
1199 obs.intermediate_qos().is_none(),
1200 "single-edge timeout should not produce intermediate measurement"
1201 );
1202
1203 Ok(())
1204 }
1205}