"""Peer profiles, selection, and banning. Ported from net.i2p.router.peermanager.PeerProfile. """ import time class PeerProfile: """Full peer profile with reputation metrics. Ported from net.i2p.router.peermanager.PeerProfile. """ def __init__(self, router_hash: bytes): self.router_hash = router_hash # Tunnel building self.tunnel_builds_succeeded = 0 self.tunnel_builds_failed = 0 self.tunnel_builds_rejected = 0 # Communication self.send_success_count = 0 self.send_failure_count = 0 self.db_store_success_count = 0 self.db_store_failure_count = 0 self.db_lookup_success_count = 0 self.db_lookup_failure_count = 0 # Timing self.last_heard_from: float = 0.0 self.first_heard_about: float = 0.0 self.last_send_success: float = 0.0 self.last_send_failure: float = 0.0 # Computed scores (updated by calculators) self.capacity: float = 0.0 self.speed: float = 0.0 self.integration: float = 0.0 # Classification self.is_expanding: bool = False self.is_active: bool = True self.is_banned: bool = False self.ban_expiry: float = 0.0 # Latency tracking self._latency_samples: list[float] = [] self._max_latency_samples = 50 # Legacy compat self._successes = 0 self._failures = 0 # --- Tunnel build recording --- def record_tunnel_build_success(self) -> None: self.tunnel_builds_succeeded += 1 def record_tunnel_build_failure(self) -> None: self.tunnel_builds_failed += 1 def record_tunnel_build_rejection(self) -> None: self.tunnel_builds_rejected += 1 # --- Communication recording --- def record_send_success(self) -> None: self.send_success_count += 1 self.last_send_success = time.time() def record_send_failure(self) -> None: self.send_failure_count += 1 self.last_send_failure = time.time() def record_db_store_success(self) -> None: self.db_store_success_count += 1 def record_db_store_failure(self) -> None: self.db_store_failure_count += 1 def record_db_lookup_success(self) -> None: self.db_lookup_success_count += 1 def record_db_lookup_failure(self) -> None: self.db_lookup_failure_count += 1 # --- Latency --- def record_latency(self, latency_ms: float) -> None: self._latency_samples.append(latency_ms) if len(self._latency_samples) > self._max_latency_samples: self._latency_samples = self._latency_samples[-self._max_latency_samples:] # --- Activity --- def heard_from(self) -> None: now = time.time() self.last_heard_from = now if self.first_heard_about == 0.0: self.first_heard_about = now # --- Computed properties --- @property def tunnel_success_rate(self) -> float: total = self.tunnel_builds_succeeded + self.tunnel_builds_failed if total == 0: return 0.0 return self.tunnel_builds_succeeded / total @property def send_success_rate(self) -> float: total = self.send_success_count + self.send_failure_count if total == 0: return 0.0 return self.send_success_count / total @property def db_success_rate(self) -> float: total = (self.db_store_success_count + self.db_store_failure_count + self.db_lookup_success_count + self.db_lookup_failure_count) if total == 0: return 0.0 successes = self.db_store_success_count + self.db_lookup_success_count return successes / total @property def average_latency(self) -> float: if not self._latency_samples: return 0.0 return sum(self._latency_samples) / len(self._latency_samples) @property def is_established(self) -> bool: """Has enough history to be classified.""" total_tunnel = self.tunnel_builds_succeeded + self.tunnel_builds_failed total_send = self.send_success_count + self.send_failure_count return total_tunnel >= 5 and total_send >= 5 # --- Ban management --- def ban(self, duration_seconds: float) -> None: self.is_banned = True self.ban_expiry = time.time() + duration_seconds def unban(self) -> None: self.is_banned = False self.ban_expiry = 0.0 @property def is_currently_banned(self) -> bool: if not self.is_banned: return False if time.time() >= self.ban_expiry: self.is_banned = False self.ban_expiry = 0.0 return False return True # --- Serialization --- def to_dict(self) -> dict: return { "router_hash": self.router_hash.hex(), "tunnel_builds_succeeded": self.tunnel_builds_succeeded, "tunnel_builds_failed": self.tunnel_builds_failed, "tunnel_builds_rejected": self.tunnel_builds_rejected, "send_success_count": self.send_success_count, "send_failure_count": self.send_failure_count, "db_store_success_count": self.db_store_success_count, "db_store_failure_count": self.db_store_failure_count, "db_lookup_success_count": self.db_lookup_success_count, "db_lookup_failure_count": self.db_lookup_failure_count, "last_heard_from": self.last_heard_from, "first_heard_about": self.first_heard_about, "last_send_success": self.last_send_success, "last_send_failure": self.last_send_failure, "capacity": self.capacity, "speed": self.speed, "integration": self.integration, "is_expanding": self.is_expanding, "is_active": self.is_active, "is_banned": self.is_banned, "ban_expiry": self.ban_expiry, "latency_samples": list(self._latency_samples), } @classmethod def from_dict(cls, data: dict) -> "PeerProfile": p = cls(bytes.fromhex(data["router_hash"])) p.tunnel_builds_succeeded = data["tunnel_builds_succeeded"] p.tunnel_builds_failed = data["tunnel_builds_failed"] p.tunnel_builds_rejected = data["tunnel_builds_rejected"] p.send_success_count = data["send_success_count"] p.send_failure_count = data["send_failure_count"] p.db_store_success_count = data["db_store_success_count"] p.db_store_failure_count = data["db_store_failure_count"] p.db_lookup_success_count = data["db_lookup_success_count"] p.db_lookup_failure_count = data["db_lookup_failure_count"] p.last_heard_from = data["last_heard_from"] p.first_heard_about = data["first_heard_about"] p.last_send_success = data["last_send_success"] p.last_send_failure = data["last_send_failure"] p.capacity = data["capacity"] p.speed = data["speed"] p.integration = data["integration"] p.is_expanding = data["is_expanding"] p.is_active = data["is_active"] p.is_banned = data["is_banned"] p.ban_expiry = data["ban_expiry"] p._latency_samples = list(data.get("latency_samples", [])) return p # --- Legacy compatibility --- def record_success(self): self._successes += 1 self.capacity = self._successes / (self._successes + self._failures + 1) def record_failure(self): self._failures += 1 self.capacity = self._successes / (self._successes + self._failures + 1) def tunnel_build_success(self): self.tunnel_builds_succeeded += 1 def tunnel_build_failure(self): self.tunnel_builds_failed += 1 def update_last_heard(self, timestamp_ms: int): self.last_heard_from = timestamp_ms def decay(self, factor: float = 0.95): self.capacity *= factor self._successes = int(self._successes * factor) self._failures = int(self._failures * factor) def score(self) -> float: total = self.tunnel_builds_succeeded + self.tunnel_builds_failed tsr = self.tunnel_builds_succeeded / total if total > 0 else 0.0 return self.capacity + tsr * 0.5 class PeerSelector: """Select peers by profile score.""" def __init__(self, profiles: list[PeerProfile]): self._profiles = profiles def select_best(self, n: int, exclude: set[bytes] | None = None) -> list[PeerProfile]: exclude = exclude or set() candidates = [p for p in self._profiles if p.router_hash not in exclude] candidates.sort(key=lambda p: p.score(), reverse=True) return candidates[:n] class BanManager: """Temporary ban management for misbehaving peers.""" def __init__(self): self._bans: dict[bytes, dict] = {} def ban(self, router_hash: bytes, reason: str, duration_ms: int, now_ms: int | None = None): if now_ms is None: now_ms = int(time.time() * 1000) self._bans[router_hash] = { "reason": reason, "expires": now_ms + duration_ms, } def unban(self, router_hash: bytes): self._bans.pop(router_hash, None) def is_banned(self, router_hash: bytes, now_ms: int | None = None) -> bool: info = self._bans.get(router_hash) if info is None: return False if now_ms is None: now_ms = int(time.time() * 1000) if now_ms >= info["expires"]: del self._bans[router_hash] return False return True def ban_count(self) -> int: return len(self._bans) def cleanup(self, now_ms: int | None = None): if now_ms is None: now_ms = int(time.time() * 1000) expired = [h for h, info in self._bans.items() if now_ms >= info["expires"]] for h in expired: del self._bans[h]