kona_p2p/rpc/
types.rs

1//! The types used in the p2p RPC API.
2
3use core::net::IpAddr;
4use derive_more::Display;
5use std::string::String;
6
7use alloy_primitives::{ChainId, map::HashMap};
8
9/// The peer info.
10///
11/// <https://github.com/ethereum-optimism/optimism/blob/develop/op-node/p2p/rpc_api.go#L15>
12#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct PeerInfo {
15    /// The peer id.
16    #[serde(rename = "peerID")]
17    pub peer_id: String,
18    /// The node id.
19    #[serde(rename = "nodeID")]
20    pub node_id: String,
21    /// The user agent.
22    pub user_agent: String,
23    /// The protocol version.
24    pub protocol_version: String,
25    /// The enr for the peer.
26    /// If the peer is not in the discovery table, this will not be set.
27    #[serde(rename = "ENR")]
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub enr: Option<String>,
30    /// The peer addresses.
31    pub addresses: Vec<String>,
32    /// Peer supported protocols
33    pub protocols: Option<Vec<String>>,
34    /// 0: "`NotConnected`",
35    /// 1: "Connected",
36    /// 2: "`CanConnect`" (gracefully disconnected)
37    /// 3: "`CannotConnect`" (tried but failed)
38    pub connectedness: Connectedness,
39    /// 0: "Unknown",
40    /// 1: "Inbound" (if the peer contacted us)
41    /// 2: "Outbound" (if we connected to them)
42    pub direction: Direction,
43    /// Whether the peer is protected.
44    pub protected: bool,
45    /// The chain id.
46    #[serde(rename = "chainID")]
47    pub chain_id: ChainId,
48    /// The peer latency in nanoseconds
49    pub latency: u64,
50    /// Whether the peer gossips
51    pub gossip_blocks: bool,
52    /// The peer scores.
53    #[serde(rename = "scores")]
54    pub peer_scores: PeerScores,
55}
56
57/// GossipSub topic-specific scoring metrics.
58///
59/// Tracks peer performance within specific gossip topics, used by the
60/// GossipSub protocol to maintain mesh quality and route messages efficiently.
61/// These scores influence peer selection for the gossip mesh topology.
62///
63/// Reference: <https://github.com/ethereum-optimism/optimism/blob/8dd17a7b114a7c25505cd2e15ce4e3d0f7e3f7c1/op-node/p2p/store/iface.go#L13>
64#[derive(Clone, Default, Debug, Copy, serde::Serialize, serde::Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct TopicScores {
67    /// Duration the peer has participated in the topic mesh.
68    ///
69    /// Longer participation indicates stability and commitment to the topic,
70    /// contributing positively to the peer's mesh score.
71    pub time_in_mesh: f64,
72
73    /// Count of first-time message deliveries from this peer.
74    ///
75    /// Measures how often this peer is the first to deliver new messages,
76    /// indicating their connectivity and responsiveness to the network.
77    pub first_message_deliveries: f64,
78
79    /// Count of messages delivered while in the mesh topology.
80    ///
81    /// Tracks consistent message forwarding behavior while the peer is
82    /// an active participant in the mesh structure.
83    pub mesh_message_deliveries: f64,
84
85    /// Count of invalid or malicious messages from this peer.
86    ///
87    /// Penalizes peers that send invalid, duplicate, or malformed messages,
88    /// helping maintain network health and preventing spam.
89    pub invalid_message_deliveries: f64,
90}
91
92/// Comprehensive GossipSub scoring metrics for peer quality assessment.
93///
94/// Aggregates various scoring factors used by the GossipSub protocol to
95/// evaluate peer quality and determine mesh topology. Higher scores indicate
96/// more reliable and well-behaved peers.
97///
98/// Reference: <https://github.com/ethereum-optimism/optimism/blob/8dd17a7b114a7c25505cd2e15ce4e3d0f7e3f7c1/op-node/p2p/store/iface.go#L20C6-L20C18>
99#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct GossipScores {
102    /// Aggregate score across all scoring dimensions.
103    ///
104    /// The final computed score that determines this peer's overall
105    /// reputation in the gossip network.
106    pub total: f64,
107
108    /// Block-specific topic scores for consensus messages.
109    ///
110    /// Tracks peer behavior specifically for block gossip, which is
111    /// the primary message type in OP Stack networks.
112    pub blocks: TopicScores,
113
114    /// Penalty for IP address colocation with other peers.
115    ///
116    /// Reduces scores for peers sharing IP addresses to prevent
117    /// eclipse attacks and improve network decentralization.
118    #[serde(rename = "IPColocationFactor")]
119    pub ip_colocation_factor: f64,
120
121    /// Penalty for problematic behavior patterns.
122    ///
123    /// Applied to peers exhibiting suspicious or harmful behavior
124    /// that doesn't fit other specific scoring categories.
125    pub behavioral_penalty: f64,
126}
127
128/// Request-response protocol scoring metrics.
129///
130/// Tracks peer performance in direct request-response interactions outside
131/// of the gossip mesh, such as block synchronization requests.
132///
133/// Reference: <https://github.com/ethereum-optimism/optimism/blob/8dd17a7b114a7c25505cd2e15ce4e3d0f7e3f7c1/op-node/p2p/store/iface.go#L31C1-L35C2>
134#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct ReqRespScores {
137    /// Number of valid responses provided by this peer.
138    ///
139    /// Counts successful request-response exchanges where the peer
140    /// provided correct and timely responses to queries.
141    pub valid_responses: f64,
142
143    /// Number of error responses or failed requests.
144    ///
145    /// Tracks cases where the peer returned errors, timeouts, or
146    /// otherwise failed to properly respond to requests.
147    pub error_responses: f64,
148    /// Number of rejected payloads.
149    pub rejected_payloads: f64,
150}
151
152/// Peer Scores
153///
154/// <https://github.com/ethereum-optimism/optimism/blob/8dd17a7b114a7c25505cd2e15ce4e3d0f7e3f7c1/op-node/p2p/store/iface.go#L81>
155#[derive(Clone, Default, Debug, Copy, serde::Serialize, serde::Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct PeerScores {
158    /// The gossip scores
159    pub gossip: GossipScores,
160    /// The request-response scores.
161    pub req_resp: ReqRespScores,
162}
163
164/// Peer count data.
165#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
166#[serde(rename_all = "camelCase")]
167pub struct PeerCount {
168    /// The total number of connected peers to the discovery service.
169    pub connected_discovery: Option<usize>,
170    /// The total number of connected peers to the gossip service.
171    pub connected_gossip: usize,
172}
173
174/// A raw peer dump.
175///
176/// <https://github.com/ethereum-optimism/optimism/blob/40750a58e7a4a6f06370d18dfe6c6eab309012d9/op-node/p2p/rpc_api.go#L36>
177#[derive(Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct PeerDump {
180    /// The total number of connected peers
181    pub total_connected: u32,
182    /// A map from peer id to peer info
183    pub peers: HashMap<String, PeerInfo>,
184    /// A list of banned peers.
185    pub banned_peers: Vec<String>,
186    /// A list of banned ip addresses.
187    #[serde(rename = "bannedIPS")]
188    pub banned_ips: Vec<IpAddr>,
189    /// The banned subnets
190    pub banned_subnets: Vec<ipnet::IpNet>,
191}
192
193/// Peer stats.
194///
195/// <https://github.com/ethereum-optimism/optimism/blob/develop/op-node/p2p/rpc_server.go#L203>
196#[derive(Clone, Default, Debug, Copy, serde::Serialize, serde::Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct PeerStats {
199    /// The number of connections
200    pub connected: u32,
201    /// The table.
202    pub table: u32,
203    /// The blocks topic.
204    #[serde(rename = "blocksTopic")]
205    pub blocks_topic: u32,
206    /// The blocks v2 topic.
207    #[serde(rename = "blocksTopicV2")]
208    pub blocks_topic_v2: u32,
209    /// The blocks v3 topic.
210    #[serde(rename = "blocksTopicV3")]
211    pub blocks_topic_v3: u32,
212    /// The blocks v4 topic.
213    #[serde(rename = "blocksTopicV4")]
214    pub blocks_topic_v4: u32,
215    /// The banned count.
216    pub banned: u32,
217    /// The known count.
218    pub known: u32,
219}
220
221/// Represents the connectivity state of a peer in a network, indicating the reachability and
222/// interaction status of a node with its peers.
223#[derive(
224    Clone,
225    Debug,
226    Display,
227    PartialEq,
228    Copy,
229    Default,
230    // We need to use `serde_repr` to serialize the enum as an integer to match the `op-node` API.
231    serde_repr::Serialize_repr,
232    serde_repr::Deserialize_repr,
233)]
234#[repr(u8)]
235pub enum Connectedness {
236    /// No current connection to the peer, and no recent history of a successful connection.
237    #[default]
238    #[display("Not Connected")]
239    NotConnected = 0,
240
241    /// An active, open connection to the peer exists.
242    #[display("Connected")]
243    Connected = 1,
244
245    /// Connection to the peer is possible but not currently established; usually implies a past
246    /// successful connection.
247    #[display("Can Connect")]
248    CanConnect = 2,
249
250    /// Recent attempts to connect to the peer failed, indicating potential issues in reachability
251    /// or peer status.
252    #[display("Cannot Connect")]
253    CannotConnect = 3,
254
255    /// Connection to the peer is limited; may not have full capabilities.
256    #[display("Limited")]
257    Limited = 4,
258}
259
260impl From<u8> for Connectedness {
261    fn from(value: u8) -> Self {
262        match value {
263            0 => Self::NotConnected,
264            1 => Self::Connected,
265            2 => Self::CanConnect,
266            3 => Self::CannotConnect,
267            4 => Self::Limited,
268            _ => Self::NotConnected,
269        }
270    }
271}
272/// Direction represents the direction of a connection.
273#[derive(Debug, Clone, Display, Copy, PartialEq, Eq, Default)]
274pub enum Direction {
275    /// Unknown is the default direction when the direction is not specified.
276    #[default]
277    #[display("Unknown")]
278    Unknown = 0,
279    /// Inbound is for when the remote peer initiated the connection.
280    #[display("Inbound")]
281    Inbound = 1,
282    /// Outbound is for when the local peer initiated the connection.
283    #[display("Outbound")]
284    Outbound = 2,
285}
286
287impl serde::Serialize for Direction {
288    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
289    where
290        S: serde::Serializer,
291    {
292        serializer.serialize_u8(*self as u8)
293    }
294}
295
296impl<'de> serde::Deserialize<'de> for Direction {
297    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
298    where
299        D: serde::Deserializer<'de>,
300    {
301        let value = u8::deserialize(deserializer)?;
302        match value {
303            0 => Ok(Self::Unknown),
304            1 => Ok(Self::Inbound),
305            2 => Ok(Self::Outbound),
306            _ => Err(serde::de::Error::invalid_value(
307                serde::de::Unexpected::Unsigned(value as u64),
308                &"a value between 0 and 2",
309            )),
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_connectedness_from_u8() {
320        assert_eq!(Connectedness::from(0), Connectedness::NotConnected);
321        assert_eq!(Connectedness::from(1), Connectedness::Connected);
322        assert_eq!(Connectedness::from(2), Connectedness::CanConnect);
323        assert_eq!(Connectedness::from(3), Connectedness::CannotConnect);
324        assert_eq!(Connectedness::from(4), Connectedness::Limited);
325        assert_eq!(Connectedness::from(5), Connectedness::NotConnected);
326    }
327
328    #[test]
329    fn test_direction_display() {
330        assert_eq!(Direction::Unknown.to_string(), "Unknown");
331        assert_eq!(Direction::Inbound.to_string(), "Inbound");
332        assert_eq!(Direction::Outbound.to_string(), "Outbound");
333    }
334
335    #[test]
336    fn test_direction_serialization() {
337        assert_eq!(
338            serde_json::to_string(&Direction::Unknown).unwrap(),
339            "0",
340            "Serialization failed for Direction::Unknown"
341        );
342        assert_eq!(
343            serde_json::to_string(&Direction::Inbound).unwrap(),
344            "1",
345            "Serialization failed for Direction::Inbound"
346        );
347        assert_eq!(
348            serde_json::to_string(&Direction::Outbound).unwrap(),
349            "2",
350            "Serialization failed for Direction::Outbound"
351        );
352    }
353
354    #[test]
355    fn test_direction_deserialization() {
356        let unknown: Direction = serde_json::from_str("0").unwrap();
357        let inbound: Direction = serde_json::from_str("1").unwrap();
358        let outbound: Direction = serde_json::from_str("2").unwrap();
359
360        assert_eq!(unknown, Direction::Unknown, "Deserialization mismatch for Direction::Unknown");
361        assert_eq!(inbound, Direction::Inbound, "Deserialization mismatch for Direction::Inbound");
362        assert_eq!(
363            outbound,
364            Direction::Outbound,
365            "Deserialization mismatch for Direction::Outbound"
366        );
367    }
368
369    #[test]
370    fn test_peer_info_connectedness_serialization() {
371        let peer_info = PeerInfo {
372            peer_id: String::from("peer123"),
373            node_id: String::from("node123"),
374            user_agent: String::from("MyUserAgent"),
375            protocol_version: String::from("v1"),
376            enr: Some(String::from("enr123")),
377            addresses: [String::from("127.0.0.1")].to_vec(),
378            protocols: Some([String::from("eth"), String::from("p2p")].to_vec()),
379            connectedness: Connectedness::Connected,
380            direction: Direction::Outbound,
381            protected: true,
382            chain_id: 1,
383            latency: 100,
384            gossip_blocks: true,
385            peer_scores: PeerScores {
386                gossip: GossipScores {
387                    total: 1.0,
388                    blocks: TopicScores {
389                        time_in_mesh: 10.0,
390                        first_message_deliveries: 5.0,
391                        mesh_message_deliveries: 2.0,
392                        invalid_message_deliveries: 0.0,
393                    },
394                    ip_colocation_factor: 0.5,
395                    behavioral_penalty: 0.1,
396                },
397                req_resp: ReqRespScores {
398                    valid_responses: 10.0,
399                    error_responses: 1.0,
400                    rejected_payloads: 0.0,
401                },
402            },
403        };
404
405        let serialized = serde_json::to_string(&peer_info).expect("Serialization failed");
406
407        let deserialized: PeerInfo =
408            serde_json::from_str(&serialized).expect("Deserialization failed");
409
410        assert_eq!(peer_info.peer_id, deserialized.peer_id);
411        assert_eq!(peer_info.node_id, deserialized.node_id);
412        assert_eq!(peer_info.user_agent, deserialized.user_agent);
413        assert_eq!(peer_info.protocol_version, deserialized.protocol_version);
414        assert_eq!(peer_info.enr, deserialized.enr);
415        assert_eq!(peer_info.addresses, deserialized.addresses);
416        assert_eq!(peer_info.protocols, deserialized.protocols);
417        assert_eq!(peer_info.connectedness, deserialized.connectedness);
418        assert_eq!(peer_info.direction, deserialized.direction);
419        assert_eq!(peer_info.protected, deserialized.protected);
420        assert_eq!(peer_info.chain_id, deserialized.chain_id);
421        assert_eq!(peer_info.latency, deserialized.latency);
422        assert_eq!(peer_info.gossip_blocks, deserialized.gossip_blocks);
423        assert_eq!(peer_info.peer_scores.gossip.total, deserialized.peer_scores.gossip.total);
424        assert_eq!(
425            peer_info.peer_scores.req_resp.valid_responses,
426            deserialized.peer_scores.req_resp.valid_responses
427        );
428    }
429}