use std::collections::{BTreeMap, HashMap, HashSet}; use indexmap::IndexMap; use rand::prelude::*; use monero_p2p::{services::ZoneSpecificPeerListEntryBase, NetZoneAddress, NetworkZone}; use monero_pruning::{PruningSeed, CRYPTONOTE_MAX_BLOCK_HEIGHT}; #[cfg(test)] pub mod tests; /// A Peer list in the address book. /// /// This could either be the white list or gray list. #[derive(Debug)] pub struct PeerList { /// The peers with their peer data. pub peers: IndexMap>, /// An index of Pruning seed to address, so can quickly grab peers with the blocks /// we want. /// /// Pruning seeds are sorted by first their log_stripes and then their stripe. /// This means the first peers in this list will store more blocks than peers /// later on. So when we need a peer with a certain block we look at the peers /// storing more blocks first then work our way to the peers storing less. /// pruning_seeds: BTreeMap>, /// A hashmap linking ban_ids to addresses. ban_ids: HashMap<::BanID, Vec>, } impl PeerList { /// Creates a new peer list. pub fn new(list: Vec>) -> PeerList { let mut peers = IndexMap::with_capacity(list.len()); let mut pruning_seeds = BTreeMap::new(); let mut ban_ids = HashMap::with_capacity(list.len()); for peer in list { pruning_seeds .entry(peer.pruning_seed) .or_insert_with(Vec::new) .push(peer.adr); ban_ids .entry(peer.adr.ban_id()) .or_insert_with(Vec::new) .push(peer.adr); peers.insert(peer.adr, peer); } PeerList { peers, pruning_seeds, ban_ids, } } /// Gets the length of the peer list pub fn len(&self) -> usize { self.peers.len() } /// Adds a new peer to the peer list pub fn add_new_peer(&mut self, peer: ZoneSpecificPeerListEntryBase) { if self.peers.insert(peer.adr, peer).is_none() { // It's more clear with this #[allow(clippy::unwrap_or_default)] self.pruning_seeds .entry(peer.pruning_seed) .or_insert_with(Vec::new) .push(peer.adr); #[allow(clippy::unwrap_or_default)] self.ban_ids .entry(peer.adr.ban_id()) .or_insert_with(Vec::new) .push(peer.adr); } } /// Returns a random peer. /// If the pruning seed is specified then we will get a random peer with /// that pruning seed otherwise we will just get a random peer in the whole /// list. /// /// The given peer will be removed from the peer list. pub fn take_random_peer( &mut self, r: &mut R, block_needed: Option, must_keep_peers: &HashSet, ) -> Option> { // Take a random peer and see if it's in the list of must_keep_peers, if it is try again. // TODO: improve this for _ in 0..3 { if let Some(needed_height) = block_needed { let (_, addresses_with_block) = self.pruning_seeds.iter().find(|(seed, _)| { // TODO: factor in peer blockchain height? seed.get_next_unpruned_block(needed_height, CRYPTONOTE_MAX_BLOCK_HEIGHT) .expect("Block needed is higher than max block allowed.") == needed_height })?; let n = r.gen_range(0..addresses_with_block.len()); let peer = addresses_with_block[n]; if must_keep_peers.contains(&peer) { continue; } return self.remove_peer(&peer); } let len = self.len(); if len == 0 { return None; } let n = r.gen_range(0..len); let (&key, _) = self.peers.get_index(n).unwrap(); if !must_keep_peers.contains(&key) { return self.remove_peer(&key); } } None } pub fn get_random_peers( &self, r: &mut R, len: usize, ) -> Vec> { let mut peers = self.peers.values().copied().choose_multiple(r, len); // Order of the returned peers is not random, I am unsure of the impact of this, potentially allowing someone to make guesses about which peers // were connected first. // So to mitigate this shuffle the result. peers.shuffle(r); peers.drain(len.min(peers.len())..peers.len()); peers } /// Returns a mutable reference to a peer. pub fn get_peer_mut( &mut self, peer: &Z::Addr, ) -> Option<&mut ZoneSpecificPeerListEntryBase> { self.peers.get_mut(peer) } /// Returns true if the list contains this peer. pub fn contains_peer(&self, peer: &Z::Addr) -> bool { self.peers.contains_key(peer) } /// Removes a peer from the pruning idx /// /// MUST NOT BE USED ALONE fn remove_peer_pruning_idx(&mut self, peer: &ZoneSpecificPeerListEntryBase) { remove_peer_idx::(self.pruning_seeds.get_mut(&peer.pruning_seed), &peer.adr); if self .pruning_seeds .get(&peer.pruning_seed) .expect("There must be a peer with this id") .is_empty() { self.pruning_seeds.remove(&peer.pruning_seed); } } /// Removes a peer from the ban idx /// /// MUST NOT BE USED ALONE fn remove_peer_ban_idx(&mut self, peer: &ZoneSpecificPeerListEntryBase) { remove_peer_idx::(self.ban_ids.get_mut(&peer.adr.ban_id()), &peer.adr); if self .ban_ids .get(&peer.adr.ban_id()) .expect("There must be a peer with this id") .is_empty() { self.ban_ids.remove(&peer.adr.ban_id()); } } /// Removes a peer from all the indexes /// /// MUST NOT BE USED ALONE fn remove_peer_from_all_idxs(&mut self, peer: &ZoneSpecificPeerListEntryBase) { self.remove_peer_pruning_idx(peer); self.remove_peer_ban_idx(peer) } /// Removes a peer from the peer list pub fn remove_peer( &mut self, peer: &Z::Addr, ) -> Option> { let peer_eb = self.peers.swap_remove(peer)?; self.remove_peer_from_all_idxs(&peer_eb); Some(peer_eb) } /// Removes all peers with a specific ban id. pub fn remove_peers_with_ban_id(&mut self, ban_id: &::BanID) { let Some(addresses) = self.ban_ids.get(ban_id) else { // No peers to ban return; }; for addr in addresses.clone() { self.remove_peer(&addr); } } /// Tries to reduce the peer list to `new_len`. /// /// This function could keep the list bigger than `new_len` if `must_keep_peers`s length /// is larger than new_len, in that case we will remove as much as we can. pub fn reduce_list(&mut self, must_keep_peers: &HashSet, new_len: usize) { if new_len >= self.len() { return; } let target_removed = self.len() - new_len; let mut removed_count = 0; let mut peers_to_remove: Vec = Vec::with_capacity(target_removed); for peer_adr in self.peers.keys() { if removed_count >= target_removed { break; } if !must_keep_peers.contains(peer_adr) { peers_to_remove.push(*peer_adr); removed_count += 1; } } for peer_adr in peers_to_remove { let _ = self.remove_peer(&peer_adr); } } } /// Remove a peer from an index. fn remove_peer_idx(peer_list: Option<&mut Vec>, addr: &Z::Addr) { if let Some(peer_list) = peer_list { if let Some(idx) = peer_list.iter().position(|peer_adr| peer_adr == addr) { peer_list.swap_remove(idx); } else { unreachable!("This function will only be called when the peer exists."); } } else { unreachable!("Index must exist if a peer has that index"); } }