Skip to main content

ethrex_p2p/
peer_table.rs

1//! Unified peer table for both discv4 and discv5 discovery protocols.
2//!
3//! This module provides a protocol-agnostic peer table that stores contact
4//! information discovered through either discv4 or discv5. The key abstraction
5//! is using `Bytes` for ping identifiers:
6//! - discv4: converts H256 ping hash to Bytes
7//! - discv5: already uses Bytes for req_id
8//!
9//! Each contact is tagged with the protocol that discovered it, allowing
10//! protocol-specific lookups to only query compatible contacts.
11
12use crate::{
13    backend,
14    metrics::METRICS,
15    rlpx::{connection::server::PeerConnection, p2p::Capability},
16    types::{Node, NodeRecord},
17    utils::distance,
18};
19use bytes::Bytes;
20use ethrex_common::{H256, U256};
21use ethrex_storage::Store;
22use indexmap::IndexMap;
23use rand::distributions::WeightedIndex;
24use rand::prelude::Distribution;
25use rand::seq::{IteratorRandom, SliceRandom};
26use rustc_hash::{FxHashMap, FxHashSet};
27use spawned_concurrency::{
28    actor,
29    error::ActorError,
30    protocol,
31    tasks::{Actor, ActorRef, ActorStart as _, Context, Handler, Response, send_message_on},
32};
33use std::{
34    net::IpAddr,
35    time::{Duration, Instant},
36};
37
38const MAX_SCORE: i64 = 50;
39const MIN_SCORE: i64 = -50;
40/// Score assigned to peers who are acting maliciously (e.g., returning a node with wrong hash)
41const MIN_SCORE_CRITICAL: i64 = MIN_SCORE * 3;
42/// Score weight for the load balancing function.
43const SCORE_WEIGHT: i64 = 1;
44/// Weight for amount of requests being handled by the peer for the load balancing function.
45const REQUESTS_WEIGHT: i64 = 1;
46/// Max amount of ongoing requests per peer.
47const MAX_CONCURRENT_REQUESTS_PER_PEER: i64 = 100;
48/// The target number of RLPx connections to reach.
49pub const TARGET_PEERS: usize = 100;
50/// Maximum number of ENRs to return in a FindNode response (discv4 compatible).
51pub(crate) const MAX_NODES_IN_NEIGHBORS_PACKET: usize = 16;
52/// Maximum number of ENRs to return in a discv5 FindNode response.
53const MAX_ENRS_PER_FINDNODE_RESPONSE: usize = 16;
54
55/// Number of k-buckets in the Kademlia routing table (one per bit of the 256-bit node ID).
56const NUMBER_OF_BUCKETS: usize = 256;
57/// Maximum number of contacts per k-bucket (Kademlia k parameter).
58pub const MAX_NODES_PER_BUCKET: usize = 16;
59/// Maximum number of replacement entries per k-bucket.
60const MAX_REPLACEMENTS_PER_BUCKET: usize = 10;
61/// Maximum number of entries in the flat connection candidate pool.
62/// This pool is separate from the k-bucket routing table and retains
63/// more contacts for RLPx connection initiation than the k-bucket
64/// structure allows (256 × 16 = 4,096 vs this larger capacity).
65/// 10K matches what Reth and Nethermind use for their candidate pools.
66const MAX_CONNECTION_POOL_SIZE: usize = 10_000;
67
68/// A single k-bucket in the Kademlia routing table.
69/// Each bucket stores contacts at a specific XOR distance range from the local node.
70#[derive(Debug, Clone, Default)]
71pub struct KBucket {
72    pub(crate) contacts: Vec<(H256, Contact)>,
73    pub(crate) replacements: Vec<(H256, Contact)>,
74}
75
76impl KBucket {
77    /// Find a contact by node ID in the main list.
78    fn get(&self, node_id: &H256) -> Option<&Contact> {
79        self.contacts
80            .iter()
81            .find(|(id, _)| id == node_id)
82            .map(|(_, c)| c)
83    }
84
85    /// Find a contact by node ID in either the main or replacement list.
86    fn get_any(&self, node_id: &H256) -> Option<&Contact> {
87        self.get(node_id).or_else(|| {
88            self.replacements
89                .iter()
90                .find(|(id, _)| id == node_id)
91                .map(|(_, c)| c)
92        })
93    }
94
95    /// Find a mutable reference to a contact by node ID (main or replacement list).
96    fn get_mut(&mut self, node_id: &H256) -> Option<&mut Contact> {
97        if let Some((_, c)) = self.contacts.iter_mut().find(|(id, _)| id == node_id) {
98            return Some(c);
99        }
100        self.replacements
101            .iter_mut()
102            .find(|(id, _)| id == node_id)
103            .map(|(_, c)| c)
104    }
105
106    /// Check if a contact exists in this bucket (main or replacement list).
107    fn contains(&self, node_id: &H256) -> bool {
108        self.contacts.iter().any(|(id, _)| id == node_id)
109            || self.replacements.iter().any(|(id, _)| id == node_id)
110    }
111
112    /// Insert a contact into the bucket. Returns true if inserted into main list.
113    /// If the bucket is full, the contact is added to the replacement list instead.
114    fn insert(&mut self, node_id: H256, contact: Contact) -> bool {
115        if self.contacts.len() < MAX_NODES_PER_BUCKET {
116            self.contacts.push((node_id, contact));
117            true
118        } else {
119            self.insert_replacement(node_id, contact);
120            false
121        }
122    }
123
124    /// Add a contact to the replacement list, evicting the oldest if full.
125    fn insert_replacement(&mut self, node_id: H256, contact: Contact) {
126        if self.replacements.len() >= MAX_REPLACEMENTS_PER_BUCKET {
127            self.replacements.remove(0);
128        }
129        self.replacements.push((node_id, contact));
130    }
131
132    /// Remove a contact from the main list and promote a replacement if available.
133    /// Returns the promoted replacement's node ID, if any.
134    fn remove_and_promote(&mut self, node_id: &H256) -> Option<H256> {
135        let idx = self.contacts.iter().position(|(id, _)| id == node_id)?;
136        self.contacts.remove(idx);
137        if !self.replacements.is_empty() {
138            let (replacement_id, replacement) = self.replacements.remove(0);
139            self.contacts.push((replacement_id, replacement));
140            Some(replacement_id)
141        } else {
142            None
143        }
144    }
145}
146
147/// Computes the bucket index for a node relative to the local node.
148/// Uses XOR distance: bucket = floor(log2(XOR(local, remote))), i.e. the
149/// position of the highest set bit minus 1.
150/// Returns None for the local node itself (XOR = 0).
151fn bucket_index(local_node_id: &H256, node_id: &H256) -> Option<usize> {
152    let xor = *local_node_id ^ *node_id;
153    let dist = U256::from_big_endian(xor.as_bytes());
154    if dist.is_zero() {
155        None
156    } else {
157        Some(dist.bits() - 1)
158    }
159}
160
161/// Computes the raw XOR distance between two node IDs.
162/// Used for comparing relative closeness: a is closer to target than b
163/// iff xor_distance(target, a) < xor_distance(target, b).
164pub(crate) fn xor_distance(a: &H256, b: &H256) -> H256 {
165    *a ^ *b
166}
167
168/// Identifies which discovery protocol was used to find a contact.
169/// This allows protocol-specific lookups to only query compatible contacts.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub enum DiscoveryProtocol {
172    /// Contact discovered via discv4 protocol
173    Discv4,
174    /// Contact discovered via discv5 protocol
175    Discv5,
176}
177
178/// Session information for discv5 protocol.
179/// Contains symmetric keys derived from ECDH for message encryption/decryption.
180pub use crate::discv5::session::Session;
181
182#[derive(Debug, Clone)]
183pub struct Contact {
184    pub node: Node,
185    /// Whether this contact is reachable via discv4 protocol.
186    pub is_discv4: bool,
187    /// Whether this contact is reachable via discv5 protocol.
188    pub is_discv5: bool,
189    /// The timestamp when the contact was last sent a ping.
190    /// If None, the contact has never been pinged.
191    pub validation_timestamp: Option<Instant>,
192    /// The identifier of the last unacknowledged ping sent to this contact, or
193    /// None if no ping was sent yet or it was already acknowledged.
194    /// - discv4: H256 hash converted to Bytes
195    /// - discv5: request ID as Bytes
196    pub ping_id: Option<Bytes>,
197
198    /// The hash of the last unacknowledged ENRRequest sent to this contact, or
199    /// None if no request was sent yet or it was already acknowledged.
200    pub enr_request_hash: Option<H256>,
201
202    /// ENR associated with this contact, if it was provided by the peer.
203    pub record: Option<NodeRecord>,
204    /// This contact failed to respond our Ping.
205    pub disposable: bool,
206    /// Set to true after we send a successful ENRResponse to it.
207    pub knows_us: bool,
208    /// This is a known-bad peer (on another network, no matching capabilities, etc)
209    pub unwanted: bool,
210    /// Whether the last known fork ID is valid, None if unknown.
211    pub is_fork_id_valid: Option<bool>,
212    /// Session information for discv5 (None for discv4 contacts)
213    session: Option<Session>,
214}
215
216impl Contact {
217    pub fn was_validated(&self) -> bool {
218        self.validation_timestamp.is_some() && !self.has_pending_ping()
219    }
220
221    pub fn has_pending_ping(&self) -> bool {
222        self.ping_id.is_some()
223    }
224
225    pub fn record_ping_sent(&mut self, ping_id: Bytes) {
226        self.validation_timestamp = Some(Instant::now());
227        self.ping_id = Some(ping_id);
228    }
229
230    pub fn record_enr_request_sent(&mut self, request_hash: H256) {
231        self.enr_request_hash = Some(request_hash);
232    }
233
234    // If hash does not match, ignore. Otherwise, reset enr_request_hash
235    pub fn record_enr_response_received(&mut self, request_hash: H256, record: NodeRecord) {
236        if self
237            .enr_request_hash
238            .take_if(|h| *h == request_hash)
239            .is_some()
240        {
241            self.record = Some(record);
242        }
243    }
244
245    pub fn has_pending_enr_request(&self) -> bool {
246        self.enr_request_hash.is_some()
247    }
248}
249
250impl Contact {
251    pub fn new(node: Node, protocol: DiscoveryProtocol) -> Self {
252        Self {
253            node,
254            is_discv4: protocol == DiscoveryProtocol::Discv4,
255            is_discv5: protocol == DiscoveryProtocol::Discv5,
256            validation_timestamp: None,
257            ping_id: None,
258            enr_request_hash: None,
259            record: None,
260            disposable: false,
261            knows_us: true,
262            unwanted: false,
263            is_fork_id_valid: None,
264            session: None,
265        }
266    }
267
268    /// Check if this contact supports the given protocol.
269    pub fn supports_protocol(&self, protocol: DiscoveryProtocol) -> bool {
270        match protocol {
271            DiscoveryProtocol::Discv4 => self.is_discv4,
272            DiscoveryProtocol::Discv5 => self.is_discv5,
273        }
274    }
275
276    /// Mark this contact as supporting the given protocol.
277    pub fn add_protocol(&mut self, protocol: DiscoveryProtocol) {
278        match protocol {
279            DiscoveryProtocol::Discv4 => self.is_discv4 = true,
280            DiscoveryProtocol::Discv5 => self.is_discv5 = true,
281        }
282    }
283}
284
285#[derive(Debug, Clone)]
286pub struct PeerData {
287    pub node: Node,
288    pub record: Option<NodeRecord>,
289    pub supported_capabilities: Vec<Capability>,
290    /// Set to true if the connection is inbound (aka the connection was started by the peer and not by this node)
291    /// It is only valid as long as is_connected is true
292    pub is_connection_inbound: bool,
293    /// communication channels between the peer data and its active connection
294    pub connection: Option<PeerConnection>,
295    /// This tracks the score of a peer
296    score: i64,
297    /// Track the amount of concurrent requests this peer is handling
298    requests: i64,
299    /// Timestamp (seconds since UNIX epoch) of the last successful response from this peer
300    pub last_response_time: Option<u64>,
301}
302
303impl PeerData {
304    pub fn new(
305        node: Node,
306        record: Option<NodeRecord>,
307        connection: Option<PeerConnection>,
308        capabilities: Vec<Capability>,
309    ) -> Self {
310        Self {
311            node,
312            record,
313            supported_capabilities: capabilities,
314            is_connection_inbound: false,
315            connection,
316            score: Default::default(),
317            requests: Default::default(),
318            last_response_time: None,
319        }
320    }
321}
322
323/// Diagnostic snapshot of a peer's state, used by admin RPC endpoints.
324#[derive(Debug, Clone, serde::Serialize)]
325pub struct PeerDiagnostics {
326    pub peer_id: H256,
327    pub score: i64,
328    pub inflight_requests: i64,
329    pub eligible: bool,
330    pub capabilities: Vec<String>,
331    pub ip: IpAddr,
332    pub client_version: String,
333    pub connection_direction: String,
334    pub last_response_time: Option<u64>,
335}
336
337/// Result of contact validation.
338#[derive(Debug, Clone)]
339pub enum ContactValidation {
340    Valid(Box<Contact>),
341    InvalidContact,
342    UnknownContact,
343    IpMismatch,
344}
345
346/// Reservation handle for a peer request slot.
347///
348/// **Contract:** when a `RequestPermit` exists, the `requests` counter for
349/// its peer has been incremented by one. Dropping the permit releases the
350/// slot via a fire-and-forget `DecRequests` message. The handler that
351/// returns the permit also bumps the counter atomically under `&mut self`,
352/// so selection and reservation cannot be observed out of order.
353///
354/// The permit must travel with whatever code owns the outstanding request —
355/// move it into spawned tasks, send it through channels alongside results,
356/// etc. Dropping early releases the slot early.
357#[must_use = "dropping this permit immediately releases the peer's request slot"]
358pub struct RequestPermit {
359    peer_table: PeerTable,
360    peer_id: H256,
361}
362
363impl RequestPermit {
364    pub(crate) fn new(peer_table: PeerTable, peer_id: H256) -> Self {
365        Self {
366            peer_table,
367            peer_id,
368        }
369    }
370}
371
372impl std::fmt::Debug for RequestPermit {
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        f.debug_struct("RequestPermit")
375            .field("peer_id", &self.peer_id)
376            .finish_non_exhaustive()
377    }
378}
379
380impl Drop for RequestPermit {
381    fn drop(&mut self) {
382        // Fire-and-forget. If the actor mailbox is closed, p2p is already
383        // shutting down — the lost decrement is a non-issue.
384        let _ = self.peer_table.dec_requests(self.peer_id);
385    }
386}
387
388#[protocol]
389pub trait PeerTableServerProtocol: Send + Sync {
390    // Send (cast) methods
391    fn new_contacts(&self, nodes: Vec<Node>, protocol: DiscoveryProtocol)
392    -> Result<(), ActorError>;
393    fn new_contact_records(&self, node_records: Vec<NodeRecord>) -> Result<(), ActorError>;
394    fn new_connected_peer(
395        &self,
396        node: Node,
397        connection: PeerConnection,
398        capabilities: Vec<Capability>,
399        is_inbound: bool,
400    ) -> Result<(), ActorError>;
401    fn set_session_info(&self, node_id: H256, session: Session) -> Result<(), ActorError>;
402    fn remove_peer(&self, node_id: H256) -> Result<(), ActorError>;
403    fn dec_requests(&self, node_id: H256) -> Result<(), ActorError>;
404    fn set_unwanted(&self, node_id: H256) -> Result<(), ActorError>;
405    fn set_is_fork_id_valid(&self, node_id: H256, valid: bool) -> Result<(), ActorError>;
406    fn record_success(&self, node_id: H256) -> Result<(), ActorError>;
407    fn record_failure(&self, node_id: H256) -> Result<(), ActorError>;
408    fn record_critical_failure(&self, node_id: H256) -> Result<(), ActorError>;
409    fn record_ping_sent(&self, node_id: H256, ping_id: Bytes) -> Result<(), ActorError>;
410    fn record_pong_received(&self, node_id: H256, ping_id: Bytes) -> Result<(), ActorError>;
411    fn record_enr_request_sent(&self, node_id: H256, request_hash: H256) -> Result<(), ActorError>;
412    fn record_enr_response_received(
413        &self,
414        node_id: H256,
415        request_hash: H256,
416        record: NodeRecord,
417    ) -> Result<(), ActorError>;
418    fn set_disposable(&self, node_id: H256) -> Result<(), ActorError>;
419    fn mark_knows_us(&self, node_id: H256) -> Result<(), ActorError>;
420    fn prune_table(&self) -> Result<(), ActorError>;
421    fn shutdown(&self) -> Result<(), ActorError>;
422
423    // Request (call) methods
424    fn peer_count(&self) -> Response<usize>;
425    fn peer_count_by_capabilities(&self, capabilities: Vec<Capability>) -> Response<usize>;
426    fn target_reached(&self) -> Response<bool>;
427    fn target_peers_reached(&self) -> Response<bool>;
428    fn target_peers_completion(&self) -> Response<f64>;
429    fn get_contact_to_initiate(&self) -> Response<Option<Box<Contact>>>;
430    fn get_contact_for_enr_lookup(&self) -> Response<Option<Box<Contact>>>;
431    fn get_closest_from_pool(&self, target: H256, count: usize) -> Response<Vec<(H256, Node)>>;
432    fn get_contact(&self, node_id: H256) -> Response<Option<Box<Contact>>>;
433    fn get_contact_to_revalidate(
434        &self,
435        revalidation_interval: Duration,
436        protocol: DiscoveryProtocol,
437    ) -> Response<Option<Box<Contact>>>;
438    fn get_best_peer(
439        &self,
440        capabilities: Vec<Capability>,
441    ) -> Response<Option<(H256, PeerConnection, RequestPermit)>>;
442    fn get_best_peer_excluding(
443        &self,
444        capabilities: Vec<Capability>,
445        excluded: Vec<H256>,
446    ) -> Response<Option<(H256, PeerConnection, RequestPermit)>>;
447    fn get_best_n_peers(
448        &self,
449        capabilities: Vec<Capability>,
450        n: usize,
451    ) -> Response<Vec<(H256, PeerConnection, RequestPermit)>>;
452    /// Read-only predicate: is there any eligible peer matching `capabilities`?
453    /// Does not reserve a slot; use for capacity/rotation probes only.
454    fn has_eligible_peer(&self, capabilities: Vec<Capability>) -> Response<bool>;
455    fn get_score(&self, node_id: H256) -> Response<i64>;
456    fn get_connected_nodes(&self) -> Response<Vec<Node>>;
457    fn get_peers_with_capabilities(&self)
458    -> Response<Vec<(H256, PeerConnection, Vec<Capability>)>>;
459    fn insert_if_new(&self, node: Node, protocol: DiscoveryProtocol) -> Response<bool>;
460    fn validate_contact(&self, node_id: H256, sender_ip: IpAddr) -> Response<ContactValidation>;
461    fn get_closest_nodes(&self, node_id: H256) -> Response<Vec<Node>>;
462    fn get_nodes_at_distances(&self, distances: Vec<u32>) -> Response<Vec<NodeRecord>>;
463    fn get_peers_data(&self) -> Response<Vec<PeerData>>;
464    fn get_random_peer(
465        &self,
466        capabilities: Vec<Capability>,
467    ) -> Response<Option<(H256, PeerConnection, RequestPermit)>>;
468    fn get_session_info(&self, node_id: H256) -> Response<Option<Session>>;
469    fn get_peer_diagnostics(&self) -> Response<Vec<PeerDiagnostics>>;
470    fn get_peer_connection(&self, peer_id: H256) -> Response<Option<PeerConnection>>;
471}
472
473#[derive(Debug)]
474pub struct PeerTableServer {
475    local_node_id: H256,
476    buckets: Vec<KBucket>,
477    peers: IndexMap<H256, PeerData>,
478    already_tried_peers: FxHashSet<H256>,
479    target_peers: usize,
480    store: Store,
481    /// Standalone session store, independent of contacts.
482    /// Allows sessions to be stored even before the contact's ENR is known/parseable.
483    sessions: FxHashMap<H256, Session>,
484    /// Flat pool of discovered contacts for RLPx connection initiation.
485    /// Decoupled from the k-bucket routing table so that connection initiation
486    /// has access to a much larger candidate pool than the k-bucket structure
487    /// allows (k-buckets: 256 × 16 = 4,096 max; this pool: up to 50,000).
488    /// K-buckets are still used for all Kademlia protocol operations.
489    connection_pool: IndexMap<H256, Node>,
490}
491
492#[actor(protocol = PeerTableServerProtocol)]
493impl PeerTableServer {
494    pub fn spawn(local_node_id: H256, target_peers: usize, store: Store) -> PeerTable {
495        PeerTableServer::new(local_node_id, target_peers, store).start()
496    }
497
498    pub(crate) fn new(local_node_id: H256, target_peers: usize, store: Store) -> Self {
499        Self {
500            local_node_id,
501            buckets: vec![KBucket::default(); NUMBER_OF_BUCKETS],
502            peers: Default::default(),
503            already_tried_peers: Default::default(),
504            target_peers,
505            store,
506            sessions: Default::default(),
507            connection_pool: IndexMap::with_capacity(MAX_CONNECTION_POOL_SIZE),
508        }
509    }
510
511    #[started]
512    async fn started(&mut self, ctx: &Context<Self>) {
513        send_message_on(
514            ctx.clone(),
515            tokio::signal::ctrl_c(),
516            peer_table_server_protocol::Shutdown,
517        );
518    }
519
520    // === Send handlers ===
521
522    #[send_handler]
523    async fn handle_new_contacts(
524        &mut self,
525        msg: peer_table_server_protocol::NewContacts,
526        _ctx: &Context<Self>,
527    ) {
528        self.do_new_contacts(msg.nodes, msg.protocol).await;
529    }
530
531    #[send_handler]
532    async fn handle_new_contact_records(
533        &mut self,
534        msg: peer_table_server_protocol::NewContactRecords,
535        _ctx: &Context<Self>,
536    ) {
537        self.do_new_contact_records(msg.node_records).await;
538    }
539
540    #[send_handler]
541    async fn handle_new_connected_peer(
542        &mut self,
543        msg: peer_table_server_protocol::NewConnectedPeer,
544        _ctx: &Context<Self>,
545    ) {
546        let new_peer_id = msg.node.node_id();
547        let mut new_peer = PeerData::new(msg.node, None, Some(msg.connection), msg.capabilities);
548        new_peer.is_connection_inbound = msg.is_inbound;
549        self.peers.insert(new_peer_id, new_peer);
550    }
551
552    #[send_handler]
553    async fn handle_set_session_info(
554        &mut self,
555        msg: peer_table_server_protocol::SetSessionInfo,
556        _ctx: &Context<Self>,
557    ) {
558        // Store in the standalone sessions map (always succeeds, no contact required).
559        self.sessions.insert(msg.node_id, msg.session.clone());
560        // Also update the contact's cached session if the contact exists.
561        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
562            contact.session = Some(msg.session);
563        }
564    }
565
566    #[send_handler]
567    async fn handle_remove_peer(
568        &mut self,
569        msg: peer_table_server_protocol::RemovePeer,
570        _ctx: &Context<Self>,
571    ) {
572        self.peers.swap_remove(&msg.node_id);
573        // Also drop the standalone discv5 session so it isn't retained after the peer leaves
574        // (the sessions map was previously insert-only and grew per handshake).
575        self.sessions.remove(&msg.node_id);
576    }
577
578    #[send_handler]
579    async fn handle_dec_requests(
580        &mut self,
581        msg: peer_table_server_protocol::DecRequests,
582        _ctx: &Context<Self>,
583    ) {
584        self.peers.entry(msg.node_id).and_modify(|peer_data| {
585            if peer_data.requests <= 0 {
586                // Expected under the reconnect race (stale permit fires
587                // after remove_peer + new_connected_peer), self-heals.
588                // Otherwise points to a bookkeeping bug worth chasing.
589                tracing::debug!(
590                    peer_id = ?msg.node_id,
591                    requests = peer_data.requests,
592                    "dec_requests with counter already <= 0",
593                );
594            }
595            peer_data.requests = peer_data.requests.saturating_sub(1).max(0)
596        });
597    }
598
599    #[send_handler]
600    async fn handle_set_unwanted(
601        &mut self,
602        msg: peer_table_server_protocol::SetUnwanted,
603        _ctx: &Context<Self>,
604    ) {
605        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
606            contact.unwanted = true;
607        }
608    }
609
610    #[send_handler]
611    async fn handle_set_is_fork_id_valid(
612        &mut self,
613        msg: peer_table_server_protocol::SetIsForkIdValid,
614        _ctx: &Context<Self>,
615    ) {
616        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
617            contact.is_fork_id_valid = Some(msg.valid);
618        }
619    }
620
621    #[send_handler]
622    async fn handle_record_success(
623        &mut self,
624        msg: peer_table_server_protocol::RecordSuccess,
625        _ctx: &Context<Self>,
626    ) {
627        let now = std::time::SystemTime::now()
628            .duration_since(std::time::UNIX_EPOCH)
629            .unwrap_or_default()
630            .as_secs();
631        self.peers.entry(msg.node_id).and_modify(|peer_data| {
632            peer_data.score = (peer_data.score + 1).min(MAX_SCORE);
633            peer_data.last_response_time = Some(now);
634        });
635    }
636
637    #[send_handler]
638    async fn handle_record_failure(
639        &mut self,
640        msg: peer_table_server_protocol::RecordFailure,
641        _ctx: &Context<Self>,
642    ) {
643        self.peers
644            .entry(msg.node_id)
645            .and_modify(|peer_data| peer_data.score = (peer_data.score - 1).max(MIN_SCORE));
646    }
647
648    #[send_handler]
649    async fn handle_record_critical_failure(
650        &mut self,
651        msg: peer_table_server_protocol::RecordCriticalFailure,
652        _ctx: &Context<Self>,
653    ) {
654        self.peers
655            .entry(msg.node_id)
656            .and_modify(|peer_data| peer_data.score = MIN_SCORE_CRITICAL);
657    }
658
659    #[send_handler]
660    async fn handle_record_ping_sent(
661        &mut self,
662        msg: peer_table_server_protocol::RecordPingSent,
663        _ctx: &Context<Self>,
664    ) {
665        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
666            contact.record_ping_sent(msg.ping_id);
667        }
668    }
669
670    #[send_handler]
671    async fn handle_record_pong_received(
672        &mut self,
673        msg: peer_table_server_protocol::RecordPongReceived,
674        _ctx: &Context<Self>,
675    ) {
676        if let Some(contact) = self.get_contact_mut(&msg.node_id)
677            && contact
678                .ping_id
679                .as_ref()
680                .map(|value| *value == msg.ping_id)
681                .unwrap_or(false)
682        {
683            contact.ping_id = None;
684        }
685    }
686
687    #[send_handler]
688    async fn handle_record_enr_request_sent(
689        &mut self,
690        msg: peer_table_server_protocol::RecordEnrRequestSent,
691        _ctx: &Context<Self>,
692    ) {
693        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
694            contact.record_enr_request_sent(msg.request_hash);
695        }
696    }
697
698    #[send_handler]
699    async fn handle_record_enr_response_received(
700        &mut self,
701        msg: peer_table_server_protocol::RecordEnrResponseReceived,
702        _ctx: &Context<Self>,
703    ) {
704        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
705            contact.record_enr_response_received(msg.request_hash, msg.record);
706        }
707    }
708
709    #[send_handler]
710    async fn handle_set_disposable(
711        &mut self,
712        msg: peer_table_server_protocol::SetDisposable,
713        _ctx: &Context<Self>,
714    ) {
715        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
716            contact.disposable = true;
717        }
718    }
719
720    #[send_handler]
721    async fn handle_mark_knows_us(
722        &mut self,
723        msg: peer_table_server_protocol::MarkKnowsUs,
724        _ctx: &Context<Self>,
725    ) {
726        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
727            contact.knows_us = true;
728        }
729    }
730
731    #[send_handler]
732    async fn handle_prune_table(
733        &mut self,
734        _msg: peer_table_server_protocol::PruneTable,
735        _ctx: &Context<Self>,
736    ) {
737        self.prune();
738    }
739
740    #[send_handler]
741    async fn handle_shutdown(
742        &mut self,
743        _msg: peer_table_server_protocol::Shutdown,
744        ctx: &Context<Self>,
745    ) {
746        ctx.stop();
747    }
748
749    // === Request handlers ===
750
751    #[request_handler]
752    async fn handle_peer_count(
753        &mut self,
754        _msg: peer_table_server_protocol::PeerCount,
755        _ctx: &Context<Self>,
756    ) -> usize {
757        self.peers.len()
758    }
759
760    #[request_handler]
761    async fn handle_peer_count_by_capabilities(
762        &mut self,
763        msg: peer_table_server_protocol::PeerCountByCapabilities,
764        _ctx: &Context<Self>,
765    ) -> usize {
766        self.do_peer_count_by_capabilities(msg.capabilities)
767    }
768
769    #[request_handler]
770    async fn handle_target_reached(
771        &mut self,
772        _msg: peer_table_server_protocol::TargetReached,
773        _ctx: &Context<Self>,
774    ) -> bool {
775        self.peers.len() >= self.target_peers
776    }
777
778    #[request_handler]
779    async fn handle_target_peers_reached(
780        &mut self,
781        _msg: peer_table_server_protocol::TargetPeersReached,
782        _ctx: &Context<Self>,
783    ) -> bool {
784        self.peers.len() >= self.target_peers
785    }
786
787    #[request_handler]
788    async fn handle_target_peers_completion(
789        &mut self,
790        _msg: peer_table_server_protocol::TargetPeersCompletion,
791        _ctx: &Context<Self>,
792    ) -> f64 {
793        self.peers.len() as f64 / self.target_peers as f64
794    }
795
796    #[request_handler]
797    async fn handle_get_contact_to_initiate(
798        &mut self,
799        _msg: peer_table_server_protocol::GetContactToInitiate,
800        _ctx: &Context<Self>,
801    ) -> Option<Box<Contact>> {
802        self.do_get_contact_to_initiate().map(Box::new)
803    }
804
805    #[request_handler]
806    async fn handle_get_closest_from_pool(
807        &mut self,
808        msg: peer_table_server_protocol::GetClosestFromPool,
809        _ctx: &Context<Self>,
810    ) -> Vec<(H256, Node)> {
811        self.do_get_closest_from_pool(msg.target, msg.count)
812    }
813
814    #[request_handler]
815    async fn handle_get_contact_for_enr_lookup(
816        &mut self,
817        _msg: peer_table_server_protocol::GetContactForEnrLookup,
818        _ctx: &Context<Self>,
819    ) -> Option<Box<Contact>> {
820        self.do_get_contact_for_enr_lookup().map(Box::new)
821    }
822
823    #[request_handler]
824    async fn handle_get_contact(
825        &mut self,
826        msg: peer_table_server_protocol::GetContact,
827        _ctx: &Context<Self>,
828    ) -> Option<Box<Contact>> {
829        self.get_contact(&msg.node_id).cloned().map(Box::new)
830    }
831
832    #[request_handler]
833    async fn handle_get_contact_to_revalidate(
834        &mut self,
835        msg: peer_table_server_protocol::GetContactToRevalidate,
836        _ctx: &Context<Self>,
837    ) -> Option<Box<Contact>> {
838        self.do_get_contact_to_revalidate(msg.revalidation_interval, msg.protocol)
839    }
840
841    #[request_handler]
842    async fn handle_get_best_peer(
843        &mut self,
844        msg: peer_table_server_protocol::GetBestPeer,
845        ctx: &Context<Self>,
846    ) -> Option<(H256, PeerConnection, RequestPermit)> {
847        let (peer_id, conn) = self.do_get_best_peer(&msg.capabilities)?;
848        self.peers
849            .get_mut(&peer_id)
850            .expect("peer returned by do_get_best_peer must be present in self.peers")
851            .requests += 1;
852        Some((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)))
853    }
854
855    #[request_handler]
856    async fn handle_get_best_peer_excluding(
857        &mut self,
858        msg: peer_table_server_protocol::GetBestPeerExcluding,
859        ctx: &Context<Self>,
860    ) -> Option<(H256, PeerConnection, RequestPermit)> {
861        let (peer_id, conn) = self.do_get_best_peer_excluding(&msg.capabilities, &msg.excluded)?;
862        self.peers
863            .get_mut(&peer_id)
864            .expect("peer returned by do_get_best_peer_excluding must be present in self.peers")
865            .requests += 1;
866        Some((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)))
867    }
868
869    #[request_handler]
870    async fn handle_get_best_n_peers(
871        &mut self,
872        msg: peer_table_server_protocol::GetBestNPeers,
873        ctx: &Context<Self>,
874    ) -> Vec<(H256, PeerConnection, RequestPermit)> {
875        let picks = self.do_get_best_n_peers(&msg.capabilities, msg.n);
876        let mut out = Vec::with_capacity(picks.len());
877        for (peer_id, conn) in picks {
878            self.peers
879                .get_mut(&peer_id)
880                .expect("peer returned by do_get_best_n_peers must be present in self.peers")
881                .requests += 1;
882            out.push((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)));
883        }
884        out
885    }
886
887    #[request_handler]
888    async fn handle_has_eligible_peer(
889        &mut self,
890        msg: peer_table_server_protocol::HasEligiblePeer,
891        _ctx: &Context<Self>,
892    ) -> bool {
893        self.peers.values().any(|peer_data| {
894            peer_data.connection.is_some()
895                && self.can_try_more_requests(&peer_data.score, &peer_data.requests)
896                && msg
897                    .capabilities
898                    .iter()
899                    .any(|cap| peer_data.supported_capabilities.contains(cap))
900        })
901    }
902
903    #[request_handler]
904    async fn handle_get_score(
905        &mut self,
906        msg: peer_table_server_protocol::GetScore,
907        _ctx: &Context<Self>,
908    ) -> i64 {
909        self.peers
910            .get(&msg.node_id)
911            .map(|peer_data| peer_data.score)
912            .unwrap_or_default()
913    }
914
915    #[request_handler]
916    async fn handle_get_connected_nodes(
917        &mut self,
918        _msg: peer_table_server_protocol::GetConnectedNodes,
919        _ctx: &Context<Self>,
920    ) -> Vec<Node> {
921        self.peers
922            .values()
923            .map(|peer_data| peer_data.node.clone())
924            .collect()
925    }
926
927    #[request_handler]
928    async fn handle_get_peers_with_capabilities(
929        &mut self,
930        _msg: peer_table_server_protocol::GetPeersWithCapabilities,
931        _ctx: &Context<Self>,
932    ) -> Vec<(H256, PeerConnection, Vec<Capability>)> {
933        self.peers
934            .iter()
935            .filter_map(|(peer_id, peer_data)| {
936                peer_data.connection.clone().map(|connection| {
937                    (
938                        *peer_id,
939                        connection,
940                        peer_data.supported_capabilities.clone(),
941                    )
942                })
943            })
944            .collect()
945    }
946
947    #[request_handler]
948    async fn handle_insert_if_new(
949        &mut self,
950        msg: peer_table_server_protocol::InsertIfNew,
951        _ctx: &Context<Self>,
952    ) -> bool {
953        let node_id = msg.node.node_id();
954        // Always add to the connection pool
955        self.insert_to_connection_pool(node_id, msg.node.clone());
956        if self.contact_exists(&node_id) {
957            return false;
958        }
959        let contact = Contact::new(msg.node, msg.protocol);
960        // Return true for any genuinely new node, even if it overflows to the
961        // replacement list.  This ensures the caller sends a reciprocal ping
962        // which establishes the bond needed for FindNode validation.
963        self.insert_contact(node_id, contact);
964        METRICS.record_new_discovery().await;
965        true
966    }
967
968    #[request_handler]
969    async fn handle_validate_contact(
970        &mut self,
971        msg: peer_table_server_protocol::ValidateContact,
972        _ctx: &Context<Self>,
973    ) -> ContactValidation {
974        self.do_validate_contact(msg.node_id, msg.sender_ip)
975    }
976
977    #[request_handler]
978    async fn handle_get_closest_nodes(
979        &mut self,
980        msg: peer_table_server_protocol::GetClosestNodes,
981        _ctx: &Context<Self>,
982    ) -> Vec<Node> {
983        self.do_get_closest_nodes(msg.node_id)
984    }
985
986    #[request_handler]
987    async fn handle_get_nodes_at_distances(
988        &mut self,
989        msg: peer_table_server_protocol::GetNodesAtDistances,
990        _ctx: &Context<Self>,
991    ) -> Vec<NodeRecord> {
992        self.do_get_nodes_at_distances(&msg.distances)
993    }
994
995    #[request_handler]
996    async fn handle_get_peers_data(
997        &mut self,
998        _msg: peer_table_server_protocol::GetPeersData,
999        _ctx: &Context<Self>,
1000    ) -> Vec<PeerData> {
1001        self.peers.values().cloned().collect()
1002    }
1003
1004    #[request_handler]
1005    async fn handle_get_random_peer(
1006        &mut self,
1007        msg: peer_table_server_protocol::GetRandomPeer,
1008        ctx: &Context<Self>,
1009    ) -> Option<(H256, PeerConnection, RequestPermit)> {
1010        let (peer_id, conn) = self.do_get_random_peer(msg.capabilities)?;
1011        self.peers
1012            .get_mut(&peer_id)
1013            .expect("peer returned by do_get_random_peer must be present in self.peers")
1014            .requests += 1;
1015        Some((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)))
1016    }
1017
1018    #[request_handler]
1019    async fn handle_get_session_info(
1020        &mut self,
1021        msg: peer_table_server_protocol::GetSessionInfo,
1022        _ctx: &Context<Self>,
1023    ) -> Option<Session> {
1024        // Check standalone sessions map first; fall back to contact.session.
1025        self.sessions
1026            .get(&msg.node_id)
1027            .cloned()
1028            .or_else(|| self.get_contact(&msg.node_id)?.session.clone())
1029    }
1030
1031    #[request_handler]
1032    async fn handle_get_peer_connection(
1033        &mut self,
1034        msg: peer_table_server_protocol::GetPeerConnection,
1035        _ctx: &Context<Self>,
1036    ) -> Option<PeerConnection> {
1037        self.peers
1038            .get(&msg.peer_id)
1039            .and_then(|peer_data| peer_data.connection.clone())
1040    }
1041
1042    #[request_handler]
1043    async fn handle_get_peer_diagnostics(
1044        &mut self,
1045        _msg: peer_table_server_protocol::GetPeerDiagnostics,
1046        _ctx: &Context<Self>,
1047    ) -> Vec<PeerDiagnostics> {
1048        self.peers
1049            .iter()
1050            .map(|(id, peer_data)| PeerDiagnostics {
1051                peer_id: *id,
1052                score: peer_data.score,
1053                inflight_requests: peer_data.requests,
1054                eligible: self.can_try_more_requests(&peer_data.score, &peer_data.requests),
1055                capabilities: peer_data
1056                    .supported_capabilities
1057                    .iter()
1058                    .map(|c| format!("{}/{}", c.protocol(), c.version))
1059                    .collect(),
1060                ip: peer_data.node.ip,
1061                client_version: peer_data.node.version.clone().unwrap_or_default(),
1062                connection_direction: if peer_data.is_connection_inbound {
1063                    "inbound".to_string()
1064                } else {
1065                    "outbound".to_string()
1066                },
1067                last_response_time: peer_data.last_response_time,
1068            })
1069            .collect()
1070    }
1071
1072    // === Private helper methods ===
1073
1074    // --- K-bucket accessors ---
1075
1076    /// Get the bucket index for a node ID, or None if it's the local node.
1077    fn bucket_for(&self, node_id: &H256) -> Option<usize> {
1078        bucket_index(&self.local_node_id, node_id)
1079    }
1080
1081    /// Look up a contact by node ID in main or replacement list (O(K) within the bucket).
1082    fn get_contact(&self, node_id: &H256) -> Option<&Contact> {
1083        let idx = self.bucket_for(node_id)?;
1084        self.buckets[idx].get_any(node_id)
1085    }
1086
1087    /// Look up a mutable reference to a contact by node ID.
1088    fn get_contact_mut(&mut self, node_id: &H256) -> Option<&mut Contact> {
1089        let idx = self.bucket_for(node_id)?;
1090        self.buckets[idx].get_mut(node_id)
1091    }
1092
1093    /// Check if a contact exists in any bucket (main or replacement list).
1094    fn contact_exists(&self, node_id: &H256) -> bool {
1095        let Some(idx) = self.bucket_for(node_id) else {
1096            return false;
1097        };
1098        self.buckets[idx].contains(node_id)
1099    }
1100
1101    /// Insert a contact into the appropriate k-bucket. Returns true if inserted
1102    /// into the main list, false if the node went to the replacement list or is
1103    /// the local node.
1104    fn insert_contact(&mut self, node_id: H256, contact: Contact) -> bool {
1105        #[cfg(feature = "metrics")]
1106        let start = std::time::Instant::now();
1107
1108        let Some(idx) = self.bucket_for(&node_id) else {
1109            return false;
1110        };
1111        let result = self.buckets[idx].insert(node_id, contact);
1112
1113        #[cfg(feature = "metrics")]
1114        {
1115            use ethrex_metrics::p2p::METRICS_P2P;
1116            METRICS_P2P.observe_insert_contact_duration(start.elapsed().as_secs_f64());
1117        }
1118
1119        result
1120    }
1121
1122    /// Insert a node into the flat connection pool for RLPx initiation.
1123    /// Evicts the oldest entry when the pool is at capacity.
1124    fn insert_to_connection_pool(&mut self, node_id: H256, node: Node) {
1125        if self.connection_pool.contains_key(&node_id) {
1126            return;
1127        }
1128        if self.connection_pool.len() >= MAX_CONNECTION_POOL_SIZE {
1129            self.connection_pool.shift_remove_index(0);
1130        }
1131        self.connection_pool.insert(node_id, node);
1132    }
1133
1134    /// Look up a contact by node ID in either the main or replacement list.
1135    fn get_contact_or_replacement(&self, node_id: &H256) -> Option<&Contact> {
1136        let idx = self.bucket_for(node_id)?;
1137        self.buckets[idx].get_any(node_id)
1138    }
1139
1140    /// Look up a mutable reference in either the main or replacement list.
1141    fn get_contact_or_replacement_mut(&mut self, node_id: &H256) -> Option<&mut Contact> {
1142        let idx = self.bucket_for(node_id)?;
1143        let bucket = &mut self.buckets[idx];
1144        // Search main list first, then replacement list.
1145        // Done inline to avoid borrow-checker issues with or_else closures.
1146        if let Some(pos) = bucket.contacts.iter().position(|(id, _)| id == node_id) {
1147            return Some(&mut bucket.contacts[pos].1);
1148        }
1149        if let Some(pos) = bucket.replacements.iter().position(|(id, _)| id == node_id) {
1150            return Some(&mut bucket.replacements[pos].1);
1151        }
1152        None
1153    }
1154
1155    /// Iterate over all contacts across all buckets (main and replacement lists).
1156    fn iter_contacts(&self) -> impl Iterator<Item = (&H256, &Contact)> {
1157        self.buckets.iter().flat_map(|bucket| {
1158            bucket
1159                .contacts
1160                .iter()
1161                .chain(bucket.replacements.iter())
1162                .map(|(id, c)| (id, c))
1163        })
1164    }
1165
1166    // --- Peer selection ---
1167
1168    fn weight_peer(&self, score: &i64, requests: &i64) -> i64 {
1169        score * SCORE_WEIGHT - requests * REQUESTS_WEIGHT
1170    }
1171
1172    fn can_try_more_requests(&self, score: &i64, requests: &i64) -> bool {
1173        let score_ratio = (score - MIN_SCORE) as f64 / (MAX_SCORE - MIN_SCORE) as f64;
1174        let max_requests = (MAX_CONCURRENT_REQUESTS_PER_PEER as f64 * score_ratio).max(1.0);
1175        (*requests as f64) < max_requests
1176    }
1177
1178    fn do_get_best_peer(&self, capabilities: &[Capability]) -> Option<(H256, PeerConnection)> {
1179        self.do_get_best_peer_excluding(capabilities, &[])
1180    }
1181
1182    /// Like `do_get_best_peer`, but excludes specific peers from selection.
1183    /// Used by `update_pivot` to rotate through peers on repeated failures.
1184    fn do_get_best_peer_excluding(
1185        &self,
1186        capabilities: &[Capability],
1187        excluded: &[H256],
1188    ) -> Option<(H256, PeerConnection)> {
1189        self.peers
1190            .iter()
1191            .filter_map(|(id, peer_data)| {
1192                if excluded.contains(id)
1193                    || !self.can_try_more_requests(&peer_data.score, &peer_data.requests)
1194                    || !capabilities
1195                        .iter()
1196                        .any(|cap| peer_data.supported_capabilities.contains(cap))
1197                {
1198                    None
1199                } else {
1200                    let connection = peer_data.connection.clone()?;
1201                    Some((*id, peer_data.score, peer_data.requests, connection))
1202                }
1203            })
1204            .max_by_key(|(_, score, reqs, _)| self.weight_peer(score, reqs))
1205            .map(|(k, _, _, v)| (k, v))
1206    }
1207
1208    /// Returns up to `n` best peers with capability overlap, sorted by weight
1209    /// descending. Excludes peers at capacity. Does NOT mutate state — caller
1210    /// is responsible for incrementing `requests` on each returned peer. The
1211    /// sort uses a pre-increment snapshot: later picks don't see earlier
1212    /// picks' bumps, which is fine for small `n`.
1213    fn do_get_best_n_peers(
1214        &self,
1215        capabilities: &[Capability],
1216        n: usize,
1217    ) -> Vec<(H256, PeerConnection)> {
1218        let mut candidates: Vec<(H256, i64, i64, PeerConnection)> = self
1219            .peers
1220            .iter()
1221            .filter_map(|(id, peer_data)| {
1222                if !self.can_try_more_requests(&peer_data.score, &peer_data.requests)
1223                    || !capabilities
1224                        .iter()
1225                        .any(|cap| peer_data.supported_capabilities.contains(cap))
1226                {
1227                    None
1228                } else {
1229                    let connection = peer_data.connection.clone()?;
1230                    Some((*id, peer_data.score, peer_data.requests, connection))
1231                }
1232            })
1233            .collect();
1234
1235        candidates.sort_by_key(|(_, score, reqs, _)| -self.weight_peer(score, reqs));
1236        candidates
1237            .into_iter()
1238            .take(n)
1239            .map(|(id, _, _, conn)| (id, conn))
1240            .collect()
1241    }
1242
1243    // --- Contact operations ---
1244
1245    /// Prune disposable contacts from both main and replacement lists.
1246    /// When a main contact is removed, a replacement is automatically promoted.
1247    /// Pruned contacts remain in the connection pool so they can be retried
1248    /// later — the RLPx handshake will reject them if they're truly bad.
1249    fn prune(&mut self) {
1250        for bucket in &mut self.buckets {
1251            // Collect disposable contacts from main list
1252            let main_disposable: Vec<H256> = bucket
1253                .contacts
1254                .iter()
1255                .filter(|(_, c)| c.disposable)
1256                .map(|(id, _)| *id)
1257                .collect();
1258
1259            // Remove from main list and promote replacements
1260            for node_id in main_disposable {
1261                bucket.remove_and_promote(&node_id);
1262            }
1263
1264            // Remove disposable contacts from replacement list
1265            // (these don't get promoted, just removed)
1266            bucket.replacements.retain(|(_, c)| !c.disposable);
1267        }
1268    }
1269
1270    fn do_get_contact_to_initiate(&mut self) -> Option<Contact> {
1271        // Draw from the flat connection pool using O(1) random index probing.
1272        // Pick a random start index and scan forward (wrapping) until we find
1273        // an eligible candidate or complete a full loop.
1274        let pool_len = self.connection_pool.len();
1275        if pool_len == 0 {
1276            return None;
1277        }
1278
1279        let start = rand::random::<usize>() % pool_len;
1280        for offset in 0..pool_len {
1281            let idx = (start + offset) % pool_len;
1282            let Some((node_id, node)) = self.connection_pool.get_index(idx) else {
1283                continue;
1284            };
1285            let node_id = *node_id;
1286
1287            if self.peers.contains_key(&node_id)
1288                || self.already_tried_peers.contains(&node_id)
1289                || self
1290                    .get_contact_or_replacement(&node_id)
1291                    .map(|c| !c.knows_us || c.unwanted || c.is_fork_id_valid == Some(false))
1292                    .unwrap_or(false)
1293            {
1294                continue;
1295            }
1296
1297            let node = node.clone();
1298            self.already_tried_peers.insert(node_id);
1299            let contact = self
1300                .get_contact_or_replacement(&node_id)
1301                .cloned()
1302                .unwrap_or_else(|| Contact::new(node, DiscoveryProtocol::Discv4));
1303            return Some(contact);
1304        }
1305
1306        // Exhausted all candidates — reset tried set for next cycle.
1307        tracing::trace!("Resetting list of tried peers.");
1308        self.already_tried_peers.clear();
1309        None
1310    }
1311
1312    /// Get the `count` closest nodes from the connection pool, sorted by XOR distance to `target`.
1313    fn do_get_closest_from_pool(&self, target: H256, count: usize) -> Vec<(H256, Node)> {
1314        let mut nodes: Vec<(H256, Node, H256)> = Vec::with_capacity(count);
1315
1316        for (node_id, node) in &self.connection_pool {
1317            let dist = xor_distance(&target, node_id);
1318            if nodes.len() < count {
1319                nodes.push((*node_id, node.clone(), dist));
1320            } else if let Some((farthest_idx, _)) =
1321                nodes.iter().enumerate().max_by_key(|(_, (_, _, d))| *d)
1322                && dist < nodes[farthest_idx].2
1323            {
1324                nodes[farthest_idx] = (*node_id, node.clone(), dist);
1325            }
1326        }
1327
1328        nodes.sort_by(|a, b| a.2.cmp(&b.2));
1329        nodes.into_iter().map(|(id, node, _)| (id, node)).collect()
1330    }
1331
1332    /// Get contact for ENR lookup (discv4 only)
1333    fn do_get_contact_for_enr_lookup(&mut self) -> Option<Contact> {
1334        self.iter_contacts()
1335            .filter(|(_, c)| {
1336                c.is_discv4
1337                    && c.was_validated()
1338                    && !c.has_pending_enr_request()
1339                    && c.record.is_none()
1340                    && !c.disposable
1341            })
1342            .map(|(_, c)| c)
1343            .collect::<Vec<_>>()
1344            .choose(&mut rand::rngs::OsRng)
1345            .cloned()
1346            .cloned()
1347    }
1348
1349    fn do_get_contact_to_revalidate(
1350        &self,
1351        revalidation_interval: Duration,
1352        protocol: DiscoveryProtocol,
1353    ) -> Option<Box<Contact>> {
1354        self.iter_contacts()
1355            .filter(|(_, c)| {
1356                c.supports_protocol(protocol)
1357                    && Self::is_validation_needed(c, revalidation_interval)
1358            })
1359            .map(|(_, c)| c)
1360            .choose(&mut rand::rngs::OsRng)
1361            .cloned()
1362            .map(Box::new)
1363    }
1364
1365    fn do_validate_contact(&self, node_id: H256, sender_ip: IpAddr) -> ContactValidation {
1366        let Some(contact) = self.get_contact(&node_id) else {
1367            return ContactValidation::UnknownContact;
1368        };
1369        if !contact.was_validated() {
1370            return ContactValidation::InvalidContact;
1371        }
1372
1373        // Check that the IP address from which we receive the request matches the one we have stored
1374        // to prevent amplification attacks.
1375        if sender_ip != contact.node.ip {
1376            return ContactValidation::IpMismatch;
1377        }
1378        ContactValidation::Valid(Box::new(contact.clone()))
1379    }
1380
1381    /// Get closest nodes using raw XOR distance for accurate ordering.
1382    fn do_get_closest_nodes(&self, node_id: H256) -> Vec<Node> {
1383        #[cfg(feature = "metrics")]
1384        let scan_start = std::time::Instant::now();
1385
1386        let mut nodes: Vec<(Node, H256)> = vec![];
1387
1388        for (contact_id, contact) in self.iter_contacts() {
1389            let dist = xor_distance(&node_id, contact_id);
1390            if nodes.len() < MAX_NODES_IN_NEIGHBORS_PACKET {
1391                nodes.push((contact.node.clone(), dist));
1392            } else if let Some((farthest_idx, _)) =
1393                nodes.iter().enumerate().max_by_key(|(_, (_, d))| *d)
1394                && dist < nodes[farthest_idx].1
1395            {
1396                nodes[farthest_idx] = (contact.node.clone(), dist);
1397            }
1398        }
1399
1400        #[cfg(feature = "metrics")]
1401        {
1402            use ethrex_metrics::p2p::METRICS_P2P;
1403            METRICS_P2P.observe_iter_contacts_duration(scan_start.elapsed().as_secs_f64());
1404        }
1405
1406        nodes.into_iter().map(|(node, _)| node).collect()
1407    }
1408
1409    /// Get nodes at distances for discv5 (returns Vec<NodeRecord>).
1410    /// Uses the discv5 spec log-distance: `floor(log2(XOR))` for non-zero XOR.
1411    /// Distance 0 is reserved for the local node itself (handled by the caller),
1412    /// so contacts start at distance >= 1.
1413    fn do_get_nodes_at_distances(&self, distances: &[u32]) -> Vec<NodeRecord> {
1414        self.iter_contacts()
1415            .filter_map(|(contact_id, contact)| {
1416                let dist = distance(&self.local_node_id, contact_id) as u32;
1417                if distances.contains(&dist) {
1418                    contact.record.clone()
1419                } else {
1420                    None
1421                }
1422            })
1423            .take(MAX_ENRS_PER_FINDNODE_RESPONSE)
1424            .collect()
1425    }
1426
1427    async fn do_new_contacts(&mut self, nodes: Vec<Node>, protocol: DiscoveryProtocol) {
1428        for node in nodes {
1429            let node_id = node.node_id();
1430            if node_id == self.local_node_id {
1431                continue;
1432            }
1433            #[cfg(feature = "metrics")]
1434            let insert_start = std::time::Instant::now();
1435
1436            // Always add to the connection pool (regardless of k-bucket capacity)
1437            self.insert_to_connection_pool(node_id, node.clone());
1438
1439            if self.contact_exists(&node_id) {
1440                // Contact already exists (main or replacement list), update protocol
1441                if let Some(contact) = self.get_contact_or_replacement_mut(&node_id) {
1442                    contact.add_protocol(protocol);
1443                }
1444            } else {
1445                let contact = Contact::new(node, protocol);
1446                self.insert_contact(node_id, contact);
1447                METRICS.record_new_discovery().await;
1448            }
1449
1450            #[cfg(feature = "metrics")]
1451            {
1452                use ethrex_metrics::p2p::METRICS_P2P;
1453                METRICS_P2P.observe_insert_contact_duration(insert_start.elapsed().as_secs_f64());
1454            }
1455        }
1456    }
1457
1458    async fn do_new_contact_records(&mut self, node_records: Vec<NodeRecord>) {
1459        for node_record in node_records {
1460            if !node_record.verify_signature() {
1461                continue;
1462            }
1463            if let Ok(node) = Node::from_enr(&node_record) {
1464                let node_id = node.node_id();
1465                if node_id == self.local_node_id {
1466                    continue;
1467                }
1468
1469                // Always add to the connection pool (regardless of k-bucket capacity)
1470                self.insert_to_connection_pool(node_id, node.clone());
1471
1472                if self.contact_exists(&node_id) {
1473                    // Check if we need to evaluate fork_id before taking
1474                    // the mutable borrow.
1475                    let should_update = self
1476                        .get_contact_or_replacement(&node_id)
1477                        .map(|c| match c.record.as_ref() {
1478                            None => true,
1479                            Some(r) => node_record.seq > r.seq,
1480                        })
1481                        .unwrap_or(false);
1482                    let is_fork_id_valid = if should_update {
1483                        Self::evaluate_fork_id(&node_record, &self.store).await
1484                    } else {
1485                        None
1486                    };
1487                    if let Some(contact) = self.get_contact_or_replacement_mut(&node_id) {
1488                        contact.add_protocol(DiscoveryProtocol::Discv5);
1489                        if should_update {
1490                            if contact.node.ip != node.ip || contact.node.udp_port != node.udp_port
1491                            {
1492                                contact.validation_timestamp = None;
1493                                contact.ping_id = None;
1494                            }
1495                            contact.node = node;
1496                            contact.record = Some(node_record);
1497                            contact.is_fork_id_valid = is_fork_id_valid;
1498                        }
1499                    }
1500                } else {
1501                    let is_fork_id_valid = Self::evaluate_fork_id(&node_record, &self.store).await;
1502                    let mut contact = Contact::new(node, DiscoveryProtocol::Discv5);
1503                    contact.is_fork_id_valid = is_fork_id_valid;
1504                    contact.record = Some(node_record);
1505                    self.insert_contact(node_id, contact);
1506                    METRICS.record_new_discovery().await;
1507                }
1508            }
1509        }
1510    }
1511
1512    async fn evaluate_fork_id(record: &NodeRecord, store: &Store) -> Option<bool> {
1513        if let Some(remote_fork_id) = record.get_fork_id() {
1514            backend::is_fork_id_valid(store, remote_fork_id)
1515                .await
1516                .ok()
1517                .or(Some(false))
1518        } else {
1519            Some(false)
1520        }
1521    }
1522
1523    fn do_peer_count_by_capabilities(&self, capabilities: Vec<Capability>) -> usize {
1524        self.peers
1525            .values()
1526            .filter(|peer_data| {
1527                capabilities
1528                    .iter()
1529                    .any(|cap| peer_data.supported_capabilities.contains(cap))
1530            })
1531            .count()
1532    }
1533
1534    fn do_get_random_peer(&self, capabilities: Vec<Capability>) -> Option<(H256, PeerConnection)> {
1535        let peers: Vec<(H256, &PeerConnection, i64)> = self
1536            .peers
1537            .iter()
1538            .filter_map(|(node_id, peer_data)| {
1539                if !capabilities
1540                    .iter()
1541                    .any(|cap| peer_data.supported_capabilities.contains(cap))
1542                {
1543                    return None;
1544                }
1545                peer_data
1546                    .connection
1547                    .as_ref()
1548                    .map(|connection| (*node_id, connection, peer_data.score))
1549            })
1550            .collect();
1551        if peers.is_empty() {
1552            return None;
1553        }
1554        // Weight by score: maps [-150, 50] to [1, 201] so bad peers are unlikely but not excluded
1555        let weights: Vec<u64> = peers
1556            .iter()
1557            .map(|(_, _, score)| (score.max(&MIN_SCORE_CRITICAL) - MIN_SCORE_CRITICAL + 1) as u64)
1558            .collect();
1559        let dist = WeightedIndex::new(&weights).ok()?;
1560        let idx = dist.sample(&mut rand::rngs::OsRng);
1561        Some((peers[idx].0, peers[idx].1.clone()))
1562    }
1563
1564    fn is_validation_needed(contact: &Contact, revalidation_interval: Duration) -> bool {
1565        if contact.disposable {
1566            return false;
1567        }
1568
1569        let sent_ping_ttl = Duration::from_secs(30);
1570
1571        if contact.has_pending_ping() {
1572            // Outstanding ping — only re-ping if it timed out (stale).
1573            contact
1574                .validation_timestamp
1575                .map(|ts| Instant::now().saturating_duration_since(ts) > sent_ping_ttl)
1576                .unwrap_or(false)
1577        } else {
1578            // No pending ping — check if never validated or validation expired.
1579            !contact.was_validated()
1580                || contact
1581                    .validation_timestamp
1582                    .map(|ts| Instant::now().saturating_duration_since(ts) > revalidation_interval)
1583                    .unwrap_or(false)
1584        }
1585    }
1586}
1587
1588pub type PeerTable = ActorRef<PeerTableServer>;
1589
1590#[cfg(test)]
1591mod tests {
1592    use super::*;
1593    use ethrex_common::H512;
1594    use std::net::Ipv4Addr;
1595
1596    /// Helper: build a dummy contact with a unique node derived from `seed`.
1597    fn dummy_contact(seed: u8) -> (H256, Contact) {
1598        let pk = H512::from_low_u64_be(seed as u64 + 1);
1599        let node = Node::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, seed)), 30303, 30303, pk);
1600        let node_id = node.node_id();
1601        let contact = Contact::new(node, DiscoveryProtocol::Discv4);
1602        (node_id, contact)
1603    }
1604
1605    // --- KBucket::insert ---
1606
1607    #[test]
1608    fn insert_into_empty_bucket() {
1609        let mut bucket = KBucket::default();
1610        let (id, contact) = dummy_contact(1);
1611        assert!(bucket.insert(id, contact));
1612        assert_eq!(bucket.contacts.len(), 1);
1613        assert!(bucket.replacements.is_empty());
1614    }
1615
1616    #[test]
1617    fn insert_fills_bucket_then_goes_to_replacements() {
1618        let mut bucket = KBucket::default();
1619
1620        // Fill the main list to capacity.
1621        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1622            let (id, contact) = dummy_contact(i);
1623            assert!(bucket.insert(id, contact), "contact {i} should go to main");
1624        }
1625        assert_eq!(bucket.contacts.len(), MAX_NODES_PER_BUCKET);
1626
1627        // The next insert should go to the replacement list.
1628        let (id, contact) = dummy_contact(200);
1629        assert!(!bucket.insert(id, contact));
1630        assert_eq!(bucket.contacts.len(), MAX_NODES_PER_BUCKET);
1631        assert_eq!(bucket.replacements.len(), 1);
1632    }
1633
1634    // --- KBucket::contains ---
1635
1636    #[test]
1637    fn contains_checks_main_and_replacement() {
1638        let mut bucket = KBucket::default();
1639
1640        let (id_main, contact_main) = dummy_contact(1);
1641        bucket.insert(id_main, contact_main);
1642        assert!(bucket.contains(&id_main));
1643
1644        // Fill bucket so next goes to replacement.
1645        for i in 2..=(MAX_NODES_PER_BUCKET as u8) {
1646            let (id, c) = dummy_contact(i);
1647            bucket.insert(id, c);
1648        }
1649        let (id_repl, contact_repl) = dummy_contact(100);
1650        bucket.insert(id_repl, contact_repl);
1651
1652        assert!(bucket.contains(&id_repl));
1653        assert!(!bucket.contains(&H256::zero()));
1654    }
1655
1656    // --- KBucket::get / get_any ---
1657
1658    #[test]
1659    fn get_returns_main_list_only() {
1660        let mut bucket = KBucket::default();
1661        let (id, contact) = dummy_contact(1);
1662        bucket.insert(id, contact);
1663        assert!(bucket.get(&id).is_some());
1664        assert!(bucket.get(&H256::zero()).is_none());
1665    }
1666
1667    #[test]
1668    fn get_any_returns_from_replacement() {
1669        let mut bucket = KBucket::default();
1670        // Fill main list.
1671        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1672            let (id, c) = dummy_contact(i);
1673            bucket.insert(id, c);
1674        }
1675        // Insert into replacements.
1676        let (id_repl, c_repl) = dummy_contact(200);
1677        bucket.insert(id_repl, c_repl);
1678
1679        assert!(bucket.get(&id_repl).is_none()); // not in main
1680        assert!(bucket.get_any(&id_repl).is_some()); // found via replacement
1681    }
1682
1683    // --- KBucket::remove_and_promote ---
1684
1685    #[test]
1686    fn remove_and_promote_with_replacement() {
1687        let mut bucket = KBucket::default();
1688
1689        // Fill main list.
1690        let mut main_ids = Vec::new();
1691        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1692            let (id, c) = dummy_contact(i);
1693            main_ids.push(id);
1694            bucket.insert(id, c);
1695        }
1696
1697        // Add a replacement.
1698        let (repl_id, repl_contact) = dummy_contact(200);
1699        bucket.insert(repl_id, repl_contact);
1700
1701        // Remove a main contact — the replacement should be promoted.
1702        let promoted = bucket.remove_and_promote(&main_ids[0]);
1703        assert_eq!(promoted, Some(repl_id));
1704        assert_eq!(bucket.contacts.len(), MAX_NODES_PER_BUCKET);
1705        assert!(bucket.replacements.is_empty());
1706        assert!(!bucket.contains(&main_ids[0]));
1707        assert!(bucket.contains(&repl_id));
1708    }
1709
1710    #[test]
1711    fn remove_and_promote_without_replacement() {
1712        let mut bucket = KBucket::default();
1713        let (id, c) = dummy_contact(1);
1714        bucket.insert(id, c);
1715
1716        let promoted = bucket.remove_and_promote(&id);
1717        assert!(promoted.is_none());
1718        assert!(bucket.contacts.is_empty());
1719    }
1720
1721    #[test]
1722    fn remove_nonexistent_returns_none() {
1723        let mut bucket = KBucket::default();
1724        assert!(bucket.remove_and_promote(&H256::zero()).is_none());
1725    }
1726
1727    // --- Replacement eviction ---
1728
1729    #[test]
1730    fn replacement_list_evicts_oldest_when_full() {
1731        let mut bucket = KBucket::default();
1732        // Fill main list.
1733        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1734            let (id, c) = dummy_contact(i);
1735            bucket.insert(id, c);
1736        }
1737
1738        // Fill replacement list beyond capacity.
1739        let mut repl_ids = Vec::new();
1740        for i in 0..(MAX_REPLACEMENTS_PER_BUCKET + 2) as u8 {
1741            let seed = 100 + i;
1742            let (id, c) = dummy_contact(seed);
1743            repl_ids.push(id);
1744            bucket.insert(id, c);
1745        }
1746
1747        assert_eq!(bucket.replacements.len(), MAX_REPLACEMENTS_PER_BUCKET);
1748        // The oldest two should have been evicted.
1749        assert!(!bucket.contains(&repl_ids[0]));
1750        assert!(!bucket.contains(&repl_ids[1]));
1751        // The most recent ones should still be there.
1752        assert!(bucket.contains(repl_ids.last().unwrap()));
1753    }
1754
1755    // --- bucket_index ---
1756
1757    #[test]
1758    fn bucket_index_self_is_none() {
1759        let id = H256::random();
1760        assert_eq!(bucket_index(&id, &id), None);
1761    }
1762
1763    #[test]
1764    fn bucket_index_minimal_distance() {
1765        let local = H256::zero();
1766        // XOR distance = 1 → highest bit is bit 0 → bucket 0
1767        let mut remote = H256::zero();
1768        remote.0[31] = 1;
1769        assert_eq!(bucket_index(&local, &remote), Some(0));
1770    }
1771
1772    #[test]
1773    fn bucket_index_maximal_distance() {
1774        let local = H256::zero();
1775        // XOR distance has highest bit at position 255 → bucket 255
1776        let mut remote = H256::zero();
1777        remote.0[0] = 0x80;
1778        assert_eq!(bucket_index(&local, &remote), Some(255));
1779    }
1780}