Skip to main content

reth_network_peers/
lib.rs

1//! Network Types and Utilities.
2//!
3//! This crate manages and converts Ethereum network entities such as node records, peer IDs, and
4//! Ethereum Node Records (ENRs)
5//!
6//! ## An overview of Node Record types
7//!
8//! Ethereum uses different types of "node records" to represent peers on the network.
9//!
10//! The simplest way to identify a peer is by public key. This is the [`PeerId`] type, which usually
11//! represents a peer's secp256k1 public key.
12//!
13//! A more complete representation of a peer is the [`NodeRecord`] type, which includes the peer's
14//! IP address, the ports where it is reachable (TCP and UDP), and the peer's public key. This is
15//! what is returned from discovery v4 queries.
16//!
17//! The most comprehensive node record type is the Ethereum Node Record ([`Enr`]), which is a
18//! signed, versioned record that includes the information from a [`NodeRecord`] along with
19//! additional metadata. This is the data structure returned from discovery v5 queries.
20//!
21//! When we need to deserialize an identifier that could be a [`PeerId`], [`NodeRecord`], [`Enr`],
22//! or [`TrustedPeer`], we use the [`AnyNode`] type. [`AnyNode`] is used in reth's
23//! `admin_addTrustedPeer` RPC method.
24//!
25//! The __final__ type is the [`TrustedPeer`] type, which is similar to a [`NodeRecord`] but may
26//! include a domain name instead of a direct IP address. It includes a `resolve` method, which can
27//! be used to resolve the domain name, producing a [`NodeRecord`]. This is useful for adding
28//! trusted peers at startup, whose IP address may not be static each time the node starts. This is
29//! common in orchestrated environments like Kubernetes, where there is reliable service discovery,
30//! but services do not necessarily have static IPs.
31//!
32//! In short, the types are as follows:
33//! - [`PeerId`]: A simple public key identifier.
34//! - [`NodeRecord`]: A more complete representation of a peer, including IP address and ports.
35//! - [`Enr`]: An Ethereum Node Record, which is a signed, versioned record that includes additional
36//!   metadata. Useful when interacting with discovery v5, or when custom metadata is required.
37//! - [`AnyNode`]: An enum over [`PeerId`], [`NodeRecord`], [`Enr`], and [`TrustedPeer`], useful in
38//!   deserialization when the type of the node record is not known.
39//! - [`TrustedPeer`]: A [`NodeRecord`] with an optional domain name, which can be resolved to a
40//!   [`NodeRecord`]. Useful for adding trusted peers at startup, whose IP address may not be
41//!   static.
42//!
43//!
44//! ## Feature Flags
45//!
46//! - `net`: Support for address lookups.
47
48#![doc(
49    html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
50    html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
51    issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
52)]
53#![cfg_attr(not(test), warn(unused_crate_dependencies))]
54#![cfg_attr(docsrs, feature(doc_cfg))]
55#![cfg_attr(not(feature = "std"), no_std)]
56
57extern crate alloc;
58
59use alloc::{
60    format,
61    string::{String, ToString},
62};
63use alloy_primitives::B512;
64use core::str::FromStr;
65
66// Re-export PeerId for ease of use.
67#[cfg(feature = "secp256k1")]
68pub use enr::Enr;
69
70/// Alias for a peer identifier
71pub type PeerId = B512;
72
73pub mod node_record;
74pub use node_record::{NodeRecord, NodeRecordParseError};
75
76pub mod trusted_peer;
77pub use trusted_peer::TrustedPeer;
78
79mod bootnodes;
80pub use bootnodes::*;
81
82/// This tag should be set to indicate to libsecp256k1 that the following bytes denote an
83/// uncompressed pubkey.
84///
85/// `SECP256K1_TAG_PUBKEY_UNCOMPRESSED` = `0x04`
86///
87/// See: <https://github.com/bitcoin-core/secp256k1/blob/master/include/secp256k1.h#L211>
88#[cfg(feature = "secp256k1")]
89const SECP256K1_TAG_PUBKEY_UNCOMPRESSED: u8 = 4;
90
91/// Converts a [`secp256k1::PublicKey`] to a [`PeerId`] by stripping the
92/// `SECP256K1_TAG_PUBKEY_UNCOMPRESSED` tag and storing the rest of the slice in the [`PeerId`].
93#[cfg(feature = "secp256k1")]
94#[inline]
95pub fn pk2id(pk: &secp256k1::PublicKey) -> PeerId {
96    PeerId::from_slice(&pk.serialize_uncompressed()[1..])
97}
98
99/// Converts a [`PeerId`] to a [`secp256k1::PublicKey`] by prepending the [`PeerId`] bytes with the
100/// `SECP256K1_TAG_PUBKEY_UNCOMPRESSED` tag.
101#[cfg(feature = "secp256k1")]
102#[inline]
103pub fn id2pk(id: PeerId) -> Result<secp256k1::PublicKey, secp256k1::Error> {
104    // NOTE: B512 is used as a PeerId because 512 bits is enough to represent an uncompressed
105    // public key.
106    let mut s = [0u8; secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE];
107    s[0] = SECP256K1_TAG_PUBKEY_UNCOMPRESSED;
108    s[1..].copy_from_slice(id.as_slice());
109    secp256k1::PublicKey::from_slice(&s)
110}
111
112/// A peer that can come in ENR or [`NodeRecord`] form.
113#[derive(
114    Debug, Clone, Eq, PartialEq, Hash, serde_with::SerializeDisplay, serde_with::DeserializeFromStr,
115)]
116pub enum AnyNode {
117    /// An "enode:" peer with full ip
118    NodeRecord(NodeRecord),
119    /// An "enr:" peer
120    #[cfg(feature = "secp256k1")]
121    Enr(Enr<secp256k1::SecretKey>),
122    /// An incomplete "enode" with only a peer id
123    PeerId(PeerId),
124    /// An "enode:" peer whose host may be a domain name instead of an IP address
125    TrustedPeer(TrustedPeer),
126}
127
128impl AnyNode {
129    /// Returns the peer id of the node.
130    #[cfg(not(feature = "secp256k1"))]
131    pub const fn peer_id(&self) -> PeerId {
132        match self {
133            Self::NodeRecord(record) => record.id,
134            Self::PeerId(peer_id) => *peer_id,
135            Self::TrustedPeer(peer) => peer.id,
136        }
137    }
138
139    /// Returns the peer id of the node.
140    #[cfg(feature = "secp256k1")]
141    pub fn peer_id(&self) -> PeerId {
142        match self {
143            Self::NodeRecord(record) => record.id,
144            Self::Enr(enr) => pk2id(&enr.public_key()),
145            Self::PeerId(peer_id) => *peer_id,
146            Self::TrustedPeer(peer) => peer.id,
147        }
148    }
149
150    /// Returns the full node record if available.
151    #[cfg(not(feature = "secp256k1"))]
152    pub const fn node_record(&self) -> Option<NodeRecord> {
153        match self {
154            Self::NodeRecord(record) => Some(*record),
155            Self::PeerId(_) | Self::TrustedPeer(_) => None,
156        }
157    }
158
159    /// Returns the full node record if available.
160    #[cfg(feature = "secp256k1")]
161    pub fn node_record(&self) -> Option<NodeRecord> {
162        match self {
163            Self::NodeRecord(record) => Some(*record),
164            Self::Enr(enr) => {
165                let node_record = NodeRecord {
166                    address: enr
167                        .ip4()
168                        .map(core::net::IpAddr::from)
169                        .or_else(|| enr.ip6().map(core::net::IpAddr::from))?,
170                    tcp_port: enr.tcp4().or_else(|| enr.tcp6())?,
171                    udp_port: enr.udp4().or_else(|| enr.udp6())?,
172                    id: pk2id(&enr.public_key()),
173                }
174                .into_ipv4_mapped();
175                Some(node_record)
176            }
177            Self::PeerId(_) | Self::TrustedPeer(_) => None,
178        }
179    }
180
181    /// Returns the [`TrustedPeer`] if this is a `TrustedPeer` variant.
182    pub const fn trusted_peer(&self) -> Option<&TrustedPeer> {
183        match self {
184            Self::TrustedPeer(peer) => Some(peer),
185            _ => None,
186        }
187    }
188}
189
190impl From<NodeRecord> for AnyNode {
191    fn from(value: NodeRecord) -> Self {
192        Self::NodeRecord(value)
193    }
194}
195
196#[cfg(feature = "secp256k1")]
197impl From<Enr<secp256k1::SecretKey>> for AnyNode {
198    fn from(value: Enr<secp256k1::SecretKey>) -> Self {
199        Self::Enr(value)
200    }
201}
202
203impl FromStr for AnyNode {
204    type Err = String;
205
206    fn from_str(s: &str) -> Result<Self, Self::Err> {
207        if let Some(rem) = s.strip_prefix("enode://") {
208            if let Ok(record) = NodeRecord::from_str(s) {
209                return Ok(Self::NodeRecord(record))
210            }
211            // NodeRecord parsing rejects domain hosts, but trusted peers may use DNS names.
212            if let Ok(trusted) = TrustedPeer::from_str(s) {
213                return Ok(Self::TrustedPeer(trusted))
214            }
215            // incomplete enode with only a peer id
216            if let Ok(peer_id) = PeerId::from_str(rem) {
217                return Ok(Self::PeerId(peer_id))
218            }
219            return Err(format!("invalid public key: {rem}"))
220        }
221        #[cfg(feature = "secp256k1")]
222        if s.starts_with("enr:") {
223            return Enr::from_str(s).map(AnyNode::Enr)
224        }
225        Err("missing 'enr:' prefix for base64-encoded record".to_string())
226    }
227}
228
229impl core::fmt::Display for AnyNode {
230    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
231        match self {
232            Self::NodeRecord(record) => write!(f, "{record}"),
233            #[cfg(feature = "secp256k1")]
234            Self::Enr(enr) => write!(f, "{enr}"),
235            Self::PeerId(peer_id) => {
236                write!(f, "enode://{}", alloy_primitives::hex::encode(peer_id.as_slice()))
237            }
238            Self::TrustedPeer(peer) => write!(f, "{peer}"),
239        }
240    }
241}
242
243/// Generic wrapper with peer id
244#[derive(Debug)]
245pub struct WithPeerId<T>(PeerId, pub T);
246
247impl<T> From<(PeerId, T)> for WithPeerId<T> {
248    fn from(value: (PeerId, T)) -> Self {
249        Self(value.0, value.1)
250    }
251}
252
253impl<T> WithPeerId<T> {
254    /// Wraps the value with the peerid.
255    pub const fn new(peer: PeerId, value: T) -> Self {
256        Self(peer, value)
257    }
258
259    /// Get the peer id
260    pub const fn peer_id(&self) -> PeerId {
261        self.0
262    }
263
264    /// Get the underlying data
265    pub const fn data(&self) -> &T {
266        &self.1
267    }
268
269    /// Returns ownership of the underlying data.
270    pub fn into_data(self) -> T {
271        self.1
272    }
273
274    /// Transform the data
275    pub fn transform<F: From<T>>(self) -> WithPeerId<F> {
276        WithPeerId(self.0, self.1.into())
277    }
278
279    /// Split the wrapper into [`PeerId`] and data tuple
280    pub fn split(self) -> (PeerId, T) {
281        (self.0, self.1)
282    }
283
284    /// Maps the inner value to a new value using the given function.
285    pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> WithPeerId<U> {
286        WithPeerId(self.0, op(self.1))
287    }
288}
289
290impl<T> WithPeerId<Option<T>> {
291    /// Returns `None` if the inner value is `None`, otherwise returns `Some(WithPeerId<T>)`.
292    pub fn transpose(self) -> Option<WithPeerId<T>> {
293        self.1.map(|v| WithPeerId(self.0, v))
294    }
295
296    /// Returns the contained Some value, consuming the self value.
297    ///
298    /// See also [`Option::unwrap`]
299    ///
300    /// # Panics
301    ///
302    /// Panics if the value is a None
303    pub fn unwrap(self) -> T {
304        self.1.unwrap()
305    }
306
307    /// Returns the transposed [`WithPeerId`] type with the contained Some value
308    ///
309    /// # Panics
310    ///
311    /// Panics if the value is a None
312    pub fn unwrapped(self) -> WithPeerId<T> {
313        self.transpose().unwrap()
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[cfg(feature = "secp256k1")]
322    #[test]
323    fn test_node_record_parse() {
324        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@10.3.58.6:30303?discport=30301";
325        let node: AnyNode = url.parse().unwrap();
326        assert_eq!(node, AnyNode::NodeRecord(NodeRecord {
327            address: std::net::IpAddr::V4([10,3,58,6].into()),
328            tcp_port: 30303,
329            udp_port: 30301,
330            id: "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap(),
331        }));
332        assert_eq!(node.to_string(), url)
333    }
334
335    #[test]
336    fn test_peer_id_parse() {
337        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0";
338        let node: AnyNode = url.parse().unwrap();
339        assert_eq!(node, AnyNode::PeerId("6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse().unwrap()));
340        assert_eq!(node.to_string(), url);
341
342        let url = "enode://";
343        let err = url.parse::<AnyNode>().unwrap_err();
344        assert_eq!(err, "invalid public key: ");
345    }
346
347    // <https://eips.ethereum.org/EIPS/eip-778>
348    #[cfg(feature = "secp256k1")]
349    #[test]
350    fn test_enr_parse() {
351        let url = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8";
352        let node: AnyNode = url.parse().unwrap();
353        assert_eq!(
354            node.peer_id(),
355            "0xca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f"
356                .parse::<PeerId>()
357                .unwrap()
358        );
359        assert_eq!(node.to_string(), url);
360    }
361
362    #[test]
363    fn test_trusted_peer_parse_hostname() {
364        let url = "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@my-node.example.com:30303";
365        let node: AnyNode = url.parse().unwrap();
366        assert!(matches!(node, AnyNode::TrustedPeer(_)));
367        assert_eq!(
368            node.peer_id(),
369            "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0".parse::<PeerId>().unwrap()
370        );
371        assert!(node.node_record().is_none());
372        assert!(node.trusted_peer().is_some());
373        assert_eq!(node.to_string(), url);
374    }
375
376    #[test]
377    #[cfg(feature = "secp256k1")]
378    fn pk2id2pk() {
379        let prikey = secp256k1::SecretKey::new(&mut rand_08::thread_rng());
380        let pubkey = secp256k1::PublicKey::from_secret_key(secp256k1::SECP256K1, &prikey);
381        assert_eq!(pubkey, id2pk(pk2id(&pubkey)).unwrap());
382    }
383}