1use hopr_api::{
2 OffchainPublicKey,
3 types::{
4 crypto_random::Randomizable,
5 internal::{
6 NodeId,
7 protocol::HoprPseudonym,
8 routing::{DestinationRouting, PathId, RoutingOptions},
9 },
10 primitive::{bounded::BoundedVec, errors::GeneralError},
11 },
12};
13
14pub struct TaggedDestinationRouting {
15 pub destination: Box<NodeId>,
17 pub pseudonym: HoprPseudonym,
19 pub forward_options: RoutingOptions,
21 pub return_options: Option<RoutingOptions>,
23}
24
25impl TaggedDestinationRouting {
26 pub fn neighbor(destination: Box<NodeId>) -> Self {
27 Self {
28 destination,
29 pseudonym: HoprPseudonym::random(),
30 forward_options: RoutingOptions::Hops(0.try_into().expect("0 is a valid u8")),
31 return_options: Some(RoutingOptions::Hops(0.try_into().expect("0 is a valid u8"))),
32 }
33 }
34
35 pub fn loopback(me: Box<NodeId>, path: BoundedVec<NodeId, { RoutingOptions::MAX_INTERMEDIATE_HOPS }>) -> Self {
36 Self {
37 destination: me,
38 pseudonym: HoprPseudonym::random(),
39 forward_options: RoutingOptions::IntermediatePath(path),
40 return_options: None,
41 }
42 }
43}
44
45impl From<TaggedDestinationRouting> for DestinationRouting {
46 fn from(value: TaggedDestinationRouting) -> Self {
47 DestinationRouting::Forward {
48 destination: value.destination,
49 pseudonym: Some(value.pseudonym),
50 forward_options: value.forward_options,
51 return_options: value.return_options,
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumDiscriminants)]
58#[strum_discriminants(vis(pub(crate)), derive(strum::FromRepr, strum::EnumCount), repr(u8))]
59pub enum NeighborProbe {
60 Ping([u8; Self::NONCE_SIZE]),
62 Pong([u8; Self::NONCE_SIZE]),
64}
65
66impl NeighborProbe {
67 pub const NONCE_SIZE: usize = 32;
68 pub const SIZE: usize = 1 + Self::NONCE_SIZE;
69
70 pub fn random_nonce() -> Self {
72 Self::Ping(hopr_api::types::crypto_random::random_bytes::<{ Self::NONCE_SIZE }>())
73 }
74
75 pub fn is_complement_to(&self, other: Self) -> bool {
76 match (self, &other) {
77 (Self::Ping(nonce1), Self::Pong(nonce2)) => nonce1 == nonce2,
78 (Self::Pong(nonce1), Self::Ping(nonce2)) => nonce1 == nonce2,
79 _ => false,
80 }
81 }
82
83 pub fn to_bytes(self) -> Box<[u8]> {
84 let mut out = Vec::with_capacity(1 + Self::NONCE_SIZE);
85 out.push(NeighborProbeDiscriminants::from(&self) as u8);
86 out.extend_from_slice(self.as_ref());
87 out.into_boxed_slice()
88 }
89}
90
91impl<'a> TryFrom<&'a [u8]> for NeighborProbe {
92 type Error = GeneralError;
93
94 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
95 if value.len() == 1 + Self::NONCE_SIZE {
96 match NeighborProbeDiscriminants::from_repr(value[0])
97 .ok_or(GeneralError::ParseError("NeighborProbe.disc".into()))?
98 {
99 NeighborProbeDiscriminants::Ping => {
100 Ok(Self::Ping((&value[1..]).try_into().map_err(|_| {
101 GeneralError::ParseError("NeighborProbe.ping_nonce".into())
102 })?))
103 }
104 NeighborProbeDiscriminants::Pong => {
105 Ok(Self::Pong((&value[1..]).try_into().map_err(|_| {
106 GeneralError::ParseError("NeighborProbe.pong_nonce".into())
107 })?))
108 }
109 }
110 } else {
111 Err(GeneralError::ParseError("NeighborProbe.size".into()))
112 }
113 }
114}
115
116impl std::fmt::Display for NeighborProbe {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 NeighborProbe::Ping(nonce) => write!(f, "Ping({})", hex::encode(nonce)),
120 NeighborProbe::Pong(nonce) => write!(f, "Pong({})", hex::encode(nonce)),
121 }
122 }
123}
124
125impl AsRef<[u8]> for NeighborProbe {
126 fn as_ref(&self) -> &[u8] {
127 match self {
128 NeighborProbe::Ping(nonce) | NeighborProbe::Pong(nonce) => nonce,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub struct PathTelemetry {
139 pub id: [u8; Self::ID_SIZE],
141 pub path: [u8; Self::PATH_SIZE],
143 pub timestamp: u128,
145}
146
147impl PathTelemetry {
148 pub const ID_SIZE: usize = 8;
149 pub const PATH_SIZE: usize = size_of::<PathId>();
150 pub const SIZE: usize = Self::ID_SIZE + Self::PATH_SIZE + size_of::<u128>();
151
152 pub fn to_bytes(self) -> Box<[u8]> {
153 let mut out = Vec::with_capacity(Self::SIZE);
154 out.extend_from_slice(&self.id);
155 out.extend_from_slice(&self.path);
156 out.extend_from_slice(&self.timestamp.to_be_bytes());
157 out.into_boxed_slice()
158 }
159}
160
161impl hopr_api::graph::MeasurablePath for PathTelemetry {
162 fn id(&self) -> &[u8] {
163 &self.id
164 }
165
166 fn path(&self) -> &[u8] {
167 &self.path
168 }
169
170 fn timestamp(&self) -> u128 {
171 self.timestamp
172 }
173}
174
175const _: () = assert!(size_of::<u128>() > PathTelemetry::ID_SIZE);
176
177impl std::fmt::Display for PathTelemetry {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 write!(
180 f,
181 "PathTelemetry(id: {}, path: {}, timestamp: {})",
182 hex::encode(self.id),
183 hex::encode(self.path),
184 self.timestamp
185 )
186 }
187}
188
189impl<'a> TryFrom<&'a [u8]> for PathTelemetry {
190 type Error = GeneralError;
191
192 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
193 if value.len() == Self::SIZE {
194 Ok(Self {
195 id: (&value[0..Self::ID_SIZE])
196 .try_into()
197 .map_err(|_| GeneralError::ParseError("PathTelemetry.id".into()))?,
198 path: (&value[Self::ID_SIZE..(Self::ID_SIZE + Self::PATH_SIZE)])
199 .try_into()
200 .map_err(|_| GeneralError::ParseError("PathTelemetry.path".into()))?,
201 timestamp: u128::from_be_bytes(
202 (&value[(Self::ID_SIZE + Self::PATH_SIZE)..Self::SIZE])
203 .try_into()
204 .map_err(|_| GeneralError::ParseError("PathTelemetry.timestamp".into()))?,
205 ),
206 })
207 } else {
208 Err(GeneralError::ParseError("PathTelemetry".into()))
209 }
210 }
211}
212
213#[derive(Debug, Clone)]
217pub struct NeighborTelemetry {
218 pub peer: OffchainPublicKey,
219 pub rtt: std::time::Duration,
220}
221
222impl hopr_api::graph::MeasurablePeer for NeighborTelemetry {
223 fn peer(&self) -> &OffchainPublicKey {
224 &self.peer
225 }
226
227 fn rtt(&self) -> std::time::Duration {
228 self.rtt
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use anyhow::Context;
235 use hopr_api::types::crypto::prelude::{Keypair, OffchainKeypair};
236 use rstest::rstest;
237
238 use super::*;
239
240 #[test]
241 fn tagged_routing_neighbor_should_create_zero_hop_forward() -> anyhow::Result<()> {
242 let dest = Box::new(NodeId::Offchain(*OffchainKeypair::random().public()));
243 let routing = TaggedDestinationRouting::neighbor(dest.clone());
244
245 anyhow::ensure!(
246 matches!(routing.forward_options, RoutingOptions::Hops(h) if u8::from(h) == 0),
247 "neighbor forward should be Hops(0)"
248 );
249 anyhow::ensure!(
250 matches!(&routing.return_options, Some(RoutingOptions::Hops(h)) if u8::from(*h) == 0),
251 "neighbor return should be Some(Hops(0))"
252 );
253 anyhow::ensure!(*routing.destination == *dest, "destination mismatch");
254 Ok(())
255 }
256
257 #[test]
258 fn tagged_routing_loopback_should_create_intermediate_path() -> anyhow::Result<()> {
259 let me = Box::new(NodeId::Offchain(*OffchainKeypair::random().public()));
260 let path = BoundedVec::try_from(vec![NodeId::Offchain(*OffchainKeypair::random().public())])
261 .context("building path")?;
262
263 let routing = TaggedDestinationRouting::loopback(me.clone(), path);
264
265 anyhow::ensure!(
266 matches!(routing.forward_options, RoutingOptions::IntermediatePath(_)),
267 "loopback forward should be IntermediatePath"
268 );
269 anyhow::ensure!(routing.return_options.is_none(), "loopback should have no return");
270 anyhow::ensure!(*routing.destination == *me, "destination mismatch");
271 Ok(())
272 }
273
274 #[test]
275 fn tagged_routing_should_convert_to_destination_routing() -> anyhow::Result<()> {
276 let dest = Box::new(NodeId::Offchain(*OffchainKeypair::random().public()));
277 let routing = TaggedDestinationRouting::neighbor(dest);
278 let converted: DestinationRouting = routing.into();
279
280 anyhow::ensure!(
281 matches!(converted, DestinationRouting::Forward { .. }),
282 "conversion should produce Forward variant"
283 );
284 Ok(())
285 }
286
287 #[rstest]
288 #[case::ping_ping(
289 NeighborProbe::Ping([1u8; NeighborProbe::NONCE_SIZE]),
290 NeighborProbe::Ping([1u8; NeighborProbe::NONCE_SIZE])
291 )]
292 #[case::pong_pong(
293 NeighborProbe::Pong([2u8; NeighborProbe::NONCE_SIZE]),
294 NeighborProbe::Pong([2u8; NeighborProbe::NONCE_SIZE])
295 )]
296 fn neighbor_probe_complement_should_return_false_when_same_variant(
297 #[case] a: NeighborProbe,
298 #[case] b: NeighborProbe,
299 ) {
300 assert!(!a.is_complement_to(b));
301 }
302
303 #[test]
304 fn neighbor_probe_deserialization_should_roundtrip_ping() -> anyhow::Result<()> {
305 let ping = NeighborProbe::Ping([42u8; NeighborProbe::NONCE_SIZE]);
306 let bytes = ping.to_bytes();
307 let restored = NeighborProbe::try_from(bytes.as_ref()).context("deserializing ping")?;
308 assert_eq!(ping, restored);
309 Ok(())
310 }
311
312 #[test]
313 fn neighbor_probe_deserialization_should_roundtrip_pong() -> anyhow::Result<()> {
314 let pong = NeighborProbe::Pong([99u8; NeighborProbe::NONCE_SIZE]);
315 let bytes = pong.to_bytes();
316 let restored = NeighborProbe::try_from(bytes.as_ref()).context("deserializing pong")?;
317 assert_eq!(pong, restored);
318 Ok(())
319 }
320
321 #[test]
322 fn neighbor_probe_deserialization_should_fail_on_wrong_size() {
323 let short = [0u8; 5];
324 assert!(matches!(
325 NeighborProbe::try_from(short.as_slice()),
326 Err(GeneralError::ParseError(ref s)) if s.contains("size")
327 ));
328 }
329
330 #[test]
331 fn neighbor_probe_display_should_show_variant_and_hex() {
332 let nonce = [0xABu8; NeighborProbe::NONCE_SIZE];
333 let ping = NeighborProbe::Ping(nonce);
334 let pong = NeighborProbe::Pong(nonce);
335
336 let ping_str = ping.to_string();
337 let pong_str = pong.to_string();
338
339 assert!(ping_str.starts_with("Ping("), "got: {ping_str}");
340 assert!(pong_str.starts_with("Pong("), "got: {pong_str}");
341 assert!(ping_str.contains("abab"), "should contain hex nonce");
342 }
343
344 #[test]
345 fn path_telemetry_roundtrip_should_preserve_all_fields() -> anyhow::Result<()> {
346 let telemetry = PathTelemetry {
347 id: [1, 2, 3, 4, 5, 6, 7, 8],
348 path: [0xFFu8; PathTelemetry::PATH_SIZE],
349 timestamp: 1234567890,
350 };
351 let bytes = telemetry.to_bytes();
352 let restored = PathTelemetry::try_from(bytes.as_ref()).context("deserializing telemetry")?;
353 assert_eq!(telemetry, restored);
354 Ok(())
355 }
356
357 #[test]
358 fn path_telemetry_display_should_include_hex_id_and_timestamp() {
359 let telemetry = PathTelemetry {
360 id: [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE],
361 path: [0u8; PathTelemetry::PATH_SIZE],
362 timestamp: 42,
363 };
364 let display = telemetry.to_string();
365 assert!(display.contains("deadbeefcafebabe"), "should contain hex id: {display}");
366 assert!(display.contains("42"), "should contain timestamp: {display}");
367 }
368}