use alloc::{string::String, vec::Vec};
use core::net::IpAddr;
use alloy_primitives::{map::HashMap, ChainId};
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct TopicScores {
pub time_in_mesh: f64,
pub first_message_deliveries: f64,
pub mesh_message_deliveries: f64,
pub invalid_message_deliveries: f64,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct GossipScores {
pub total: f64,
pub blocks: TopicScores,
#[cfg_attr(feature = "serde", serde(rename = "IPColocationFactor"))]
pub ip_colocation_factor: f64,
pub behavioral_penalty: f64,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ReqRespScores {
pub valid_responses: f64,
pub error_responses: f64,
pub rejected_payloads: f64,
}
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct PeerScores {
pub gossip: GossipScores,
pub req_resp: ReqRespScores,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct PeerInfo {
#[cfg_attr(feature = "serde", serde(rename = "peerID"))]
pub peer_id: String,
#[cfg_attr(feature = "serde", serde(rename = "nodeID"))]
pub node_id: String,
pub user_agent: String,
pub protocol_version: String,
#[cfg_attr(feature = "serde", serde(rename = "ENR"))]
pub enr: String,
pub addresses: Vec<String>,
pub protocols: Option<Vec<String>>,
pub connectedness: Connectedness,
pub direction: Direction,
pub protected: bool,
#[cfg_attr(feature = "serde", serde(rename = "chainID"))]
pub chain_id: ChainId,
pub latency: u64,
pub gossip_blocks: bool,
#[cfg_attr(feature = "serde", serde(rename = "scores"))]
pub peer_scores: PeerScores,
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct PeerDump {
pub total_connected: u32,
pub peers: HashMap<String, PeerInfo>,
pub banned_peers: Vec<String>,
#[cfg_attr(feature = "serde", serde(rename = "bannedIPS"))]
pub banned_ips: Vec<IpAddr>,
pub banned_subnets: Vec<IpAddr>,
}
#[derive(Clone, Debug, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct PeerStats {
pub connected: u32,
pub table: u32,
#[cfg_attr(feature = "serde", serde(rename = "blocksTopic"))]
pub blocks_topic: u32,
#[cfg_attr(feature = "serde", serde(rename = "blocksTopicV2"))]
pub blocks_topic_v2: u32,
#[cfg_attr(feature = "serde", serde(rename = "blocksTopicV3"))]
pub blocks_topic_v3: u32,
pub banned: u32,
pub known: u32,
}
#[derive(Clone, Debug, PartialEq, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum Connectedness {
#[default]
NotConnected = 0,
Connected = 1,
CanConnect = 2,
CannotConnect = 3,
Limited = 4,
}
impl core::fmt::Display for Connectedness {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotConnected => write!(f, "Not Connected"),
Self::Connected => write!(f, "Connected"),
Self::CanConnect => write!(f, "Can Connect"),
Self::CannotConnect => write!(f, "Cannot Connect"),
Self::Limited => write!(f, "Limited"),
}
}
}
impl From<u8> for Connectedness {
fn from(value: u8) -> Self {
match value {
0 => Self::NotConnected,
1 => Self::Connected,
2 => Self::CanConnect,
3 => Self::CannotConnect,
4 => Self::Limited,
_ => Self::NotConnected,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
Unknown = 0,
Inbound = 1,
Outbound = 2,
}
#[cfg(feature = "serde")]
impl serde::Serialize for Direction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(*self as u8)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Direction {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = u8::deserialize(deserializer)?;
match value {
0 => Ok(Self::Unknown),
1 => Ok(Self::Inbound),
2 => Ok(Self::Outbound),
_ => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Unsigned(value as u64),
&"a value between 0 and 2",
)),
}
}
}
impl core::fmt::Display for Direction {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
match self {
Self::Unknown => "Unknown",
Self::Inbound => "Inbound",
Self::Outbound => "Outbound",
}
)
}
}
#[cfg(test)]
#[cfg(feature = "serde")]
mod tests {
use super::*;
#[test]
#[cfg(feature = "serde")]
fn test_direction_serialization() {
assert_eq!(
serde_json::to_string(&Direction::Unknown).unwrap(),
"0",
"Serialization failed for Direction::Unknown"
);
assert_eq!(
serde_json::to_string(&Direction::Inbound).unwrap(),
"1",
"Serialization failed for Direction::Inbound"
);
assert_eq!(
serde_json::to_string(&Direction::Outbound).unwrap(),
"2",
"Serialization failed for Direction::Outbound"
);
}
#[test]
#[cfg(feature = "serde")]
fn test_direction_deserialization() {
let unknown: Direction = serde_json::from_str("0").unwrap();
let inbound: Direction = serde_json::from_str("1").unwrap();
let outbound: Direction = serde_json::from_str("2").unwrap();
assert_eq!(unknown, Direction::Unknown, "Deserialization mismatch for Direction::Unknown");
assert_eq!(inbound, Direction::Inbound, "Deserialization mismatch for Direction::Inbound");
assert_eq!(
outbound,
Direction::Outbound,
"Deserialization mismatch for Direction::Outbound"
);
}
#[test]
#[cfg(feature = "serde")]
fn test_peer_info_connectedness_serialization() {
let peer_info = PeerInfo {
peer_id: String::from("peer123"),
node_id: String::from("node123"),
user_agent: String::from("MyUserAgent"),
protocol_version: String::from("v1"),
enr: String::from("enr123"),
addresses: [String::from("127.0.0.1")].to_vec(),
protocols: Some([String::from("eth"), String::from("p2p")].to_vec()),
connectedness: Connectedness::Connected,
direction: Direction::Outbound,
protected: true,
chain_id: 1,
latency: 100,
gossip_blocks: true,
peer_scores: PeerScores {
gossip: GossipScores {
total: 1.0,
blocks: TopicScores {
time_in_mesh: 10.0,
first_message_deliveries: 5.0,
mesh_message_deliveries: 2.0,
invalid_message_deliveries: 0.0,
},
ip_colocation_factor: 0.5,
behavioral_penalty: 0.1,
},
req_resp: ReqRespScores {
valid_responses: 10.0,
error_responses: 1.0,
rejected_payloads: 0.0,
},
},
};
let serialized = serde_json::to_string(&peer_info).expect("Serialization failed");
let deserialized: PeerInfo =
serde_json::from_str(&serialized).expect("Deserialization failed");
assert_eq!(peer_info.peer_id, deserialized.peer_id);
assert_eq!(peer_info.node_id, deserialized.node_id);
assert_eq!(peer_info.user_agent, deserialized.user_agent);
assert_eq!(peer_info.protocol_version, deserialized.protocol_version);
assert_eq!(peer_info.enr, deserialized.enr);
assert_eq!(peer_info.addresses, deserialized.addresses);
assert_eq!(peer_info.protocols, deserialized.protocols);
assert_eq!(peer_info.connectedness, deserialized.connectedness);
assert_eq!(peer_info.direction, deserialized.direction);
assert_eq!(peer_info.protected, deserialized.protected);
assert_eq!(peer_info.chain_id, deserialized.chain_id);
assert_eq!(peer_info.latency, deserialized.latency);
assert_eq!(peer_info.gossip_blocks, deserialized.gossip_blocks);
assert_eq!(peer_info.peer_scores.gossip.total, deserialized.peer_scores.gossip.total);
assert_eq!(
peer_info.peer_scores.req_resp.valid_responses,
deserialized.peer_scores.req_resp.valid_responses
);
}
}