use super::peer_info::{PeerConnectionStatus, PeerInfo}; use super::peer_sync_status::PeerSyncStatus; use super::score::{Score, ScoreState}; use crate::rpc::methods::MetaData; use crate::PeerId; use rand::seq::SliceRandom; use slog::{crit, debug, trace, warn}; use std::collections::HashMap; use std::time::Instant; use types::{EthSpec, SubnetId}; /// Max number of disconnected nodes to remember. const MAX_DC_PEERS: usize = 500; /// The maximum number of banned nodes to remember. const MAX_BANNED_PEERS: usize = 1000; /// Storage of known peers, their reputation and information pub struct PeerDB { /// The collection of known connected peers, their status and reputation peers: HashMap>, /// The number of disconnected nodes in the database. disconnected_peers: usize, /// The number of banned peers in the database. banned_peers: usize, /// PeerDB's logger log: slog::Logger, } impl PeerDB { pub fn new(log: &slog::Logger) -> Self { Self { log: log.clone(), disconnected_peers: 0, banned_peers: 0, peers: HashMap::new(), } } /* Getters */ /// Gives the score of a peer, or default score if it is unknown. pub fn score(&self, peer_id: &PeerId) -> Score { self.peers .get(peer_id) .map_or(Score::default(), |info| info.score) } /// Returns an iterator over all peers in the db. pub fn peers(&self) -> impl Iterator)> { self.peers.iter() } /// Returns an iterator over all peers in the db. pub(super) fn peers_mut(&mut self) -> impl Iterator)> { self.peers.iter_mut() } /// Gives the ids of all known peers. pub fn peer_ids(&self) -> impl Iterator { self.peers.keys() } /// Returns a peer's info, if known. pub fn peer_info(&self, peer_id: &PeerId) -> Option<&PeerInfo> { self.peers.get(peer_id) } /// Returns a mutable reference to a peer's info if known. /// TODO: make pub(super) to ensure that peer management is unified pub fn peer_info_mut(&mut self, peer_id: &PeerId) -> Option<&mut PeerInfo> { self.peers.get_mut(peer_id) } /// Returns if the peer is already connected. pub fn is_connected(&self, peer_id: &PeerId) -> bool { if let Some(PeerConnectionStatus::Connected { .. }) = self.connection_status(peer_id) { true } else { false } } /// If we are connected or currently dialing the peer returns true. pub fn is_connected_or_dialing(&self, peer_id: &PeerId) -> bool { match self.connection_status(peer_id) { Some(PeerConnectionStatus::Connected { .. }) | Some(PeerConnectionStatus::Dialing { .. }) => true, _ => false, } } /// Returns true if the peer is synced at least to our current head. pub fn is_synced(&self, peer_id: &PeerId) -> bool { match self.peers.get(peer_id).map(|info| &info.sync_status) { Some(PeerSyncStatus::Synced { .. }) => true, Some(_) => false, None => false, } } /// Returns true if the Peer is banned. pub fn is_banned(&self, peer_id: &PeerId) -> bool { match self.peers.get(peer_id).map(|info| info.score.state()) { Some(ScoreState::Banned) => true, _ => false, } } /// Returns true if the Peer is either banned or in the disconnected state. pub fn is_banned_or_disconnected(&self, peer_id: &PeerId) -> bool { match self.peers.get(peer_id).map(|info| info.score.state()) { Some(ScoreState::Banned) | Some(ScoreState::Disconnected) => true, _ => false, } } /// Gives the ids of all known connected peers. pub fn connected_peers(&self) -> impl Iterator)> { self.peers .iter() .filter(|(_, info)| info.connection_status.is_connected()) } /// Gives the ids of all known connected peers. pub fn connected_peer_ids(&self) -> impl Iterator { self.peers .iter() .filter(|(_, info)| info.connection_status.is_connected()) .map(|(peer_id, _)| peer_id) } /// Connected or dialing peers pub fn connected_or_dialing_peers(&self) -> impl Iterator { self.peers .iter() .filter(|(_, info)| { info.connection_status.is_connected() || info.connection_status.is_dialing() }) .map(|(peer_id, _)| peer_id) } /// Gives the `peer_id` of all known connected and synced peers. pub fn synced_peers(&self) -> impl Iterator { self.peers .iter() .filter(|(_, info)| { if info.sync_status.is_synced() || info.sync_status.is_advanced() { return info.connection_status.is_connected(); } false }) .map(|(peer_id, _)| peer_id) } /// Gives an iterator of all peers on a given subnet. pub fn peers_on_subnet(&self, subnet_id: SubnetId) -> impl Iterator { self.peers .iter() .filter(move |(_, info)| { info.connection_status.is_connected() && info.on_subnet(subnet_id) }) .map(|(peer_id, _)| peer_id) } /// Gives the ids of all known disconnected peers. pub fn disconnected_peers(&self) -> impl Iterator { self.peers .iter() .filter(|(_, info)| info.connection_status.is_disconnected()) .map(|(peer_id, _)| peer_id) } /// Gives the ids of all known banned peers. pub fn banned_peers(&self) -> impl Iterator { self.peers .iter() .filter(|(_, info)| info.connection_status.is_banned()) .map(|(peer_id, _)| peer_id) } /// Returns a vector of all connected peers sorted by score beginning with the worst scores. /// Ties get broken randomly. pub fn worst_connected_peers(&self) -> Vec<(&PeerId, &PeerInfo)> { let mut connected = self .peers .iter() .filter(|(_, info)| info.connection_status.is_connected()) .collect::>(); connected.shuffle(&mut rand::thread_rng()); connected.sort_by_key(|(_, info)| info.score); connected } /// Returns a vector containing peers (their ids and info), sorted by /// score from highest to lowest, and filtered using `is_status` pub fn best_peers_by_status(&self, is_status: F) -> Vec<(&PeerId, &PeerInfo)> where F: Fn(&PeerConnectionStatus) -> bool, { let mut by_status = self .peers .iter() .filter(|(_, info)| is_status(&info.connection_status)) .collect::>(); by_status.sort_by_key(|(_, info)| info.score); by_status.into_iter().rev().collect() } /// Returns the peer with highest reputation that satisfies `is_status` pub fn best_by_status(&self, is_status: F) -> Option<&PeerId> where F: Fn(&PeerConnectionStatus) -> bool, { self.peers .iter() .filter(|(_, info)| is_status(&info.connection_status)) .max_by_key(|(_, info)| info.score) .map(|(id, _)| id) } /// Returns the peer's connection status. Returns unknown if the peer is not in the DB. pub fn connection_status(&self, peer_id: &PeerId) -> Option { self.peer_info(peer_id) .map(|info| info.connection_status.clone()) } /* Setters */ /// A peer is being dialed. pub fn dialing_peer(&mut self, peer_id: &PeerId) { let info = self.peers.entry(peer_id.clone()).or_default(); if info.connection_status.is_disconnected() { self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } if info.connection_status.is_banned() { self.banned_peers = self.banned_peers.saturating_sub(1); } info.connection_status = PeerConnectionStatus::Dialing { since: Instant::now(), }; } /// Update min ttl of a peer. pub fn update_min_ttl(&mut self, peer_id: &PeerId, min_ttl: Instant) { let info = self.peers.entry(peer_id.clone()).or_default(); // only update if the ttl is longer if info.min_ttl.is_none() || Some(min_ttl) > info.min_ttl { info.min_ttl = Some(min_ttl); let min_ttl_secs = min_ttl .checked_duration_since(Instant::now()) .map(|duration| duration.as_secs()) .unwrap_or_else(|| 0); debug!(self.log, "Updating the time a peer is required for"; "peer_id" => peer_id.to_string(), "future_min_ttl_secs" => min_ttl_secs); } } /// Extends the ttl of all peers on the given subnet that have a shorter /// min_ttl than what's given. pub fn extend_peers_on_subnet(&mut self, subnet_id: SubnetId, min_ttl: Instant) { let log = &self.log; self.peers.iter_mut() .filter(move |(_, info)| { info.connection_status.is_connected() && info.on_subnet(subnet_id) }) .for_each(|(peer_id,info)| { if info.min_ttl.is_none() || Some(min_ttl) > info.min_ttl { info.min_ttl = Some(min_ttl); } let min_ttl_secs = min_ttl .checked_duration_since(Instant::now()) .map(|duration| duration.as_secs()) .unwrap_or_else(|| 0); trace!(log, "Updating minimum duration a peer is required for"; "peer_id" => peer_id.to_string(), "min_ttl" => min_ttl_secs); }); } /// Sets a peer as connected with an ingoing connection. pub fn connect_ingoing(&mut self, peer_id: &PeerId) { let info = self.peers.entry(peer_id.clone()).or_default(); if info.connection_status.is_disconnected() { self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } if info.connection_status.is_banned() { self.banned_peers = self.banned_peers.saturating_sub(1); } info.connection_status.connect_ingoing(); } /// Sets a peer as connected with an outgoing connection. pub fn connect_outgoing(&mut self, peer_id: &PeerId) { let info = self.peers.entry(peer_id.clone()).or_default(); if info.connection_status.is_disconnected() { self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } if info.connection_status.is_banned() { self.banned_peers = self.banned_peers.saturating_sub(1); } info.connection_status.connect_outgoing(); } /// Sets the peer as disconnected. A banned peer remains banned pub fn disconnect(&mut self, peer_id: &PeerId) { // Note that it could be the case we prevent new nodes from joining. In this instance, // we don't bother tracking the new node. if let Some(info) = self.peers.get_mut(peer_id) { if !info.connection_status.is_disconnected() && !info.connection_status.is_banned() { info.connection_status.disconnect(); self.disconnected_peers += 1; } self.shrink_to_fit(); } } /// Marks a peer as banned. pub fn ban(&mut self, peer_id: &PeerId) { let log_ref = &self.log; let info = self.peers.entry(peer_id.clone()).or_insert_with(|| { warn!(log_ref, "Banning unknown peer"; "peer_id" => peer_id.to_string()); PeerInfo::default() }); if info.connection_status.is_disconnected() { self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } if !info.connection_status.is_banned() { info.connection_status.ban(); self.banned_peers += 1; } self.shrink_to_fit(); } /// Unbans a peer. pub fn unban(&mut self, peer_id: &PeerId) { let log_ref = &self.log; let info = self.peers.entry(peer_id.clone()).or_insert_with(|| { warn!(log_ref, "UnBanning unknown peer"; "peer_id" => peer_id.to_string()); PeerInfo::default() }); if info.connection_status.is_banned() { info.connection_status.unban(); self.banned_peers = self.banned_peers.saturating_sub(1); } self.shrink_to_fit(); } /// Removes banned and disconnected peers from the DB if we have reached any of our limits. /// Drops the peers with the lowest reputation so that the number of /// disconnected peers is less than MAX_DC_PEERS pub fn shrink_to_fit(&mut self) { // Remove excess baned peers while self.banned_peers > MAX_BANNED_PEERS { if let Some(to_drop) = self .peers .iter() .filter(|(_, info)| info.connection_status.is_banned()) .min_by(|(_, info_a), (_, info_b)| { info_a .score .partial_cmp(&info_b.score) .unwrap_or(std::cmp::Ordering::Equal) }) .map(|(id, _)| id.clone()) { debug!(self.log, "Removing old banned peer"; "peer_id" => to_drop.to_string()); self.peers.remove(&to_drop); } // If there is no minimum, this is a coding error. For safety we decrease // the count to avoid a potential infinite loop. self.banned_peers = self.banned_peers.saturating_sub(1); } // Remove excess disconnected peers while self.disconnected_peers > MAX_DC_PEERS { if let Some(to_drop) = self .peers .iter() .filter(|(_, info)| info.connection_status.is_disconnected()) .min_by(|(_, info_a), (_, info_b)| { info_a .score .partial_cmp(&info_b.score) .unwrap_or(std::cmp::Ordering::Equal) }) .map(|(id, _)| id.clone()) { debug!(self.log, "Removing old disconnected peer"; "peer_id" => to_drop.to_string()); self.peers.remove(&to_drop); } // If there is no minimum, this is a coding error. For safety we decrease // the count to avoid a potential infinite loop. self.disconnected_peers = self.disconnected_peers.saturating_sub(1); } } /// Add the meta data of a peer. pub fn add_metadata(&mut self, peer_id: &PeerId, meta_data: MetaData) { if let Some(peer_info) = self.peers.get_mut(peer_id) { peer_info.meta_data = Some(meta_data); } else { warn!(self.log, "Tried to add meta data for a non-existant peer"; "peer_id" => peer_id.to_string()); } } /// Sets the syncing status of a peer. pub fn set_sync_status(&mut self, peer_id: &PeerId, sync_status: PeerSyncStatus) { if let Some(peer_info) = self.peers.get_mut(peer_id) { peer_info.sync_status = sync_status; } else { crit!(self.log, "Tried to the sync status for an unknown peer"; "peer_id" => peer_id.to_string()); } } } #[cfg(test)] mod tests { use super::*; use slog::{o, Drain}; use types::MinimalEthSpec; type M = MinimalEthSpec; pub fn build_log(level: slog::Level, enabled: bool) -> slog::Logger { let decorator = slog_term::TermDecorator::new().build(); let drain = slog_term::FullFormat::new(decorator).build().fuse(); let drain = slog_async::Async::new(drain).build().fuse(); if enabled { slog::Logger::root(drain.filter_level(level).fuse(), o!()) } else { slog::Logger::root(drain.filter(|_| false).fuse(), o!()) } } fn add_score(db: &mut PeerDB, peer_id: &PeerId, score: f64) { if let Some(info) = db.peer_info_mut(peer_id) { info.score.add(score); } } fn get_db() -> PeerDB { let log = build_log(slog::Level::Debug, false); PeerDB::new(&log) } #[test] fn test_peer_connected_successfully() { let mut pdb = get_db(); let random_peer = PeerId::random(); let (n_in, n_out) = (10, 20); for _ in 0..n_in { pdb.connect_ingoing(&random_peer); } for _ in 0..n_out { pdb.connect_outgoing(&random_peer); } // the peer is known let peer_info = pdb.peer_info(&random_peer); assert!(peer_info.is_some()); // this is the only peer assert_eq!(pdb.peers().count(), 1); // the peer has the default reputation assert_eq!(pdb.score(&random_peer).score(), Score::default().score()); // it should be connected, and therefore not counted as disconnected assert_eq!(pdb.disconnected_peers, 0); assert!(peer_info.unwrap().connection_status.is_connected()); assert_eq!( peer_info.unwrap().connection_status.connections(), (n_in, n_out) ); } #[test] fn test_disconnected_are_bounded() { let mut pdb = get_db(); for _ in 0..MAX_DC_PEERS + 1 { let p = PeerId::random(); pdb.connect_ingoing(&p); } assert_eq!(pdb.disconnected_peers, 0); for p in pdb.connected_peer_ids().cloned().collect::>() { pdb.disconnect(&p); } assert_eq!(pdb.disconnected_peers, MAX_DC_PEERS); } #[test] fn test_banned_are_bounded() { let mut pdb = get_db(); for _ in 0..MAX_BANNED_PEERS + 1 { let p = PeerId::random(); pdb.connect_ingoing(&p); } assert_eq!(pdb.banned_peers, 0); for p in pdb.connected_peer_ids().cloned().collect::>() { pdb.ban(&p); } assert_eq!(pdb.banned_peers, MAX_BANNED_PEERS); } #[test] fn test_best_peers() { let mut pdb = get_db(); let p0 = PeerId::random(); let p1 = PeerId::random(); let p2 = PeerId::random(); pdb.connect_ingoing(&p0); pdb.connect_ingoing(&p1); pdb.connect_ingoing(&p2); add_score(&mut pdb, &p0, 70.0); add_score(&mut pdb, &p1, 100.0); add_score(&mut pdb, &p2, 50.0); let best_peers: Vec<&PeerId> = pdb .best_peers_by_status(PeerConnectionStatus::is_connected) .iter() .map(|p| p.0) .collect(); assert_eq!(vec![&p1, &p0, &p2], best_peers); } #[test] fn test_the_best_peer() { let mut pdb = get_db(); let p0 = PeerId::random(); let p1 = PeerId::random(); let p2 = PeerId::random(); pdb.connect_ingoing(&p0); pdb.connect_ingoing(&p1); pdb.connect_ingoing(&p2); add_score(&mut pdb, &p0, 70.0); add_score(&mut pdb, &p1, 100.0); add_score(&mut pdb, &p2, 50.0); let the_best = pdb.best_by_status(PeerConnectionStatus::is_connected); assert!(the_best.is_some()); // Consistency check let best_peers = pdb.best_peers_by_status(PeerConnectionStatus::is_connected); assert_eq!(the_best, best_peers.iter().next().map(|p| p.0)); } #[test] fn test_disconnected_consistency() { let mut pdb = get_db(); let random_peer = PeerId::random(); pdb.connect_ingoing(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.connect_ingoing(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.connect_outgoing(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.ban(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); dbg!("1"); } #[test] fn test_disconnected_ban_consistency() { let mut pdb = get_db(); let random_peer = PeerId::random(); let random_peer1 = PeerId::random(); let random_peer2 = PeerId::random(); let random_peer3 = PeerId::random(); pdb.connect_ingoing(&random_peer); pdb.connect_ingoing(&random_peer1); pdb.connect_ingoing(&random_peer2); pdb.connect_ingoing(&random_peer3); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.connect_ingoing(&random_peer); pdb.disconnect(&random_peer1); pdb.ban(&random_peer2); pdb.connect_ingoing(&random_peer3); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.ban(&random_peer1); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.connect_outgoing(&random_peer2); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.ban(&random_peer3); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.ban(&random_peer3); pdb.connect_ingoing(&random_peer1); pdb.disconnect(&random_peer2); pdb.ban(&random_peer3); pdb.connect_ingoing(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.disconnect(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); assert_eq!(pdb.banned_peers, pdb.banned_peers().count()); pdb.ban(&random_peer); assert_eq!(pdb.disconnected_peers, pdb.disconnected_peers().count()); } }