1use std::collections::HashMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use rand::seq::SliceRandom;
7use ruc::*;
8use serde::{Deserialize, Serialize};
9
10use hotmint_types::validator::ValidatorId;
11use litep2p::PeerId;
12use litep2p::types::multiaddr::Multiaddr;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum PeerRole {
17 Validator,
19 Fullnode,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct PeerInfo {
26 pub peer_id: String,
27 pub role: PeerRole,
28 pub validator_id: Option<u64>,
29 pub addresses: Vec<String>,
30 pub last_seen: u64,
31 pub score: i32,
32}
33
34const BAN_THRESHOLD: i32 = -100;
35
36impl PeerInfo {
37 pub fn new(peer_id: PeerId, role: PeerRole, addresses: Vec<Multiaddr>) -> Self {
38 Self {
39 peer_id: peer_id.to_string(),
40 role,
41 validator_id: None,
42 addresses: addresses.iter().map(|a| a.to_string()).collect(),
43 last_seen: now_secs(),
44 score: 0,
45 }
46 }
47
48 pub fn with_validator(mut self, vid: ValidatorId) -> Self {
49 self.validator_id = Some(vid.0);
50 self
51 }
52
53 pub fn is_banned(&self) -> bool {
54 self.score <= BAN_THRESHOLD
55 }
56
57 pub fn touch(&mut self) {
58 self.last_seen = now_secs();
59 }
60}
61
62pub struct PeerBook {
64 peers: HashMap<String, PeerInfo>,
65 path: PathBuf,
66}
67
68impl PeerBook {
69 pub fn new(path: impl AsRef<Path>) -> Self {
70 Self {
71 peers: HashMap::new(),
72 path: path.as_ref().to_path_buf(),
73 }
74 }
75
76 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
77 let p = path.as_ref();
78 if !p.exists() {
79 return Ok(Self::new(p));
80 }
81 let contents = fs::read_to_string(p).c(d!("read peer book"))?;
82 let peers: HashMap<String, PeerInfo> =
83 serde_json::from_str(&contents).c(d!("parse peer book"))?;
84 Ok(Self {
85 peers,
86 path: p.to_path_buf(),
87 })
88 }
89
90 pub fn save(&self) -> Result<()> {
91 let contents = serde_json::to_string_pretty(&self.peers).c(d!("serialize peer book"))?;
92 let tmp = format!("{}.tmp", self.path.display());
93 fs::write(&tmp, &contents).c(d!("write peer book tmp"))?;
94 fs::rename(&tmp, &self.path).c(d!("rename peer book tmp"))
95 }
96
97 pub fn add_peer(&mut self, info: PeerInfo) {
98 self.peers.insert(info.peer_id.clone(), info);
99 }
100
101 pub fn remove_peer(&mut self, peer_id: &str) {
102 self.peers.remove(peer_id);
103 }
104
105 pub fn get(&self, peer_id: &str) -> Option<&PeerInfo> {
106 self.peers.get(peer_id)
107 }
108
109 pub fn get_mut(&mut self, peer_id: &str) -> Option<&mut PeerInfo> {
110 self.peers.get_mut(peer_id)
111 }
112
113 pub fn get_peers_by_role(&self, role: PeerRole) -> Vec<&PeerInfo> {
114 self.peers
115 .values()
116 .filter(|p| p.role == role && !p.is_banned())
117 .collect()
118 }
119
120 pub fn get_random_peers(&self, n: usize) -> Vec<&PeerInfo> {
121 let mut candidates: Vec<&PeerInfo> =
122 self.peers.values().filter(|p| !p.is_banned()).collect();
123 let mut rng = rand::thread_rng();
124 candidates.shuffle(&mut rng);
125 candidates.truncate(n);
126 candidates
127 }
128
129 pub fn adjust_score(&mut self, peer_id: &str, delta: i32) {
130 if let Some(peer) = self.peers.get_mut(peer_id) {
131 peer.score = peer.score.saturating_add(delta).min(100);
132 }
133 }
134
135 pub fn prune_stale(&mut self, max_age_secs: u64) {
136 let cutoff = now_secs().saturating_sub(max_age_secs);
137 self.peers
138 .retain(|_, p| p.last_seen >= cutoff || p.role == PeerRole::Validator);
139 }
140
141 pub fn len(&self) -> usize {
142 self.peers.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
146 self.peers.is_empty()
147 }
148
149 pub fn all_peers(&self) -> impl Iterator<Item = &PeerInfo> {
150 self.peers.values()
151 }
152}
153
154fn now_secs() -> u64 {
155 SystemTime::now()
156 .duration_since(UNIX_EPOCH)
157 .unwrap_or_default()
158 .as_secs()
159}