"""Peer profile organizer — classifies and selects peers. Ported from net.i2p.router.peermanager.ProfileOrganizer. """ from __future__ import annotations import random import time from enum import Enum from typing import TYPE_CHECKING if TYPE_CHECKING: from i2p_peer.profile import PeerProfile class PeerTier(Enum): FAST = "fast" # High speed + high capacity HIGH_CAPACITY = "high_cap" # High capacity, any speed STANDARD = "standard" # Meets minimum thresholds FAILING = "failing" # Below minimum thresholds BANNED = "banned" # Explicitly banned # Tier ordering for comparison (lower index = better tier) _TIER_ORDER = [PeerTier.FAST, PeerTier.HIGH_CAPACITY, PeerTier.STANDARD, PeerTier.FAILING, PeerTier.BANNED] class ProfileOrganizer: """Classifies peers into tiers and selects peers for tunnel building. Tier thresholds (configurable): - FAST: capacity >= 0.8 AND speed >= 0.7 - HIGH_CAPACITY: capacity >= 0.6 - STANDARD: capacity >= 0.3 - FAILING: capacity < 0.3 - BANNED: explicitly banned """ # Configurable thresholds FAST_CAPACITY_THRESHOLD = 0.8 FAST_SPEED_THRESHOLD = 0.7 HIGH_CAP_THRESHOLD = 0.6 STANDARD_THRESHOLD = 0.3 MIN_ESTABLISHED_FOR_FAST = 10 # Min tunnel builds before eligible for fast tier def __init__(self): self._profiles: dict[bytes, PeerProfile] = {} self._tiers: dict[PeerTier, set[bytes]] = {t: set() for t in PeerTier} def add_profile(self, profile: PeerProfile) -> None: """Add or update a peer profile and reclassify.""" h = profile.router_hash # Remove from old tier if present self._remove_from_tiers(h) self._profiles[h] = profile tier = self.classify(profile) self._tiers[tier].add(h) def remove_profile(self, router_hash: bytes) -> None: self._remove_from_tiers(router_hash) self._profiles.pop(router_hash, None) def get_profile(self, router_hash: bytes) -> PeerProfile | None: return self._profiles.get(router_hash) def classify(self, profile: PeerProfile) -> PeerTier: """Determine which tier a peer belongs to.""" if profile.is_currently_banned: return PeerTier.BANNED total_builds = profile.tunnel_builds_succeeded + profile.tunnel_builds_failed if (profile.capacity >= self.FAST_CAPACITY_THRESHOLD and profile.speed >= self.FAST_SPEED_THRESHOLD and total_builds >= self.MIN_ESTABLISHED_FOR_FAST): return PeerTier.FAST if profile.capacity >= self.HIGH_CAP_THRESHOLD: return PeerTier.HIGH_CAPACITY if profile.capacity >= self.STANDARD_THRESHOLD: return PeerTier.STANDARD return PeerTier.FAILING def reclassify_all(self) -> None: """Reclassify all peers (call periodically).""" self._tiers = {t: set() for t in PeerTier} for h, profile in self._profiles.items(): tier = self.classify(profile) self._tiers[tier].add(h) def select_fast_peers(self, count: int, exclude: set[bytes] | None = None) -> list[bytes]: """Select peers from FAST tier for tunnel building.""" return self._select_from_tier(PeerTier.FAST, count, exclude) def select_high_capacity_peers(self, count: int, exclude: set[bytes] | None = None) -> list[bytes]: """Select from HIGH_CAPACITY tier.""" return self._select_from_tier(PeerTier.HIGH_CAPACITY, count, exclude) def select_peers(self, count: int, min_tier: PeerTier = PeerTier.STANDARD, exclude: set[bytes] | None = None) -> list[bytes]: """Select peers at or above minimum tier.""" exclude = exclude or set() min_idx = _TIER_ORDER.index(min_tier) eligible_tiers = _TIER_ORDER[:min_idx + 1] candidates = [] for tier in eligible_tiers: for h in self._tiers[tier]: if h not in exclude: candidates.append(h) random.shuffle(candidates) return candidates[:count] def get_tier(self, router_hash: bytes) -> PeerTier: for tier, hashes in self._tiers.items(): if router_hash in hashes: return tier return PeerTier.FAILING @property def fast_count(self) -> int: return len(self._tiers[PeerTier.FAST]) @property def high_capacity_count(self) -> int: return len(self._tiers[PeerTier.HIGH_CAPACITY]) @property def standard_count(self) -> int: return len(self._tiers[PeerTier.STANDARD]) @property def failing_count(self) -> int: return len(self._tiers[PeerTier.FAILING]) @property def total_count(self) -> int: return len(self._profiles) def _remove_from_tiers(self, router_hash: bytes) -> None: for tier_set in self._tiers.values(): tier_set.discard(router_hash) def _select_from_tier(self, tier: PeerTier, count: int, exclude: set[bytes] | None = None) -> list[bytes]: exclude = exclude or set() candidates = [h for h in self._tiers[tier] if h not in exclude] random.shuffle(candidates) return candidates[:count]