reth_network_peers/
trusted_peer.rs

1//! `NodeRecord` type that uses a domain instead of an IP.
2
3use crate::{NodeRecord, PeerId};
4use alloc::string::{String, ToString};
5use core::{
6    fmt::{self, Write},
7    net::IpAddr,
8    num::ParseIntError,
9    str::FromStr,
10};
11use serde_with::{DeserializeFromStr, SerializeDisplay};
12use url::Host;
13
14/// Represents the node record of a trusted peer. The only difference between this and a
15/// [`NodeRecord`] is that this does not contain the IP address of the peer, but rather a domain
16/// __or__ IP address.
17///
18/// This is useful when specifying nodes which are in internal infrastructure and may only be
19/// discoverable reliably using DNS.
20///
21/// This should NOT be used for any use case other than in trusted peer lists.
22#[derive(Clone, Debug, Eq, PartialEq, Hash, SerializeDisplay, DeserializeFromStr)]
23pub struct TrustedPeer {
24    /// The host of a node.
25    pub host: Host,
26    /// TCP port of the port that accepts connections.
27    pub tcp_port: u16,
28    /// UDP discovery port.
29    pub udp_port: u16,
30    /// Public key of the discovery service
31    pub id: PeerId,
32}
33
34impl TrustedPeer {
35    /// Derive the [`NodeRecord`] from the secret key and addr
36    #[cfg(feature = "secp256k1")]
37    pub fn from_secret_key(host: Host, port: u16, sk: &secp256k1::SecretKey) -> Self {
38        let pk = secp256k1::PublicKey::from_secret_key(secp256k1::SECP256K1, sk);
39        let id = PeerId::from_slice(&pk.serialize_uncompressed()[1..]);
40        Self::new(host, port, id)
41    }
42
43    /// Creates a new record from a socket addr and peer id.
44    pub const fn new(host: Host, port: u16, id: PeerId) -> Self {
45        Self { host, tcp_port: port, udp_port: port, id }
46    }
47
48    #[cfg(any(test, feature = "std"))]
49    const fn to_node_record(&self, ip: IpAddr) -> NodeRecord {
50        NodeRecord { address: ip, id: self.id, tcp_port: self.tcp_port, udp_port: self.udp_port }
51    }
52
53    /// Tries to resolve directly to a [`NodeRecord`] if the host is an IP address.
54    #[cfg(any(test, feature = "std"))]
55    fn try_node_record(&self) -> Result<NodeRecord, &str> {
56        match &self.host {
57            Host::Ipv4(ip) => Ok(self.to_node_record((*ip).into())),
58            Host::Ipv6(ip) => Ok(self.to_node_record((*ip).into())),
59            Host::Domain(domain) => Err(domain),
60        }
61    }
62
63    /// Resolves the host in a [`TrustedPeer`] to an IP address, returning a [`NodeRecord`].
64    ///
65    /// This use [`ToSocketAddr`](std::net::ToSocketAddrs) to resolve the host to an IP address.
66    #[cfg(any(test, feature = "std"))]
67    pub fn resolve_blocking(&self) -> Result<NodeRecord, std::io::Error> {
68        let domain = match self.try_node_record() {
69            Ok(record) => return Ok(record),
70            Err(domain) => domain,
71        };
72        // Resolve the domain to an IP address
73        let mut ips = std::net::ToSocketAddrs::to_socket_addrs(&(domain, 0))?;
74        let ip = ips.next().ok_or_else(|| {
75            std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, "No IP found")
76        })?;
77
78        Ok(self.to_node_record(ip.ip()))
79    }
80
81    /// Resolves the host in a [`TrustedPeer`] to an IP address, returning a [`NodeRecord`].
82    #[cfg(any(test, feature = "net"))]
83    pub async fn resolve(&self) -> Result<NodeRecord, std::io::Error> {
84        let domain = match self.try_node_record() {
85            Ok(record) => return Ok(record),
86            Err(domain) => domain,
87        };
88
89        // Resolve the domain to an IP address
90        let mut ips = tokio::net::lookup_host(format!("{domain}:0")).await?;
91        let ip = ips.next().ok_or_else(|| {
92            std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, "No IP found")
93        })?;
94
95        Ok(self.to_node_record(ip.ip()))
96    }
97}
98
99impl fmt::Display for TrustedPeer {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str("enode://")?;
102        alloy_primitives::hex::encode(self.id.as_slice()).fmt(f)?;
103        f.write_char('@')?;
104        self.host.fmt(f)?;
105        f.write_char(':')?;
106        self.tcp_port.fmt(f)?;
107        if self.tcp_port != self.udp_port {
108            f.write_str("?discport=")?;
109            self.udp_port.fmt(f)?;
110        }
111
112        Ok(())
113    }
114}
115
116/// Possible error types when parsing a [`NodeRecord`]
117#[derive(Debug, thiserror::Error)]
118pub enum NodeRecordParseError {
119    /// Invalid url
120    #[error("Failed to parse url: {0}")]
121    InvalidUrl(String),
122    /// Invalid id
123    #[error("Failed to parse id")]
124    InvalidId(String),
125    /// Invalid discport
126    #[error("Failed to discport query: {0}")]
127    Discport(ParseIntError),
128}
129
130impl FromStr for TrustedPeer {
131    type Err = NodeRecordParseError;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        use url::Url;
135
136        // Parse the URL with enode prefix replaced with http.
137        // The enode prefix causes the parser to use parse_opaque() on
138        // the host str which only handles domains and ipv6, not ipv4.
139        let url = Url::parse(s.replace("enode://", "http://").as_str())
140            .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
141
142        let host = url
143            .host()
144            .ok_or_else(|| NodeRecordParseError::InvalidUrl("no host specified".to_string()))?
145            .to_owned();
146
147        let port = url
148            .port()
149            .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
150
151        let udp_port = if let Some(discovery_port) = url
152            .query_pairs()
153            .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
154        {
155            discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
156        } else {
157            port
158        };
159
160        let id = url
161            .username()
162            .parse::<PeerId>()
163            .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
164
165        Ok(Self { host, id, tcp_port: port, udp_port })
166    }
167}
168
169impl From<NodeRecord> for TrustedPeer {
170    fn from(record: NodeRecord) -> Self {
171        let host = match record.address {
172            IpAddr::V4(ip) => Host::Ipv4(ip),
173            IpAddr::V6(ip) => Host::Ipv6(ip),
174        };
175
176        Self { host, tcp_port: record.tcp_port, udp_port: record.udp_port, id: record.id }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use std::net::Ipv6Addr;
184
185    #[test]
186    fn test_url_parse() {
187        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
188        let node: TrustedPeer = url.parse().unwrap();
189        assert_eq!(node, TrustedPeer {
190            host: Host::Ipv4([10,3,58,6].into()),
191            tcp_port: 30303,
192            udp_port: 30301,
193            id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
194        })
195    }
196
197    #[test]
198    fn test_node_display() {
199        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
200        let node: TrustedPeer = url.parse().unwrap();
201        assert_eq!(url, &format!("{node}"));
202    }
203
204    #[test]
205    fn test_node_display_discport() {
206        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
207        let node: TrustedPeer = url.parse().unwrap();
208        assert_eq!(url, &format!("{node}"));
209    }
210
211    #[test]
212    fn test_node_serialize() {
213        let cases = vec![
214            // IPv4
215            (
216                TrustedPeer {
217                    host: Host::Ipv4([10, 3, 58, 6].into()),
218                    tcp_port: 30303u16,
219                    udp_port: 30301u16,
220                    id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
221                },
222                "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
223            ),
224            // IPv6
225            (
226                TrustedPeer {
227                    host: Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12)),
228                    tcp_port: 52150u16,
229                    udp_port: 52151u16,
230                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
231                },
232                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\""
233            ),
234            // URL
235            (
236                TrustedPeer {
237                    host: Host::Domain("my-domain".to_string()),
238                    tcp_port: 52150u16,
239                    udp_port: 52151u16,
240                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
241                },
242                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@my-domain:52150?discport=52151\""
243            ),
244        ];
245
246        for (node, expected) in cases {
247            let ser = serde_json::to_string::<TrustedPeer>(&node).expect("couldn't serialize");
248            assert_eq!(ser, expected);
249        }
250    }
251
252    #[test]
253    fn test_node_deserialize() {
254        let cases = vec![
255            // IPv4
256            (
257                "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
258                TrustedPeer {
259                    host: Host::Ipv4([10, 3, 58, 6].into()),
260                    tcp_port: 30303u16,
261                    udp_port: 30301u16,
262                    id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
263                }
264            ),
265            // IPv6
266            (
267                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
268                TrustedPeer {
269                    host: Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12)),
270                    tcp_port: 52150u16,
271                    udp_port: 52151u16,
272                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
273                }
274            ),
275            // URL
276            (
277                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@my-domain:52150?discport=52151\"",
278                TrustedPeer {
279                    host: Host::Domain("my-domain".to_string()),
280                    tcp_port: 52150u16,
281                    udp_port: 52151u16,
282                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
283                }
284            ),
285        ];
286
287        for (url, expected) in cases {
288            let node: TrustedPeer = serde_json::from_str(url).expect("couldn't deserialize");
289            assert_eq!(node, expected);
290        }
291    }
292
293    #[tokio::test]
294    async fn test_resolve_dns_node_record() {
295        // Set up tests
296        let tests = vec![("localhost")];
297
298        // Run tests
299        for domain in tests {
300            // Construct record
301            let rec =
302                TrustedPeer::new(url::Host::Domain(domain.to_owned()), 30300, PeerId::random());
303
304            // Resolve domain and validate
305            let ensure = |rec: NodeRecord| match rec.address {
306                IpAddr::V4(addr) => {
307                    assert_eq!(addr, std::net::Ipv4Addr::new(127, 0, 0, 1))
308                }
309                IpAddr::V6(addr) => {
310                    assert_eq!(addr, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
311                }
312            };
313            ensure(rec.resolve().await.unwrap());
314            ensure(rec.resolve_blocking().unwrap());
315        }
316    }
317}