Skip to main content

hotmint_network/
peer.rs

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/// Role of a peer in the network.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum PeerRole {
17    /// Consensus validator (participates in voting and block production).
18    Validator,
19    /// Full node (syncs blocks, optionally relays messages, serves RPC).
20    Fullnode,
21}
22
23/// Information about a known peer.
24#[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
62/// Persistent address book of known peers.
63pub 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}