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        use url::Url;
120
121        // Parse the URL with enode prefix replaced with http.
122        // The enode prefix causes the parser to use parse_opaque() on
123        // the host str which only handles domains and ipv6, not ipv4.
124        let url = Url::parse(s.replace("enode://", "http://").as_str())
125            .map_err(|e| NodeRecordParseError::InvalidUrl(e.to_string()))?;
126
127        let host = url
128            .host()
129            .ok_or_else(|| NodeRecordParseError::InvalidUrl("no host specified".to_string()))?
130            .to_owned();
131
132        let port = url
133            .port()
134            .ok_or_else(|| NodeRecordParseError::InvalidUrl("no port specified".to_string()))?;
135
136        let udp_port = if let Some(discovery_port) = url
137            .query_pairs()
138            .find_map(|(maybe_disc, port)| (maybe_disc.as_ref() == "discport").then_some(port))
139        {
140            discovery_port.parse::<u16>().map_err(NodeRecordParseError::Discport)?
141        } else {
142            port
143        };
144
145        let id = url
146            .username()
147            .parse::<PeerId>()
148            .map_err(|e| NodeRecordParseError::InvalidId(e.to_string()))?;
149
150        Ok(Self { host, id, tcp_port: port, udp_port })
151    }
152}
153
154impl From<NodeRecord> for TrustedPeer {
155    fn from(record: NodeRecord) -> Self {
156        let host = match record.address {
157            IpAddr::V4(ip) => Host::Ipv4(ip),
158            IpAddr::V6(ip) => Host::Ipv6(ip),
159        };
160
161        Self { host, tcp_port: record.tcp_port, udp_port: record.udp_port, id: record.id }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::net::Ipv6Addr;
169
170    #[test]
171    fn test_url_parse() {
172        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
173        let node: TrustedPeer = url.parse().unwrap();
174        assert_eq!(node, TrustedPeer {
175            host: Host::Ipv4([10,3,58,6].into()),
176            tcp_port: 30303,
177            udp_port: 30301,
178            id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
179        })
180    }
181
182    #[test]
183    fn test_node_display() {
184        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303";
185        let node: TrustedPeer = url.parse().unwrap();
186        assert_eq!(url, &format!("{node}"));
187    }
188
189    #[test]
190    fn test_node_display_discport() {
191        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
192        let node: TrustedPeer = url.parse().unwrap();
193        assert_eq!(url, &format!("{node}"));
194    }
195
196    #[test]
197    fn test_node_serialize() {
198        let cases = vec![
199            // IPv4
200            (
201                TrustedPeer {
202                    host: Host::Ipv4([10, 3, 58, 6].into()),
203                    tcp_port: 30303u16,
204                    udp_port: 30301u16,
205                    id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
206                },
207                "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\""
208            ),
209            // IPv6
210            (
211                TrustedPeer {
212                    host: Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12)),
213                    tcp_port: 52150u16,
214                    udp_port: 52151u16,
215                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
216                },
217                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\""
218            ),
219            // URL
220            (
221                TrustedPeer {
222                    host: Host::Domain("my-domain".to_string()),
223                    tcp_port: 52150u16,
224                    udp_port: 52151u16,
225                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
226                },
227                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@my-domain:52150?discport=52151\""
228            ),
229        ];
230
231        for (node, expected) in cases {
232            let ser = serde_json::to_string::<TrustedPeer>(&node).expect("couldn't serialize");
233            assert_eq!(ser, expected);
234        }
235    }
236
237    #[test]
238    fn test_node_deserialize() {
239        let cases = vec![
240            // IPv4
241            (
242                "\"enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301\"",
243                TrustedPeer {
244                    host: Host::Ipv4([10, 3, 58, 6].into()),
245                    tcp_port: 30303u16,
246                    udp_port: 30301u16,
247                    id: PeerId::from_str("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0").unwrap(),
248                }
249            ),
250            // IPv6
251            (
252                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@[2001:db8:3c4d:15::abcd:ef12]:52150?discport=52151\"",
253                TrustedPeer {
254                    host: Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0x3c4d, 0x15, 0x0, 0x0, 0xabcd, 0xef12)),
255                    tcp_port: 52150u16,
256                    udp_port: 52151u16,
257                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
258                }
259            ),
260            // URL
261            (
262                "\"enode://1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439@my-domain:52150?discport=52151\"",
263                TrustedPeer {
264                    host: Host::Domain("my-domain".to_string()),
265                    tcp_port: 52150u16,
266                    udp_port: 52151u16,
267                    id: PeerId::from_str("1dd9d65c4552b5eb43d5ad55a2ee3f56c6cbc1c64a5c8d659f51fcd51bace24351232b8d7821617d2b29b54b81cdefb9b3e9c37d7fd5f63270bcc9e1a6f6a439").unwrap(),
268                }
269            ),
270        ];
271
272        for (url, expected) in cases {
273            let node: TrustedPeer = serde_json::from_str(url).expect("couldn't deserialize");
274            assert_eq!(node, expected);
275        }
276    }
277
278    #[tokio::test]
279    async fn test_resolve_dns_node_record() {
280        // Set up tests
281        let tests = vec![("localhost")];
282
283        // Run tests
284        for domain in tests {
285            // Construct record
286            let rec =
287                TrustedPeer::new(url::Host::Domain(domain.to_owned()), 30300, PeerId::random());
288
289            // Resolve domain and validate
290            let ensure = |rec: NodeRecord| match rec.address {
291                IpAddr::V4(addr) => {
292                    assert_eq!(addr, std::net::Ipv4Addr::new(127, 0, 0, 1))
293                }
294                IpAddr::V6(addr) => {
295                    assert_eq!(addr, Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
296                }
297            };
298            ensure(rec.resolve().await.unwrap());
299            ensure(rec.resolve_blocking().unwrap());
300        }
301    }
302}