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    ) -> Result<(), ActorError>;
400    fn set_session_info(&self, node_id: H256, session: Session) -> Result<(), ActorError>;
401    fn remove_peer(&self, node_id: H256) -> Result<(), ActorError>;
402    fn dec_requests(&self, node_id: H256) -> Result<(), ActorError>;
403    fn set_unwanted(&self, node_id: H256) -> Result<(), ActorError>;
404    fn set_is_fork_id_valid(&self, node_id: H256, valid: bool) -> Result<(), ActorError>;
405    fn record_success(&self, node_id: H256) -> Result<(), ActorError>;
406    fn record_failure(&self, node_id: H256) -> Result<(), ActorError>;
407    fn record_critical_failure(&self, node_id: H256) -> Result<(), ActorError>;
408    fn record_ping_sent(&self, node_id: H256, ping_id: Bytes) -> Result<(), ActorError>;
409    fn record_pong_received(&self, node_id: H256, ping_id: Bytes) -> Result<(), ActorError>;
410    fn record_enr_request_sent(&self, node_id: H256, request_hash: H256) -> Result<(), ActorError>;
411    fn record_enr_response_received(
412        &self,
413        node_id: H256,
414        request_hash: H256,
415        record: NodeRecord,
416    ) -> Result<(), ActorError>;
417    fn set_disposable(&self, node_id: H256) -> Result<(), ActorError>;
418    fn mark_knows_us(&self, node_id: H256) -> Result<(), ActorError>;
419    fn prune_table(&self) -> Result<(), ActorError>;
420    fn shutdown(&self) -> Result<(), ActorError>;
421
422    // Request (call) methods
423    fn peer_count(&self) -> Response<usize>;
424    fn peer_count_by_capabilities(&self, capabilities: Vec<Capability>) -> Response<usize>;
425    fn target_reached(&self) -> Response<bool>;
426    fn target_peers_reached(&self) -> Response<bool>;
427    fn target_peers_completion(&self) -> Response<f64>;
428    fn get_contact_to_initiate(&self) -> Response<Option<Box<Contact>>>;
429    fn get_contact_for_enr_lookup(&self) -> Response<Option<Box<Contact>>>;
430    fn get_closest_from_pool(&self, target: H256, count: usize) -> Response<Vec<(H256, Node)>>;
431    fn get_contact(&self, node_id: H256) -> Response<Option<Box<Contact>>>;
432    fn get_contact_to_revalidate(
433        &self,
434        revalidation_interval: Duration,
435        protocol: DiscoveryProtocol,
436    ) -> Response<Option<Box<Contact>>>;
437    fn get_best_peer(
438        &self,
439        capabilities: Vec<Capability>,
440    ) -> Response<Option<(H256, PeerConnection, RequestPermit)>>;
441    fn get_best_peer_excluding(
442        &self,
443        capabilities: Vec<Capability>,
444        excluded: Vec<H256>,
445    ) -> Response<Option<(H256, PeerConnection, RequestPermit)>>;
446    fn get_best_n_peers(
447        &self,
448        capabilities: Vec<Capability>,
449        n: usize,
450    ) -> Response<Vec<(H256, PeerConnection, RequestPermit)>>;
451    /// Read-only predicate: is there any eligible peer matching `capabilities`?
452    /// Does not reserve a slot; use for capacity/rotation probes only.
453    fn has_eligible_peer(&self, capabilities: Vec<Capability>) -> Response<bool>;
454    fn get_score(&self, node_id: H256) -> Response<i64>;
455    fn get_connected_nodes(&self) -> Response<Vec<Node>>;
456    fn get_peers_with_capabilities(&self)
457    -> Response<Vec<(H256, PeerConnection, Vec<Capability>)>>;
458    fn insert_if_new(&self, node: Node, protocol: DiscoveryProtocol) -> Response<bool>;
459    fn validate_contact(&self, node_id: H256, sender_ip: IpAddr) -> Response<ContactValidation>;
460    fn get_closest_nodes(&self, node_id: H256) -> Response<Vec<Node>>;
461    fn get_nodes_at_distances(&self, distances: Vec<u32>) -> Response<Vec<NodeRecord>>;
462    fn get_peers_data(&self) -> Response<Vec<PeerData>>;
463    fn get_random_peer(
464        &self,
465        capabilities: Vec<Capability>,
466    ) -> Response<Option<(H256, PeerConnection, RequestPermit)>>;
467    fn get_session_info(&self, node_id: H256) -> Response<Option<Session>>;
468    fn get_peer_diagnostics(&self) -> Response<Vec<PeerDiagnostics>>;
469    fn get_peer_connection(&self, peer_id: H256) -> Response<Option<PeerConnection>>;
470}
471
472#[derive(Debug)]
473pub struct PeerTableServer {
474    local_node_id: H256,
475    buckets: Vec<KBucket>,
476    peers: IndexMap<H256, PeerData>,
477    already_tried_peers: FxHashSet<H256>,
478    target_peers: usize,
479    store: Store,
480    /// Standalone session store, independent of contacts.
481    /// Allows sessions to be stored even before the contact's ENR is known/parseable.
482    sessions: FxHashMap<H256, Session>,
483    /// Flat pool of discovered contacts for RLPx connection initiation.
484    /// Decoupled from the k-bucket routing table so that connection initiation
485    /// has access to a much larger candidate pool than the k-bucket structure
486    /// allows (k-buckets: 256 × 16 = 4,096 max; this pool: up to 50,000).
487    /// K-buckets are still used for all Kademlia protocol operations.
488    connection_pool: IndexMap<H256, Node>,
489}
490
491#[actor(protocol = PeerTableServerProtocol)]
492impl PeerTableServer {
493    pub fn spawn(local_node_id: H256, target_peers: usize, store: Store) -> PeerTable {
494        PeerTableServer::new(local_node_id, target_peers, store).start()
495    }
496
497    pub(crate) fn new(local_node_id: H256, target_peers: usize, store: Store) -> Self {
498        Self {
499            local_node_id,
500            buckets: vec![KBucket::default(); NUMBER_OF_BUCKETS],
501            peers: Default::default(),
502            already_tried_peers: Default::default(),
503            target_peers,
504            store,
505            sessions: Default::default(),
506            connection_pool: IndexMap::with_capacity(MAX_CONNECTION_POOL_SIZE),
507        }
508    }
509
510    #[started]
511    async fn started(&mut self, ctx: &Context<Self>) {
512        send_message_on(
513            ctx.clone(),
514            tokio::signal::ctrl_c(),
515            peer_table_server_protocol::Shutdown,
516        );
517    }
518
519    // === Send handlers ===
520
521    #[send_handler]
522    async fn handle_new_contacts(
523        &mut self,
524        msg: peer_table_server_protocol::NewContacts,
525        _ctx: &Context<Self>,
526    ) {
527        self.do_new_contacts(msg.nodes, msg.protocol).await;
528    }
529
530    #[send_handler]
531    async fn handle_new_contact_records(
532        &mut self,
533        msg: peer_table_server_protocol::NewContactRecords,
534        _ctx: &Context<Self>,
535    ) {
536        self.do_new_contact_records(msg.node_records).await;
537    }
538
539    #[send_handler]
540    async fn handle_new_connected_peer(
541        &mut self,
542        msg: peer_table_server_protocol::NewConnectedPeer,
543        _ctx: &Context<Self>,
544    ) {
545        let new_peer_id = msg.node.node_id();
546        let new_peer = PeerData::new(msg.node, None, Some(msg.connection), msg.capabilities);
547        self.peers.insert(new_peer_id, new_peer);
548    }
549
550    #[send_handler]
551    async fn handle_set_session_info(
552        &mut self,
553        msg: peer_table_server_protocol::SetSessionInfo,
554        _ctx: &Context<Self>,
555    ) {
556        // Store in the standalone sessions map (always succeeds, no contact required).
557        self.sessions.insert(msg.node_id, msg.session.clone());
558        // Also update the contact's cached session if the contact exists.
559        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
560            contact.session = Some(msg.session);
561        }
562    }
563
564    #[send_handler]
565    async fn handle_remove_peer(
566        &mut self,
567        msg: peer_table_server_protocol::RemovePeer,
568        _ctx: &Context<Self>,
569    ) {
570        self.peers.swap_remove(&msg.node_id);
571        // Also drop the standalone discv5 session so it isn't retained after the peer leaves
572        // (the sessions map was previously insert-only and grew per handshake).
573        self.sessions.remove(&msg.node_id);
574    }
575
576    #[send_handler]
577    async fn handle_dec_requests(
578        &mut self,
579        msg: peer_table_server_protocol::DecRequests,
580        _ctx: &Context<Self>,
581    ) {
582        self.peers.entry(msg.node_id).and_modify(|peer_data| {
583            if peer_data.requests <= 0 {
584                // Expected under the reconnect race (stale permit fires
585                // after remove_peer + new_connected_peer), self-heals.
586                // Otherwise points to a bookkeeping bug worth chasing.
587                tracing::debug!(
588                    peer_id = ?msg.node_id,
589                    requests = peer_data.requests,
590                    "dec_requests with counter already <= 0",
591                );
592            }
593            peer_data.requests = peer_data.requests.saturating_sub(1).max(0)
594        });
595    }
596
597    #[send_handler]
598    async fn handle_set_unwanted(
599        &mut self,
600        msg: peer_table_server_protocol::SetUnwanted,
601        _ctx: &Context<Self>,
602    ) {
603        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
604            contact.unwanted = true;
605        }
606    }
607
608    #[send_handler]
609    async fn handle_set_is_fork_id_valid(
610        &mut self,
611        msg: peer_table_server_protocol::SetIsForkIdValid,
612        _ctx: &Context<Self>,
613    ) {
614        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
615            contact.is_fork_id_valid = Some(msg.valid);
616        }
617    }
618
619    #[send_handler]
620    async fn handle_record_success(
621        &mut self,
622        msg: peer_table_server_protocol::RecordSuccess,
623        _ctx: &Context<Self>,
624    ) {
625        let now = std::time::SystemTime::now()
626            .duration_since(std::time::UNIX_EPOCH)
627            .unwrap_or_default()
628            .as_secs();
629        self.peers.entry(msg.node_id).and_modify(|peer_data| {
630            peer_data.score = (peer_data.score + 1).min(MAX_SCORE);
631            peer_data.last_response_time = Some(now);
632        });
633    }
634
635    #[send_handler]
636    async fn handle_record_failure(
637        &mut self,
638        msg: peer_table_server_protocol::RecordFailure,
639        _ctx: &Context<Self>,
640    ) {
641        self.peers
642            .entry(msg.node_id)
643            .and_modify(|peer_data| peer_data.score = (peer_data.score - 1).max(MIN_SCORE));
644    }
645
646    #[send_handler]
647    async fn handle_record_critical_failure(
648        &mut self,
649        msg: peer_table_server_protocol::RecordCriticalFailure,
650        _ctx: &Context<Self>,
651    ) {
652        self.peers
653            .entry(msg.node_id)
654            .and_modify(|peer_data| peer_data.score = MIN_SCORE_CRITICAL);
655    }
656
657    #[send_handler]
658    async fn handle_record_ping_sent(
659        &mut self,
660        msg: peer_table_server_protocol::RecordPingSent,
661        _ctx: &Context<Self>,
662    ) {
663        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
664            contact.record_ping_sent(msg.ping_id);
665        }
666    }
667
668    #[send_handler]
669    async fn handle_record_pong_received(
670        &mut self,
671        msg: peer_table_server_protocol::RecordPongReceived,
672        _ctx: &Context<Self>,
673    ) {
674        if let Some(contact) = self.get_contact_mut(&msg.node_id)
675            && contact
676                .ping_id
677                .as_ref()
678                .map(|value| *value == msg.ping_id)
679                .unwrap_or(false)
680        {
681            contact.ping_id = None;
682        }
683    }
684
685    #[send_handler]
686    async fn handle_record_enr_request_sent(
687        &mut self,
688        msg: peer_table_server_protocol::RecordEnrRequestSent,
689        _ctx: &Context<Self>,
690    ) {
691        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
692            contact.record_enr_request_sent(msg.request_hash);
693        }
694    }
695
696    #[send_handler]
697    async fn handle_record_enr_response_received(
698        &mut self,
699        msg: peer_table_server_protocol::RecordEnrResponseReceived,
700        _ctx: &Context<Self>,
701    ) {
702        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
703            contact.record_enr_response_received(msg.request_hash, msg.record);
704        }
705    }
706
707    #[send_handler]
708    async fn handle_set_disposable(
709        &mut self,
710        msg: peer_table_server_protocol::SetDisposable,
711        _ctx: &Context<Self>,
712    ) {
713        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
714            contact.disposable = true;
715        }
716    }
717
718    #[send_handler]
719    async fn handle_mark_knows_us(
720        &mut self,
721        msg: peer_table_server_protocol::MarkKnowsUs,
722        _ctx: &Context<Self>,
723    ) {
724        if let Some(contact) = self.get_contact_mut(&msg.node_id) {
725            contact.knows_us = true;
726        }
727    }
728
729    #[send_handler]
730    async fn handle_prune_table(
731        &mut self,
732        _msg: peer_table_server_protocol::PruneTable,
733        _ctx: &Context<Self>,
734    ) {
735        self.prune();
736    }
737
738    #[send_handler]
739    async fn handle_shutdown(
740        &mut self,
741        _msg: peer_table_server_protocol::Shutdown,
742        ctx: &Context<Self>,
743    ) {
744        ctx.stop();
745    }
746
747    // === Request handlers ===
748
749    #[request_handler]
750    async fn handle_peer_count(
751        &mut self,
752        _msg: peer_table_server_protocol::PeerCount,
753        _ctx: &Context<Self>,
754    ) -> usize {
755        self.peers.len()
756    }
757
758    #[request_handler]
759    async fn handle_peer_count_by_capabilities(
760        &mut self,
761        msg: peer_table_server_protocol::PeerCountByCapabilities,
762        _ctx: &Context<Self>,
763    ) -> usize {
764        self.do_peer_count_by_capabilities(msg.capabilities)
765    }
766
767    #[request_handler]
768    async fn handle_target_reached(
769        &mut self,
770        _msg: peer_table_server_protocol::TargetReached,
771        _ctx: &Context<Self>,
772    ) -> bool {
773        self.peers.len() >= self.target_peers
774    }
775
776    #[request_handler]
777    async fn handle_target_peers_reached(
778        &mut self,
779        _msg: peer_table_server_protocol::TargetPeersReached,
780        _ctx: &Context<Self>,
781    ) -> bool {
782        self.peers.len() >= self.target_peers
783    }
784
785    #[request_handler]
786    async fn handle_target_peers_completion(
787        &mut self,
788        _msg: peer_table_server_protocol::TargetPeersCompletion,
789        _ctx: &Context<Self>,
790    ) -> f64 {
791        self.peers.len() as f64 / self.target_peers as f64
792    }
793
794    #[request_handler]
795    async fn handle_get_contact_to_initiate(
796        &mut self,
797        _msg: peer_table_server_protocol::GetContactToInitiate,
798        _ctx: &Context<Self>,
799    ) -> Option<Box<Contact>> {
800        self.do_get_contact_to_initiate().map(Box::new)
801    }
802
803    #[request_handler]
804    async fn handle_get_closest_from_pool(
805        &mut self,
806        msg: peer_table_server_protocol::GetClosestFromPool,
807        _ctx: &Context<Self>,
808    ) -> Vec<(H256, Node)> {
809        self.do_get_closest_from_pool(msg.target, msg.count)
810    }
811
812    #[request_handler]
813    async fn handle_get_contact_for_enr_lookup(
814        &mut self,
815        _msg: peer_table_server_protocol::GetContactForEnrLookup,
816        _ctx: &Context<Self>,
817    ) -> Option<Box<Contact>> {
818        self.do_get_contact_for_enr_lookup().map(Box::new)
819    }
820
821    #[request_handler]
822    async fn handle_get_contact(
823        &mut self,
824        msg: peer_table_server_protocol::GetContact,
825        _ctx: &Context<Self>,
826    ) -> Option<Box<Contact>> {
827        self.get_contact(&msg.node_id).cloned().map(Box::new)
828    }
829
830    #[request_handler]
831    async fn handle_get_contact_to_revalidate(
832        &mut self,
833        msg: peer_table_server_protocol::GetContactToRevalidate,
834        _ctx: &Context<Self>,
835    ) -> Option<Box<Contact>> {
836        self.do_get_contact_to_revalidate(msg.revalidation_interval, msg.protocol)
837    }
838
839    #[request_handler]
840    async fn handle_get_best_peer(
841        &mut self,
842        msg: peer_table_server_protocol::GetBestPeer,
843        ctx: &Context<Self>,
844    ) -> Option<(H256, PeerConnection, RequestPermit)> {
845        let (peer_id, conn) = self.do_get_best_peer(&msg.capabilities)?;
846        self.peers
847            .get_mut(&peer_id)
848            .expect("peer returned by do_get_best_peer must be present in self.peers")
849            .requests += 1;
850        Some((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)))
851    }
852
853    #[request_handler]
854    async fn handle_get_best_peer_excluding(
855        &mut self,
856        msg: peer_table_server_protocol::GetBestPeerExcluding,
857        ctx: &Context<Self>,
858    ) -> Option<(H256, PeerConnection, RequestPermit)> {
859        let (peer_id, conn) = self.do_get_best_peer_excluding(&msg.capabilities, &msg.excluded)?;
860        self.peers
861            .get_mut(&peer_id)
862            .expect("peer returned by do_get_best_peer_excluding must be present in self.peers")
863            .requests += 1;
864        Some((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)))
865    }
866
867    #[request_handler]
868    async fn handle_get_best_n_peers(
869        &mut self,
870        msg: peer_table_server_protocol::GetBestNPeers,
871        ctx: &Context<Self>,
872    ) -> Vec<(H256, PeerConnection, RequestPermit)> {
873        let picks = self.do_get_best_n_peers(&msg.capabilities, msg.n);
874        let mut out = Vec::with_capacity(picks.len());
875        for (peer_id, conn) in picks {
876            self.peers
877                .get_mut(&peer_id)
878                .expect("peer returned by do_get_best_n_peers must be present in self.peers")
879                .requests += 1;
880            out.push((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)));
881        }
882        out
883    }
884
885    #[request_handler]
886    async fn handle_has_eligible_peer(
887        &mut self,
888        msg: peer_table_server_protocol::HasEligiblePeer,
889        _ctx: &Context<Self>,
890    ) -> bool {
891        self.peers.values().any(|peer_data| {
892            peer_data.connection.is_some()
893                && self.can_try_more_requests(&peer_data.score, &peer_data.requests)
894                && msg
895                    .capabilities
896                    .iter()
897                    .any(|cap| peer_data.supported_capabilities.contains(cap))
898        })
899    }
900
901    #[request_handler]
902    async fn handle_get_score(
903        &mut self,
904        msg: peer_table_server_protocol::GetScore,
905        _ctx: &Context<Self>,
906    ) -> i64 {
907        self.peers
908            .get(&msg.node_id)
909            .map(|peer_data| peer_data.score)
910            .unwrap_or_default()
911    }
912
913    #[request_handler]
914    async fn handle_get_connected_nodes(
915        &mut self,
916        _msg: peer_table_server_protocol::GetConnectedNodes,
917        _ctx: &Context<Self>,
918    ) -> Vec<Node> {
919        self.peers
920            .values()
921            .map(|peer_data| peer_data.node.clone())
922            .collect()
923    }
924
925    #[request_handler]
926    async fn handle_get_peers_with_capabilities(
927        &mut self,
928        _msg: peer_table_server_protocol::GetPeersWithCapabilities,
929        _ctx: &Context<Self>,
930    ) -> Vec<(H256, PeerConnection, Vec<Capability>)> {
931        self.peers
932            .iter()
933            .filter_map(|(peer_id, peer_data)| {
934                peer_data.connection.clone().map(|connection| {
935                    (
936                        *peer_id,
937                        connection,
938                        peer_data.supported_capabilities.clone(),
939                    )
940                })
941            })
942            .collect()
943    }
944
945    #[request_handler]
946    async fn handle_insert_if_new(
947        &mut self,
948        msg: peer_table_server_protocol::InsertIfNew,
949        _ctx: &Context<Self>,
950    ) -> bool {
951        let node_id = msg.node.node_id();
952        // Always add to the connection pool
953        self.insert_to_connection_pool(node_id, msg.node.clone());
954        if self.contact_exists(&node_id) {
955            return false;
956        }
957        let contact = Contact::new(msg.node, msg.protocol);
958        // Return true for any genuinely new node, even if it overflows to the
959        // replacement list.  This ensures the caller sends a reciprocal ping
960        // which establishes the bond needed for FindNode validation.
961        self.insert_contact(node_id, contact);
962        METRICS.record_new_discovery().await;
963        true
964    }
965
966    #[request_handler]
967    async fn handle_validate_contact(
968        &mut self,
969        msg: peer_table_server_protocol::ValidateContact,
970        _ctx: &Context<Self>,
971    ) -> ContactValidation {
972        self.do_validate_contact(msg.node_id, msg.sender_ip)
973    }
974
975    #[request_handler]
976    async fn handle_get_closest_nodes(
977        &mut self,
978        msg: peer_table_server_protocol::GetClosestNodes,
979        _ctx: &Context<Self>,
980    ) -> Vec<Node> {
981        self.do_get_closest_nodes(msg.node_id)
982    }
983
984    #[request_handler]
985    async fn handle_get_nodes_at_distances(
986        &mut self,
987        msg: peer_table_server_protocol::GetNodesAtDistances,
988        _ctx: &Context<Self>,
989    ) -> Vec<NodeRecord> {
990        self.do_get_nodes_at_distances(&msg.distances)
991    }
992
993    #[request_handler]
994    async fn handle_get_peers_data(
995        &mut self,
996        _msg: peer_table_server_protocol::GetPeersData,
997        _ctx: &Context<Self>,
998    ) -> Vec<PeerData> {
999        self.peers.values().cloned().collect()
1000    }
1001
1002    #[request_handler]
1003    async fn handle_get_random_peer(
1004        &mut self,
1005        msg: peer_table_server_protocol::GetRandomPeer,
1006        ctx: &Context<Self>,
1007    ) -> Option<(H256, PeerConnection, RequestPermit)> {
1008        let (peer_id, conn) = self.do_get_random_peer(msg.capabilities)?;
1009        self.peers
1010            .get_mut(&peer_id)
1011            .expect("peer returned by do_get_random_peer must be present in self.peers")
1012            .requests += 1;
1013        Some((peer_id, conn, RequestPermit::new(ctx.actor_ref(), peer_id)))
1014    }
1015
1016    #[request_handler]
1017    async fn handle_get_session_info(
1018        &mut self,
1019        msg: peer_table_server_protocol::GetSessionInfo,
1020        _ctx: &Context<Self>,
1021    ) -> Option<Session> {
1022        // Check standalone sessions map first; fall back to contact.session.
1023        self.sessions
1024            .get(&msg.node_id)
1025            .cloned()
1026            .or_else(|| self.get_contact(&msg.node_id)?.session.clone())
1027    }
1028
1029    #[request_handler]
1030    async fn handle_get_peer_connection(
1031        &mut self,
1032        msg: peer_table_server_protocol::GetPeerConnection,
1033        _ctx: &Context<Self>,
1034    ) -> Option<PeerConnection> {
1035        self.peers
1036            .get(&msg.peer_id)
1037            .and_then(|peer_data| peer_data.connection.clone())
1038    }
1039
1040    #[request_handler]
1041    async fn handle_get_peer_diagnostics(
1042        &mut self,
1043        _msg: peer_table_server_protocol::GetPeerDiagnostics,
1044        _ctx: &Context<Self>,
1045    ) -> Vec<PeerDiagnostics> {
1046        self.peers
1047            .iter()
1048            .map(|(id, peer_data)| PeerDiagnostics {
1049                peer_id: *id,
1050                score: peer_data.score,
1051                inflight_requests: peer_data.requests,
1052                eligible: self.can_try_more_requests(&peer_data.score, &peer_data.requests),
1053                capabilities: peer_data
1054                    .supported_capabilities
1055                    .iter()
1056                    .map(|c| format!("{}/{}", c.protocol(), c.version))
1057                    .collect(),
1058                ip: peer_data.node.ip,
1059                client_version: peer_data.node.version.clone().unwrap_or_default(),
1060                connection_direction: if peer_data.is_connection_inbound {
1061                    "inbound".to_string()
1062                } else {
1063                    "outbound".to_string()
1064                },
1065                last_response_time: peer_data.last_response_time,
1066            })
1067            .collect()
1068    }
1069
1070    // === Private helper methods ===
1071
1072    // --- K-bucket accessors ---
1073
1074    /// Get the bucket index for a node ID, or None if it's the local node.
1075    fn bucket_for(&self, node_id: &H256) -> Option<usize> {
1076        bucket_index(&self.local_node_id, node_id)
1077    }
1078
1079    /// Look up a contact by node ID in main or replacement list (O(K) within the bucket).
1080    fn get_contact(&self, node_id: &H256) -> Option<&Contact> {
1081        let idx = self.bucket_for(node_id)?;
1082        self.buckets[idx].get_any(node_id)
1083    }
1084
1085    /// Look up a mutable reference to a contact by node ID.
1086    fn get_contact_mut(&mut self, node_id: &H256) -> Option<&mut Contact> {
1087        let idx = self.bucket_for(node_id)?;
1088        self.buckets[idx].get_mut(node_id)
1089    }
1090
1091    /// Check if a contact exists in any bucket (main or replacement list).
1092    fn contact_exists(&self, node_id: &H256) -> bool {
1093        let Some(idx) = self.bucket_for(node_id) else {
1094            return false;
1095        };
1096        self.buckets[idx].contains(node_id)
1097    }
1098
1099    /// Insert a contact into the appropriate k-bucket. Returns true if inserted
1100    /// into the main list, false if the node went to the replacement list or is
1101    /// the local node.
1102    fn insert_contact(&mut self, node_id: H256, contact: Contact) -> bool {
1103        #[cfg(feature = "metrics")]
1104        let start = std::time::Instant::now();
1105
1106        let Some(idx) = self.bucket_for(&node_id) else {
1107            return false;
1108        };
1109        let result = self.buckets[idx].insert(node_id, contact);
1110
1111        #[cfg(feature = "metrics")]
1112        {
1113            use ethrex_metrics::p2p::METRICS_P2P;
1114            METRICS_P2P.observe_insert_contact_duration(start.elapsed().as_secs_f64());
1115        }
1116
1117        result
1118    }
1119
1120    /// Insert a node into the flat connection pool for RLPx initiation.
1121    /// Evicts the oldest entry when the pool is at capacity.
1122    fn insert_to_connection_pool(&mut self, node_id: H256, node: Node) {
1123        if self.connection_pool.contains_key(&node_id) {
1124            return;
1125        }
1126        if self.connection_pool.len() >= MAX_CONNECTION_POOL_SIZE {
1127            self.connection_pool.shift_remove_index(0);
1128        }
1129        self.connection_pool.insert(node_id, node);
1130    }
1131
1132    /// Look up a contact by node ID in either the main or replacement list.
1133    fn get_contact_or_replacement(&self, node_id: &H256) -> Option<&Contact> {
1134        let idx = self.bucket_for(node_id)?;
1135        self.buckets[idx].get_any(node_id)
1136    }
1137
1138    /// Look up a mutable reference in either the main or replacement list.
1139    fn get_contact_or_replacement_mut(&mut self, node_id: &H256) -> Option<&mut Contact> {
1140        let idx = self.bucket_for(node_id)?;
1141        let bucket = &mut self.buckets[idx];
1142        // Search main list first, then replacement list.
1143        // Done inline to avoid borrow-checker issues with or_else closures.
1144        if let Some(pos) = bucket.contacts.iter().position(|(id, _)| id == node_id) {
1145            return Some(&mut bucket.contacts[pos].1);
1146        }
1147        if let Some(pos) = bucket.replacements.iter().position(|(id, _)| id == node_id) {
1148            return Some(&mut bucket.replacements[pos].1);
1149        }
1150        None
1151    }
1152
1153    /// Iterate over all contacts across all buckets (main and replacement lists).
1154    fn iter_contacts(&self) -> impl Iterator<Item = (&H256, &Contact)> {
1155        self.buckets.iter().flat_map(|bucket| {
1156            bucket
1157                .contacts
1158                .iter()
1159                .chain(bucket.replacements.iter())
1160                .map(|(id, c)| (id, c))
1161        })
1162    }
1163
1164    // --- Peer selection ---
1165
1166    fn weight_peer(&self, score: &i64, requests: &i64) -> i64 {
1167        score * SCORE_WEIGHT - requests * REQUESTS_WEIGHT
1168    }
1169
1170    fn can_try_more_requests(&self, score: &i64, requests: &i64) -> bool {
1171        let score_ratio = (score - MIN_SCORE) as f64 / (MAX_SCORE - MIN_SCORE) as f64;
1172        let max_requests = (MAX_CONCURRENT_REQUESTS_PER_PEER as f64 * score_ratio).max(1.0);
1173        (*requests as f64) < max_requests
1174    }
1175
1176    fn do_get_best_peer(&self, capabilities: &[Capability]) -> Option<(H256, PeerConnection)> {
1177        self.do_get_best_peer_excluding(capabilities, &[])
1178    }
1179
1180    /// Like `do_get_best_peer`, but excludes specific peers from selection.
1181    /// Used by `update_pivot` to rotate through peers on repeated failures.
1182    fn do_get_best_peer_excluding(
1183        &self,
1184        capabilities: &[Capability],
1185        excluded: &[H256],
1186    ) -> Option<(H256, PeerConnection)> {
1187        self.peers
1188            .iter()
1189            .filter_map(|(id, peer_data)| {
1190                if excluded.contains(id)
1191                    || !self.can_try_more_requests(&peer_data.score, &peer_data.requests)
1192                    || !capabilities
1193                        .iter()
1194                        .any(|cap| peer_data.supported_capabilities.contains(cap))
1195                {
1196                    None
1197                } else {
1198                    let connection = peer_data.connection.clone()?;
1199                    Some((*id, peer_data.score, peer_data.requests, connection))
1200                }
1201            })
1202            .max_by_key(|(_, score, reqs, _)| self.weight_peer(score, reqs))
1203            .map(|(k, _, _, v)| (k, v))
1204    }
1205
1206    /// Returns up to `n` best peers with capability overlap, sorted by weight
1207    /// descending. Excludes peers at capacity. Does NOT mutate state — caller
1208    /// is responsible for incrementing `requests` on each returned peer. The
1209    /// sort uses a pre-increment snapshot: later picks don't see earlier
1210    /// picks' bumps, which is fine for small `n`.
1211    fn do_get_best_n_peers(
1212        &self,
1213        capabilities: &[Capability],
1214        n: usize,
1215    ) -> Vec<(H256, PeerConnection)> {
1216        let mut candidates: Vec<(H256, i64, i64, PeerConnection)> = self
1217            .peers
1218            .iter()
1219            .filter_map(|(id, peer_data)| {
1220                if !self.can_try_more_requests(&peer_data.score, &peer_data.requests)
1221                    || !capabilities
1222                        .iter()
1223                        .any(|cap| peer_data.supported_capabilities.contains(cap))
1224                {
1225                    None
1226                } else {
1227                    let connection = peer_data.connection.clone()?;
1228                    Some((*id, peer_data.score, peer_data.requests, connection))
1229                }
1230            })
1231            .collect();
1232
1233        candidates.sort_by_key(|(_, score, reqs, _)| -self.weight_peer(score, reqs));
1234        candidates
1235            .into_iter()
1236            .take(n)
1237            .map(|(id, _, _, conn)| (id, conn))
1238            .collect()
1239    }
1240
1241    // --- Contact operations ---
1242
1243    /// Prune disposable contacts from both main and replacement lists.
1244    /// When a main contact is removed, a replacement is automatically promoted.
1245    /// Pruned contacts remain in the connection pool so they can be retried
1246    /// later — the RLPx handshake will reject them if they're truly bad.
1247    fn prune(&mut self) {
1248        for bucket in &mut self.buckets {
1249            // Collect disposable contacts from main list
1250            let main_disposable: Vec<H256> = bucket
1251                .contacts
1252                .iter()
1253                .filter(|(_, c)| c.disposable)
1254                .map(|(id, _)| *id)
1255                .collect();
1256
1257            // Remove from main list and promote replacements
1258            for node_id in main_disposable {
1259                bucket.remove_and_promote(&node_id);
1260            }
1261
1262            // Remove disposable contacts from replacement list
1263            // (these don't get promoted, just removed)
1264            bucket.replacements.retain(|(_, c)| !c.disposable);
1265        }
1266    }
1267
1268    fn do_get_contact_to_initiate(&mut self) -> Option<Contact> {
1269        // Draw from the flat connection pool using O(1) random index probing.
1270        // Pick a random start index and scan forward (wrapping) until we find
1271        // an eligible candidate or complete a full loop.
1272        let pool_len = self.connection_pool.len();
1273        if pool_len == 0 {
1274            return None;
1275        }
1276
1277        let start = rand::random::<usize>() % pool_len;
1278        for offset in 0..pool_len {
1279            let idx = (start + offset) % pool_len;
1280            let Some((node_id, node)) = self.connection_pool.get_index(idx) else {
1281                continue;
1282            };
1283            let node_id = *node_id;
1284
1285            if self.peers.contains_key(&node_id)
1286                || self.already_tried_peers.contains(&node_id)
1287                || self
1288                    .get_contact_or_replacement(&node_id)
1289                    .map(|c| !c.knows_us || c.unwanted || c.is_fork_id_valid == Some(false))
1290                    .unwrap_or(false)
1291            {
1292                continue;
1293            }
1294
1295            let node = node.clone();
1296            self.already_tried_peers.insert(node_id);
1297            let contact = self
1298                .get_contact_or_replacement(&node_id)
1299                .cloned()
1300                .unwrap_or_else(|| Contact::new(node, DiscoveryProtocol::Discv4));
1301            return Some(contact);
1302        }
1303
1304        // Exhausted all candidates — reset tried set for next cycle.
1305        tracing::trace!("Resetting list of tried peers.");
1306        self.already_tried_peers.clear();
1307        None
1308    }
1309
1310    /// Get the `count` closest nodes from the connection pool, sorted by XOR distance to `target`.
1311    fn do_get_closest_from_pool(&self, target: H256, count: usize) -> Vec<(H256, Node)> {
1312        let mut nodes: Vec<(H256, Node, H256)> = Vec::with_capacity(count);
1313
1314        for (node_id, node) in &self.connection_pool {
1315            let dist = xor_distance(&target, node_id);
1316            if nodes.len() < count {
1317                nodes.push((*node_id, node.clone(), dist));
1318            } else if let Some((farthest_idx, _)) =
1319                nodes.iter().enumerate().max_by_key(|(_, (_, _, d))| *d)
1320                && dist < nodes[farthest_idx].2
1321            {
1322                nodes[farthest_idx] = (*node_id, node.clone(), dist);
1323            }
1324        }
1325
1326        nodes.sort_by(|a, b| a.2.cmp(&b.2));
1327        nodes.into_iter().map(|(id, node, _)| (id, node)).collect()
1328    }
1329
1330    /// Get contact for ENR lookup (discv4 only)
1331    fn do_get_contact_for_enr_lookup(&mut self) -> Option<Contact> {
1332        self.iter_contacts()
1333            .filter(|(_, c)| {
1334                c.is_discv4
1335                    && c.was_validated()
1336                    && !c.has_pending_enr_request()
1337                    && c.record.is_none()
1338                    && !c.disposable
1339            })
1340            .map(|(_, c)| c)
1341            .collect::<Vec<_>>()
1342            .choose(&mut rand::rngs::OsRng)
1343            .cloned()
1344            .cloned()
1345    }
1346
1347    fn do_get_contact_to_revalidate(
1348        &self,
1349        revalidation_interval: Duration,
1350        protocol: DiscoveryProtocol,
1351    ) -> Option<Box<Contact>> {
1352        self.iter_contacts()
1353            .filter(|(_, c)| {
1354                c.supports_protocol(protocol)
1355                    && Self::is_validation_needed(c, revalidation_interval)
1356            })
1357            .map(|(_, c)| c)
1358            .choose(&mut rand::rngs::OsRng)
1359            .cloned()
1360            .map(Box::new)
1361    }
1362
1363    fn do_validate_contact(&self, node_id: H256, sender_ip: IpAddr) -> ContactValidation {
1364        let Some(contact) = self.get_contact(&node_id) else {
1365            return ContactValidation::UnknownContact;
1366        };
1367        if !contact.was_validated() {
1368            return ContactValidation::InvalidContact;
1369        }
1370
1371        // Check that the IP address from which we receive the request matches the one we have stored
1372        // to prevent amplification attacks.
1373        if sender_ip != contact.node.ip {
1374            return ContactValidation::IpMismatch;
1375        }
1376        ContactValidation::Valid(Box::new(contact.clone()))
1377    }
1378
1379    /// Get closest nodes using raw XOR distance for accurate ordering.
1380    fn do_get_closest_nodes(&self, node_id: H256) -> Vec<Node> {
1381        #[cfg(feature = "metrics")]
1382        let scan_start = std::time::Instant::now();
1383
1384        let mut nodes: Vec<(Node, H256)> = vec![];
1385
1386        for (contact_id, contact) in self.iter_contacts() {
1387            let dist = xor_distance(&node_id, contact_id);
1388            if nodes.len() < MAX_NODES_IN_NEIGHBORS_PACKET {
1389                nodes.push((contact.node.clone(), dist));
1390            } else if let Some((farthest_idx, _)) =
1391                nodes.iter().enumerate().max_by_key(|(_, (_, d))| *d)
1392                && dist < nodes[farthest_idx].1
1393            {
1394                nodes[farthest_idx] = (contact.node.clone(), dist);
1395            }
1396        }
1397
1398        #[cfg(feature = "metrics")]
1399        {
1400            use ethrex_metrics::p2p::METRICS_P2P;
1401            METRICS_P2P.observe_iter_contacts_duration(scan_start.elapsed().as_secs_f64());
1402        }
1403
1404        nodes.into_iter().map(|(node, _)| node).collect()
1405    }
1406
1407    /// Get nodes at distances for discv5 (returns Vec<NodeRecord>).
1408    /// Uses the discv5 spec log-distance: `floor(log2(XOR))` for non-zero XOR.
1409    /// Distance 0 is reserved for the local node itself (handled by the caller),
1410    /// so contacts start at distance >= 1.
1411    fn do_get_nodes_at_distances(&self, distances: &[u32]) -> Vec<NodeRecord> {
1412        self.iter_contacts()
1413            .filter_map(|(contact_id, contact)| {
1414                let dist = distance(&self.local_node_id, contact_id) as u32;
1415                if distances.contains(&dist) {
1416                    contact.record.clone()
1417                } else {
1418                    None
1419                }
1420            })
1421            .take(MAX_ENRS_PER_FINDNODE_RESPONSE)
1422            .collect()
1423    }
1424
1425    async fn do_new_contacts(&mut self, nodes: Vec<Node>, protocol: DiscoveryProtocol) {
1426        for node in nodes {
1427            let node_id = node.node_id();
1428            if node_id == self.local_node_id {
1429                continue;
1430            }
1431            #[cfg(feature = "metrics")]
1432            let insert_start = std::time::Instant::now();
1433
1434            // Always add to the connection pool (regardless of k-bucket capacity)
1435            self.insert_to_connection_pool(node_id, node.clone());
1436
1437            if self.contact_exists(&node_id) {
1438                // Contact already exists (main or replacement list), update protocol
1439                if let Some(contact) = self.get_contact_or_replacement_mut(&node_id) {
1440                    contact.add_protocol(protocol);
1441                }
1442            } else {
1443                let contact = Contact::new(node, protocol);
1444                self.insert_contact(node_id, contact);
1445                METRICS.record_new_discovery().await;
1446            }
1447
1448            #[cfg(feature = "metrics")]
1449            {
1450                use ethrex_metrics::p2p::METRICS_P2P;
1451                METRICS_P2P.observe_insert_contact_duration(insert_start.elapsed().as_secs_f64());
1452            }
1453        }
1454    }
1455
1456    async fn do_new_contact_records(&mut self, node_records: Vec<NodeRecord>) {
1457        for node_record in node_records {
1458            if !node_record.verify_signature() {
1459                continue;
1460            }
1461            if let Ok(node) = Node::from_enr(&node_record) {
1462                let node_id = node.node_id();
1463                if node_id == self.local_node_id {
1464                    continue;
1465                }
1466
1467                // Always add to the connection pool (regardless of k-bucket capacity)
1468                self.insert_to_connection_pool(node_id, node.clone());
1469
1470                if self.contact_exists(&node_id) {
1471                    // Check if we need to evaluate fork_id before taking
1472                    // the mutable borrow.
1473                    let should_update = self
1474                        .get_contact_or_replacement(&node_id)
1475                        .map(|c| match c.record.as_ref() {
1476                            None => true,
1477                            Some(r) => node_record.seq > r.seq,
1478                        })
1479                        .unwrap_or(false);
1480                    let is_fork_id_valid = if should_update {
1481                        Self::evaluate_fork_id(&node_record, &self.store).await
1482                    } else {
1483                        None
1484                    };
1485                    if let Some(contact) = self.get_contact_or_replacement_mut(&node_id) {
1486                        contact.add_protocol(DiscoveryProtocol::Discv5);
1487                        if should_update {
1488                            if contact.node.ip != node.ip || contact.node.udp_port != node.udp_port
1489                            {
1490                                contact.validation_timestamp = None;
1491                                contact.ping_id = None;
1492                            }
1493                            contact.node = node;
1494                            contact.record = Some(node_record);
1495                            contact.is_fork_id_valid = is_fork_id_valid;
1496                        }
1497                    }
1498                } else {
1499                    let is_fork_id_valid = Self::evaluate_fork_id(&node_record, &self.store).await;
1500                    let mut contact = Contact::new(node, DiscoveryProtocol::Discv5);
1501                    contact.is_fork_id_valid = is_fork_id_valid;
1502                    contact.record = Some(node_record);
1503                    self.insert_contact(node_id, contact);
1504                    METRICS.record_new_discovery().await;
1505                }
1506            }
1507        }
1508    }
1509
1510    async fn evaluate_fork_id(record: &NodeRecord, store: &Store) -> Option<bool> {
1511        if let Some(remote_fork_id) = record.get_fork_id() {
1512            backend::is_fork_id_valid(store, remote_fork_id)
1513                .await
1514                .ok()
1515                .or(Some(false))
1516        } else {
1517            Some(false)
1518        }
1519    }
1520
1521    fn do_peer_count_by_capabilities(&self, capabilities: Vec<Capability>) -> usize {
1522        self.peers
1523            .values()
1524            .filter(|peer_data| {
1525                capabilities
1526                    .iter()
1527                    .any(|cap| peer_data.supported_capabilities.contains(cap))
1528            })
1529            .count()
1530    }
1531
1532    fn do_get_random_peer(&self, capabilities: Vec<Capability>) -> Option<(H256, PeerConnection)> {
1533        let peers: Vec<(H256, &PeerConnection, i64)> = self
1534            .peers
1535            .iter()
1536            .filter_map(|(node_id, peer_data)| {
1537                if !capabilities
1538                    .iter()
1539                    .any(|cap| peer_data.supported_capabilities.contains(cap))
1540                {
1541                    return None;
1542                }
1543                peer_data
1544                    .connection
1545                    .as_ref()
1546                    .map(|connection| (*node_id, connection, peer_data.score))
1547            })
1548            .collect();
1549        if peers.is_empty() {
1550            return None;
1551        }
1552        // Weight by score: maps [-150, 50] to [1, 201] so bad peers are unlikely but not excluded
1553        let weights: Vec<u64> = peers
1554            .iter()
1555            .map(|(_, _, score)| (score.max(&MIN_SCORE_CRITICAL) - MIN_SCORE_CRITICAL + 1) as u64)
1556            .collect();
1557        let dist = WeightedIndex::new(&weights).ok()?;
1558        let idx = dist.sample(&mut rand::rngs::OsRng);
1559        Some((peers[idx].0, peers[idx].1.clone()))
1560    }
1561
1562    fn is_validation_needed(contact: &Contact, revalidation_interval: Duration) -> bool {
1563        if contact.disposable {
1564            return false;
1565        }
1566
1567        let sent_ping_ttl = Duration::from_secs(30);
1568
1569        if contact.has_pending_ping() {
1570            // Outstanding ping — only re-ping if it timed out (stale).
1571            contact
1572                .validation_timestamp
1573                .map(|ts| Instant::now().saturating_duration_since(ts) > sent_ping_ttl)
1574                .unwrap_or(false)
1575        } else {
1576            // No pending ping — check if never validated or validation expired.
1577            !contact.was_validated()
1578                || contact
1579                    .validation_timestamp
1580                    .map(|ts| Instant::now().saturating_duration_since(ts) > revalidation_interval)
1581                    .unwrap_or(false)
1582        }
1583    }
1584}
1585
1586pub type PeerTable = ActorRef<PeerTableServer>;
1587
1588#[cfg(test)]
1589mod tests {
1590    use super::*;
1591    use ethrex_common::H512;
1592    use std::net::Ipv4Addr;
1593
1594    /// Helper: build a dummy contact with a unique node derived from `seed`.
1595    fn dummy_contact(seed: u8) -> (H256, Contact) {
1596        let pk = H512::from_low_u64_be(seed as u64 + 1);
1597        let node = Node::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, seed)), 30303, 30303, pk);
1598        let node_id = node.node_id();
1599        let contact = Contact::new(node, DiscoveryProtocol::Discv4);
1600        (node_id, contact)
1601    }
1602
1603    // --- KBucket::insert ---
1604
1605    #[test]
1606    fn insert_into_empty_bucket() {
1607        let mut bucket = KBucket::default();
1608        let (id, contact) = dummy_contact(1);
1609        assert!(bucket.insert(id, contact));
1610        assert_eq!(bucket.contacts.len(), 1);
1611        assert!(bucket.replacements.is_empty());
1612    }
1613
1614    #[test]
1615    fn insert_fills_bucket_then_goes_to_replacements() {
1616        let mut bucket = KBucket::default();
1617
1618        // Fill the main list to capacity.
1619        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1620            let (id, contact) = dummy_contact(i);
1621            assert!(bucket.insert(id, contact), "contact {i} should go to main");
1622        }
1623        assert_eq!(bucket.contacts.len(), MAX_NODES_PER_BUCKET);
1624
1625        // The next insert should go to the replacement list.
1626        let (id, contact) = dummy_contact(200);
1627        assert!(!bucket.insert(id, contact));
1628        assert_eq!(bucket.contacts.len(), MAX_NODES_PER_BUCKET);
1629        assert_eq!(bucket.replacements.len(), 1);
1630    }
1631
1632    // --- KBucket::contains ---
1633
1634    #[test]
1635    fn contains_checks_main_and_replacement() {
1636        let mut bucket = KBucket::default();
1637
1638        let (id_main, contact_main) = dummy_contact(1);
1639        bucket.insert(id_main, contact_main);
1640        assert!(bucket.contains(&id_main));
1641
1642        // Fill bucket so next goes to replacement.
1643        for i in 2..=(MAX_NODES_PER_BUCKET as u8) {
1644            let (id, c) = dummy_contact(i);
1645            bucket.insert(id, c);
1646        }
1647        let (id_repl, contact_repl) = dummy_contact(100);
1648        bucket.insert(id_repl, contact_repl);
1649
1650        assert!(bucket.contains(&id_repl));
1651        assert!(!bucket.contains(&H256::zero()));
1652    }
1653
1654    // --- KBucket::get / get_any ---
1655
1656    #[test]
1657    fn get_returns_main_list_only() {
1658        let mut bucket = KBucket::default();
1659        let (id, contact) = dummy_contact(1);
1660        bucket.insert(id, contact);
1661        assert!(bucket.get(&id).is_some());
1662        assert!(bucket.get(&H256::zero()).is_none());
1663    }
1664
1665    #[test]
1666    fn get_any_returns_from_replacement() {
1667        let mut bucket = KBucket::default();
1668        // Fill main list.
1669        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1670            let (id, c) = dummy_contact(i);
1671            bucket.insert(id, c);
1672        }
1673        // Insert into replacements.
1674        let (id_repl, c_repl) = dummy_contact(200);
1675        bucket.insert(id_repl, c_repl);
1676
1677        assert!(bucket.get(&id_repl).is_none()); // not in main
1678        assert!(bucket.get_any(&id_repl).is_some()); // found via replacement
1679    }
1680
1681    // --- KBucket::remove_and_promote ---
1682
1683    #[test]
1684    fn remove_and_promote_with_replacement() {
1685        let mut bucket = KBucket::default();
1686
1687        // Fill main list.
1688        let mut main_ids = Vec::new();
1689        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1690            let (id, c) = dummy_contact(i);
1691            main_ids.push(id);
1692            bucket.insert(id, c);
1693        }
1694
1695        // Add a replacement.
1696        let (repl_id, repl_contact) = dummy_contact(200);
1697        bucket.insert(repl_id, repl_contact);
1698
1699        // Remove a main contact — the replacement should be promoted.
1700        let promoted = bucket.remove_and_promote(&main_ids[0]);
1701        assert_eq!(promoted, Some(repl_id));
1702        assert_eq!(bucket.contacts.len(), MAX_NODES_PER_BUCKET);
1703        assert!(bucket.replacements.is_empty());
1704        assert!(!bucket.contains(&main_ids[0]));
1705        assert!(bucket.contains(&repl_id));
1706    }
1707
1708    #[test]
1709    fn remove_and_promote_without_replacement() {
1710        let mut bucket = KBucket::default();
1711        let (id, c) = dummy_contact(1);
1712        bucket.insert(id, c);
1713
1714        let promoted = bucket.remove_and_promote(&id);
1715        assert!(promoted.is_none());
1716        assert!(bucket.contacts.is_empty());
1717    }
1718
1719    #[test]
1720    fn remove_nonexistent_returns_none() {
1721        let mut bucket = KBucket::default();
1722        assert!(bucket.remove_and_promote(&H256::zero()).is_none());
1723    }
1724
1725    // --- Replacement eviction ---
1726
1727    #[test]
1728    fn replacement_list_evicts_oldest_when_full() {
1729        let mut bucket = KBucket::default();
1730        // Fill main list.
1731        for i in 0..MAX_NODES_PER_BUCKET as u8 {
1732            let (id, c) = dummy_contact(i);
1733            bucket.insert(id, c);
1734        }
1735
1736        // Fill replacement list beyond capacity.
1737        let mut repl_ids = Vec::new();
1738        for i in 0..(MAX_REPLACEMENTS_PER_BUCKET + 2) as u8 {
1739            let seed = 100 + i;
1740            let (id, c) = dummy_contact(seed);
1741            repl_ids.push(id);
1742            bucket.insert(id, c);
1743        }
1744
1745        assert_eq!(bucket.replacements.len(), MAX_REPLACEMENTS_PER_BUCKET);
1746        // The oldest two should have been evicted.
1747        assert!(!bucket.contains(&repl_ids[0]));
1748        assert!(!bucket.contains(&repl_ids[1]));
1749        // The most recent ones should still be there.
1750        assert!(bucket.contains(repl_ids.last().unwrap()));
1751    }
1752
1753    // --- bucket_index ---
1754
1755    #[test]
1756    fn bucket_index_self_is_none() {
1757        let id = H256::random();
1758        assert_eq!(bucket_index(&id, &id), None);
1759    }
1760
1761    #[test]
1762    fn bucket_index_minimal_distance() {
1763        let local = H256::zero();
1764        // XOR distance = 1 → highest bit is bit 0 → bucket 0
1765        let mut remote = H256::zero();
1766        remote.0[31] = 1;
1767        assert_eq!(bucket_index(&local, &remote), Some(0));
1768    }
1769
1770    #[test]
1771    fn bucket_index_maximal_distance() {
1772        let local = H256::zero();
1773        // XOR distance has highest bit at position 255 → bucket 255
1774        let mut remote = H256::zero();
1775        remote.0[0] = 0x80;
1776        assert_eq!(bucket_index(&local, &remote), Some(255));
1777    }
1778}