Skip to main content

reth_network_peers/
trusted_peer.rs

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