Skip to main content

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