Skip to main content

sui_network/discovery/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use anemo::types::PeerAffinity;
5use anemo::types::PeerInfo;
6use anemo::{Network, Peer, PeerId, Request, Response, types::PeerEvent};
7use fastcrypto::ed25519::{Ed25519PublicKey, Ed25519Signature};
8use futures::StreamExt;
9use mysten_common::debug_fatal;
10use serde::{Deserialize, Serialize};
11use shared_crypto::intent::IntentScope;
12use std::{
13    collections::{BTreeMap, HashMap, HashSet},
14    path::PathBuf,
15    sync::{Arc, RwLock},
16    time::{Duration, Instant},
17};
18
19use crate::endpoint_manager::{AddressSource, EndpointId, EndpointManager};
20use store::{load_stored_peers, save_stored_peers};
21use sui_config::p2p::{AccessType, DiscoveryConfig, P2pConfig};
22use sui_types::crypto::{NetworkKeyPair, NetworkPublicKey, Signer, ToFromBytes, VerifyingKey};
23use sui_types::digests::Digest;
24use sui_types::message_envelope::{Envelope, Message, VerifiedEnvelope};
25use sui_types::multiaddr::Multiaddr;
26use tap::{Pipe, TapFallible};
27use tokio::sync::broadcast::error::RecvError;
28use tokio::sync::mpsc;
29use tokio::{
30    sync::oneshot,
31    task::{AbortHandle, JoinSet},
32};
33use tracing::{debug, info, trace};
34
35const TIMEOUT: Duration = Duration::from_secs(1);
36const ONE_DAY_MILLISECONDS: u64 = 24 * 60 * 60 * 1_000;
37const MAX_ADDRESS_LENGTH: usize = 300;
38const MAX_PEERS_TO_SEND: usize = 200;
39const MAX_ADDRESSES_PER_PEER: usize = 2;
40
41#[allow(clippy::result_large_err)]
42mod generated {
43    include!(concat!(env!("OUT_DIR"), "/sui.Discovery.rs"));
44}
45mod builder;
46mod metrics;
47mod server;
48mod store;
49#[cfg(test)]
50mod tests;
51
52pub use builder::{Builder, UnstartedDiscovery};
53pub use generated::{
54    discovery_client::DiscoveryClient,
55    discovery_server::{Discovery, DiscoveryServer},
56};
57pub use server::{GetKnownPeersRequestV3, GetKnownPeersResponseV2, GetKnownPeersResponseV3};
58
59/// Per-source P2P addresses for trusted peers, keyed by peer then by address source.
60/// Returned by [`Sender::trusted_peer_p2p_addresses`].
61pub type TrustedPeerP2pAddresses =
62    BTreeMap<PeerId, BTreeMap<AddressSource, Vec<anemo::types::Address>>>;
63
64/// Message types for the discovery system mailbox.
65#[derive(Debug)]
66pub enum DiscoveryMessage {
67    /// An external source (e.g. admin API, node config) updated a peer's address.
68    PeerAddressChange {
69        peer_id: PeerId,
70        source: AddressSource,
71        addresses: Vec<anemo::types::Address>,
72    },
73    /// Node info was received from inbound RPC.
74    ReceivedNodeInfo {
75        peer_info: Box<SignedVersionedNodeInfo>,
76    },
77    /// A spawned peer-query task discovered updated info for a trusted peer,
78    /// signaling the event loop to update the stored peer addresses.
79    TrustedPeersUpdated,
80    /// A peer has been reported as continuously failing, triggering disconnect and cooldown.
81    PeerFailureReport { peer_id: PeerId },
82    /// Request a snapshot of per-source P2P addresses, for trusted peers with at least one
83    /// recorded address.
84    GetTrustedPeerP2pAddresses {
85        reply: oneshot::Sender<TrustedPeerP2pAddresses>,
86    },
87}
88
89/// A Handle to the Discovery subsystem. The Discovery system will be shut down once all Handles
90/// have been dropped.
91#[derive(Clone, Debug)]
92pub struct Handle {
93    pub(super) _shutdown_handle: Arc<oneshot::Sender<()>>,
94    pub(super) sender: Sender,
95}
96
97impl Handle {
98    pub fn sender(&self) -> Sender {
99        self.sender.clone()
100    }
101}
102
103/// A lightweight handle for sending messages to the discovery event loop
104/// without holding a shutdown reference.
105#[derive(Clone, Debug)]
106pub struct Sender {
107    pub(super) sender: mpsc::Sender<DiscoveryMessage>,
108}
109
110impl Sender {
111    pub fn peer_address_change(
112        &self,
113        peer_id: PeerId,
114        source: AddressSource,
115        addresses: Vec<anemo::types::Address>,
116    ) {
117        self.sender
118            .try_send(DiscoveryMessage::PeerAddressChange {
119                peer_id,
120                source,
121                addresses,
122            })
123            .expect("Discovery mailbox should not overflow or be closed")
124    }
125
126    pub fn report_peer_failure(&self, peer_id: PeerId) {
127        let _ = self
128            .sender
129            .try_send(DiscoveryMessage::PeerFailureReport { peer_id });
130    }
131
132    /// Snapshot of per-source P2P addresses for trusted peers only (configured/seed/allowlisted
133    /// peers and on-chain validators). Used by the address prober to enumerate probe candidates;
134    /// returns an empty map if the discovery event loop has shut down.
135    pub async fn trusted_peer_p2p_addresses(&self) -> TrustedPeerP2pAddresses {
136        let (s, r) = oneshot::channel();
137        if self
138            .sender
139            .send(DiscoveryMessage::GetTrustedPeerP2pAddresses { reply: s })
140            .await
141            .is_err()
142        {
143            return TrustedPeerP2pAddresses::default();
144        }
145        r.await.unwrap_or_default()
146    }
147}
148
149use self::metrics::Metrics;
150
151/// The internal discovery state shared between the main event loop and the request handler
152struct State {
153    our_info: Option<SignedNodeInfo>,
154    our_info_v2: Option<SignedVersionedNodeInfo>,
155    connected_peers: HashMap<PeerId, ()>,
156    known_peers: HashMap<PeerId, VerifiedSignedNodeInfo>,
157    known_peers_v2: HashMap<PeerId, VerifiedSignedVersionedNodeInfo>,
158    peer_addresses: HashMap<PeerId, BTreeMap<AddressSource, Vec<anemo::types::Address>>>,
159}
160
161/// The information necessary to dial another peer.
162///
163/// `NodeInfo` contains all the information that is shared with other nodes via the discovery
164/// service to advertise how a node can be reached.
165#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
166pub struct NodeInfo {
167    pub peer_id: PeerId,
168    pub addresses: Vec<Multiaddr>,
169
170    /// Creation time.
171    ///
172    /// This is used to determine which of two NodeInfo's from the same PeerId should be retained.
173    pub timestamp_ms: u64,
174
175    /// See docstring for `AccessType`.
176    pub access_type: AccessType,
177}
178
179impl NodeInfo {
180    fn sign(self, keypair: &NetworkKeyPair) -> SignedNodeInfo {
181        let msg = bcs::to_bytes(&self).expect("BCS serialization should not fail");
182        let sig = keypair.sign(&msg);
183        SignedNodeInfo::new_from_data_and_sig(self, sig)
184    }
185}
186
187pub type SignedNodeInfo = Envelope<NodeInfo, Ed25519Signature>;
188
189pub type VerifiedSignedNodeInfo = VerifiedEnvelope<NodeInfo, Ed25519Signature>;
190
191#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
192pub struct NodeInfoDigest(Digest);
193
194impl NodeInfoDigest {
195    pub const fn new(digest: [u8; 32]) -> Self {
196        Self(Digest::new(digest))
197    }
198}
199
200impl Message for NodeInfo {
201    type DigestType = NodeInfoDigest;
202    const SCOPE: IntentScope = IntentScope::DiscoveryPeers;
203
204    fn digest(&self) -> Self::DigestType {
205        unreachable!("NodeInfoDigest is not used today")
206    }
207}
208
209/// NodeInfoV2 supports multiple address types keyed by EndpointId.
210// TODO: Remove support for V1 once V2 is available in all production networks.
211#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
212pub struct NodeInfoV2 {
213    pub addresses: BTreeMap<EndpointId, Vec<Multiaddr>>,
214    pub timestamp_ms: u64,
215    pub access_type: AccessType,
216}
217
218impl NodeInfoV2 {
219    /// Derive the P2P PeerId from the addresses map.
220    /// Returns None if no P2P endpoint is present.
221    pub fn peer_id(&self) -> Option<PeerId> {
222        self.addresses.keys().find_map(|k| match k {
223            EndpointId::P2p(peer_id) => Some(*peer_id),
224            EndpointId::Consensus(_) => None,
225        })
226    }
227
228    pub fn p2p_addresses(&self) -> &[Multiaddr] {
229        self.addresses
230            .iter()
231            .find_map(|(k, v)| match k {
232                EndpointId::P2p(_) => Some(v.as_slice()),
233                EndpointId::Consensus(_) => None,
234            })
235            .unwrap_or(&[])
236    }
237}
238
239/// Versioned wrapper for NodeInfo types.
240#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub enum VersionedNodeInfo {
242    V1(NodeInfo),
243    V2(NodeInfoV2),
244}
245
246impl VersionedNodeInfo {
247    pub fn peer_id(&self) -> Option<PeerId> {
248        match self {
249            VersionedNodeInfo::V1(info) => Some(info.peer_id),
250            VersionedNodeInfo::V2(info) => info.peer_id(),
251        }
252    }
253
254    pub fn timestamp_ms(&self) -> u64 {
255        match self {
256            VersionedNodeInfo::V1(info) => info.timestamp_ms,
257            VersionedNodeInfo::V2(info) => info.timestamp_ms,
258        }
259    }
260
261    pub fn access_type(&self) -> AccessType {
262        match self {
263            VersionedNodeInfo::V1(info) => info.access_type,
264            VersionedNodeInfo::V2(info) => info.access_type,
265        }
266    }
267
268    pub fn p2p_addresses(&self) -> &[Multiaddr] {
269        match self {
270            VersionedNodeInfo::V1(info) => &info.addresses,
271            VersionedNodeInfo::V2(info) => info.p2p_addresses(),
272        }
273    }
274
275    pub fn sign(self, keypair: &NetworkKeyPair) -> SignedVersionedNodeInfo {
276        let msg = bcs::to_bytes(&self).expect("BCS serialization should not fail");
277        let sig = keypair.sign(&msg);
278        SignedVersionedNodeInfo::new_from_data_and_sig(self, sig)
279    }
280}
281
282#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
283pub struct VersionedNodeInfoDigest(Digest);
284
285impl Message for VersionedNodeInfo {
286    type DigestType = VersionedNodeInfoDigest;
287    const SCOPE: IntentScope = IntentScope::DiscoveryPeers;
288
289    fn digest(&self) -> Self::DigestType {
290        unreachable!("VersionedNodeInfoDigest is not used today")
291    }
292}
293
294pub type SignedVersionedNodeInfo = Envelope<VersionedNodeInfo, Ed25519Signature>;
295pub type VerifiedSignedVersionedNodeInfo = VerifiedEnvelope<VersionedNodeInfo, Ed25519Signature>;
296
297/// Verifies the signature and endpoint identity consistency of a
298/// `SignedVersionedNodeInfo`, returning a `VerifiedSignedVersionedNodeInfo`
299/// on success.
300///
301/// Checks:
302/// 1. The signature is valid for the P2P peer_id embedded in the node info.
303/// 2. Any non-P2P endpoint identities (e.g. `Consensus`) match the P2P
304///    peer_id, preventing a peer from advertising endpoints under another
305///    key.
306fn verify_versioned_node_info(
307    peer_info: &SignedVersionedNodeInfo,
308) -> Result<VerifiedSignedVersionedNodeInfo, &'static str> {
309    let peer_id = peer_info.peer_id().ok_or("missing P2P peer_id")?;
310
311    let public_key =
312        Ed25519PublicKey::from_bytes(&peer_id.0).map_err(|_| "invalid peer_id public key")?;
313
314    let msg = bcs::to_bytes(peer_info.data()).expect("BCS serialization should not fail");
315    public_key
316        .verify(&msg, peer_info.auth_sig())
317        .map_err(|_| "signature verification failed")?;
318
319    match peer_info.data() {
320        VersionedNodeInfo::V1(info) => {
321            if info.addresses.len() > MAX_ADDRESSES_PER_PEER {
322                return Err("too many addresses");
323            }
324            if !info
325                .addresses
326                .iter()
327                .all(|addr| addr.len() < MAX_ADDRESS_LENGTH && addr.to_anemo_address().is_ok())
328            {
329                return Err("invalid address");
330            }
331        }
332        VersionedNodeInfo::V2(info_v2) => {
333            // Each endpoint variant (P2p, Consensus) may appear at most once.
334            let mut seen_variants = Vec::new();
335            for endpoint_id in info_v2.addresses.keys() {
336                let variant = std::mem::discriminant(endpoint_id);
337                if seen_variants.contains(&variant) {
338                    return Err("duplicate endpoint variant");
339                }
340                seen_variants.push(variant);
341            }
342
343            for (endpoint_id, addrs) in &info_v2.addresses {
344                if addrs.len() > MAX_ADDRESSES_PER_PEER {
345                    return Err("too many addresses for endpoint");
346                }
347                if !addrs.iter().all(|addr| addr.len() < MAX_ADDRESS_LENGTH) {
348                    return Err("address too long");
349                }
350                if matches!(endpoint_id, EndpointId::P2p(_))
351                    && !addrs.iter().all(|addr| addr.to_anemo_address().is_ok())
352                {
353                    return Err("invalid P2P address");
354                }
355            }
356
357            let identities_valid = info_v2.addresses.keys().all(|eid| match eid {
358                EndpointId::P2p(_) => true,
359                EndpointId::Consensus(pubkey) => pubkey.as_bytes() == peer_id.0,
360            });
361            if !identities_valid {
362                return Err("non-P2P endpoint identity mismatch");
363            }
364        }
365    }
366
367    Ok(VerifiedSignedVersionedNodeInfo::new_from_verified(
368        peer_info.clone(),
369    ))
370}
371
372struct DiscoveryEventLoop {
373    config: P2pConfig,
374    discovery_config: Arc<DiscoveryConfig>,
375    configured_peers: Arc<HashMap<PeerId, PeerInfo>>,
376    chain_peers: Arc<RwLock<HashSet<PeerId>>>,
377    unidentified_seed_peers: Vec<anemo::types::Address>,
378    network: Network,
379    keypair: NetworkKeyPair,
380    tasks: JoinSet<()>,
381    pending_dials: HashMap<PeerId, AbortHandle>,
382    dial_seed_peers_task: Option<AbortHandle>,
383    shutdown_handle: oneshot::Receiver<()>,
384    state: Arc<RwLock<State>>,
385    mailbox: mpsc::Receiver<DiscoveryMessage>,
386    mailbox_tx: mpsc::Sender<DiscoveryMessage>,
387    metrics: Metrics,
388    consensus_external_address: Option<Multiaddr>,
389    endpoint_manager: EndpointManager,
390    store_path: Option<PathBuf>,
391    peer_cooldowns: HashMap<PeerId, Instant>,
392}
393
394impl DiscoveryEventLoop {
395    pub async fn start(mut self) {
396        info!("Discovery started");
397
398        self.construct_our_info();
399        self.configure_preferred_peers();
400        self.load_stored_peers_on_startup();
401
402        let mut interval = tokio::time::interval(self.discovery_config.interval_period());
403        let mut peer_events = {
404            let (subscriber, _peers) = self.network.subscribe().unwrap();
405            subscriber
406        };
407
408        loop {
409            tokio::select! {
410                now = interval.tick() => {
411                    let now_unix = now_unix();
412                    self.handle_tick(now.into_std(), now_unix);
413                }
414                peer_event = peer_events.recv() => {
415                    self.handle_peer_event(peer_event);
416                },
417                Some(message) = self.mailbox.recv() => {
418                    self.handle_message(message);
419                }
420                Some(task_result) = self.tasks.join_next() => {
421                    match task_result {
422                        Ok(()) => {},
423                        Err(e) => {
424                            if e.is_cancelled() {
425                                // avoid crashing on ungraceful shutdown
426                            } else if e.is_panic() {
427                                // propagate panics.
428                                std::panic::resume_unwind(e.into_panic());
429                            } else {
430                                panic!("task failed: {e}");
431                            }
432                        },
433                    };
434                },
435                // Once the shutdown notification resolves we can terminate the event loop
436                _ = &mut self.shutdown_handle => {
437                    break;
438                }
439            }
440        }
441
442        self.save_stored_peers();
443        info!("Discovery ended");
444    }
445
446    fn handle_message(&mut self, message: DiscoveryMessage) {
447        match message {
448            DiscoveryMessage::PeerAddressChange {
449                peer_id,
450                source,
451                addresses,
452            } => {
453                self.handle_peer_address_change(peer_id, source, addresses);
454            }
455            DiscoveryMessage::ReceivedNodeInfo { peer_info } => {
456                let changed = update_known_peers_versioned(
457                    self.state.clone(),
458                    vec![*peer_info],
459                    self.configured_peers.clone(),
460                    &self.chain_peers,
461                    &self.endpoint_manager,
462                );
463                if changed {
464                    self.save_stored_peers();
465                }
466            }
467            DiscoveryMessage::TrustedPeersUpdated => {
468                self.save_stored_peers();
469            }
470            DiscoveryMessage::PeerFailureReport { peer_id } => {
471                self.handle_peer_failure_report(peer_id);
472            }
473            DiscoveryMessage::GetTrustedPeerP2pAddresses { reply } => {
474                let _ = reply.send(self.trusted_peer_p2p_addresses());
475            }
476        }
477    }
478
479    fn trusted_peer_p2p_addresses(&self) -> TrustedPeerP2pAddresses {
480        let state = self.state.read().unwrap();
481        state
482            .peer_addresses
483            .iter()
484            .filter(|(peer_id, _)| {
485                is_trusted_peer(peer_id, &self.configured_peers, &self.chain_peers)
486            })
487            .map(|(peer_id, sources)| (*peer_id, sources.clone()))
488            .collect()
489    }
490
491    fn handle_peer_failure_report(&mut self, peer_id: PeerId) {
492        if self.is_trusted_peer(&peer_id) {
493            info!(?peer_id, "ignoring failure report for trusted peer");
494            return;
495        }
496        let min_peers = self.discovery_config.min_peers_for_disconnect();
497        let connected_count = self.state.read().unwrap().connected_peers.len();
498        if connected_count < min_peers {
499            info!(
500                ?peer_id,
501                connected_count, min_peers, "skipping disconnect, too few connected peers"
502            );
503            return;
504        }
505        info!(
506            ?peer_id,
507            "peer failure reported, disconnecting and adding cooldown"
508        );
509        let _ = self.network.disconnect(peer_id);
510        self.peer_cooldowns.insert(peer_id, Instant::now());
511    }
512
513    fn construct_our_info(&mut self) {
514        if self.state.read().unwrap().our_info.is_some() {
515            return;
516        }
517
518        let peer_id = self.network.peer_id();
519        let timestamp_ms = now_unix();
520        let access_type = self.discovery_config.access_type();
521
522        let addresses: Vec<Multiaddr> = self
523            .config
524            .external_address
525            .clone()
526            .and_then(|addr| addr.to_anemo_address().ok().map(|_| addr))
527            .into_iter()
528            .collect();
529
530        let our_info = NodeInfo {
531            peer_id,
532            addresses: addresses.clone(),
533            timestamp_ms,
534            access_type,
535        }
536        .sign(&self.keypair);
537
538        let mut addresses_map = BTreeMap::new();
539        addresses_map.insert(EndpointId::P2p(peer_id), addresses);
540        if let Some(consensus_addr) = &self.consensus_external_address {
541            // Populates `Consensus` EndpointId from our P2P (anemo) `PeerId`.
542            // This is safe because both the P2P and consensus networks use the same
543            // ed25519 network keypair. Both originate from `NodeConfig::network_key_pair()`.
544            let network_pubkey =
545                NetworkPublicKey::from_bytes(&peer_id.0).expect("PeerId is a valid public key");
546            addresses_map.insert(
547                EndpointId::Consensus(network_pubkey),
548                vec![consensus_addr.clone()],
549            );
550        }
551        let our_info_v2 = VersionedNodeInfo::V2(NodeInfoV2 {
552            addresses: addresses_map,
553            timestamp_ms,
554            access_type,
555        })
556        .sign(&self.keypair);
557
558        let mut state = self.state.write().unwrap();
559        state.our_info = Some(our_info);
560        state.our_info_v2 = Some(our_info_v2);
561    }
562
563    fn configure_preferred_peers(&mut self) {
564        let peers: Vec<_> = self.configured_peers.values().cloned().collect();
565        for peer_info in peers {
566            debug!(?peer_info, "Add configured preferred peer");
567            match peer_info.affinity {
568                PeerAffinity::High => {
569                    self.handle_peer_address_change(
570                        peer_info.peer_id,
571                        AddressSource::Seed,
572                        peer_info.address,
573                    );
574                }
575                _ => {
576                    // Allowed peers accept inbound but aren't actively dialed,
577                    // so insert them directly without address priority tracking.
578                    self.network.known_peers().insert(peer_info);
579                }
580            }
581        }
582    }
583
584    fn update_our_info_timestamp(&mut self, now_unix: u64) {
585        let state = &mut self.state.write().unwrap();
586
587        if let Some(our_info) = &state.our_info {
588            let mut data = our_info.data().clone();
589            data.timestamp_ms = now_unix;
590            state.our_info = Some(data.sign(&self.keypair));
591        }
592
593        if let Some(our_info_v2) = &state.our_info_v2 {
594            let mut data = our_info_v2.data().clone();
595            match &mut data {
596                VersionedNodeInfo::V1(info) => info.timestamp_ms = now_unix,
597                VersionedNodeInfo::V2(info) => info.timestamp_ms = now_unix,
598            }
599            state.our_info_v2 = Some(data.sign(&self.keypair));
600        }
601    }
602
603    fn handle_peer_address_change(
604        &mut self,
605        peer_id: PeerId,
606        source: AddressSource,
607        addresses: Vec<anemo::types::Address>,
608    ) {
609        debug!(
610            ?peer_id,
611            ?source,
612            ?addresses,
613            "Received peer address change"
614        );
615
616        // Update set of trusted peers from on-chain validator configs.
617        if source == AddressSource::Chain {
618            if addresses.is_empty() {
619                self.chain_peers.write().unwrap().remove(&peer_id);
620            } else {
621                self.chain_peers.write().unwrap().insert(peer_id);
622            }
623        }
624
625        // Update stored addresses.
626        {
627            let mut state = self.state.write().unwrap();
628            let source_map = state.peer_addresses.entry(peer_id).or_default();
629
630            if addresses.is_empty() {
631                source_map.remove(&source);
632                if source_map.is_empty() {
633                    state.peer_addresses.remove(&peer_id);
634                }
635            } else {
636                source_map.insert(source, addresses);
637            }
638
639            // When a Chain address arrives, check if there are cached Discovery
640            // P2P addresses in known_peers_v2 that should take priority. This
641            // handles the startup race where cached NodeInfo is loaded before
642            // chain_peers is populated (so update_known_peers_versioned couldn't
643            // forward the Discovery addresses at load time).
644            if source == AddressSource::Chain
645                && !state
646                    .peer_addresses
647                    .get(&peer_id)
648                    .is_some_and(|s| s.contains_key(&AddressSource::Discovery))
649                && let Some(addrs) = state
650                    .known_peers_v2
651                    .get(&peer_id)
652                    .and_then(|info| match info.data() {
653                        VersionedNodeInfo::V2(v2) => Some(v2.p2p_addresses()),
654                        _ => None,
655                    })
656            {
657                let anemo_addrs: Vec<_> = addrs
658                    .iter()
659                    .filter_map(|a| a.to_anemo_address().ok())
660                    .collect();
661                if !anemo_addrs.is_empty() {
662                    state
663                        .peer_addresses
664                        .entry(peer_id)
665                        .or_default()
666                        .insert(AddressSource::Discovery, anemo_addrs);
667                }
668            }
669        }
670
671        // Check if we should use the updated addresses.
672        self.reconfigure_peer_addresses(peer_id);
673    }
674
675    /// Reads the highest-priority addresses from `peer_addresses` for the given peer
676    /// and updates `network.known_peers()` if they differ from the current addresses.
677    fn reconfigure_peer_addresses(&mut self, peer_id: PeerId) {
678        let priority = self
679            .state
680            .read()
681            .unwrap()
682            .peer_addresses
683            .get(&peer_id)
684            .and_then(|sources| {
685                sources
686                    .first_key_value()
687                    .map(|(source, addrs)| (*source, addrs.clone()))
688            });
689
690        // Record the active P2P address source for trusted peers (one gauge per
691        // peer, value-encoded). Untrusted peers are skipped to bound metric
692        // cardinality. A `None` priority (no installed source) records 0.
693        if self.is_trusted_peer(&peer_id) {
694            self.metrics.set_active_p2p_address_source(
695                &peer_id.to_string(),
696                priority.as_ref().map(|(source, _)| *source),
697            );
698        }
699
700        let priority_addresses = priority.map(|(_, addrs)| addrs).unwrap_or_default();
701        let current_addresses = self
702            .network
703            .known_peers()
704            .get(&peer_id)
705            .map(|info| info.address.clone())
706            .unwrap_or_default();
707        if priority_addresses != current_addresses {
708            let new_peer_info = PeerInfo {
709                peer_id,
710                affinity: PeerAffinity::High,
711                address: priority_addresses.clone(),
712            };
713
714            self.network.known_peers().insert(new_peer_info);
715
716            // Override existing connection if there might be one.
717            if !current_addresses.is_empty() {
718                let _ = self.network.disconnect(peer_id);
719
720                if let Some(address) = priority_addresses.first().cloned() {
721                    let network = self.network.clone();
722                    self.tasks.spawn(async move {
723                        // If this fails, ConnectionManager will retry.
724                        let _ = network.connect_with_peer_id(address, peer_id).await;
725                    });
726                }
727            }
728        }
729    }
730
731    fn load_stored_peers_on_startup(&mut self) {
732        let Some(path) = &self.store_path else {
733            return;
734        };
735        let entries = load_stored_peers(path);
736        if entries.is_empty() {
737            return;
738        }
739        info!(
740            count = entries.len(),
741            "Loaded stored peer addresses from {}",
742            path.display()
743        );
744
745        update_known_peers_versioned(
746            self.state.clone(),
747            entries,
748            self.configured_peers.clone(),
749            &self.chain_peers,
750            &self.endpoint_manager,
751        );
752
753        // update_known_peers_versioned forwards addresses through the mailbox.
754        // Drain it now so addresses are applied before the event loop starts.
755        while let Ok(msg) = self.mailbox.try_recv() {
756            self.handle_message(msg);
757        }
758    }
759
760    fn save_stored_peers(&self) {
761        let Some(path) = &self.store_path else {
762            return;
763        };
764        let state = self.state.read().unwrap();
765        let peers_to_save: Vec<SignedVersionedNodeInfo> = state
766            .known_peers_v2
767            .iter()
768            .filter(|(pid, _)| self.is_trusted_peer(pid))
769            .map(|(_, verified)| verified.inner().clone())
770            .collect();
771        drop(state);
772        save_stored_peers(path, &peers_to_save);
773    }
774
775    fn handle_peer_event(&mut self, peer_event: Result<PeerEvent, RecvError>) {
776        match peer_event {
777            Ok(PeerEvent::NewPeer(peer_id)) => {
778                if let Some(peer) = self.network.peer(peer_id) {
779                    self.state
780                        .write()
781                        .unwrap()
782                        .connected_peers
783                        .insert(peer_id, ());
784
785                    // Query the new node for any peers
786                    self.tasks.spawn(query_peer_for_their_known_peers(
787                        peer,
788                        self.discovery_config.clone(),
789                        self.state.clone(),
790                        self.configured_peers.clone(),
791                        self.chain_peers.clone(),
792                        self.endpoint_manager.clone(),
793                        self.mailbox_sender(),
794                    ));
795                }
796            }
797            Ok(PeerEvent::LostPeer(peer_id, _)) => {
798                self.state.write().unwrap().connected_peers.remove(&peer_id);
799            }
800
801            Err(RecvError::Closed) => {
802                panic!("PeerEvent channel shouldn't be able to be closed");
803            }
804
805            Err(RecvError::Lagged(_)) => {
806                trace!("State-Sync fell behind processing PeerEvents");
807            }
808        }
809    }
810
811    fn handle_tick(&mut self, _now: std::time::Instant, now_unix: u64) {
812        self.update_our_info_timestamp(now_unix);
813
814        self.tasks
815            .spawn(query_connected_peers_for_their_known_peers(
816                self.network.clone(),
817                self.discovery_config.clone(),
818                self.state.clone(),
819                self.configured_peers.clone(),
820                self.chain_peers.clone(),
821                self.endpoint_manager.clone(),
822                self.mailbox_sender(),
823            ));
824
825        // Cull old peers older than a day
826        let mut culled_trusted_peers = Vec::new();
827        {
828            let mut state = self.state.write().unwrap();
829            state
830                .known_peers
831                .retain(|_k, v| now_unix.saturating_sub(v.timestamp_ms) < ONE_DAY_MILLISECONDS);
832            state.known_peers_v2.retain(|k, v| {
833                let keep = now_unix.saturating_sub(v.timestamp_ms()) < ONE_DAY_MILLISECONDS;
834                if !keep && self.is_trusted_peer(k) {
835                    culled_trusted_peers.push(*k);
836                }
837                keep
838            });
839        }
840
841        // Clear Discovery-sourced addresses for trusted peers whose
842        // signed info expired, allowing fallback to lower-priority sources.
843        for peer_id in culled_trusted_peers {
844            self.endpoint_manager
845                .clear_source(peer_id, AddressSource::Discovery);
846        }
847
848        // Clean out the pending_dials
849        self.pending_dials.retain(|_k, v| !v.is_finished());
850        if let Some(abort_handle) = &self.dial_seed_peers_task
851            && abort_handle.is_finished()
852        {
853            self.dial_seed_peers_task = None;
854        }
855
856        let cooldown = self.discovery_config.peer_failure_cooldown();
857        self.peer_cooldowns
858            .retain(|_, since| since.elapsed() < cooldown);
859
860        // Spawn some dials
861        let state = self.state.read().unwrap();
862        let our_peer_id = self.network.peer_id();
863
864        // Recompute the number of distinct peers with an external address from the
865        // deduped union of both known-peer maps. With v3 enabled a peer can appear
866        // in both maps, so a HashSet avoids double-counting; recomputing each tick
867        // (rather than inc/dec) also avoids drift when expired entries are culled.
868        let mut peers_with_external_address: HashSet<PeerId> = HashSet::new();
869        for (peer_id, info) in state.known_peers.iter() {
870            if !info.addresses.is_empty() {
871                peers_with_external_address.insert(*peer_id);
872            }
873        }
874        for (peer_id, info) in state.known_peers_v2.iter() {
875            if !info.p2p_addresses().is_empty() {
876                peers_with_external_address.insert(*peer_id);
877            }
878        }
879        self.metrics
880            .set_num_peers_with_external_address(peers_with_external_address.len() as i64);
881
882        // Collect eligible peers from both known_peers (V2) and known_peers_v2 (V3),
883        // preferring fresher timestamps when a peer appears in both maps.
884        // Partition into preferred (not on cooldown) and cooldown peers.
885        let mut preferred: HashMap<PeerId, NodeInfo> = HashMap::new();
886        let mut cooldown_peers: HashMap<PeerId, NodeInfo> = HashMap::new();
887
888        for (peer_id, info) in state.known_peers.iter() {
889            if *peer_id != our_peer_id
890                && !info.addresses.is_empty()
891                && !state.connected_peers.contains_key(peer_id)
892                && !self.pending_dials.contains_key(peer_id)
893                && !state.peer_addresses.contains_key(peer_id)
894            {
895                if self.peer_cooldowns.contains_key(peer_id) {
896                    cooldown_peers.insert(*peer_id, info.data().clone());
897                } else {
898                    preferred.insert(*peer_id, info.data().clone());
899                }
900            }
901        }
902        for (peer_id, info) in state.known_peers_v2.iter() {
903            let p2p_addresses = info.p2p_addresses();
904            if *peer_id != our_peer_id
905                && !p2p_addresses.is_empty()
906                && !state.connected_peers.contains_key(peer_id)
907                && !self.pending_dials.contains_key(peer_id)
908                && !state.peer_addresses.contains_key(peer_id)
909            {
910                let node_info = NodeInfo {
911                    peer_id: *peer_id,
912                    addresses: p2p_addresses.to_vec(),
913                    timestamp_ms: info.timestamp_ms(),
914                    access_type: info.access_type(),
915                };
916                if self.peer_cooldowns.contains_key(peer_id) {
917                    if cooldown_peers
918                        .get(peer_id)
919                        .is_none_or(|existing| info.timestamp_ms() > existing.timestamp_ms)
920                    {
921                        cooldown_peers.insert(*peer_id, node_info);
922                    }
923                } else if preferred
924                    .get(peer_id)
925                    .is_none_or(|existing| info.timestamp_ms() > existing.timestamp_ms)
926                {
927                    preferred.insert(*peer_id, node_info);
928                }
929            }
930        }
931
932        let number_of_connections = state.connected_peers.len();
933        let number_to_dial = self
934            .discovery_config
935            .target_concurrent_connections()
936            .saturating_sub(number_of_connections);
937
938        use rand::seq::IteratorRandom;
939        let mut rng = rand::thread_rng();
940
941        let mut to_dial: Vec<_> = preferred
942            .into_iter()
943            .choose_multiple(&mut rng, number_to_dial);
944
945        let remaining = number_to_dial.saturating_sub(to_dial.len());
946        to_dial.extend(
947            cooldown_peers
948                .into_iter()
949                .choose_multiple(&mut rng, remaining),
950        );
951
952        for (peer_id, info) in to_dial {
953            let abort_handle = self
954                .tasks
955                .spawn(try_to_connect_to_peer(self.network.clone(), info));
956            self.pending_dials.insert(peer_id, abort_handle);
957        }
958
959        // If we aren't connected to anything and we aren't presently trying to connect to anyone
960        // we need to try the configured peers with High affinity (seed peers)
961        let has_peers_to_dial = || {
962            self.configured_peers
963                .values()
964                .any(|p| p.affinity == PeerAffinity::High)
965                || !self.unidentified_seed_peers.is_empty()
966        };
967        if self.dial_seed_peers_task.is_none()
968            && state.connected_peers.is_empty()
969            && self.pending_dials.is_empty()
970            && has_peers_to_dial()
971        {
972            let abort_handle = self.tasks.spawn(try_to_connect_to_seed_peers(
973                self.network.clone(),
974                self.discovery_config.clone(),
975                self.configured_peers.clone(),
976                self.unidentified_seed_peers.clone(),
977            ));
978
979            self.dial_seed_peers_task = Some(abort_handle);
980        }
981    }
982
983    fn is_trusted_peer(&self, peer_id: &PeerId) -> bool {
984        is_trusted_peer(peer_id, &self.configured_peers, &self.chain_peers)
985    }
986
987    fn mailbox_sender(&self) -> mpsc::Sender<DiscoveryMessage> {
988        self.mailbox_tx.clone()
989    }
990}
991
992async fn try_to_connect_to_peer(network: Network, info: NodeInfo) {
993    debug!("Connecting to peer {info:?}");
994    for multiaddr in &info.addresses {
995        if let Ok(address) = multiaddr.to_anemo_address() {
996            // Ignore the result and just log the error if there is one
997            if network
998                .connect_with_peer_id(address, info.peer_id)
999                .await
1000                .tap_err(|e| {
1001                    debug!(
1002                        "error dialing {} at address '{}': {e}",
1003                        info.peer_id.short_display(4),
1004                        multiaddr
1005                    )
1006                })
1007                .is_ok()
1008            {
1009                return;
1010            }
1011        }
1012    }
1013}
1014
1015async fn try_to_connect_to_seed_peers(
1016    network: Network,
1017    config: Arc<DiscoveryConfig>,
1018    configured_peers: Arc<HashMap<PeerId, PeerInfo>>,
1019    unidentified_seed_peers: Vec<anemo::types::Address>,
1020) {
1021    let high_affinity_peers: Vec<_> = configured_peers
1022        .values()
1023        .filter(|p| p.affinity == PeerAffinity::High)
1024        .cloned()
1025        .collect();
1026    debug!(
1027        ?high_affinity_peers,
1028        ?unidentified_seed_peers,
1029        "Connecting to seed peers"
1030    );
1031    let network = &network;
1032
1033    // Attempt connection to all high-affinity and seed peers.
1034    let with_peer_id = high_affinity_peers.into_iter().flat_map(|peer_info| {
1035        peer_info
1036            .address
1037            .into_iter()
1038            .map(move |addr| (Some(peer_info.peer_id), addr))
1039    });
1040    let without_peer_id = unidentified_seed_peers.into_iter().map(|addr| (None, addr));
1041    futures::stream::iter(with_peer_id.chain(without_peer_id))
1042        .for_each_concurrent(
1043            config.target_concurrent_connections(),
1044            |(peer_id, address)| async move {
1045                // Ignore the result and just log the error if there is one
1046                let _ = if let Some(peer_id) = peer_id {
1047                    network
1048                        .connect_with_peer_id(address.clone(), peer_id)
1049                        .await
1050                        .tap_err(|e| {
1051                            debug!(
1052                                "error dialing peer {} at '{}': {e}",
1053                                peer_id.short_display(4),
1054                                address
1055                            )
1056                        })
1057                } else {
1058                    network
1059                        .connect(address.clone())
1060                        .await
1061                        .tap_err(|e| debug!("error dialing address '{}': {e}", address))
1062                };
1063            },
1064        )
1065        .await;
1066}
1067
1068async fn query_peer_for_known_peers_v2(peer: Peer) -> Option<Vec<SignedNodeInfo>> {
1069    let mut client = DiscoveryClient::new(peer);
1070    let request = Request::new(()).with_timeout(TIMEOUT);
1071    client
1072        .get_known_peers_v2(request)
1073        .await
1074        .ok()
1075        .map(Response::into_inner)
1076        .map(
1077            |GetKnownPeersResponseV2 {
1078                 own_info,
1079                 mut known_peers,
1080             }| {
1081                if !own_info.addresses.is_empty() {
1082                    known_peers.push(own_info)
1083                }
1084                known_peers
1085            },
1086        )
1087}
1088
1089async fn query_peer_for_their_known_peers(
1090    peer: Peer,
1091    discovery_config: Arc<DiscoveryConfig>,
1092    state: Arc<RwLock<State>>,
1093    configured_peers: Arc<HashMap<PeerId, PeerInfo>>,
1094    chain_peers: Arc<RwLock<HashSet<PeerId>>>,
1095    endpoint_manager: EndpointManager,
1096    mailbox_tx: mpsc::Sender<DiscoveryMessage>,
1097) {
1098    // Query V3 concurrently with V2 when enabled
1099    if discovery_config.use_get_known_peers_v3() {
1100        let our_info_v2 = state.read().unwrap().our_info_v2.clone();
1101        if let Some(own_info) = our_info_v2 {
1102            let peer_for_v3 = peer.clone();
1103            let v3_query = async move {
1104                let mut client = DiscoveryClient::new(peer_for_v3);
1105                let request =
1106                    Request::new(GetKnownPeersRequestV3 { own_info }).with_timeout(TIMEOUT);
1107                client
1108                    .get_known_peers_v3(request)
1109                    .await
1110                    .ok()
1111                    .map(Response::into_inner)
1112                    .map(
1113                        |GetKnownPeersResponseV3 {
1114                             own_info,
1115                             mut known_peers,
1116                         }| {
1117                            if !own_info.p2p_addresses().is_empty() {
1118                                known_peers.push(own_info)
1119                            }
1120                            known_peers
1121                        },
1122                    )
1123            };
1124
1125            let (found_peers_v2, found_peers_v3) =
1126                tokio::join!(query_peer_for_known_peers_v2(peer), v3_query);
1127
1128            if let Some(found_peers) = found_peers_v2 {
1129                update_known_peers(
1130                    state.clone(),
1131                    found_peers,
1132                    configured_peers.clone(),
1133                    &chain_peers,
1134                );
1135            }
1136            if let Some(found_peers) = found_peers_v3 {
1137                let changed = update_known_peers_versioned(
1138                    state,
1139                    found_peers,
1140                    configured_peers,
1141                    &chain_peers,
1142                    &endpoint_manager,
1143                );
1144                if changed {
1145                    let _ = mailbox_tx.try_send(DiscoveryMessage::TrustedPeersUpdated);
1146                }
1147            }
1148            return;
1149        }
1150    }
1151
1152    // V3 not enabled or our_info_v2 not available - just run V2
1153    if let Some(found_peers) = query_peer_for_known_peers_v2(peer).await {
1154        update_known_peers(state, found_peers, configured_peers, &chain_peers);
1155    }
1156}
1157
1158async fn query_connected_peers_for_their_known_peers(
1159    network: Network,
1160    config: Arc<DiscoveryConfig>,
1161    state: Arc<RwLock<State>>,
1162    configured_peers: Arc<HashMap<PeerId, PeerInfo>>,
1163    chain_peers: Arc<RwLock<HashSet<PeerId>>>,
1164    endpoint_manager: EndpointManager,
1165    mailbox_tx: mpsc::Sender<DiscoveryMessage>,
1166) {
1167    use rand::seq::IteratorRandom;
1168
1169    let peers_to_query: Vec<_> = network
1170        .peers()
1171        .into_iter()
1172        .flat_map(|id| network.peer(id))
1173        .choose_multiple(&mut rand::thread_rng(), config.peers_to_query());
1174
1175    // Query V2 to keep known_peers populated for V2 clients
1176    let v2_query = {
1177        let peers = peers_to_query.clone();
1178        let peers_to_query_count = config.peers_to_query();
1179        async move {
1180            peers
1181                .into_iter()
1182                .map(DiscoveryClient::new)
1183                .map(|mut client| async move {
1184                    let request = Request::new(()).with_timeout(TIMEOUT);
1185                    client
1186                        .get_known_peers_v2(request)
1187                        .await
1188                        .ok()
1189                        .map(Response::into_inner)
1190                        .map(
1191                            |GetKnownPeersResponseV2 {
1192                                 own_info,
1193                                 mut known_peers,
1194                             }| {
1195                                if !own_info.addresses.is_empty() {
1196                                    known_peers.push(own_info)
1197                                }
1198                                known_peers
1199                            },
1200                        )
1201                })
1202                .pipe(futures::stream::iter)
1203                .buffer_unordered(peers_to_query_count)
1204                .filter_map(std::future::ready)
1205                .flat_map(futures::stream::iter)
1206                .collect::<Vec<_>>()
1207                .await
1208        }
1209    };
1210
1211    // Additionally query V3 when enabled, concurrently with V2
1212    if config.use_get_known_peers_v3() {
1213        let our_info_v2 = state.read().unwrap().our_info_v2.clone();
1214        if let Some(own_info) = our_info_v2 {
1215            let v3_query = {
1216                let peers_to_query_count = config.peers_to_query();
1217                async move {
1218                    peers_to_query
1219                        .into_iter()
1220                        .map(DiscoveryClient::new)
1221                        .map(|mut client| {
1222                            let own_info = own_info.clone();
1223                            async move {
1224                                let request = Request::new(GetKnownPeersRequestV3 { own_info })
1225                                    .with_timeout(TIMEOUT);
1226                                client
1227                                    .get_known_peers_v3(request)
1228                                    .await
1229                                    .ok()
1230                                    .map(Response::into_inner)
1231                                    .map(
1232                                        |GetKnownPeersResponseV3 {
1233                                             own_info,
1234                                             mut known_peers,
1235                                         }| {
1236                                            if !own_info.p2p_addresses().is_empty() {
1237                                                known_peers.push(own_info)
1238                                            }
1239                                            known_peers
1240                                        },
1241                                    )
1242                            }
1243                        })
1244                        .pipe(futures::stream::iter)
1245                        .buffer_unordered(peers_to_query_count)
1246                        .filter_map(std::future::ready)
1247                        .flat_map(futures::stream::iter)
1248                        .collect::<Vec<_>>()
1249                        .await
1250                }
1251            };
1252
1253            let (found_peers_v2, found_peers_v3) = tokio::join!(v2_query, v3_query);
1254
1255            update_known_peers(
1256                state.clone(),
1257                found_peers_v2,
1258                configured_peers.clone(),
1259                &chain_peers,
1260            );
1261            let changed = update_known_peers_versioned(
1262                state,
1263                found_peers_v3,
1264                configured_peers,
1265                &chain_peers,
1266                &endpoint_manager,
1267            );
1268            if changed {
1269                let _ = mailbox_tx.try_send(DiscoveryMessage::TrustedPeersUpdated);
1270            }
1271            return;
1272        }
1273    }
1274
1275    // V3 not enabled or our_info_v2 not available - just run V2
1276    let found_peers_v2 = v2_query.await;
1277    update_known_peers(state, found_peers_v2, configured_peers, &chain_peers);
1278}
1279
1280fn update_known_peers(
1281    state: Arc<RwLock<State>>,
1282    found_peers: Vec<SignedNodeInfo>,
1283    configured_peers: Arc<HashMap<PeerId, PeerInfo>>,
1284    chain_peers: &Arc<RwLock<HashSet<PeerId>>>,
1285) {
1286    use std::collections::hash_map::Entry;
1287
1288    let now_unix = now_unix();
1289    let our_peer_id = state.read().unwrap().our_info.clone().unwrap().peer_id;
1290    let known_peers = &mut state.write().unwrap().known_peers;
1291    // only take the first MAX_PEERS_TO_SEND peers
1292    for peer_info in found_peers.into_iter().take(MAX_PEERS_TO_SEND + 1) {
1293        // +1 to account for the "own_info" of the serving peer
1294        // Skip peers whose timestamp is too far in the future from our clock
1295        // or that are too old
1296        if peer_info.timestamp_ms > now_unix.saturating_add(30 * 1_000) // 30 seconds
1297            || now_unix.saturating_sub(peer_info.timestamp_ms) > ONE_DAY_MILLISECONDS
1298        {
1299            continue;
1300        }
1301
1302        if peer_info.peer_id == our_peer_id {
1303            continue;
1304        }
1305
1306        let is_restricted = match peer_info.access_type {
1307            AccessType::Public => false,
1308            AccessType::Private | AccessType::Trusted => true,
1309        };
1310        if is_restricted && !is_trusted_peer(&peer_info.peer_id, &configured_peers, chain_peers) {
1311            continue;
1312        }
1313
1314        // Skip entries that have too many addresses as a means to cap the size of a node's info
1315        if peer_info.addresses.len() > MAX_ADDRESSES_PER_PEER {
1316            continue;
1317        }
1318
1319        // verify that all addresses provided are valid anemo addresses
1320        if !peer_info
1321            .addresses
1322            .iter()
1323            .all(|addr| addr.len() < MAX_ADDRESS_LENGTH && addr.to_anemo_address().is_ok())
1324        {
1325            continue;
1326        }
1327        let Ok(public_key) = Ed25519PublicKey::from_bytes(&peer_info.peer_id.0) else {
1328            debug_fatal!(
1329                // This should never happen.
1330                "Failed to convert anemo PeerId {:?} to Ed25519PublicKey",
1331                peer_info.peer_id
1332            );
1333            continue;
1334        };
1335        let msg = bcs::to_bytes(peer_info.data()).expect("BCS serialization should not fail");
1336        if let Err(e) = public_key.verify(&msg, peer_info.auth_sig()) {
1337            info!(
1338                "Discovery failed to verify signature for NodeInfo for peer {:?}: {e:?}",
1339                peer_info.peer_id
1340            );
1341            // TODO: consider denylisting the source of bad NodeInfo from future requests.
1342            continue;
1343        }
1344        let peer = VerifiedSignedNodeInfo::new_from_verified(peer_info);
1345
1346        match known_peers.entry(peer.peer_id) {
1347            Entry::Occupied(mut o) => {
1348                if peer.timestamp_ms > o.get().timestamp_ms {
1349                    o.insert(peer);
1350                }
1351            }
1352            Entry::Vacant(v) => {
1353                v.insert(peer);
1354            }
1355        }
1356    }
1357}
1358
1359/// Returns true if any trusted peer was inserted or updated.
1360fn update_known_peers_versioned(
1361    state: Arc<RwLock<State>>,
1362    found_peers: Vec<SignedVersionedNodeInfo>,
1363    configured_peers: Arc<HashMap<PeerId, PeerInfo>>,
1364    chain_peers: &Arc<RwLock<HashSet<PeerId>>>,
1365    endpoint_manager: &EndpointManager,
1366) -> bool {
1367    use std::collections::hash_map::Entry;
1368
1369    let now_unix = now_unix();
1370    let our_peer_id = state
1371        .read()
1372        .unwrap()
1373        .our_info_v2
1374        .as_ref()
1375        .and_then(|info| info.peer_id());
1376
1377    // Filter and verify before taking the state write lock: signature
1378    // verification is CPU-heavy (up to MAX_PEERS_TO_SEND peers per call).
1379    let mut verified_peers = Vec::new();
1380    let mut endpoint_updates = Vec::new();
1381
1382    for peer_info in found_peers.into_iter().take(MAX_PEERS_TO_SEND + 1) {
1383        let timestamp_ms = peer_info.timestamp_ms();
1384
1385        if timestamp_ms > now_unix.saturating_add(30 * 1_000)
1386            || now_unix.saturating_sub(timestamp_ms) > ONE_DAY_MILLISECONDS
1387        {
1388            continue;
1389        }
1390
1391        let Some(peer_id) = peer_info.peer_id() else {
1392            continue;
1393        };
1394
1395        if Some(peer_id) == our_peer_id {
1396            continue;
1397        }
1398
1399        let is_restricted = match peer_info.access_type() {
1400            AccessType::Public => false,
1401            AccessType::Private | AccessType::Trusted => true,
1402        };
1403        let is_trusted = is_trusted_peer(&peer_id, &configured_peers, chain_peers);
1404        if is_restricted && !is_trusted {
1405            continue;
1406        }
1407
1408        let peer = match verify_versioned_node_info(&peer_info) {
1409            Ok(verified) => verified,
1410            Err(reason) => {
1411                info!("Discovery rejecting VersionedNodeInfo for peer {peer_id:?}: {reason}");
1412                continue;
1413            }
1414        };
1415
1416        // Collect discovered addresses of trusted peers to forward to the
1417        // EndpointManager after the merge below.
1418        if is_trusted && let VersionedNodeInfo::V2(info_v2) = peer_info.data() {
1419            for (endpoint_id, addrs) in &info_v2.addresses {
1420                if !addrs.is_empty() {
1421                    endpoint_updates.push((endpoint_id.clone(), addrs.clone()));
1422                }
1423            }
1424        }
1425
1426        verified_peers.push((peer_id, peer, is_trusted));
1427    }
1428
1429    // Merge under a brief write lock; the guard must drop before the
1430    // forwarding below.
1431    let mut trusted_peer_changed = false;
1432    {
1433        let known_peers_v2 = &mut state.write().unwrap().known_peers_v2;
1434        for (peer_id, peer, is_trusted) in verified_peers {
1435            match known_peers_v2.entry(peer_id) {
1436                Entry::Occupied(mut o) => {
1437                    if peer.timestamp_ms() > o.get().timestamp_ms() {
1438                        o.insert(peer);
1439                        if is_trusted {
1440                            trusted_peer_changed = true;
1441                        }
1442                    }
1443                }
1444                Entry::Vacant(v) => {
1445                    v.insert(peer);
1446                    if is_trusted {
1447                        trusted_peer_changed = true;
1448                    }
1449                }
1450            }
1451        }
1452    }
1453
1454    // Forward after the merge: handle_peer_address_change's Chain-source
1455    // fallback reads known_peers_v2, so this batch must be visible there
1456    // before these updates are processed. update_endpoint also acquires locks
1457    // in other subsystems, so it must not run under the state lock.
1458    for (endpoint_id, addrs) in endpoint_updates {
1459        let _ = endpoint_manager.update_endpoint(endpoint_id, AddressSource::Discovery, addrs);
1460    }
1461
1462    trusted_peer_changed
1463}
1464
1465/// A trusted peer is one that appears in the static configured_peers (seed/allowlisted)
1466/// or was dynamically added via on-chain validator info (chain_peers).
1467pub(super) fn is_trusted_peer(
1468    peer_id: &PeerId,
1469    configured_peers: &HashMap<PeerId, PeerInfo>,
1470    chain_peers: &Arc<RwLock<HashSet<PeerId>>>,
1471) -> bool {
1472    configured_peers.contains_key(peer_id) || chain_peers.read().unwrap().contains(peer_id)
1473}
1474
1475pub(super) fn now_unix() -> u64 {
1476    use std::time::{SystemTime, UNIX_EPOCH};
1477
1478    SystemTime::now()
1479        .duration_since(UNIX_EPOCH)
1480        .unwrap()
1481        .as_millis() as u64
1482}