use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::RwLock;
use std::time::{Duration, Instant};
const LAN_PEER_SCORE_MULTIPLIER: f64 = 3.0;
pub fn is_lan_peer(addr: &SocketAddr) -> bool {
match addr.ip() {
IpAddr::V4(ip) => {
ip.is_private() || ip.is_loopback() || ip.is_link_local() }
IpAddr::V6(ip) => {
ip.is_loopback() || {
let segments = ip.segments();
(segments[0] & 0xff00) == 0xfd00 || (segments[0] & 0xffc0) == 0xfe80 }
}
}
}
#[derive(Debug, Clone)]
pub struct PeerStats {
pub bytes_received: u64,
pub blocks_received: u64,
pub connection_duration_secs: f64,
pub avg_block_latency_ms: f64,
pub failures: u64,
pub last_block_time: Option<Instant>,
pub bandwidth_bytes_per_sec: f64,
pub score: f64,
pub is_lan: bool,
}
impl Default for PeerStats {
fn default() -> Self {
Self {
bytes_received: 0,
blocks_received: 0,
connection_duration_secs: 0.0,
avg_block_latency_ms: 1000.0, failures: 0,
last_block_time: None,
bandwidth_bytes_per_sec: 0.0,
score: 1.0, is_lan: false,
}
}
}
impl PeerStats {
pub fn update_bandwidth(&mut self) {
if self.connection_duration_secs > 0.0 {
self.bandwidth_bytes_per_sec =
self.bytes_received as f64 / self.connection_duration_secs;
}
}
pub fn calculate_score(&mut self) {
if self.blocks_received == 0 {
self.score = if self.is_lan {
1.0 * LAN_PEER_SCORE_MULTIPLIER
} else {
if self.avg_block_latency_ms > 0.0 {
(1000.0 / self.avg_block_latency_ms.max(1.0)).min(2.0)
} else {
1.0
}
};
return;
}
let bandwidth_score = self.bandwidth_bytes_per_sec / 100_000.0;
let failure_penalty = self.failures as f64 * 0.1;
let activity_bonus = match self.last_block_time {
Some(t) if t.elapsed() < Duration::from_secs(30) => 0.2,
Some(t) if t.elapsed() < Duration::from_secs(60) => 0.1,
_ => 0.0,
};
let latency_penalty = (self.avg_block_latency_ms / 1000.0) * 0.1;
let base_score =
(bandwidth_score - failure_penalty + activity_bonus - latency_penalty).max(0.1);
self.score = if self.is_lan {
base_score * LAN_PEER_SCORE_MULTIPLIER
} else {
base_score
};
}
}
pub struct PeerScorer {
stats: RwLock<HashMap<SocketAddr, PeerStats>>,
start_time: Instant,
}
impl PeerScorer {
pub fn new() -> Self {
Self {
stats: RwLock::new(HashMap::new()),
start_time: Instant::now(),
}
}
pub fn record_bytes(&self, peer: SocketAddr, bytes: u64) {
let mut stats = self.stats.write().unwrap();
let entry = stats.entry(peer).or_insert_with(|| {
let mut s = PeerStats::default();
s.is_lan = is_lan_peer(&peer);
s
});
entry.bytes_received += bytes;
entry.connection_duration_secs = self.start_time.elapsed().as_secs_f64();
entry.update_bandwidth();
entry.calculate_score();
}
pub fn record_block(&self, peer: SocketAddr, block_size: u64, latency_ms: f64) {
let mut stats = self.stats.write().unwrap();
let entry = stats.entry(peer).or_insert_with(|| {
let mut s = PeerStats::default();
s.is_lan = is_lan_peer(&peer);
s
});
entry.blocks_received += 1;
entry.bytes_received += block_size;
entry.connection_duration_secs = self.start_time.elapsed().as_secs_f64();
entry.last_block_time = Some(Instant::now());
let alpha = 0.3; entry.avg_block_latency_ms =
entry.avg_block_latency_ms * (1.0 - alpha) + latency_ms * alpha;
entry.update_bandwidth();
entry.calculate_score();
}
pub fn record_latency_sample(&self, peer: SocketAddr, latency_ms: f64) {
let mut stats = self.stats.write().unwrap();
let entry = stats.entry(peer).or_insert_with(|| {
let mut s = PeerStats::default();
s.is_lan = is_lan_peer(&peer);
s
});
let alpha = 0.3;
entry.avg_block_latency_ms =
entry.avg_block_latency_ms * (1.0 - alpha) + latency_ms * alpha;
entry.calculate_score();
}
pub fn record_failure(&self, peer: SocketAddr) {
let mut stats = self.stats.write().unwrap();
let entry = stats.entry(peer).or_insert_with(|| {
let mut s = PeerStats::default();
s.is_lan = is_lan_peer(&peer);
s
});
entry.failures += 1;
entry.calculate_score();
}
pub fn is_peer_lan(&self, peer: &SocketAddr) -> bool {
self.stats
.read()
.unwrap()
.get(peer)
.map(|s| s.is_lan)
.unwrap_or_else(|| is_lan_peer(peer))
}
pub fn lan_peer_count(&self) -> usize {
self.stats
.read()
.unwrap()
.values()
.filter(|s| s.is_lan)
.count()
}
pub fn get_lan_peers(&self) -> Vec<SocketAddr> {
self.stats
.read()
.unwrap()
.iter()
.filter(|(_, s)| s.is_lan)
.map(|(addr, _)| *addr)
.collect()
}
pub fn get_score(&self, peer: &SocketAddr) -> f64 {
self.stats
.read()
.unwrap()
.get(peer)
.map(|s| s.score)
.unwrap_or_else(|| {
if is_lan_peer(peer) {
1.0 * LAN_PEER_SCORE_MULTIPLIER } else {
1.0 }
})
}
pub fn get_stats(&self, peer: &SocketAddr) -> Option<PeerStats> {
self.stats.read().unwrap().get(peer).cloned()
}
pub fn get_sorted_peers(&self) -> Vec<(SocketAddr, f64)> {
let stats = self.stats.read().unwrap();
let mut peers: Vec<_> = stats.iter().map(|(addr, s)| (*addr, s.score)).collect();
peers.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
peers
}
pub fn select_best_peers(&self, available: &[SocketAddr], count: usize) -> Vec<SocketAddr> {
if available.is_empty() {
return vec![];
}
if available.len() <= count {
return available.to_vec();
}
let stats = self.stats.read().unwrap();
let mut scored: Vec<_> = available
.iter()
.map(|addr| {
let score = stats.get(addr).map(|s| s.score).unwrap_or(1.0);
(*addr, score)
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored
.into_iter()
.take(count)
.map(|(addr, _)| addr)
.collect()
}
pub fn get_best_peer(&self, available: &[SocketAddr]) -> Option<SocketAddr> {
self.select_best_peers(available, 1).into_iter().next()
}
pub fn summary(&self) -> String {
let stats = self.stats.read().unwrap();
if stats.is_empty() {
return "No peer stats yet".to_string();
}
let mut total_bytes = 0u64;
let mut total_blocks = 0u64;
let mut lan_blocks = 0u64;
let mut lan_count = 0usize;
let mut best_peer: Option<(SocketAddr, f64, bool)> = None;
for (addr, s) in stats.iter() {
total_bytes += s.bytes_received;
total_blocks += s.blocks_received;
if s.is_lan {
lan_count += 1;
lan_blocks += s.blocks_received;
}
if best_peer.is_none() || s.score > best_peer.as_ref().unwrap().1 {
best_peer = Some((*addr, s.score, s.is_lan));
}
}
let lan_pct = if total_blocks > 0 {
(lan_blocks * 100) / total_blocks
} else {
0
};
format!(
"Peers: {} ({} LAN), Total: {} blocks / {} MB, LAN blocks: {}%, Best: {:?} (score: {:.2}{})",
stats.len(),
lan_count,
total_blocks,
total_bytes / 1_000_000,
lan_pct,
best_peer.as_ref().map(|(addr, _, _)| addr),
best_peer.as_ref().map(|(_, s, _)| *s).unwrap_or(0.0),
if best_peer.as_ref().map(|(_, _, is_lan)| *is_lan).unwrap_or(false) { " LAN" } else { "" }
)
}
}
impl Default for PeerScorer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peer_scoring_basic() {
let scorer = PeerScorer::new();
let peer1: SocketAddr = "127.0.0.1:8333".parse().unwrap();
let peer2: SocketAddr = "127.0.0.2:8333".parse().unwrap();
scorer.record_block(peer1, 500_000, 100.0); scorer.record_block(peer1, 500_000, 100.0);
scorer.record_block(peer1, 500_000, 100.0);
scorer.record_block(peer2, 500_000, 500.0); scorer.record_failure(peer2);
let score1 = scorer.get_score(&peer1);
let score2 = scorer.get_score(&peer2);
assert!(
score1 > score2,
"Peer1 score {score1} should be > Peer2 score {score2}"
);
}
#[test]
fn test_peer_selection() {
let scorer = PeerScorer::new();
let peer1: SocketAddr = "127.0.0.1:8333".parse().unwrap();
let peer2: SocketAddr = "127.0.0.2:8333".parse().unwrap();
let peer3: SocketAddr = "127.0.0.3:8333".parse().unwrap();
scorer.record_block(peer1, 1_000_000, 50.0);
scorer.record_block(peer1, 1_000_000, 50.0);
scorer.record_block(peer2, 500_000, 200.0);
scorer.record_failure(peer3);
scorer.record_failure(peer3);
let available = vec![peer1, peer2, peer3];
let selected = scorer.select_best_peers(&available, 2);
assert_eq!(selected.len(), 2);
assert!(selected.contains(&peer1));
assert!(selected.contains(&peer2));
assert!(!selected.contains(&peer3));
}
#[test]
fn test_is_lan_peer_ipv4_private_10_range() {
assert!(is_lan_peer(&"10.0.0.1:8333".parse().unwrap()));
assert!(is_lan_peer(&"10.255.255.254:8333".parse().unwrap()));
assert!(is_lan_peer(&"10.123.45.67:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv4_private_172_range() {
assert!(is_lan_peer(&"172.16.0.1:8333".parse().unwrap()));
assert!(is_lan_peer(&"172.31.255.254:8333".parse().unwrap()));
assert!(is_lan_peer(&"172.20.5.10:8333".parse().unwrap()));
assert!(!is_lan_peer(&"172.32.0.1:8333".parse().unwrap()));
assert!(!is_lan_peer(&"172.15.0.1:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv4_private_192_168_range() {
assert!(is_lan_peer(&"192.168.0.1:8333".parse().unwrap()));
assert!(is_lan_peer(&"192.168.1.1:8333".parse().unwrap()));
assert!(is_lan_peer(&"192.168.2.100:8333".parse().unwrap())); assert!(is_lan_peer(&"192.168.255.254:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv4_loopback() {
assert!(is_lan_peer(&"127.0.0.1:8333".parse().unwrap()));
assert!(is_lan_peer(&"127.255.255.254:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv4_link_local() {
assert!(is_lan_peer(&"169.254.0.1:8333".parse().unwrap()));
assert!(is_lan_peer(&"169.254.255.254:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv4_public() {
assert!(!is_lan_peer(&"8.8.8.8:8333".parse().unwrap())); assert!(!is_lan_peer(&"1.1.1.1:8333".parse().unwrap())); assert!(!is_lan_peer(&"45.33.20.159:8333".parse().unwrap())); assert!(!is_lan_peer(&"216.107.135.194:8333".parse().unwrap())); }
#[test]
fn test_is_lan_peer_ipv6_loopback() {
assert!(is_lan_peer(&"[::1]:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv6_unique_local() {
assert!(is_lan_peer(&"[fd00::1]:8333".parse().unwrap()));
assert!(is_lan_peer(&"[fd12:3456:789a::1]:8333".parse().unwrap()));
assert!(is_lan_peer(
&"[fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:8333"
.parse()
.unwrap()
));
}
#[test]
fn test_is_lan_peer_ipv6_link_local() {
assert!(is_lan_peer(&"[fe80::1]:8333".parse().unwrap()));
assert!(is_lan_peer(&"[fe80::abcd:1234]:8333".parse().unwrap()));
}
#[test]
fn test_is_lan_peer_ipv6_public() {
assert!(!is_lan_peer(
&"[2001:4860:4860::8888]:8333".parse().unwrap()
)); assert!(!is_lan_peer(
&"[2606:4700:4700::1111]:8333".parse().unwrap()
)); }
#[test]
fn test_lan_peer_gets_10x_score_multiplier() {
let scorer = PeerScorer::new();
let lan_peer: SocketAddr = "192.168.2.100:8333".parse().unwrap();
let internet_peer: SocketAddr = "45.33.20.159:8333".parse().unwrap();
scorer.record_block(lan_peer, 1_000_000, 100.0);
scorer.record_block(internet_peer, 1_000_000, 100.0);
let lan_score = scorer.get_score(&lan_peer);
let internet_score = scorer.get_score(&internet_peer);
assert!(
lan_score > internet_score * 2.0,
"LAN peer score {lan_score} should be significantly higher than internet peer {internet_score} (3x multiplier expected)"
);
assert!(
scorer.is_peer_lan(&lan_peer),
"192.168.2.100 should be detected as LAN"
);
assert!(
!scorer.is_peer_lan(&internet_peer),
"45.33.20.159 should NOT be detected as LAN"
);
}
#[test]
fn test_lan_peer_dominates_best_peer_selection() {
let scorer = PeerScorer::new();
let lan_peer: SocketAddr = "192.168.1.50:8333".parse().unwrap();
scorer.record_block(lan_peer, 1_000_000, 80.0);
let internet_peer: SocketAddr = "8.8.8.8:8333".parse().unwrap();
scorer.record_block(internet_peer, 1_200_000, 70.0);
let available = vec![lan_peer, internet_peer];
let best = scorer.get_best_peer(&available);
assert_eq!(
best,
Some(lan_peer),
"LAN peer should be selected as best despite worse raw performance"
);
}
#[test]
fn test_lan_peer_count() {
let scorer = PeerScorer::new();
scorer.record_block("192.168.1.1:8333".parse().unwrap(), 100, 10.0);
scorer.record_block("192.168.2.100:8333".parse().unwrap(), 100, 10.0);
scorer.record_block("10.0.0.5:8333".parse().unwrap(), 100, 10.0);
scorer.record_block("8.8.8.8:8333".parse().unwrap(), 100, 10.0);
scorer.record_block("1.1.1.1:8333".parse().unwrap(), 100, 10.0);
assert_eq!(scorer.lan_peer_count(), 3, "Should have 3 LAN peers");
let lan_peers = scorer.get_lan_peers();
assert_eq!(lan_peers.len(), 3, "get_lan_peers should return 3 peers");
}
#[test]
fn test_failures_reduce_score() {
let scorer = PeerScorer::new();
let peer: SocketAddr = "8.8.8.8:8333".parse().unwrap();
scorer.record_block(peer, 1_000_000, 100.0);
let score_before = scorer.get_score(&peer);
scorer.record_failure(peer);
scorer.record_failure(peer);
scorer.record_failure(peer);
let score_after = scorer.get_score(&peer);
assert!(
score_after < score_before,
"Score should decrease after failures: {score_before} -> {score_after}"
);
}
#[test]
fn test_minimum_score_preserved() {
let scorer = PeerScorer::new();
let peer: SocketAddr = "8.8.8.8:8333".parse().unwrap();
for _ in 0..100 {
scorer.record_failure(peer);
}
let score = scorer.get_score(&peer);
assert!(
score >= 0.1,
"Score should not go below minimum 0.1: got {score}"
);
}
}