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