ethers_providers/ext/
admin.rs

1use enr::{k256::ecdsa::SigningKey, Enr};
2use ethers_core::{
3    types::{serde_helpers::deserialize_stringified_numeric, H256, U256},
4    utils::ChainConfig,
5};
6use serde::{Deserialize, Serialize};
7use std::net::{IpAddr, SocketAddr};
8
9/// This includes general information about a running node, spanning networking and protocol
10/// details.
11#[derive(Clone, Debug, Deserialize, Serialize)]
12pub struct NodeInfo {
13    /// The node's private key.
14    pub id: H256,
15
16    /// The node's user agent, containing a client name, version, OS, and other metadata.
17    pub name: String,
18
19    /// The enode URL of the connected node.
20    pub enode: String,
21
22    /// The [ENR](https://eips.ethereum.org/EIPS/eip-778) of the running client.
23    pub enr: Enr<SigningKey>,
24
25    /// The IP address of the connected node.
26    pub ip: IpAddr,
27
28    /// The node's listening ports.
29    pub ports: Ports,
30
31    /// The node's listening address.
32    #[serde(rename = "listenAddr")]
33    pub listen_addr: String,
34
35    /// The protocols that the node supports, with protocol metadata.
36    pub protocols: ProtocolInfo,
37}
38
39/// Represents a node's discovery and listener ports.
40#[derive(Clone, Debug, Deserialize, Serialize)]
41pub struct Ports {
42    /// The node's discovery port.
43    pub discovery: u16,
44
45    /// The node's listener port.
46    pub listener: u16,
47}
48
49/// Represents protocols that the connected RPC node supports.
50///
51/// This contains protocol information reported by the connected RPC node.
52#[derive(Clone, Debug, Deserialize, Serialize)]
53pub struct ProtocolInfo {
54    /// Details about the node's supported eth protocol. `None` if unsupported
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub eth: Option<EthProtocolInfo>,
57
58    /// Details about the node's supported snap protocol. `None` if unsupported
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub snap: Option<SnapProtocolInfo>,
61}
62
63/// Represents a short summary of the `eth` sub-protocol metadata known about the host peer.
64///
65/// See [geth's `NodeInfo`
66/// struct](https://github.com/ethereum/go-ethereum/blob/c2e0abce2eedc1ba2a1b32c46fd07ef18a25354a/eth/protocols/eth/handler.go#L129)
67/// for how these fields are determined.
68#[derive(Clone, Debug, Deserialize, Serialize)]
69pub struct EthProtocolInfo {
70    /// The eth network version.
71    pub network: u64,
72
73    /// The total difficulty of the host's blockchain.
74    #[serde(deserialize_with = "deserialize_stringified_numeric")]
75    pub difficulty: U256,
76
77    /// The Keccak hash of the host's genesis block.
78    pub genesis: H256,
79
80    /// The chain configuration for the host's fork rules.
81    pub config: ChainConfig,
82
83    /// The hash of the host's best known block.
84    pub head: H256,
85}
86
87/// Represents a short summary of the host's `snap` sub-protocol metadata.
88///
89/// This is just an empty struct, because [geth's internal representation is
90/// empty](https://github.com/ethereum/go-ethereum/blob/c2e0abce2eedc1ba2a1b32c46fd07ef18a25354a/eth/protocols/snap/handler.go#L571-L576).
91#[derive(Clone, Debug, Deserialize, Serialize)]
92pub struct SnapProtocolInfo {}
93
94/// Represents the protocols that a peer supports.
95///
96/// This differs from [`ProtocolInfo`] in that [`PeerProtocolInfo`] contains protocol information
97/// gathered from the protocol handshake, and [`ProtocolInfo`] contains information reported by the
98/// connected RPC node.
99#[derive(Clone, Debug, Deserialize, Serialize)]
100pub struct PeerProtocolInfo {
101    /// Details about the peer's supported eth protocol. `None` if unsupported
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub eth: Option<EthPeerInfo>,
104
105    /// Details about the peer's supported snap protocol. `None` if unsupported
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub snap: Option<SnapPeerInfo>,
108}
109
110/// Can contain either eth protocol info or a string "handshake", which geth uses if the peer is
111/// still completing the handshake for the protocol.
112#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
113#[serde(untagged)]
114pub enum EthPeerInfo {
115    /// The `eth` sub-protocol metadata known about the host peer.
116    Info(Box<EthInfo>),
117
118    /// The string "handshake" if the peer is still completing the handshake for the protocol.
119    #[serde(deserialize_with = "deser_handshake", serialize_with = "ser_handshake")]
120    Handshake,
121}
122
123/// Represents a short summary of the `eth` sub-protocol metadata known about a connected peer
124///
125/// See [geth's `ethPeerInfo`
126/// struct](https://github.com/ethereum/go-ethereum/blob/53d1ae096ac0515173e17f0f81a553e5f39027f7/eth/peer.go#L28)
127/// for how these fields are determined.
128#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
129pub struct EthInfo {
130    /// The negotiated eth version.
131    #[serde(default)]
132    pub version: u64,
133
134    /// The total difficulty of the peer's blockchain.
135    #[serde(default, deserialize_with = "deserialize_stringified_numeric")]
136    pub difficulty: U256,
137
138    /// The hash of the peer's best known block.
139    #[serde(default)]
140    pub head: H256,
141}
142
143/// Can contain either snap protocol info or a string "handshake", which geth uses if the peer is
144/// still completing the handshake for the protocol.
145#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
146#[serde(untagged)]
147pub enum SnapPeerInfo {
148    /// The `snap` sub-protocol metadata known about the host peer.
149    Info(SnapInfo),
150
151    /// The string "handshake" if the peer is still completing the handshake for the protocol.
152    #[serde(deserialize_with = "deser_handshake", serialize_with = "ser_handshake")]
153    Handshake,
154}
155
156/// Represents a short summary of the `snap` sub-protocol metadata known about a connected peer.
157///
158/// See [geth's `snapPeerInfo`
159/// struct](https://github.com/ethereum/go-ethereum/blob/53d1ae096ac0515173e17f0f81a553e5f39027f7/eth/peer.go#L53)
160/// for how these fields are determined.
161#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
162pub struct SnapInfo {
163    /// The negotiated snap version.
164    pub version: u64,
165}
166
167/// Represents a short summary of information known about a connected peer.
168///
169/// See [geth's `PeerInfo` struct](https://github.com/ethereum/go-ethereum/blob/64dccf7aa411c5c7cd36090c3d9b9892945ae813/p2p/peer.go#L484) for the source of each field.
170#[derive(Clone, Debug, Deserialize, Serialize)]
171pub struct PeerInfo {
172    /// The peer's ENR.
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub enr: Option<Enr<SigningKey>>,
175
176    /// The peer's enode URL.
177    pub enode: String,
178
179    /// The peer's enode ID.
180    pub id: String,
181
182    /// The peer's name.
183    pub name: String,
184
185    /// The peer's capabilities.
186    pub caps: Vec<String>,
187
188    /// Networking information about the peer.
189    pub network: PeerNetworkInfo,
190
191    /// The protocols that the peer supports, with protocol metadata.
192    pub protocols: PeerProtocolInfo,
193}
194
195/// Represents networking related information about the peer, including details about whether or
196/// not it is inbound, trusted, or static.
197#[derive(Clone, Debug, Deserialize, Serialize)]
198#[serde(rename_all = "camelCase")]
199pub struct PeerNetworkInfo {
200    /// The local endpoint of the TCP connection.
201    pub local_address: SocketAddr,
202
203    /// The remote endpoint of the TCP connection.
204    pub remote_address: SocketAddr,
205
206    /// Whether or not the peer is inbound.
207    pub inbound: bool,
208
209    /// Whether or not the peer is trusted.
210    pub trusted: bool,
211
212    /// Whether or not the peer is a static peer.
213    #[serde(rename = "static")]
214    pub static_node: bool,
215}
216
217fn deser_handshake<'de, D>(deserializer: D) -> Result<(), D::Error>
218where
219    D: serde::Deserializer<'de>,
220{
221    let s = String::deserialize(deserializer)?;
222    if s == "handshake" {
223        Ok(())
224    } else {
225        Err(serde::de::Error::custom(
226            "expected \"handshake\" if protocol info did not appear in the response",
227        ))
228    }
229}
230
231fn ser_handshake<S>(serializer: S) -> Result<S::Ok, S::Error>
232where
233    S: serde::Serializer,
234{
235    serializer.serialize_str("handshake")
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn deserialize_peer_info() {
244        let response = r#"{
245            "enode":"enode://bb37b7302f79e47c1226d6e3ccf0ef6d51146019efdcc1f6e861fd1c1a78d5e84e486225a6a8a503b93d5c50125ee980835c92bde7f7d12f074c16f4e439a578@127.0.0.1:60872",
246            "id":"ca23c04b7e796da5d6a5f04a62b81c88d41b1341537db85a2b6443e838d8339b",
247            "name":"Geth/v1.10.19-stable/darwin-arm64/go1.18.3",
248            "caps":["eth/66","eth/67","snap/1"],
249            "network":{
250                "localAddress":"127.0.0.1:30304",
251                "remoteAddress":"127.0.0.1:60872",
252                "inbound":true,
253                "trusted":false,
254                "static":false
255            },
256            "protocols":{
257                "eth":{
258                    "version":67,
259                    "difficulty":0,
260                    "head":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea"
261                },
262                "snap":{"version":1}
263            }
264        }"#;
265        let peer_info: PeerInfo = serde_json::from_str(response).unwrap();
266
267        assert_eq!(peer_info.enode, "enode://bb37b7302f79e47c1226d6e3ccf0ef6d51146019efdcc1f6e861fd1c1a78d5e84e486225a6a8a503b93d5c50125ee980835c92bde7f7d12f074c16f4e439a578@127.0.0.1:60872");
268    }
269
270    #[test]
271    fn deserialize_node_info() {
272        // this response also has an enr
273        let response = r#"{
274            "id":"6e2fe698f3064cd99410926ce16734e35e3cc947d4354461d2594f2d2dd9f7b6",
275            "name":"Geth/v1.10.19-stable/darwin-arm64/go1.18.3",
276            "enode":"enode://d7dfaea49c7ef37701e668652bcf1bc63d3abb2ae97593374a949e175e4ff128730a2f35199f3462a56298b981dfc395a5abebd2d6f0284ffe5bdc3d8e258b86@127.0.0.1:30304?discport=0",
277            "enr":"enr:-Jy4QIvS0dKBLjTTV_RojS8hjriwWsJNHRVyOh4Pk4aUXc5SZjKRVIOeYc7BqzEmbCjLdIY4Ln7x5ZPf-2SsBAc2_zqGAYSwY1zog2V0aMfGhNegsXuAgmlkgnY0gmlwhBiT_DiJc2VjcDI1NmsxoQLX366knH7zdwHmaGUrzxvGPTq7Kul1kzdKlJ4XXk_xKIRzbmFwwIN0Y3CCdmA",
278            "ip":"127.0.0.1",
279            "ports":{
280                "discovery":0,
281                "listener":30304
282            },
283            "listenAddr":"[::]:30304",
284            "protocols":{
285                "eth":{
286                    "network":1337,
287                    "difficulty":0,
288                    "genesis":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea",
289                    "config":{
290                        "chainId":0,
291                        "eip150Hash":"0x0000000000000000000000000000000000000000000000000000000000000000"
292                    },
293                    "head":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea"
294                },
295                "snap":{}
296            }
297        }"#;
298
299        let _: NodeInfo = serde_json::from_str(response).unwrap();
300    }
301
302    #[test]
303    fn deserialize_node_info_post_merge() {
304        // this response also has an enr
305        let response = r#"{
306            "id":"6e2fe698f3064cd99410926ce16734e35e3cc947d4354461d2594f2d2dd9f7b6",
307            "name":"Geth/v1.10.19-stable/darwin-arm64/go1.18.3",
308            "enode":"enode://d7dfaea49c7ef37701e668652bcf1bc63d3abb2ae97593374a949e175e4ff128730a2f35199f3462a56298b981dfc395a5abebd2d6f0284ffe5bdc3d8e258b86@127.0.0.1:30304?discport=0",
309            "enr":"enr:-Jy4QIvS0dKBLjTTV_RojS8hjriwWsJNHRVyOh4Pk4aUXc5SZjKRVIOeYc7BqzEmbCjLdIY4Ln7x5ZPf-2SsBAc2_zqGAYSwY1zog2V0aMfGhNegsXuAgmlkgnY0gmlwhBiT_DiJc2VjcDI1NmsxoQLX366knH7zdwHmaGUrzxvGPTq7Kul1kzdKlJ4XXk_xKIRzbmFwwIN0Y3CCdmA",
310            "ip":"127.0.0.1",
311            "ports":{
312                "discovery":0,
313                "listener":30304
314            },
315            "listenAddr":"[::]:30304",
316            "protocols":{
317                "eth":{
318                    "network":1337,
319                    "difficulty":0,
320                    "genesis":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea",
321                    "config":{
322                        "chainId":0,
323                        "eip150Hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
324                        "terminalTotalDifficulty": "0xC70D808A128D7380000",
325                        "terminalTotalDifficultyPassed":true,
326                        "ethash":{}
327                    },
328                    "head":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea"
329                },
330                "snap":{}
331            }
332        }"#;
333
334        let _: NodeInfo = serde_json::from_str(response).unwrap();
335    }
336
337    #[test]
338    fn deserialize_node_info_mainnet_full() {
339        let actual_response = r#"{
340            "id": "74477ca052fcf55ee9eafb369fafdb3e91ad7b64fbd7ae15a4985bfdc43696f2",
341            "name": "Geth/v1.10.26-stable/darwin-arm64/go1.19.3",
342            "enode": "enode://962184c6f2a19e064e2ddf0d5c5a788c8c5ed3a4909b7f75fb4dad967392ff542772bcc498cd7f15e13eecbde830265f379779c6da1f71fb8fe1a4734dfc0a1e@127.0.0.1:13337?discport=0",
343            "enr": "enr:-J-4QFttJyL3f2-B2TQmBZNFxex99TSBv1YtB_8jqUbXWkf6LOREKQAPW2bIn8kJ8QvHbWxCQNFzTX6sehjbrz1ZkSuGAYSyQ0_rg2V0aMrJhPxk7ASDEYwwgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKWIYTG8qGeBk4t3w1cWniMjF7TpJCbf3X7Ta2Wc5L_VIRzbmFwwIN0Y3CCNBk",
344            "ip": "127.0.0.1",
345            "ports": {
346                "discovery": 0,
347                "listener": 13337
348            },
349            "listenAddr": "[::]:13337",
350            "protocols": {
351                "eth": {
352                    "network": 1337,
353                    "difficulty": 17179869184,
354                    "genesis": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3",
355                    "config": {
356                        "chainId": 1,
357                        "homesteadBlock": 1150000,
358                        "daoForkBlock": 1920000,
359                        "daoForkSupport": true,
360                        "eip150Block": 2463000,
361                        "eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0",
362                        "eip155Block": 2675000,
363                        "eip158Block": 2675000,
364                        "byzantiumBlock": 4370000,
365                        "constantinopleBlock": 7280000,
366                        "petersburgBlock": 7280000,
367                        "istanbulBlock": 9069000,
368                        "muirGlacierBlock": 9200000,
369                        "berlinBlock": 12244000,
370                        "londonBlock": 12965000,
371                        "arrowGlacierBlock": 13773000,
372                        "grayGlacierBlock": 15050000,
373                        "terminalTotalDifficulty": "0xC70D808A128D7380000",
374                        "terminalTotalDifficultyPassed": true,
375                        "ethash": {}
376                    },
377                    "head": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
378                },
379                "snap": {}
380            }
381        }"#;
382
383        let _: NodeInfo = serde_json::from_str(actual_response).unwrap();
384    }
385
386    #[test]
387    fn deserialize_peer_info_handshake() {
388        let response = r#"{
389            "enode": "enode://a997fde0023537ad01e536ebf2eeeb4b4b3d5286707586727b704f32e8e2b4959e08b6db5b27eb6b7e9f6efcbb53657f4e2bd16900aa77a89426dc3382c29ce0@[::1]:60948",
390            "id": "df6f8bc331005962c2ef1f5236486a753bc6b2ddb5ef04370757999d1ca832d4",
391            "name": "Geth/v1.10.26-stable-e5eb32ac/linux-amd64/go1.18.5",
392            "caps": ["eth/66","eth/67","snap/1"],
393            "network":{
394                "localAddress":"[::1]:30304",
395                "remoteAddress":"[::1]:60948",
396                "inbound":true,
397                "trusted":false,
398                "static":false
399            },
400            "protocols":{
401                "eth":"handshake",
402                "snap":"handshake"
403            }
404        }"#;
405
406        let info: PeerInfo = serde_json::from_str(response).unwrap();
407        assert_eq!(info.protocols.eth, Some(EthPeerInfo::Handshake));
408        assert_eq!(info.protocols.snap, Some(SnapPeerInfo::Handshake));
409    }
410}