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 }