hopr_transport_probe/
content.rs

1use anyhow::Context;
2use hopr_transport_packet::prelude::{ApplicationData, ReservedTag, Tag};
3
4/// Serializable and deserializable enum for the probe message content
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
6pub enum NeighborProbe {
7    /// Ping message with a random nonce
8    Ping([u8; Self::SIZE]),
9    /// Pong message repliying to a specific nonce
10    Pong([u8; Self::SIZE]),
11}
12
13impl NeighborProbe {
14    pub const SIZE: usize = 32;
15
16    /// Returns the nonce of the message
17    pub fn random_nonce() -> Self {
18        const SIZE: usize = NeighborProbe::SIZE;
19        Self::Ping(hopr_crypto_random::random_bytes::<SIZE>())
20    }
21
22    pub fn is_complement_to(&self, other: Self) -> bool {
23        match (self, &other) {
24            (Self::Ping(nonce1), Self::Pong(nonce2)) => nonce1 == nonce2,
25            (Self::Pong(nonce1), Self::Ping(nonce2)) => nonce1 == nonce2,
26            _ => false,
27        }
28    }
29}
30
31impl std::fmt::Display for NeighborProbe {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            NeighborProbe::Ping(nonce) => write!(f, "Ping({})", hex::encode(nonce)),
35            NeighborProbe::Pong(nonce) => write!(f, "Pong({})", hex::encode(nonce)),
36        }
37    }
38}
39
40impl From<NeighborProbe> for [u8; NeighborProbe::SIZE] {
41    fn from(probe: NeighborProbe) -> Self {
42        match probe {
43            NeighborProbe::Ping(nonce) | NeighborProbe::Pong(nonce) => nonce,
44        }
45    }
46}
47
48/// TODO: TBD as a separate task for network discovery
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
50pub struct PathTelemetry {
51    id: [u8; 10],
52    path: [u8; 10],
53    timestamp: u128,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
57pub enum Message {
58    Telemetry(PathTelemetry),
59    Probe(NeighborProbe),
60}
61
62impl std::fmt::Display for Message {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Message::Telemetry(telemetry) => write!(f, "Telemetry({:?})", telemetry),
66            Message::Probe(probe) => write!(f, "Probe({})", probe),
67        }
68    }
69}
70
71impl TryFrom<Message> for ApplicationData {
72    type Error = anyhow::Error;
73
74    fn try_from(message: Message) -> Result<Self, Self::Error> {
75        let tag: Tag = ReservedTag::Ping.into();
76        Ok(ApplicationData {
77            application_tag: tag,
78            plain_text: bitcode::serialize(&message)
79                .context("Failed to serialize message")?
80                .into_boxed_slice(),
81        })
82    }
83}
84
85impl TryFrom<ApplicationData> for Message {
86    type Error = anyhow::Error;
87
88    fn try_from(value: ApplicationData) -> Result<Self, Self::Error> {
89        let reserved_probe_tag: Tag = ReservedTag::Ping.into();
90
91        if value.application_tag == reserved_probe_tag {
92            let message: Message = bitcode::deserialize(&value.plain_text).context("Failed to deserialize message")?;
93            Ok(message)
94        } else {
95            Err(anyhow::anyhow!("Non probing application tag found"))
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use hopr_platform::time::native::current_time;
103    use hopr_primitive_types::traits::AsUnixTimestamp;
104    use more_asserts::assert_lt;
105
106    use super::*;
107
108    #[test]
109    fn random_generation_of_a_neighbor_probe_produces_a_ping() {
110        let ping = NeighborProbe::random_nonce();
111        assert!(matches!(ping, NeighborProbe::Ping(_)));
112    }
113
114    #[test]
115    fn check_for_complement_works_for_ping_and_pong_with_the_same_nonce() {
116        let ping = NeighborProbe::random_nonce();
117        let pong = { NeighborProbe::Pong(ping.into()) };
118
119        assert!(ping.is_complement_to(pong));
120    }
121
122    #[test]
123    fn check_for_complement_fails_for_ping_and_pong_with_different_nonce() {
124        let ping = NeighborProbe::random_nonce();
125        let pong = {
126            let mut other: [u8; NeighborProbe::SIZE] = ping.into();
127            other[0] = other[0].wrapping_add(1); // Modify the first byte to ensure it's different
128            NeighborProbe::Pong(other)
129        };
130
131        assert!(!ping.is_complement_to(pong));
132    }
133
134    #[test]
135    fn check_that_at_least_one_surb_can_fit_into_the_payload_for_direct_probing() -> anyhow::Result<()> {
136        let ping = NeighborProbe::random_nonce();
137        let as_data: ApplicationData = Message::Probe(ping).try_into()?;
138
139        assert_lt!(
140            as_data.plain_text.len(),
141            ApplicationData::PAYLOAD_SIZE - hopr_db_api::protocol::HoprSurb::SIZE
142        );
143
144        Ok(())
145    }
146
147    #[test]
148    fn check_that_at_least_one_surb_can_fit_into_the_payload_for_path_telemetry() -> anyhow::Result<()> {
149        let telemetry = PathTelemetry {
150            id: [1; 10],
151            path: [1; 10],
152            timestamp: current_time().as_unix_timestamp().as_millis(),
153        };
154        let as_data: ApplicationData = Message::Telemetry(telemetry).try_into()?;
155
156        assert_lt!(
157            as_data.plain_text.len(),
158            ApplicationData::PAYLOAD_SIZE - hopr_db_api::protocol::HoprSurb::SIZE
159        );
160
161        Ok(())
162    }
163}