reth_network_peers/
node_record.rs

1//! Commonly used `NodeRecord` type for peers.
2
3use crate::PeerId;
4use alloc::{
5    format,
6    string::{String, ToString},
7};
8use alloy_rlp::{RlpDecodable, RlpEncodable};
9use core::{
10    fmt,
11    fmt::Write,
12    net::{IpAddr, Ipv4Addr, SocketAddr},
13    num::ParseIntError,
14    str::FromStr,
15};
16use serde_with::{DeserializeFromStr, SerializeDisplay};
17
18#[cfg(feature = "secp256k1")]
19use enr::Enr;
20
21/// Represents an ENR in discovery.
22///
23/// Note: this is only an excerpt of the [`NodeRecord`] data structure.
24#[derive(
25    Clone,
26    Copy,
27    Debug,
28    Eq,
29    PartialEq,
30    Hash,
31    SerializeDisplay,
32    DeserializeFromStr,
33    RlpEncodable,
34    RlpDecodable,
35)]
36pub struct NodeRecord {
37    /// The Address of a node.
38    pub address: IpAddr,
39    /// UDP discovery port.
40    pub udp_port: u16,
41    /// TCP port of the port that accepts connections.
42    pub tcp_port: u16,
43    /// Public key of the discovery service
44    pub id: PeerId,
45}
46
47impl NodeRecord {
48    /// Derive the [`NodeRecord`] from the secret key and addr.
49    ///
50    /// Note: this will set both the TCP and UDP ports to the port of the addr.
51    #[cfg(feature = "secp256k1")]
52    pub fn from_secret_key(addr: SocketAddr, sk: &secp256k1::SecretKey) -> Self {
53        let pk = secp256k1::PublicKey::from_secret_key(secp256k1::SECP256K1, sk);
54        let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]);
55        Self::new(addr, id)
56    }
57
58    /// Converts the `address` into an [`Ipv4Addr`] if the `address` is a mapped
59    /// [`Ipv6Addr`](std::net::Ipv6Addr).
60    ///
61    /// Returns `true` if the address was converted.
62    ///
63    /// See also [`std::net::Ipv6Addr::to_ipv4_mapped`]
64    pub fn convert_ipv4_mapped(&mut self) -> bool {
65        // convert IPv4 mapped IPv6 address
66        if let IpAddr::V6(v6) = self.address {
67            if let Some(v4) = v6.to_ipv4_mapped() {
68                self.address = v4.into();
69                return true
70            }
71        }
72        false
73    }
74
75    /// Same as [`Self::convert_ipv4_mapped`] but consumes the type
76    pub fn into_ipv4_mapped(mut self) -> Self {
77        self.convert_ipv4_mapped();
78        self
79    }
80
81    /// Sets the tcp port
82    pub const fn with_tcp_port(mut self, port: u16) -> Self {
83        self.tcp_port = port;
84        self
85    }
86
87    /// Sets the udp port
88    pub const fn with_udp_port(mut self, port: u16) -> Self {
89        self.udp_port = port;
90        self
91    }
92
93    /// Creates a new record from a socket addr and peer id.
94    pub const fn new(addr: SocketAddr, id: PeerId) -> Self {
95        Self { address: addr.ip(), tcp_port: addr.port(), udp_port: addr.port(), id }
96    }
97
98    /// Creates a new record from an ip address and ports.
99    pub fn new_with_ports(
100        ip_addr: IpAddr,
101        tcp_port: u16,
102        udp_port: Option<u16>,
103        id: PeerId,
104    ) -> Self {
105        let udp_port = udp_port.unwrap_or(tcp_port);
106        Self { address: ip_addr, tcp_port, udp_port, id }
107    }
108
109    /// The TCP socket address of this node
110    #[must_use]
111    pub const fn tcp_addr(&self) -> SocketAddr {
112        SocketAddr::new(self.address, self.tcp_port)
113    }
114
115    /// The UDP socket address of this node
116    #[must_use]
117    pub const fn udp_addr(&self) -> SocketAddr {
118        SocketAddr::new(self.address, self.udp_port)
119    }
120}
121
122impl fmt::Display for NodeRecord {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.write_str("enode://")?;
125        alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?;
126        f.write_char('@')?;
127        match self.address {
128            IpAddr::V4(ip) => {
129                ip.fmt(f)?;
130            }
131            IpAddr::V6(ip) => {
132                // encapsulate with brackets
133                f.write_char('[')?;
134                ip.fmt(f)?;
135                f.write_char(']')?;
136            }
137        }
138        f.write_char(':')?;
139        self.tcp_port.fmt(f)?;
140        if self.tcp_port != self.udp_port {
141            f.write_str("?discport=")?;
142            self.udp_port.fmt(f)?;
143        }
144
145        Ok(())
146    }
147}
148
149/// Possible error types when parsing a [`NodeRecord`]
150#[derive(Debug, thiserror::Error)]
151pub enum NodeRecordParseError {
152    /// Invalid url
153    #[error("Failed to parse url: {0}")]
154    InvalidUrl(String),
155    /// Invalid id
156    #[error("Failed to parse id")]
157    InvalidId(String),
158    /// Invalid discport
159    #[error("Failed to discport query: {0}")]
160    Discport(ParseIntError),
161}
162
163impl FromStr for NodeRecord {
164    type Err = NodeRecordParseError;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        use url::{Host, Url};
168
169        let url = Url::parse(s).map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
170
171        let address = match url.host() {
172            Some(Host::Ipv4(ip)) => IpAddr::V4(ip),
173            Some(Host::Ipv6(ip)) => IpAddr::V6(ip),
174            Some(Host::Domain(ip)) => IpAddr::V4(
175                Ipv4Addr::from_str(ip)
176                    .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?,
177            ),
178            _ => return Err(NodeRecordParseError::InvalidUrl(format!("invalid host: {url:?}"))),
179        };
180        let port = url
181            .port()
182            .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
183
184        let udp_port = if let Some(discovery_port) = url
185            .query_pairs()
186            .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
187        {
188            discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
189        } else {
190            port
191        };
192
193        let id = url
194            .username()
195            .parse::<PeerId>()
196            .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
197
198        Ok(Self { address, id, tcp_port: port, udp_port })
199    }
200}
201
202#[cfg(feature = "secp256k1")]
203impl TryFrom<Enr<secp256k1::SecretKey>> for NodeRecord {
204    type Error = NodeRecordParseError;
205
206    fn try_from(enr: Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
207        (&enr).try_into()
208    }
209}
210
211#[cfg(feature = "secp256k1")]
212impl TryFrom<&Enr<secp256k1::SecretKey>> for NodeRecord {
213    type Error = NodeRecordParseError;
214
215    fn try_from(enr: &Enr<secp256k1::SecretKey>) -> Result<Self, Self::Error> {
216        let Some(address) = enr.ip4().map(IpAddr::from).or_else(|| enr.ip6().map(IpAddr::from))
217        else {
218            return Err(NodeRecordParseError::InvalidUrl("ip missing".to_string()))
219        };
220
221        let Some(udp_port) = enr.udp4().or_else(|| enr.udp6()) else {
222            return Err(NodeRecordParseError::InvalidUrl("udp port missing".to_string()))
223        };
224
225        let Some(tcp_port) = enr.tcp4().or_else(|| enr.tcp6()) else {
226            return Err(NodeRecordParseError::InvalidUrl("tcp port missing".to_string()))
227        };
228
229        let id = crate::pk2id(&enr.public_key());
230
231        Ok(Self { address, tcp_port, udp_port, id }.into_ipv4_mapped())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use alloy_rlp::Decodable;
239    use rand::{thread_rng, Rng, RngCore};
240    use std::net::Ipv6Addr;
241
242    #[test]
243    fn test_mapped_ipv6() {
244        let mut rng = thread_rng();
245
246        let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
247        let v6 = v4.to_ipv6_mapped();
248
249        let record = NodeRecord {
250            address: v6.into(),
251            tcp_port: rng.gen(),
252            udp_port: rng.gen(),
253            id: rng.gen(),
254        };
255
256        assert!(record.clone().convert_ipv4_mapped());
257        assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
258    }
259
260    #[test]
261    fn test_mapped_ipv4() {
262        let mut rng = thread_rng();
263        let v4: Ipv4Addr = "0.0.0.0".parse().unwrap();
264
265        let record = NodeRecord {
266            address: v4.into(),
267            tcp_port: rng.gen(),
268            udp_port: rng.gen(),
269            id: rng.gen(),
270        };
271
272        assert!(!record.clone().convert_ipv4_mapped());
273        assert_eq!(record.into_ipv4_mapped().address, IpAddr::from(v4));
274    }
275
276    #[test]
277    fn test_noderecord_codec_ipv4() {
278        let mut rng = thread_rng();
279        for _ in 0..100 {
280            let mut ip = [0u8; 4];
281            rng.fill_bytes(&mut ip);
282            let record = NodeRecord {
283                address: IpAddr::V4(ip.into()),
284                tcp_port: rng.gen(),
285                udp_port: rng.gen(),
286                id: rng.gen(),
287            };
288
289            let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
290            assert_eq!(record, decoded);
291        }
292    }
293
294    #[test]
295    fn test_noderecord_codec_ipv6() {
296        let mut rng = thread_rng();
297        for _ in 0..100 {
298            let mut ip = [0u8; 16];
299            rng.fill_bytes(&mut ip);
300            let record = NodeRecord {
301                address: IpAddr::V6(ip.into()),
302                tcp_port: rng.gen(),
303                udp_port: rng.gen(),
304                id: rng.gen(),
305            };
306
307            let decoded = NodeRecord::decode(&mut alloy_rlp::encode(record).as_slice()).unwrap();
308            assert_eq!(record, decoded);
309        }
310    }
311
312    #[test]
313    fn test_url_parse() {
314        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
315        let node: NodeRecord = url.parse().unwrap();
316        assert_eq!(node, NodeRecord {
317            address: IpAddr::V4([10,3,58,6].into()),
318            tcp_port: 30303,
319            udp_port: 30301,
320            id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
321        })
322    }
323
324    #[test]
325    fn test_node_display() {
326        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
327        let node: NodeRecord = url.parse().unwrap();
328        assert_eq!(url, &format!("{node}"));
329    }
330
331    #[test]
332    fn test_node_display_discport() {
333        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
334        let node: NodeRecord = url.parse().unwrap();
335        assert_eq!(url, &format!("{node}"));
336    }
337
338    #[test]
339    fn test_node_serialize() {
340        let cases = vec![
341            // IPv4
342            (
343                NodeRecord {
344                    address: IpAddr::V4([10, 3, 58, 6].into()),
345                    tcp_port: 30303u16,
346                    udp_port: 30301u16,
347                    id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
348                },
349                "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
350            ),
351            // IPv6
352            (
353                NodeRecord {
354                    address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
355                    tcp_port: 52150u16,
356                    udp_port: 52151u16,
357                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
358                },
359                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
360            )
361        ];
362
363        for (node, expected) in cases {
364            let ser = serde_json::to_string::<NodeRecord>(&node).expect("couldn't serialize");
365            assert_eq!(ser, expected);
366        }
367    }
368
369    #[test]
370    fn test_node_deserialize() {
371        let cases = vec![
372            // IPv4
373            (
374                "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
375                NodeRecord {
376                    address: IpAddr::V4([10, 3, 58, 6].into()),
377                    tcp_port: 30303u16,
378                    udp_port: 30301u16,
379                    id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
380                }
381            ),
382            // IPv6
383            (
384                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
385                NodeRecord {
386                    address: Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12).into(),
387                    tcp_port: 52150u16,
388                    udp_port: 52151u16,
389                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
390                }
391            ),
392        ];
393
394        for (url, expected) in cases {
395            let node: NodeRecord = serde_json::from_str(url).expect("couldn't deserialize");
396            assert_eq!(node, expected);
397        }
398    }
399}