iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

peer_book.rs (20545B)


      1 use std::{collections::BTreeMap, net::SocketAddr};
      2 
      3 use serde::{Deserialize, Serialize};
      4 
      5 use super::{
      6     PEER_CLOCK_OFFSET_ACCEPTANCE_MS, PEER_CLOCK_OFFSET_STALE_MS, PEER_MISBEHAVIOR_BAN_MS,
      7     PEER_MISBEHAVIOR_BAN_SCORE, ProtocolHello, now_ms,
      8 };
      9 
     10 pub const MAX_DISCOVERED_PEERS: usize = 256;
     11 pub const MAX_DISCOVERED_PEERS_PER_IP: usize = 4;
     12 pub const MAX_DISCOVERED_PEERS_PER_IPV4_PREFIX: usize = 16;
     13 pub const MAX_DISCOVERED_PEERS_PER_IPV6_PREFIX: usize = 16;
     14 
     15 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
     16 pub struct PeerBook {
     17     peers: BTreeMap<String, PeerInfo>,
     18 }
     19 
     20 impl PeerBook {
     21     pub fn from_addresses(addresses: Vec<String>) -> Self {
     22         let mut book = Self::default();
     23         for address in addresses {
     24             book.add_peer(address);
     25         }
     26         book
     27     }
     28 
     29     pub fn add_peer(&mut self, address: impl Into<String>) {
     30         let address = address.into();
     31         let peer = self
     32             .peers
     33             .entry(address.clone())
     34             .or_insert_with(|| PeerInfo::new(address, PeerDirection::Outbound));
     35         if peer.direction != PeerDirection::Outbound {
     36             peer.direction = PeerDirection::Outbound;
     37         }
     38     }
     39 
     40     pub fn add_discovered_peer(&mut self, address: impl Into<String>) -> bool {
     41         let address = address.into();
     42         if let Some(direction) = self.peers.get(&address).map(|peer| peer.direction.clone()) {
     43             if direction == PeerDirection::Inbound && !self.discovered_peer_has_room(&address) {
     44                 return false;
     45             }
     46             if let Some(peer) = self.peers.get_mut(&address) {
     47                 if peer.direction == PeerDirection::Inbound {
     48                     peer.direction = PeerDirection::Discovered;
     49                 }
     50                 peer.last_contact_ms = peer.last_contact_ms.or_else(|| Some(now_ms()));
     51             }
     52             return true;
     53         }
     54         if !self.discovered_peer_has_room(&address) {
     55             return false;
     56         }
     57         let mut peer = PeerInfo::new(address.clone(), PeerDirection::Discovered);
     58         peer.last_contact_ms = Some(now_ms());
     59         self.peers.insert(address, peer);
     60         true
     61     }
     62 
     63     fn discovered_peer_has_room(&self, address: &str) -> bool {
     64         if self.discovered_peer_count() >= MAX_DISCOVERED_PEERS {
     65             return false;
     66         }
     67         let Ok(candidate) = address.parse::<SocketAddr>() else {
     68             return false;
     69         };
     70         let candidate_ip = candidate.ip();
     71         let mut same_ip = 0usize;
     72         let mut same_group = 0usize;
     73         for peer in self
     74             .peers
     75             .values()
     76             .filter(|peer| peer.direction == PeerDirection::Discovered)
     77         {
     78             let Ok(existing) = peer.address.parse::<SocketAddr>() else {
     79                 continue;
     80             };
     81             let existing_ip = existing.ip();
     82             if existing_ip == candidate_ip {
     83                 same_ip += 1;
     84             }
     85             if same_discovery_group(existing, candidate) {
     86                 same_group += 1;
     87             }
     88         }
     89         if same_ip >= MAX_DISCOVERED_PEERS_PER_IP {
     90             return false;
     91         }
     92         match candidate {
     93             SocketAddr::V4(_) => same_group < MAX_DISCOVERED_PEERS_PER_IPV4_PREFIX,
     94             SocketAddr::V6(_) => same_group < MAX_DISCOVERED_PEERS_PER_IPV6_PREFIX,
     95         }
     96     }
     97 
     98     fn discovered_peer_count(&self) -> usize {
     99         self.peers
    100             .values()
    101             .filter(|peer| peer.direction == PeerDirection::Discovered)
    102             .count()
    103     }
    104 
    105     pub fn promote_discovered_peer(&mut self, address: &str) {
    106         if let Some(peer) = self.peers.get_mut(address) {
    107             if peer.direction == PeerDirection::Discovered {
    108                 peer.direction = PeerDirection::Outbound;
    109             }
    110         }
    111     }
    112 
    113     pub fn discovered_peer_count_for_tests(&self) -> usize {
    114         self.discovered_peer_count()
    115     }
    116 
    117     pub fn discovered_peer_capacity_for_tests(&self) -> usize {
    118         MAX_DISCOVERED_PEERS
    119     }
    120 
    121     pub fn direction_for_tests(&self, address: &str) -> Option<PeerDirection> {
    122         self.peers.get(address).map(|peer| peer.direction.clone())
    123     }
    124 
    125     pub fn peer_count_for_tests(&self) -> usize {
    126         self.peers.len()
    127     }
    128 
    129     pub fn add_discovered_peer_at(&mut self, address: impl Into<String>, now_ms: u64) -> bool {
    130         let address = address.into();
    131         let added = self.add_discovered_peer(address.clone());
    132         if added {
    133             if let Some(peer) = self.peers.get_mut(&address) {
    134                 peer.last_contact_ms = Some(now_ms);
    135             }
    136         }
    137         added
    138     }
    139 
    140     fn prune_stale_discovered_peer(peer: &PeerInfo, now_ms: u64, max_age_ms: u64) -> bool {
    141         if peer.direction != PeerDirection::Discovered || peer.is_banned_at(now_ms) {
    142             return true;
    143         }
    144         let Some(last_contact) = peer.last_success_ms.or(peer.last_contact_ms) else {
    145             return false;
    146         };
    147         now_ms.saturating_sub(last_contact) <= max_age_ms
    148     }
    149 
    150     fn prune_stale_inbound_peer(peer: &PeerInfo, now_ms: u64, max_age_ms: u64) -> bool {
    151         if peer.direction != PeerDirection::Inbound || peer.is_banned_at(now_ms) {
    152             return true;
    153         }
    154         peer.last_contact_ms
    155             .is_some_and(|last_contact| now_ms.saturating_sub(last_contact) <= max_age_ms)
    156     }
    157 
    158     pub fn prune_stale_peers_at(
    159         &mut self,
    160         now_ms: u64,
    161         inbound_max_age_ms: u64,
    162         discovered_max_age_ms: u64,
    163     ) -> usize {
    164         let before = self.peers.len();
    165         self.peers.retain(|_, peer| {
    166             Self::prune_stale_inbound_peer(peer, now_ms, inbound_max_age_ms)
    167                 && Self::prune_stale_discovered_peer(peer, now_ms, discovered_max_age_ms)
    168         });
    169         before.saturating_sub(self.peers.len())
    170     }
    171 
    172     pub fn observe_inbound_peer(&mut self, address: impl Into<String>) {
    173         let address = address.into();
    174         self.peers
    175             .entry(address.clone())
    176             .or_insert_with(|| PeerInfo::new(address, PeerDirection::Inbound));
    177     }
    178 
    179     pub fn replace_peer_address(&mut self, from: &str, to: impl Into<String>) {
    180         let to = to.into();
    181         if from == to {
    182             if !self.peers.contains_key(from) {
    183                 self.add_peer(to);
    184             }
    185             return;
    186         }
    187 
    188         let Some(from_peer) = self.peers.remove(from) else {
    189             self.add_peer(to);
    190             return;
    191         };
    192 
    193         let to_peer = self
    194             .peers
    195             .entry(to.clone())
    196             .or_insert_with(|| PeerInfo::new(to, from_peer.direction.clone()));
    197         if from_peer.direction == PeerDirection::Outbound {
    198             to_peer.direction = PeerDirection::Outbound;
    199         } else if from_peer.direction == PeerDirection::Discovered
    200             && to_peer.direction == PeerDirection::Inbound
    201         {
    202             to_peer.direction = PeerDirection::Discovered;
    203         }
    204         to_peer.messages_sent = to_peer
    205             .messages_sent
    206             .saturating_add(from_peer.messages_sent);
    207         to_peer.messages_received = to_peer
    208             .messages_received
    209             .saturating_add(from_peer.messages_received);
    210         to_peer.last_known_height = to_peer.last_known_height.or(from_peer.last_known_height);
    211         to_peer.last_known_tip_hash = to_peer
    212             .last_known_tip_hash
    213             .clone()
    214             .or(from_peer.last_known_tip_hash);
    215         if to_peer.last_hello.is_none() {
    216             to_peer.last_hello = from_peer.last_hello;
    217         }
    218         if from_peer.last_clock_observed_ms > to_peer.last_clock_observed_ms {
    219             to_peer.last_clock_offset_ms = from_peer.last_clock_offset_ms;
    220             to_peer.last_clock_offset_accepted = from_peer.last_clock_offset_accepted;
    221             to_peer.last_clock_observed_ms = from_peer.last_clock_observed_ms;
    222         }
    223         to_peer.last_contact_ms = to_peer.last_contact_ms.max(from_peer.last_contact_ms);
    224         to_peer.last_success_ms = to_peer.last_success_ms.max(from_peer.last_success_ms);
    225         to_peer.last_error_ms = to_peer.last_error_ms.max(from_peer.last_error_ms);
    226         if to_peer.last_error.is_none() {
    227             to_peer.last_error = from_peer.last_error;
    228         }
    229         to_peer.misbehavior_score = to_peer
    230             .misbehavior_score
    231             .saturating_add(from_peer.misbehavior_score);
    232         to_peer.banned_until_ms = to_peer.banned_until_ms.max(from_peer.banned_until_ms);
    233         if to_peer.ban_reason.is_none() {
    234             to_peer.ban_reason = from_peer.ban_reason;
    235         }
    236     }
    237 
    238     pub fn remove_peer(&mut self, address: &str) -> bool {
    239         if self
    240             .peers
    241             .get(address)
    242             .is_some_and(|peer| peer.direction != PeerDirection::Inbound)
    243         {
    244             self.peers.remove(address);
    245             true
    246         } else {
    247             false
    248         }
    249     }
    250 
    251     pub fn is_connectable_peer(&self, address: &str) -> bool {
    252         self.peers
    253             .get(address)
    254             .is_some_and(|peer| peer.direction != PeerDirection::Inbound)
    255     }
    256 
    257     pub fn addresses(&self) -> Vec<String> {
    258         self.outbound_addresses_at(now_ms())
    259     }
    260 
    261     pub fn connectable_addresses_at(&self, now_ms: u64) -> Vec<String> {
    262         self.peers
    263             .values()
    264             .filter(|peer| peer.direction != PeerDirection::Inbound)
    265             .filter(|peer| !peer.is_banned_at(now_ms))
    266             .map(|peer| peer.address.clone())
    267             .collect()
    268     }
    269 
    270     pub fn outbound_addresses_at(&self, now_ms: u64) -> Vec<String> {
    271         self.peers
    272             .values()
    273             .filter(|peer| peer.direction == PeerDirection::Outbound)
    274             .filter(|peer| !peer.is_banned_at(now_ms))
    275             .map(|peer| peer.address.clone())
    276             .collect()
    277     }
    278 
    279     pub fn outbound_session_candidates_at(
    280         &self,
    281         now_ms: u64,
    282         max_discovered: usize,
    283     ) -> Vec<String> {
    284         let mut outbound = Vec::new();
    285         let mut discovered = self
    286             .peers
    287             .values()
    288             .filter(|peer| peer.direction == PeerDirection::Discovered)
    289             .filter(|peer| !peer.is_banned_at(now_ms))
    290             .cloned()
    291             .collect::<Vec<_>>();
    292         discovered.sort_by(|left, right| {
    293             right
    294                 .last_success_ms
    295                 .cmp(&left.last_success_ms)
    296                 .then_with(|| left.last_error_ms.cmp(&right.last_error_ms))
    297                 .then_with(|| left.address.cmp(&right.address))
    298         });
    299 
    300         for peer in self
    301             .peers
    302             .values()
    303             .filter(|peer| peer.direction == PeerDirection::Outbound)
    304             .filter(|peer| !peer.is_banned_at(now_ms))
    305         {
    306             outbound.push(peer.address.clone());
    307         }
    308         outbound.extend(
    309             discovered
    310                 .into_iter()
    311                 .take(max_discovered)
    312                 .map(|peer| peer.address),
    313         );
    314         outbound
    315     }
    316 
    317     pub fn addresses_except(&self, excluded: &str) -> Vec<String> {
    318         self.outbound_addresses_at(now_ms())
    319             .into_iter()
    320             .filter(|address| address != excluded)
    321             .collect()
    322     }
    323 
    324     pub fn list(&self) -> Vec<PeerInfo> {
    325         self.peers.values().cloned().collect()
    326     }
    327 
    328     pub fn prune_stale_inbound_peers_at(&mut self, now_ms: u64, max_age_ms: u64) -> usize {
    329         self.prune_stale_peers_at(now_ms, max_age_ms, u64::MAX)
    330     }
    331 
    332     pub fn record_sent(&mut self, address: &str, count: u64) {
    333         let now = now_ms();
    334         let peer = self.ensure(address, PeerDirection::Outbound);
    335         if peer.direction == PeerDirection::Discovered {
    336             peer.direction = PeerDirection::Outbound;
    337         }
    338         peer.messages_sent += count;
    339         peer.last_contact_ms = Some(now);
    340         peer.last_success_ms = Some(now);
    341         if !peer.is_banned_at(now) {
    342             peer.last_error = None;
    343             peer.clear_misbehavior();
    344         }
    345     }
    346 
    347     pub fn record_status(&mut self, address: &str, height: u64, tip_hash: String) {
    348         let now = now_ms();
    349         let peer = self.ensure(address, PeerDirection::Outbound);
    350         if peer.direction == PeerDirection::Discovered {
    351             peer.direction = PeerDirection::Outbound;
    352         }
    353         peer.last_known_height = Some(height);
    354         peer.last_known_tip_hash = Some(tip_hash);
    355         peer.last_contact_ms = Some(now);
    356         peer.last_success_ms = Some(now);
    357         if !peer.is_banned_at(now) {
    358             peer.last_error = None;
    359             peer.clear_misbehavior();
    360         }
    361     }
    362 
    363     pub fn record_hello(&mut self, address: &str, direction: PeerDirection, hello: ProtocolHello) {
    364         self.ensure(address, direction).last_hello = Some(hello);
    365     }
    366 
    367     pub fn record_clock_observation(
    368         &mut self,
    369         address: &str,
    370         direction: PeerDirection,
    371         remote_time_ms: u64,
    372         local_receive_time_ms: u64,
    373     ) {
    374         if remote_time_ms == 0 {
    375             return;
    376         }
    377         let offset = remote_time_ms as i128 - local_receive_time_ms as i128;
    378         let offset = offset.clamp(i64::MIN as i128, i64::MAX as i128) as i64;
    379         let accepted = offset.abs() <= PEER_CLOCK_OFFSET_ACCEPTANCE_MS;
    380         let peer = self.ensure(address, direction);
    381         peer.last_clock_offset_ms = Some(offset);
    382         peer.last_clock_offset_accepted = Some(accepted);
    383         peer.last_clock_observed_ms = Some(local_receive_time_ms);
    384     }
    385 
    386     pub fn network_time_offset_ms_at(&self, now_ms: u64) -> Option<i64> {
    387         median_i64(
    388             self.peers
    389                 .values()
    390                 .filter(|peer| !peer.is_banned_at(now_ms))
    391                 .filter(|peer| peer.last_error.is_none())
    392                 .filter(|peer| peer.last_clock_offset_accepted == Some(true))
    393                 .filter(|peer| {
    394                     peer.last_clock_observed_ms.is_some_and(|observed_ms| {
    395                         now_ms.saturating_sub(observed_ms) <= PEER_CLOCK_OFFSET_STALE_MS
    396                     })
    397                 })
    398                 .filter_map(|peer| peer.last_clock_offset_ms)
    399                 .collect(),
    400         )
    401     }
    402 
    403     pub fn adjusted_time_ms_at(&self, now_ms: u64) -> u64 {
    404         match self.network_time_offset_ms_at(now_ms) {
    405             Some(offset) if offset >= 0 => now_ms.saturating_add(offset as u64),
    406             Some(offset) => now_ms.saturating_sub(offset.unsigned_abs()),
    407             None => now_ms,
    408         }
    409     }
    410 
    411     pub fn bad_clock_peer_count_at(&self, now_ms: u64) -> usize {
    412         self.peers
    413             .values()
    414             .filter(|peer| !peer.is_banned_at(now_ms))
    415             .filter(|peer| {
    416                 peer.last_clock_observed_ms.is_some_and(|observed_ms| {
    417                     now_ms.saturating_sub(observed_ms) <= PEER_CLOCK_OFFSET_STALE_MS
    418                 })
    419             })
    420             .filter(|peer| peer.last_clock_offset_accepted == Some(false))
    421             .count()
    422     }
    423 
    424     pub fn record_error(&mut self, address: &str, error: impl Into<String>) {
    425         let now = now_ms();
    426         let peer = self.ensure(address, PeerDirection::Outbound);
    427         peer.last_contact_ms = Some(now);
    428         peer.last_error_ms = Some(now);
    429         peer.last_error = Some(error.into());
    430     }
    431 
    432     pub fn record_inbound_error(&mut self, address: &str, error: impl Into<String>) {
    433         let now = now_ms();
    434         let peer = self.ensure(address, PeerDirection::Inbound);
    435         peer.last_contact_ms = Some(now);
    436         peer.last_error_ms = Some(now);
    437         peer.last_error = Some(error.into());
    438     }
    439 
    440     pub fn record_received(&mut self, address: &str, count: u64) {
    441         let now = now_ms();
    442         let peer = self.ensure(address, PeerDirection::Inbound);
    443         peer.messages_received += count;
    444         peer.last_contact_ms = Some(now);
    445         peer.last_success_ms = Some(now);
    446         if !peer.is_banned_at(now) {
    447             peer.last_error = None;
    448             peer.clear_misbehavior();
    449         }
    450     }
    451 
    452     pub fn record_misbehavior(&mut self, address: &str, reason: impl Into<String>) {
    453         self.record_misbehavior_at(address, reason, now_ms());
    454     }
    455 
    456     pub fn record_misbehavior_at(&mut self, address: &str, reason: impl Into<String>, now_ms: u64) {
    457         self.record_misbehavior_with_direction(address, reason, now_ms, PeerDirection::Outbound);
    458     }
    459 
    460     pub fn record_inbound_misbehavior(&mut self, address: &str, reason: impl Into<String>) {
    461         self.record_misbehavior_with_direction(address, reason, now_ms(), PeerDirection::Inbound);
    462     }
    463 
    464     fn record_misbehavior_with_direction(
    465         &mut self,
    466         address: &str,
    467         reason: impl Into<String>,
    468         now_ms: u64,
    469         direction: PeerDirection,
    470     ) {
    471         let reason = reason.into();
    472         let peer = self.ensure(address, direction);
    473         peer.last_contact_ms = Some(now_ms);
    474         peer.last_error_ms = Some(now_ms);
    475         peer.last_error = Some(reason.clone());
    476         peer.misbehavior_score = peer.misbehavior_score.saturating_add(1);
    477         peer.ban_reason = Some(reason);
    478         if peer.misbehavior_score >= PEER_MISBEHAVIOR_BAN_SCORE {
    479             peer.banned_until_ms = Some(now_ms.saturating_add(PEER_MISBEHAVIOR_BAN_MS));
    480         }
    481     }
    482 
    483     pub fn is_banned(&self, address: &str) -> bool {
    484         self.is_banned_at(address, now_ms())
    485     }
    486 
    487     pub fn is_banned_at(&self, address: &str, now_ms: u64) -> bool {
    488         self.peers
    489             .get(address)
    490             .is_some_and(|peer| peer.is_banned_at(now_ms))
    491     }
    492 
    493     fn ensure(&mut self, address: &str, direction: PeerDirection) -> &mut PeerInfo {
    494         self.peers
    495             .entry(address.to_string())
    496             .or_insert_with(|| PeerInfo::new(address.to_string(), direction))
    497     }
    498 }
    499 
    500 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    501 pub struct PeerInfo {
    502     pub address: String,
    503     pub direction: PeerDirection,
    504     pub messages_sent: u64,
    505     pub messages_received: u64,
    506     pub last_known_height: Option<u64>,
    507     pub last_known_tip_hash: Option<String>,
    508     #[serde(default, skip_serializing_if = "Option::is_none")]
    509     pub last_hello: Option<ProtocolHello>,
    510     #[serde(default)]
    511     pub last_clock_offset_ms: Option<i64>,
    512     #[serde(default)]
    513     pub last_clock_offset_accepted: Option<bool>,
    514     #[serde(default)]
    515     pub last_clock_observed_ms: Option<u64>,
    516     pub last_error: Option<String>,
    517     pub last_contact_ms: Option<u64>,
    518     pub last_success_ms: Option<u64>,
    519     pub last_error_ms: Option<u64>,
    520     pub misbehavior_score: u32,
    521     pub banned_until_ms: Option<u64>,
    522     pub ban_reason: Option<String>,
    523 }
    524 
    525 impl PeerInfo {
    526     fn new(address: String, direction: PeerDirection) -> Self {
    527         Self {
    528             address,
    529             direction,
    530             messages_sent: 0,
    531             messages_received: 0,
    532             last_known_height: None,
    533             last_known_tip_hash: None,
    534             last_hello: None,
    535             last_clock_offset_ms: None,
    536             last_clock_offset_accepted: None,
    537             last_clock_observed_ms: None,
    538             last_error: None,
    539             last_contact_ms: None,
    540             last_success_ms: None,
    541             last_error_ms: None,
    542             misbehavior_score: 0,
    543             banned_until_ms: None,
    544             ban_reason: None,
    545         }
    546     }
    547 
    548     pub fn is_banned_at(&self, now_ms: u64) -> bool {
    549         self.banned_until_ms
    550             .is_some_and(|banned_until| banned_until > now_ms)
    551     }
    552 
    553     fn clear_misbehavior(&mut self) {
    554         self.misbehavior_score = 0;
    555         self.banned_until_ms = None;
    556         self.ban_reason = None;
    557     }
    558 }
    559 
    560 fn median_i64(mut values: Vec<i64>) -> Option<i64> {
    561     if values.is_empty() {
    562         return None;
    563     }
    564     values.sort_unstable();
    565     Some(values[values.len() / 2])
    566 }
    567 
    568 fn same_discovery_group(left: SocketAddr, right: SocketAddr) -> bool {
    569     match (left, right) {
    570         (SocketAddr::V4(left), SocketAddr::V4(right)) => {
    571             let left = left.ip().octets();
    572             let right = right.ip().octets();
    573             left[0] == right[0] && left[1] == right[1]
    574         }
    575         (SocketAddr::V6(left), SocketAddr::V6(right)) => {
    576             let left = left.ip().segments();
    577             let right = right.ip().segments();
    578             left[0] == right[0] && left[1] == right[1]
    579         }
    580         _ => false,
    581     }
    582 }
    583 
    584 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
    585 #[serde(rename_all = "snake_case")]
    586 pub enum PeerDirection {
    587     Outbound,
    588     Discovered,
    589     Inbound,
    590 }