Skip to main content

reth_eth_wire/
hello.rs

1use crate::{Capability, EthVersion, ProtocolVersion};
2use alloy_rlp::{Decodable, Header, RlpEncodable};
3use reth_codecs::add_arbitrary_tests;
4use reth_network_peers::PeerId;
5use reth_primitives_traits::constants::RETH_CLIENT_VERSION;
6
7/// The default tcp port for p2p.
8///
9/// Note: this is the same as discovery port: `DEFAULT_DISCOVERY_PORT`
10pub(crate) const DEFAULT_TCP_PORT: u16 = 30303;
11
12use crate::protocol::Protocol;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16/// This is a superset of [`HelloMessage`] that provides additional protocol [Protocol] information
17/// about the number of messages used by each capability in order to do proper message ID
18/// multiplexing.
19///
20/// This type is required for the `p2p` handshake because the [`HelloMessage`] does not share the
21/// number of messages used by each capability.
22///
23/// To get the encodable [`HelloMessage`] without the additional protocol information, use the
24/// [`HelloMessageWithProtocols::message`].
25#[derive(Debug, Clone, PartialEq, Eq)]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27pub struct HelloMessageWithProtocols {
28    /// The version of the `p2p` protocol.
29    pub protocol_version: ProtocolVersion,
30    /// Specifies the client software identity, as a human-readable string (e.g.
31    /// "Ethereum(++)/1.0.0").
32    pub client_version: String,
33    /// The list of supported capabilities and their versions.
34    pub protocols: Vec<Protocol>,
35    /// The port that the client is listening on, zero indicates the client is not listening.
36    ///
37    /// By default this is `30303` which is the same as the default discovery port.
38    pub port: u16,
39    /// The secp256k1 public key corresponding to the node's private key.
40    pub id: PeerId,
41}
42
43impl HelloMessageWithProtocols {
44    /// Starts a new `HelloMessageProtocolsBuilder`
45    ///
46    /// ```
47    /// use reth_eth_wire::HelloMessageWithProtocols;
48    /// use reth_network_peers::pk2id;
49    /// use secp256k1::SecretKey;
50    /// let secret_key = SecretKey::new(&mut rand::rng());
51    /// let id = pk2id(&secret_key.public_key());
52    /// let status = HelloMessageWithProtocols::builder(id).build();
53    /// ```
54    pub const fn builder(id: PeerId) -> HelloMessageBuilder {
55        HelloMessageBuilder::new(id)
56    }
57
58    /// Returns the raw [`HelloMessage`] without the additional protocol information.
59    #[inline]
60    pub fn message(&self) -> HelloMessage {
61        HelloMessage {
62            protocol_version: self.protocol_version,
63            client_version: self.client_version.clone(),
64            capabilities: self.protocols.iter().map(|p| p.cap.clone()).collect(),
65            port: self.port,
66            id: self.id,
67        }
68    }
69
70    /// Converts the type into a [`HelloMessage`] without the additional protocol information.
71    pub fn into_message(self) -> HelloMessage {
72        HelloMessage {
73            protocol_version: self.protocol_version,
74            client_version: self.client_version,
75            capabilities: self.protocols.into_iter().map(|p| p.cap).collect(),
76            port: self.port,
77            id: self.id,
78        }
79    }
80
81    /// Returns true if the set of protocols contains the given protocol.
82    #[inline]
83    pub fn contains_protocol(&self, protocol: &Protocol) -> bool {
84        self.protocols.iter().any(|p| p.cap == protocol.cap)
85    }
86
87    /// Adds a new protocol to the set.
88    ///
89    /// Returns an error if the protocol already exists.
90    #[inline]
91    pub fn try_add_protocol(&mut self, protocol: Protocol) -> Result<(), Protocol> {
92        if self.contains_protocol(&protocol) {
93            Err(protocol)
94        } else {
95            self.protocols.push(protocol);
96            Ok(())
97        }
98    }
99
100    /// Toggles advertisement of the `snap/2` satellite protocol (EIP-8189).
101    ///
102    /// snap/2 is negotiated as an `RLPx` capability and rides alongside `eth` on the same
103    /// connection. Disabling removes any advertised `snap` capability of any version.
104    pub fn with_snap(mut self, enable: bool) -> Self {
105        if enable {
106            let _ = self.try_add_protocol(Protocol::snap_2());
107        } else {
108            self.protocols.retain(|p| p.cap.name != "snap");
109        }
110        self
111    }
112}
113
114/// Raw rlpx protocol message used in the `p2p` handshake, containing information about the
115/// supported `RLPx` protocol version and capabilities.
116///
117/// Decoding ignores list elements after `id`, which the spec reserves for future versions (EIP-8).
118///
119/// See also <https://github.com/ethereum/devp2p/blob/master/rlpx.md#hello-0x00>
120#[derive(Clone, Debug, PartialEq, Eq, RlpEncodable)]
121#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
122#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
123#[add_arbitrary_tests(rlp)]
124pub struct HelloMessage {
125    /// The version of the `p2p` protocol.
126    pub protocol_version: ProtocolVersion,
127    /// Specifies the client software identity, as a human-readable string (e.g.
128    /// "Ethereum(++)/1.0.0").
129    pub client_version: String,
130    /// The list of supported capabilities and their versions.
131    pub capabilities: Vec<Capability>,
132    /// The port that the client is listening on, zero indicates the client is not listening.
133    pub port: u16,
134    /// The secp256k1 public key corresponding to the node's private key.
135    pub id: PeerId,
136}
137
138// === impl HelloMessage ===
139
140impl Decodable for HelloMessage {
141    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
142        let mut payload = Header::decode_bytes(buf, true)?;
143        Ok(Self {
144            protocol_version: Decodable::decode(&mut payload)?,
145            client_version: Decodable::decode(&mut payload)?,
146            capabilities: Decodable::decode(&mut payload)?,
147            port: Decodable::decode(&mut payload)?,
148            id: Decodable::decode(&mut payload)?,
149        })
150    }
151}
152
153impl HelloMessage {
154    /// Starts a new `HelloMessageBuilder`
155    ///
156    /// ```
157    /// use reth_eth_wire::HelloMessage;
158    /// use reth_network_peers::pk2id;
159    /// use secp256k1::SecretKey;
160    /// let secret_key = SecretKey::new(&mut rand::rng());
161    /// let id = pk2id(&secret_key.public_key());
162    /// let status = HelloMessage::builder(id).build();
163    /// ```
164    pub const fn builder(id: PeerId) -> HelloMessageBuilder {
165        HelloMessageBuilder::new(id)
166    }
167}
168
169/// Builder for [`HelloMessageWithProtocols`]
170#[derive(Debug)]
171pub struct HelloMessageBuilder {
172    /// The version of the `p2p` protocol.
173    pub protocol_version: Option<ProtocolVersion>,
174    /// Specifies the client software identity, as a human-readable string (e.g.
175    /// "Ethereum(++)/1.0.0").
176    pub client_version: Option<String>,
177    /// The list of supported protocols.
178    pub protocols: Option<Vec<Protocol>>,
179    /// The port that the client is listening on, zero indicates the client is not listening.
180    pub port: Option<u16>,
181    /// The secp256k1 public key corresponding to the node's private key.
182    pub id: PeerId,
183}
184
185// === impl HelloMessageBuilder ===
186
187impl HelloMessageBuilder {
188    /// Create a new builder to configure a [`HelloMessage`]
189    pub const fn new(id: PeerId) -> Self {
190        Self { protocol_version: None, client_version: None, protocols: None, port: None, id }
191    }
192
193    /// Sets the port the client is listening on
194    pub const fn port(mut self, port: u16) -> Self {
195        self.port = Some(port);
196        self
197    }
198
199    /// Adds a new protocol to use.
200    pub fn protocol(mut self, protocols: impl Into<Protocol>) -> Self {
201        self.protocols.get_or_insert_with(Vec::new).push(protocols.into());
202        self
203    }
204
205    /// Sets protocols to use.
206    pub fn protocols(mut self, protocols: impl IntoIterator<Item = Protocol>) -> Self {
207        self.protocols.get_or_insert_with(Vec::new).extend(protocols);
208        self
209    }
210
211    /// Sets client version.
212    pub fn client_version(mut self, client_version: impl Into<String>) -> Self {
213        self.client_version = Some(client_version.into());
214        self
215    }
216
217    /// Sets protocol version.
218    pub const fn protocol_version(mut self, protocol_version: ProtocolVersion) -> Self {
219        self.protocol_version = Some(protocol_version);
220        self
221    }
222
223    /// Consumes the type and returns the configured [`HelloMessage`]
224    ///
225    /// Unset fields will be set to their default values:
226    /// - `protocol_version`: [`ProtocolVersion::V5`]
227    /// - `client_version`: [`RETH_CLIENT_VERSION`]
228    /// - `capabilities`: All [`EthVersion`]
229    pub fn build(self) -> HelloMessageWithProtocols {
230        let Self { protocol_version, client_version, protocols, port, id } = self;
231        HelloMessageWithProtocols {
232            protocol_version: protocol_version.unwrap_or_default(),
233            client_version: client_version.unwrap_or_else(|| RETH_CLIENT_VERSION.to_string()),
234            protocols: protocols.unwrap_or_else(|| {
235                EthVersion::ALL_VERSIONS.iter().copied().map(Into::into).collect()
236            }),
237            port: port.unwrap_or(DEFAULT_TCP_PORT),
238            id,
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use crate::{
246        p2pstream::P2PMessage, Capability, EthVersion, HelloMessage, HelloMessageWithProtocols,
247        ProtocolVersion,
248    };
249    use alloy_rlp::{Decodable, Encodable, Header, EMPTY_STRING_CODE};
250    use reth_network_peers::pk2id;
251    use secp256k1::SecretKey;
252
253    #[test]
254    fn test_hello_encoding_round_trip() {
255        let secret_key = SecretKey::new(&mut rand::rng());
256        let id = pk2id(&secret_key.public_key());
257        let hello = P2PMessage::Hello(HelloMessage {
258            protocol_version: ProtocolVersion::V5,
259            client_version: "reth/0.1.0".to_string(),
260            capabilities: vec![Capability::new_static("eth", EthVersion::Eth67 as usize)],
261            port: 30303,
262            id,
263        });
264
265        let mut hello_encoded = Vec::new();
266        hello.encode(&mut hello_encoded);
267
268        let hello_decoded = P2PMessage::decode(&mut &hello_encoded[..]).unwrap();
269
270        assert_eq!(hello, hello_decoded);
271    }
272
273    #[test]
274    fn hello_encoding_length() {
275        let secret_key = SecretKey::new(&mut rand::rng());
276        let id = pk2id(&secret_key.public_key());
277        let hello = P2PMessage::Hello(HelloMessage {
278            protocol_version: ProtocolVersion::V5,
279            client_version: "reth/0.1.0".to_string(),
280            capabilities: vec![Capability::new_static("eth", EthVersion::Eth67 as usize)],
281            port: 30303,
282            id,
283        });
284
285        let mut hello_encoded = Vec::new();
286        hello.encode(&mut hello_encoded);
287
288        assert_eq!(hello_encoded.len(), hello.length());
289    }
290    //TODO: add test for eth70 here once we have fully support it
291
292    #[test]
293    fn test_default_protocols_still_include_eth69() {
294        // ensure that older eth/69 remains advertised for compatibility
295        let secret_key = SecretKey::new(&mut rand::rng());
296        let id = pk2id(&secret_key.public_key());
297        let hello = HelloMessageWithProtocols::builder(id).build();
298
299        let has_eth69 = hello
300            .protocols
301            .iter()
302            .any(|p| p.cap.name == "eth" && p.cap.version == EthVersion::Eth69 as usize);
303        assert!(has_eth69, "Default protocols should include Eth69");
304    }
305
306    #[test]
307    fn hello_message_id_prefix() {
308        // ensure that the hello message id is prefixed
309        let secret_key = SecretKey::new(&mut rand::rng());
310        let id = pk2id(&secret_key.public_key());
311        let hello = P2PMessage::Hello(HelloMessage {
312            protocol_version: ProtocolVersion::V5,
313            client_version: "reth/0.1.0".to_string(),
314            capabilities: vec![Capability::new_static("eth", EthVersion::Eth67 as usize)],
315            port: 30303,
316            id,
317        });
318
319        let mut hello_encoded = Vec::new();
320        hello.encode(&mut hello_encoded);
321
322        // zero is encoded as 0x80, the empty string code in RLP
323        assert_eq!(hello_encoded[0], EMPTY_STRING_CODE);
324    }
325
326    #[test]
327    fn hello_decode_ignores_extra_fields() {
328        // EIP-8: additional list elements must be ignored
329        let secret_key = SecretKey::new(&mut rand::rng());
330        let hello = HelloMessage {
331            protocol_version: ProtocolVersion::V5,
332            client_version: "reth/0.1.0".to_string(),
333            capabilities: vec![Capability::new_static("eth", EthVersion::Eth68 as usize)],
334            port: 30303,
335            id: pk2id(&secret_key.public_key()),
336        };
337
338        let mut payload = Vec::new();
339        hello.protocol_version.encode(&mut payload);
340        hello.client_version.encode(&mut payload);
341        hello.capabilities.encode(&mut payload);
342        hello.port.encode(&mut payload);
343        hello.id.encode(&mut payload);
344        42u8.encode(&mut payload);
345        "future".encode(&mut payload);
346
347        let mut encoded = Vec::new();
348        Header { list: true, payload_length: payload.len() }.encode(&mut encoded);
349        encoded.extend_from_slice(&payload);
350        encoded.push(0xff);
351
352        let mut buf = encoded.as_slice();
353        assert_eq!(HelloMessage::decode(&mut buf).unwrap(), hello);
354        assert_eq!(buf, [0xff]);
355    }
356}