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#[derive(Clone, Debug, Deserialize, Serialize)]
12pub struct NodeInfo {
13 pub id: H256,
15
16 pub name: String,
18
19 pub enode: String,
21
22 pub enr: Enr<SigningKey>,
24
25 pub ip: IpAddr,
27
28 pub ports: Ports,
30
31 #[serde(rename = "listenAddr")]
33 pub listen_addr: String,
34
35 pub protocols: ProtocolInfo,
37}
38
39#[derive(Clone, Debug, Deserialize, Serialize)]
41pub struct Ports {
42 pub discovery: u16,
44
45 pub listener: u16,
47}
48
49#[derive(Clone, Debug, Deserialize, Serialize)]
53pub struct ProtocolInfo {
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub eth: Option<EthProtocolInfo>,
57
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub snap: Option<SnapProtocolInfo>,
61}
62
63#[derive(Clone, Debug, Deserialize, Serialize)]
69pub struct EthProtocolInfo {
70 pub network: u64,
72
73 #[serde(deserialize_with = "deserialize_stringified_numeric")]
75 pub difficulty: U256,
76
77 pub genesis: H256,
79
80 pub config: ChainConfig,
82
83 pub head: H256,
85}
86
87#[derive(Clone, Debug, Deserialize, Serialize)]
92pub struct SnapProtocolInfo {}
93
94#[derive(Clone, Debug, Deserialize, Serialize)]
100pub struct PeerProtocolInfo {
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub eth: Option<EthPeerInfo>,
104
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub snap: Option<SnapPeerInfo>,
108}
109
110#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
113#[serde(untagged)]
114pub enum EthPeerInfo {
115 Info(Box<EthInfo>),
117
118 #[serde(deserialize_with = "deser_handshake", serialize_with = "ser_handshake")]
120 Handshake,
121}
122
123#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
129pub struct EthInfo {
130 #[serde(default)]
132 pub version: u64,
133
134 #[serde(default, deserialize_with = "deserialize_stringified_numeric")]
136 pub difficulty: U256,
137
138 #[serde(default)]
140 pub head: H256,
141}
142
143#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
146#[serde(untagged)]
147pub enum SnapPeerInfo {
148 Info(SnapInfo),
150
151 #[serde(deserialize_with = "deser_handshake", serialize_with = "ser_handshake")]
153 Handshake,
154}
155
156#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
162pub struct SnapInfo {
163 pub version: u64,
165}
166
167#[derive(Clone, Debug, Deserialize, Serialize)]
171pub struct PeerInfo {
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub enr: Option<Enr<SigningKey>>,
175
176 pub enode: String,
178
179 pub id: String,
181
182 pub name: String,
184
185 pub caps: Vec<String>,
187
188 pub network: PeerNetworkInfo,
190
191 pub protocols: PeerProtocolInfo,
193}
194
195#[derive(Clone, Debug, Deserialize, Serialize)]
198#[serde(rename_all = "camelCase")]
199pub struct PeerNetworkInfo {
200 pub local_address: SocketAddr,
202
203 pub remote_address: SocketAddr,
205
206 pub inbound: bool,
208
209 pub trusted: bool,
211
212 #[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 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 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}