Skip to main content

hotshot_libp2p_networking/network/
node.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7/// configuration for the libp2p network (e.g. how it should be built)
8mod config;
9
10/// libp2p network handle
11/// allows for control over the libp2p network
12mod handle;
13
14use std::{
15    collections::{HashMap, HashSet},
16    iter,
17    num::{NonZeroU32, NonZeroUsize},
18    pin::pin,
19    sync::Arc,
20    time::{Duration, Instant},
21};
22
23use alloy::primitives::U256;
24use bimap::BiMap;
25use futures::{SinkExt, StreamExt, channel::mpsc};
26use hotshot_types::{
27    constants::KAD_DEFAULT_REPUB_INTERVAL_SEC, traits::node_implementation::NodeType,
28};
29use libp2p::{
30    Multiaddr, StreamProtocol, Swarm, SwarmBuilder,
31    core::{transport::ListenerId, upgrade::Version::V1Lazy},
32    gossipsub::{
33        Behaviour as Gossipsub, ConfigBuilder as GossipsubConfigBuilder, Event as GossipEvent,
34        Message as GossipsubMessage, MessageAuthenticity, MessageId, Topic, ValidationMode,
35    },
36    identify::{
37        Behaviour as IdentifyBehaviour, Config as IdentifyConfig, Event as IdentifyEvent,
38        Info as IdentifyInfo,
39    },
40    identity::Keypair,
41    kad::{Behaviour, Config, Mode, Record, store::MemoryStore},
42    request_response::{
43        Behaviour as RequestResponse, Config as Libp2pRequestResponseConfig, ProtocolSupport,
44    },
45    swarm::SwarmEvent,
46};
47use libp2p_identity::PeerId;
48use parking_lot::Mutex;
49use rand::{prelude::SliceRandom, thread_rng};
50use tokio::{
51    select, spawn,
52    sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
53    task::JoinHandle,
54    time::sleep,
55};
56use tracing::{Instrument, debug, error, info, info_span, instrument, warn};
57
58pub use self::{
59    config::{
60        DEFAULT_REPLICATION_FACTOR, GossipConfig, NetworkNodeConfig, NetworkNodeConfigBuilder,
61        NetworkNodeConfigBuilderError, RequestResponseConfig,
62    },
63    handle::{NetworkNodeHandle, NetworkNodeReceiver, spawn_network_node},
64};
65use super::{
66    BoxedTransport, ClientRequest, NetworkDef, NetworkError, NetworkEvent, NetworkEventInternal,
67    behaviours::dht::{
68        bootstrap::{DHTBootstrapTask, InputEvent},
69        store::{
70            persistent::{DhtPersistentStorage, PersistentStore},
71            validated::ValidatedStore,
72        },
73    },
74    cbor::Cbor,
75    gen_transport,
76};
77use crate::network::{
78    behaviours::{
79        dht::{DHTBehaviour, DHTProgress, KadPutQuery},
80        direct_message::{DMBehaviour, DMRequest},
81        exponential_backoff::ExponentialBackoff,
82    },
83    log_summary::LogEvent,
84};
85
86/// Join handle for the spawned swarm event-loop task.
87pub type SwarmTaskHandle = JoinHandle<Result<(), NetworkError>>;
88
89/// Maximum size of a message
90pub const MAX_GOSSIP_MSG_SIZE: usize = 2_000_000_000;
91
92/// Wrapped num of connections
93pub const ESTABLISHED_LIMIT: NonZeroU32 = NonZeroU32::new(ESTABLISHED_LIMIT_UNWR).unwrap();
94/// Number of connections to a single peer before logging an error
95pub const ESTABLISHED_LIMIT_UNWR: u32 = 10;
96
97/// How long to wait for the first inbound connection.
98const NO_INBOUND_CONNECTION_GRACE_PERIOD: Duration = Duration::from_secs(300);
99
100/// Mainnet libp2p protocol identifiers. The snapshot tests below lock these down so a
101/// change that would partition mainnet (e.g. a stray `protocol_id_prefix` call) is caught.
102/// `None` for gossipsub means "do not call `protocol_id_prefix`" — libp2p's defaults
103/// (`/meshsub/1.1.0` and `/meshsub/1.0.0`) are then used.
104pub(crate) fn mainnet_gossipsub_prefix() -> Option<&'static str> {
105    None
106}
107pub(crate) fn mainnet_kad_protocol() -> StreamProtocol {
108    StreamProtocol::new("/ipfs/kad/1.0.0")
109}
110pub(crate) fn mainnet_direct_message_protocol() -> StreamProtocol {
111    StreamProtocol::new("/HotShot/direct_message/1.0")
112}
113pub(crate) fn mainnet_identify_protocol() -> &'static str {
114    "HotShot/identify/1.0"
115}
116
117/// Resolve the gossipsub `protocol_id_prefix` for the given network discriminator.
118/// `None` returns the mainnet value (the libp2p default).
119pub(crate) fn gossipsub_prefix(discriminator: Option<U256>) -> Option<String> {
120    match discriminator {
121        None => mainnet_gossipsub_prefix().map(String::from),
122        Some(d) => Some(format!("/HotShot/gossipsub/1.0/{d:#x}")),
123    }
124}
125
126/// Resolve the kademlia stream protocol for the given network discriminator.
127pub(crate) fn kad_protocol(discriminator: Option<U256>) -> Result<StreamProtocol, NetworkError> {
128    match discriminator {
129        None => Ok(mainnet_kad_protocol()),
130        Some(d) => StreamProtocol::try_from_owned(format!("/ipfs/kad/1.0.0/{d:#x}"))
131            .map_err(|err| NetworkError::ConfigError(format!("invalid kademlia protocol: {err}"))),
132    }
133}
134
135/// Resolve the identify protocol string for the given network discriminator.
136pub(crate) fn identify_protocol(discriminator: Option<U256>) -> String {
137    match discriminator {
138        None => mainnet_identify_protocol().to_string(),
139        Some(d) => format!("HotShot/identify/1.0/{d:#x}"),
140    }
141}
142
143/// Resolve the direct-message stream protocol for the given network discriminator.
144pub(crate) fn direct_message_protocol(
145    discriminator: Option<U256>,
146) -> Result<StreamProtocol, NetworkError> {
147    match discriminator {
148        None => Ok(mainnet_direct_message_protocol()),
149        Some(d) => StreamProtocol::try_from_owned(format!("/HotShot/direct_message/1.0/{d:#x}"))
150            .map_err(|err| {
151                NetworkError::ConfigError(format!("invalid direct_message protocol: {err}"))
152            }),
153    }
154}
155
156/// A peer's network is identifiable only by its advertised kademlia `StreamProtocol`.
157fn should_keep_peer(expected: &StreamProtocol, peer_protocols: &[StreamProtocol]) -> bool {
158    peer_protocols.contains(expected)
159}
160
161fn resolve_put_quorum(
162    quorum_override: Option<NonZeroUsize>,
163    replication_factor: NonZeroUsize,
164) -> NonZeroUsize {
165    quorum_override.unwrap_or_else(|| {
166        NonZeroUsize::new(replication_factor.get() / 2)
167            .expect("replication factor should be bigger than 0")
168    })
169}
170
171/// Network definition
172#[derive(derive_more::Debug)]
173pub struct NetworkNode<T: NodeType, D: DhtPersistentStorage> {
174    /// peer id of network node
175    peer_id: PeerId,
176    /// the swarm of networkbehaviours
177    #[debug(skip)]
178    swarm: Swarm<NetworkDef<T::SignatureKey, D>>,
179    /// The Kademlia record TTL
180    kademlia_record_ttl: Duration,
181    /// The map from consensus keys to peer IDs
182    consensus_key_to_pid_map: Arc<Mutex<BiMap<T::SignatureKey, PeerId>>>,
183    /// the listener id we are listening on, if it exists
184    listener_id: Option<ListenerId>,
185    /// Handler for direct messages
186    direct_message_state: DMBehaviour,
187    /// Handler for DHT Events
188    dht_handler: DHTBehaviour<T::SignatureKey, D>,
189    /// Channel to resend requests, set to Some when we call `spawn_listeners`
190    resend_tx: Option<UnboundedSender<ClientRequest>>,
191    /// Whether a peer ever connected to us.
192    ///
193    /// Used to warn the operator when the advertised address is likely not
194    /// publicly reachable.
195    saw_inbound_connection: bool,
196    /// Kademlia `StreamProtocol` expected from same-network peers.
197    expected_kad_protocol: StreamProtocol,
198    /// Peers confirmed via identify to share our network discriminator. Gates
199    /// `NewExternalAddrOfPeer`, whose event carries no protocols to check directly.
200    same_network_peers: HashSet<PeerId>,
201    dht_put_quorum: Option<NonZeroUsize>,
202}
203
204impl<T: NodeType, D: DhtPersistentStorage> NetworkNode<T, D> {
205    /// Returns number of peers this node is connected to
206    pub fn num_connected(&self) -> usize {
207        self.swarm.connected_peers().count()
208    }
209
210    /// return hashset of PIDs this node is connected to
211    pub fn connected_pids(&self) -> HashSet<PeerId> {
212        self.swarm.connected_peers().copied().collect()
213    }
214
215    /// starts the swarm listening on `listen_addr`
216    /// and optionally dials into peer `known_peer`
217    /// returns the address the swarm is listening upon
218    #[instrument(skip(self))]
219    pub async fn start_listen(
220        &mut self,
221        listen_addr: Multiaddr,
222    ) -> Result<Multiaddr, NetworkError> {
223        self.listener_id = Some(self.swarm.listen_on(listen_addr).map_err(|err| {
224            NetworkError::ListenError(format!("failed to listen for Libp2p: {err}"))
225        })?);
226        let addr = loop {
227            if let Some(SwarmEvent::NewListenAddr { address, .. }) = self.swarm.next().await {
228                break address;
229            }
230        };
231        info!("Libp2p listening on {addr:?}");
232        Ok(addr)
233    }
234
235    /// initialize the DHT with known peers
236    /// add the peers to kademlia and then
237    /// the `spawn_listeners` function
238    /// will start connecting to peers
239    #[instrument(skip(self))]
240    pub fn add_known_peers(&mut self, known_peers: &[(PeerId, Multiaddr)]) {
241        debug!("Adding {} known peers", known_peers.len());
242        let behaviour = self.swarm.behaviour_mut();
243        let mut bs_nodes = HashMap::<PeerId, HashSet<Multiaddr>>::new();
244        let mut shuffled = known_peers.iter().collect::<Vec<_>>();
245        shuffled.shuffle(&mut thread_rng());
246        for (peer_id, addr) in shuffled {
247            if *peer_id != self.peer_id {
248                behaviour.dht.add_address(peer_id, addr.clone());
249                bs_nodes.insert(*peer_id, iter::once(addr.clone()).collect());
250            }
251        }
252    }
253
254    /// Creates a new `Network` with the given settings.
255    ///
256    /// Currently:
257    ///   * Generates a random key pair and associated [`PeerId`]
258    ///   * Launches a hopefully production ready transport: QUIC v1 (RFC 9000) + DNS
259    ///   * Generates a connection to the "broadcast" topic
260    ///   * Creates a swarm to manage peers and events
261    ///
262    /// # Errors
263    /// - If we fail to generate the transport or any of the behaviours
264    ///
265    /// # Panics
266    /// If 5 < 0
267    #[allow(clippy::too_many_lines)]
268    pub async fn new(
269        config: NetworkNodeConfig,
270        dht_persistent_storage: D,
271        consensus_key_to_pid_map: Arc<Mutex<BiMap<T::SignatureKey, PeerId>>>,
272    ) -> Result<Self, NetworkError> {
273        // Generate a random `KeyPair` if one is not specified
274        let keypair = config
275            .keypair
276            .clone()
277            .unwrap_or_else(Keypair::generate_ed25519);
278
279        // Get the `PeerId` from the `KeyPair`
280        let peer_id = PeerId::from(keypair.public());
281
282        // Generate the transport from the keypair and auth message
283        let transport: BoxedTransport = gen_transport::<T>(
284            keypair.clone(),
285            config.auth_message.clone(),
286            Arc::clone(&consensus_key_to_pid_map),
287        )
288        .await?;
289
290        // Calculate the record republication interval
291        let kademlia_record_republication_interval = config
292            .republication_interval
293            .unwrap_or(Duration::from_secs(KAD_DEFAULT_REPUB_INTERVAL_SEC));
294
295        // Calculate the Kademlia record TTL
296        let kademlia_ttl = config
297            .ttl
298            .unwrap_or(16 * kademlia_record_republication_interval);
299
300        let expected_kad_protocol = kad_protocol(config.network_discriminator)?;
301
302        // Generate the swarm
303        let mut swarm: Swarm<NetworkDef<T::SignatureKey, D>> = {
304            // Use the `Blake3` hash of the message's contents as the ID
305            let message_id_fn = |message: &GossipsubMessage| {
306                let hash = blake3::hash(&message.data);
307                MessageId::from(hash.as_bytes().to_vec())
308            };
309
310            // Derive a `Gossipsub` config from our gossip config
311            let mut gossipsub_builder = GossipsubConfigBuilder::default();
312            if let Some(prefix) = gossipsub_prefix(config.network_discriminator) {
313                gossipsub_builder.protocol_id_prefix(prefix);
314            }
315            let gossipsub_config = gossipsub_builder
316                .message_id_fn(message_id_fn) // Use the (blake3) hash of a message as its ID
317                .validation_mode(ValidationMode::Strict) // Force all messages to have valid signatures
318                .heartbeat_interval(config.gossip_config.heartbeat_interval) // Time between gossip heartbeats
319                .history_gossip(config.gossip_config.history_gossip) // Number of heartbeats to gossip about
320                .history_length(config.gossip_config.history_length) // Number of heartbeats to remember the full message for
321                .mesh_n(config.gossip_config.mesh_n) // Target number of mesh peers
322                .mesh_n_high(config.gossip_config.mesh_n_high) // Upper limit of mesh peers
323                .mesh_n_low(config.gossip_config.mesh_n_low) // Lower limit of mesh peers
324                .mesh_outbound_min(config.gossip_config.mesh_outbound_min) // Minimum number of outbound peers in mesh
325                .max_transmit_size(config.gossip_config.max_transmit_size) // Maximum size of a message
326                .max_ihave_length(config.gossip_config.max_ihave_length) // Maximum number of messages to include in an IHAVE message
327                .max_ihave_messages(config.gossip_config.max_ihave_messages) // Maximum number of IHAVE messages to accept from a peer within a heartbeat
328                .published_message_ids_cache_time(
329                    config.gossip_config.published_message_ids_cache_time,
330                ) // Cache duration for published message IDs
331                .iwant_followup_time(config.gossip_config.iwant_followup_time) // Time to wait for a message requested through IWANT following an IHAVE advertisement
332                .max_messages_per_rpc(config.gossip_config.max_messages_per_rpc) // The maximum number of messages we will process in a given RPC
333                .gossip_retransimission(config.gossip_config.gossip_retransmission) // Controls how many times we will allow a peer to request the same message id through IWANT gossip before we start ignoring them.
334                .flood_publish(config.gossip_config.flood_publish) // If enabled newly created messages will always be sent to all peers that are subscribed to the topic and have a good enough score.
335                .duplicate_cache_time(config.gossip_config.duplicate_cache_time) // The time period that messages are stored in the cache
336                .fanout_ttl(config.gossip_config.fanout_ttl) // Time to live for fanout peers
337                .heartbeat_initial_delay(config.gossip_config.heartbeat_initial_delay) // Initial delay in each heartbeat
338                .gossip_factor(config.gossip_config.gossip_factor) // Affects how many peers we will emit gossip to at each heartbeat
339                .gossip_lazy(config.gossip_config.gossip_lazy) // Minimum number of peers to emit gossip to during a heartbeat
340                .build()
341                .map_err(|err| {
342                    NetworkError::ConfigError(format!("error building gossipsub config: {err:?}"))
343                })?;
344
345            // - Build a gossipsub network behavior
346            let gossipsub: Gossipsub = Gossipsub::new(
347                MessageAuthenticity::Signed(keypair.clone()),
348                gossipsub_config,
349            )
350            .map_err(|err| {
351                NetworkError::ConfigError(format!("error building gossipsub behaviour: {err:?}"))
352            })?;
353
354            //   Build a identify network behavior needed for own
355            //   node connection information
356            //   E.g. this will answer the question: how are other nodes
357            //   seeing the peer from behind a NAT
358            let identify_cfg = IdentifyConfig::new(
359                identify_protocol(config.network_discriminator),
360                keypair.public(),
361            );
362            let identify = IdentifyBehaviour::new(identify_cfg);
363
364            // - Build DHT needed for peer discovery
365            let mut kconfig = Config::new(expected_kad_protocol.clone());
366            kconfig
367                .set_parallelism(NonZeroUsize::new(5).unwrap())
368                .set_provider_publication_interval(Some(kademlia_record_republication_interval))
369                .set_publication_interval(Some(kademlia_record_republication_interval))
370                .set_record_ttl(Some(kademlia_ttl));
371
372            // allowing panic here because something is very wrong if this fails
373            #[allow(clippy::panic)]
374            if let Some(factor) = config.replication_factor {
375                kconfig.set_replication_factor(factor);
376            } else {
377                panic!("Replication factor not set");
378            }
379
380            // Create the DHT behaviour with the given persistent storage
381            let mut kadem = Behaviour::with_config(
382                peer_id,
383                PersistentStore::new(
384                    ValidatedStore::new(MemoryStore::new(peer_id)),
385                    dht_persistent_storage,
386                    5,
387                )
388                .await,
389                kconfig,
390            );
391            kadem.set_mode(Some(Mode::Server));
392
393            let rrconfig = Libp2pRequestResponseConfig::default();
394
395            // Create a new `cbor` codec with the given request and response sizes
396            let cbor = Cbor::new(
397                config.request_response_config.request_size_maximum,
398                config.request_response_config.response_size_maximum,
399            );
400
401            let direct_message: super::cbor::Behaviour<Vec<u8>, Vec<u8>> =
402                RequestResponse::with_codec(
403                    cbor,
404                    [(
405                        direct_message_protocol(config.network_discriminator)?,
406                        ProtocolSupport::Full,
407                    )],
408                    rrconfig.clone(),
409                );
410
411            let network = NetworkDef::new(gossipsub, kadem, identify, direct_message);
412
413            // build swarm
414            let swarm = SwarmBuilder::with_existing_identity(keypair.clone());
415            let swarm = swarm.with_tokio();
416
417            swarm
418                .with_other_transport(|_| transport)
419                .unwrap()
420                .with_behaviour(|_| network)
421                .unwrap()
422                .with_swarm_config(|cfg| {
423                    cfg.with_idle_connection_timeout(Duration::from_secs(10))
424                        .with_substream_upgrade_protocol_override(V1Lazy)
425                })
426                .build()
427        };
428        for (peer, addr) in &config.to_connect_addrs {
429            if peer != swarm.local_peer_id() {
430                swarm.behaviour_mut().add_address(peer, addr.clone());
431                swarm.add_peer_address(*peer, addr.clone());
432            }
433        }
434
435        for addr in &config.announce_addresses {
436            info!("Adding announce address {addr}");
437            swarm.add_external_address(addr.clone());
438        }
439
440        Ok(Self {
441            peer_id,
442            swarm,
443            kademlia_record_ttl: kademlia_ttl,
444            consensus_key_to_pid_map,
445            listener_id: None,
446            direct_message_state: DMBehaviour::default(),
447            dht_handler: DHTBehaviour::new(
448                peer_id,
449                config
450                    .replication_factor
451                    .unwrap_or(NonZeroUsize::new(4).unwrap()),
452            ),
453            resend_tx: None,
454            saw_inbound_connection: false,
455            expected_kad_protocol,
456            same_network_peers: HashSet::new(),
457            dht_put_quorum: config.dht_put_quorum,
458        })
459    }
460
461    /// Identify is the first point a peer's network is detectable; drop other networks.
462    fn on_identify_received(&mut self, peer_id: PeerId, info: IdentifyInfo) {
463        if should_keep_peer(&self.expected_kad_protocol, &info.protocols) {
464            self.same_network_peers.insert(peer_id);
465            let behaviour = self.swarm.behaviour_mut();
466            // Deduplicate before inserting (duplicates are common in practice).
467            for addr in info.listen_addrs.iter().collect::<HashSet<_>>() {
468                behaviour.dht.add_address(&peer_id, addr.clone());
469            }
470        } else {
471            debug!(
472                "Dropping peer {peer_id}: its kad protocol does not match ours ({})",
473                self.expected_kad_protocol
474            );
475            self.same_network_peers.remove(&peer_id);
476            self.swarm.behaviour_mut().dht.remove_peer(&peer_id);
477            let _ = self.swarm.disconnect_peer_id(peer_id);
478        }
479    }
480
481    /// Publish a key/value to the record store.
482    ///
483    /// # Panics
484    /// If the default replication factor is `None`
485    pub fn put_record(&mut self, mut query: KadPutQuery) {
486        // Create the new record
487        let mut record = Record::new(query.key.clone(), query.value.clone());
488
489        // Set the record's expiration time to the proper time
490        record.expires = Some(Instant::now() + self.kademlia_record_ttl);
491
492        let quorum = resolve_put_quorum(self.dht_put_quorum, self.dht_handler.replication_factor());
493        match self
494            .swarm
495            .behaviour_mut()
496            .dht
497            .put_record(record, libp2p::kad::Quorum::N(quorum))
498        {
499            Err(e) => {
500                // failed try again later
501                query.progress = DHTProgress::NotStarted;
502                query.backoff.start_next(false);
503                error!("Error publishing to DHT: {e:?} for peer {:?}", self.peer_id);
504            },
505            Ok(qid) => {
506                debug!("Published record to DHT with qid {qid:?}");
507                let query = KadPutQuery {
508                    progress: DHTProgress::InProgress(qid),
509                    ..query
510                };
511                self.dht_handler.put_record(qid, query);
512            },
513        }
514    }
515
516    /// event handler for client events
517    /// currently supported actions include
518    /// - shutting down the swarm
519    /// - gossipping a message to known peers on the `global` topic
520    /// - returning the id of the current peer
521    /// - subscribing to a topic
522    /// - unsubscribing from a toipc
523    /// - direct messaging a peer
524    #[instrument(skip(self))]
525    async fn handle_client_requests(
526        &mut self,
527        msg: Option<ClientRequest>,
528    ) -> Result<bool, NetworkError> {
529        let behaviour = self.swarm.behaviour_mut();
530        match msg {
531            Some(msg) => {
532                match msg {
533                    ClientRequest::BeginBootstrap => {
534                        debug!("Beginning Libp2p bootstrap");
535                        let _ = self.swarm.behaviour_mut().dht.bootstrap();
536                    },
537                    ClientRequest::LookupPeer(pid, chan) => {
538                        let id = self.swarm.behaviour_mut().dht.get_closest_peers(pid);
539                        self.dht_handler
540                            .in_progress_get_closest_peers
541                            .insert(id, chan);
542                    },
543                    ClientRequest::GetRoutingTable(chan) => {
544                        self.dht_handler
545                            .print_routing_table(&mut self.swarm.behaviour_mut().dht);
546                        if chan.send(()).is_err() {
547                            warn!("Tried to notify client but client not tracking anymore");
548                        }
549                    },
550                    ClientRequest::PutDHT { key, value, notify } => {
551                        let query = KadPutQuery {
552                            progress: DHTProgress::NotStarted,
553                            notify,
554                            key,
555                            value,
556                            backoff: ExponentialBackoff::default(),
557                        };
558                        self.put_record(query);
559                    },
560                    ClientRequest::GetConnectedPeerNum(s) => {
561                        if s.send(self.num_connected()).is_err() {
562                            error!("error sending peer number to client");
563                        }
564                    },
565                    ClientRequest::GetConnectedPeers(s) => {
566                        if s.send(self.connected_pids()).is_err() {
567                            error!("error sending peer set to client");
568                        }
569                    },
570                    ClientRequest::GetKadRoutingPeers(s) => {
571                        let peers: HashSet<PeerId> = self
572                            .swarm
573                            .behaviour_mut()
574                            .dht
575                            .kbuckets()
576                            .flat_map(|b| {
577                                b.iter().map(|e| *e.node.key.preimage()).collect::<Vec<_>>()
578                            })
579                            .collect();
580                        if s.send(peers).is_err() {
581                            error!("error sending kad routing peers to client");
582                        }
583                    },
584                    ClientRequest::GetDHT {
585                        key,
586                        notify,
587                        retry_count,
588                    } => {
589                        self.dht_handler.get_record(
590                            key,
591                            notify,
592                            ExponentialBackoff::default(),
593                            retry_count,
594                            &mut self.swarm.behaviour_mut().dht,
595                        );
596                    },
597                    ClientRequest::IgnorePeers(_peers) => {
598                        // NOTE used by test with conductor only
599                    },
600                    ClientRequest::Shutdown => {
601                        if let Some(listener_id) = self.listener_id {
602                            self.swarm.remove_listener(listener_id);
603                        }
604
605                        return Ok(true);
606                    },
607                    ClientRequest::GossipMsg(topic, contents) => {
608                        behaviour.publish_gossip(Topic::new(topic.clone()), contents.clone());
609                    },
610                    ClientRequest::Subscribe(t, chan) => {
611                        behaviour.subscribe_gossip(&t);
612                        if let Some(chan) = chan
613                            && chan.send(()).is_err()
614                        {
615                            error!("finished subscribing but response channel dropped");
616                        }
617                    },
618                    ClientRequest::Unsubscribe(t, chan) => {
619                        behaviour.unsubscribe_gossip(&t);
620                        if let Some(chan) = chan
621                            && chan.send(()).is_err()
622                        {
623                            error!("finished unsubscribing but response channel dropped");
624                        }
625                    },
626                    ClientRequest::DirectRequest {
627                        pid,
628                        contents,
629                        retry_count,
630                    } => {
631                        debug!("Sending direct request to {pid:?}");
632                        let id = behaviour.add_direct_request(pid, contents.clone());
633                        let req = DMRequest {
634                            peer_id: pid,
635                            data: contents,
636                            backoff: ExponentialBackoff::default(),
637                            retry_count,
638                        };
639                        self.direct_message_state.add_direct_request(req, id);
640                    },
641                    ClientRequest::DirectResponse(chan, msg) => {
642                        behaviour.add_direct_response(chan, msg);
643                    },
644                    ClientRequest::AddKnownPeers(peers) => {
645                        self.add_known_peers(&peers);
646                    },
647                    ClientRequest::Prune(pid) => {
648                        if self.swarm.disconnect_peer_id(pid).is_err() {
649                            warn!("Could not disconnect from {pid:?}");
650                        }
651                    },
652                }
653            },
654            None => {
655                error!("Error receiving msg in main behaviour loop: channel closed");
656            },
657        }
658        Ok(false)
659    }
660
661    /// event handler for events emitted from the swarm
662    #[allow(clippy::type_complexity)]
663    #[instrument(skip(self))]
664    async fn handle_swarm_events(
665        &mut self,
666        event: SwarmEvent<NetworkEventInternal>,
667        send_to_client: &UnboundedSender<NetworkEvent>,
668    ) -> Result<(), NetworkError> {
669        // Make the match cleaner
670        debug!("Swarm event observed {:?}", event);
671
672        #[allow(deprecated)]
673        match event {
674            SwarmEvent::ConnectionEstablished {
675                connection_id: _,
676                peer_id,
677                endpoint,
678                num_established,
679                concurrent_dial_errors,
680                established_in: _established_in,
681            } => {
682                if num_established > ESTABLISHED_LIMIT {
683                    error!(
684                        "Num concurrent connections to a single peer exceeding \
685                         {ESTABLISHED_LIMIT:?} at {num_established:?}!"
686                    );
687                } else {
688                    debug!(
689                        "Connection established with {peer_id:?} at {endpoint:?} with \
690                         {concurrent_dial_errors:?} concurrent dial errors"
691                    );
692                }
693
694                if endpoint.is_listener() {
695                    self.saw_inbound_connection = true;
696                }
697
698                // Send the number of connected peers to the client
699                send_to_client
700                    .send(NetworkEvent::ConnectedPeersUpdate(self.num_connected()))
701                    .map_err(|err| NetworkError::ChannelSendError(err.to_string()))?;
702            },
703            SwarmEvent::ConnectionClosed {
704                connection_id: _,
705                peer_id,
706                endpoint,
707                num_established,
708                cause,
709            } => {
710                if num_established > ESTABLISHED_LIMIT_UNWR {
711                    error!(
712                        "Num concurrent connections to a single peer exceeding \
713                         {ESTABLISHED_LIMIT:?} at {num_established:?}!"
714                    );
715                } else {
716                    debug!("Connection closed with {peer_id:?} at {endpoint:?} due to {cause:?}");
717                }
718
719                // If we are no longer connected to the peer, remove the consensus key from the map
720                // and reset verified status so reconnecting peers are re-verified via identify.
721                if num_established == 0 {
722                    self.consensus_key_to_pid_map
723                        .lock()
724                        .remove_by_right(&peer_id);
725                    self.same_network_peers.remove(&peer_id);
726                }
727
728                // Send the number of connected peers to the client
729                send_to_client
730                    .send(NetworkEvent::ConnectedPeersUpdate(self.num_connected()))
731                    .map_err(|err| NetworkError::ChannelSendError(err.to_string()))?;
732            },
733            SwarmEvent::Dialing {
734                peer_id,
735                connection_id: _,
736            } => {
737                debug!("Attempting to dial {peer_id:?}");
738            },
739            SwarmEvent::ListenerClosed {
740                listener_id: _,
741                addresses: _,
742                reason: _,
743            }
744            | SwarmEvent::NewListenAddr {
745                listener_id: _,
746                address: _,
747            }
748            | SwarmEvent::ExpiredListenAddr {
749                listener_id: _,
750                address: _,
751            }
752            | SwarmEvent::NewExternalAddrCandidate { .. }
753            | SwarmEvent::ExternalAddrExpired { .. }
754            | SwarmEvent::IncomingConnection {
755                connection_id: _,
756                local_addr: _,
757                send_back_addr: _,
758            } => {},
759            SwarmEvent::Behaviour(b) => {
760                let maybe_event = match b {
761                    NetworkEventInternal::DHTEvent(e) => self
762                        .dht_handler
763                        .dht_handle_event(e, self.swarm.behaviour_mut().dht.store_mut()),
764                    NetworkEventInternal::IdentifyEvent(e) => {
765                        if let IdentifyEvent::Received {
766                            peer_id,
767                            info,
768                            connection_id: _,
769                        } = *e
770                        {
771                            self.on_identify_received(peer_id, info);
772                        }
773                        None
774                    },
775                    NetworkEventInternal::GossipEvent(e) => match *e {
776                        GossipEvent::Message {
777                            propagation_source: _peer_id,
778                            message_id: _id,
779                            message,
780                        } => Some(NetworkEvent::GossipMsg(message.data)),
781                        GossipEvent::Subscribed { peer_id, topic } => {
782                            debug!("Peer {peer_id:?} subscribed to topic {topic:?}");
783                            None
784                        },
785                        GossipEvent::Unsubscribed { peer_id, topic } => {
786                            debug!("Peer {peer_id:?} unsubscribed from topic {topic:?}");
787                            None
788                        },
789                        GossipEvent::GossipsubNotSupported { peer_id } => {
790                            LogEvent::GossipsubNotSupported.record();
791                            debug!("Peer {peer_id:?} does not support gossipsub");
792                            None
793                        },
794                        GossipEvent::SlowPeer {
795                            peer_id,
796                            failed_messages: _,
797                        } => {
798                            LogEvent::GossipsubSlowPeer.record();
799                            debug!("Peer {peer_id:?} is slow");
800                            None
801                        },
802                    },
803                    NetworkEventInternal::DMEvent(e) => self
804                        .direct_message_state
805                        .handle_dm_event(e, self.resend_tx.clone()),
806                };
807
808                if let Some(event) = maybe_event {
809                    // forward messages directly to Client
810                    send_to_client
811                        .send(event)
812                        .map_err(|err| NetworkError::ChannelSendError(err.to_string()))?;
813                }
814            },
815            SwarmEvent::OutgoingConnectionError {
816                connection_id: _,
817                peer_id,
818                error,
819            } => {
820                LogEvent::DialFailure.record();
821                debug!("Outgoing connection error to {peer_id:?}: {error:?}");
822            },
823            SwarmEvent::IncomingConnectionError {
824                connection_id: _,
825                local_addr: _,
826                send_back_addr: _,
827                error,
828                peer_id: _,
829            } => {
830                LogEvent::IncomingConnError.record();
831                debug!("Incoming connection error: {error:?}");
832            },
833            SwarmEvent::ListenerError {
834                listener_id: _,
835                error,
836            } => {
837                LogEvent::ListenerError.record();
838                debug!("Listener error: {error:?}");
839            },
840            SwarmEvent::ExternalAddrConfirmed { address } => {
841                let my_id = *self.swarm.local_peer_id();
842                self.swarm
843                    .behaviour_mut()
844                    .dht
845                    .add_address(&my_id, address.clone());
846            },
847            SwarmEvent::NewExternalAddrOfPeer { peer_id, address } => {
848                // Only same-network peers; foreign peers carry no protocols on this event.
849                if self.same_network_peers.contains(&peer_id) {
850                    self.swarm
851                        .behaviour_mut()
852                        .dht
853                        .add_address(&peer_id, address.clone());
854                }
855            },
856            _ => {
857                debug!("Unhandled swarm event {event:?}");
858            },
859        }
860        Ok(())
861    }
862
863    /// Spawn a task to listen for requests on the returned channel
864    /// as well as any events produced by libp2p
865    ///
866    /// # Errors
867    /// - If we fail to create the channels or the bootstrap channel
868    pub fn spawn_listeners(
869        mut self,
870    ) -> Result<
871        (
872            UnboundedSender<ClientRequest>,
873            UnboundedReceiver<NetworkEvent>,
874            SwarmTaskHandle,
875        ),
876        NetworkError,
877    > {
878        let (s_input, mut s_output) = unbounded_channel::<ClientRequest>();
879        let (r_input, r_output) = unbounded_channel::<NetworkEvent>();
880        let (mut bootstrap_tx, bootstrap_rx) = mpsc::channel(100);
881        self.resend_tx = Some(s_input.clone());
882        self.dht_handler.set_bootstrap_sender(bootstrap_tx.clone());
883
884        DHTBootstrapTask::run(bootstrap_rx, s_input.clone());
885        // Keep the task's `JoinHandle` so callers can await the swarm loop
886        // ending on shutdown, ensuring the listening socket is released.
887        let task = spawn(
888            async move {
889                let mut inbound_check = pin!(sleep(NO_INBOUND_CONNECTION_GRACE_PERIOD));
890                let mut inbound_check_pending = true;
891                loop {
892                    select! {
893                        event = self.swarm.next() => {
894                            debug!("peerid {:?}\t\thandling maybe event {:?}", self.peer_id, event);
895                            if let Some(event) = event {
896                                debug!("peerid {:?}\t\thandling event {:?}", self.peer_id, event);
897                                self.handle_swarm_events(event, &r_input).await?;
898                            }
899                        },
900                        msg = s_output.recv() => {
901                            debug!("peerid {:?}\t\thandling msg {:?}", self.peer_id, msg);
902                            let shutdown = self.handle_client_requests(msg).await?;
903                            if shutdown {
904                                let _ = bootstrap_tx.send(InputEvent::ShutdownBootstrap).await;
905                                break
906                            }
907                        },
908                        () = &mut inbound_check, if inbound_check_pending => {
909                            inbound_check_pending = false;
910                            self.warn_if_unreachable();
911                        }
912                    }
913                }
914                drop(self.swarm);
915                Ok::<(), NetworkError>(())
916            }
917            .instrument(info_span!("Libp2p NetworkBehaviour Handler")),
918        );
919        Ok((s_input, r_output, task))
920    }
921
922    /// Get a reference to the network node's peer id.
923    pub fn peer_id(&self) -> PeerId {
924        self.peer_id
925    }
926
927    /// One-shot reachability check.
928    ///
929    /// Run [`NO_INBOUND_CONNECTION_GRACE_PERIOD`] after the swarm loop starts.
930    /// A healthy, publicly reachable node receives inbound connections shortly
931    /// after joining. If we are listening but no peer has connected to us by
932    /// now, the advertised address is likely wrong or blocked, so point the
933    /// operator at it. If we have no connections at all, not even outbound,
934    /// the problem is more fundamental than the advertised address.
935    fn warn_if_unreachable(&self) {
936        if self.listener_id.is_none() || self.saw_inbound_connection {
937            return;
938        }
939        if self.num_connected() == 0 {
940            error!(
941                "No connections at all within {NO_INBOUND_CONNECTION_GRACE_PERIOD:?} of startup. \
942                 This node could not reach any peer. Check the bootstrap node configuration and \
943                 that outbound UDP traffic is allowed (firewall/NAT/security group)."
944            );
945        } else {
946            error!(
947                "No peer has connected to us within {NO_INBOUND_CONNECTION_GRACE_PERIOD:?} of \
948                 startup. This node is likely not publicly reachable: peers cannot direct-message \
949                 us, so leader views may fail and we may accumulate missed slots. Verify \
950                 --libp2p-advertise-address (env ESPRESSO_NODE_LIBP2P_ADVERTISE_ADDRESS) is set \
951                 to a publicly reachable host:port, and ensure inbound UDP at that port is open \
952                 from the public internet (firewall/NAT/security group)."
953            );
954        }
955    }
956}
957
958#[cfg(test)]
959mod tests {
960    use std::{collections::HashSet, num::NonZeroUsize, sync::Arc, time::Duration};
961
962    use hotshot_example_types::node_types::TestTypes;
963    use parking_lot::Mutex;
964
965    use super::{
966        U256, direct_message_protocol, gossipsub_prefix, identify_protocol, kad_protocol,
967        resolve_put_quorum, should_keep_peer,
968    };
969    use crate::network::{
970        NetworkNodeConfigBuilder, behaviours::dht::store::persistent::DhtNoPersistence,
971        node::handle::spawn_network_node,
972    };
973
974    /// Spawn a node with the given discriminator and initial peer set. Returns the handle and
975    /// a background task draining the event receiver so the swarm loop never stalls on backpressure.
976    async fn spawn_node(
977        discriminator: Option<U256>,
978        connect_to: HashSet<(libp2p_identity::PeerId, libp2p::Multiaddr)>,
979        id: usize,
980    ) -> crate::network::node::handle::NetworkNodeHandle<TestTypes> {
981        use bimap::BiMap;
982
983        let config = NetworkNodeConfigBuilder::default()
984            .network_discriminator(discriminator)
985            .to_connect_addrs(connect_to)
986            .build()
987            .expect("config build");
988
989        let key_map = Arc::new(Mutex::new(BiMap::default()));
990        let (mut receiver, handle) = spawn_network_node::<TestTypes, DhtNoPersistence>(
991            config,
992            DhtNoPersistence,
993            key_map,
994            id,
995        )
996        .await
997        .expect("spawn node");
998
999        // Drain events so the swarm loop is never blocked on a full channel.
1000        tokio::spawn(async move {
1001            loop {
1002                if receiver.recv().await.is_err() {
1003                    break;
1004                }
1005            }
1006        });
1007
1008        handle
1009    }
1010
1011    fn snapshot_for(discriminator: Option<U256>) -> String {
1012        format!(
1013            "gossipsub_prefix: {:?}\nkad: {}\ndirect_message: {}\nidentify: {}",
1014            gossipsub_prefix(discriminator),
1015            kad_protocol(discriminator).unwrap(),
1016            direct_message_protocol(discriminator).unwrap(),
1017            identify_protocol(discriminator),
1018        )
1019    }
1020
1021    #[test]
1022    fn mainnet_libp2p_protocol_identifiers() {
1023        insta::assert_snapshot!("mainnet_libp2p_protocol_identifiers", snapshot_for(None));
1024    }
1025
1026    #[test]
1027    fn decaf_libp2p_protocol_identifiers() {
1028        insta::assert_snapshot!(
1029            "decaf_libp2p_protocol_identifiers",
1030            snapshot_for(Some(U256::from(0xdecafu64)))
1031        );
1032    }
1033
1034    #[test]
1035    fn should_keep_peer_empty_protocols() {
1036        let expected = kad_protocol(None).unwrap();
1037        assert!(!should_keep_peer(&expected, &[]));
1038    }
1039
1040    #[test]
1041    fn should_keep_peer_discriminator_matrix() {
1042        let discriminators = [None, Some(U256::from(1u64)), Some(U256::from(2u64))];
1043        for ours in discriminators {
1044            for theirs in discriminators {
1045                let expected = kad_protocol(ours).unwrap();
1046                let peer_protocols = [
1047                    direct_message_protocol(theirs).unwrap(),
1048                    kad_protocol(theirs).unwrap(),
1049                ];
1050                assert_eq!(
1051                    should_keep_peer(&expected, &peer_protocols),
1052                    ours == theirs,
1053                    "ours={ours:?} theirs={theirs:?}",
1054                );
1055            }
1056        }
1057    }
1058
1059    /// Cross-network peer must not persist in a's Kademlia routing table after identify-triggered
1060    /// disconnect.
1061    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1062    async fn evicts_cross_network_peer_from_routing_table() {
1063        tokio::time::timeout(Duration::from_secs(90), async {
1064            let a = spawn_node(Some(U256::from(1u64)), HashSet::new(), 0).await;
1065
1066            let hub = HashSet::from([(a.peer_id(), a.listen_addr())]);
1067            let b = spawn_node(Some(U256::from(1u64)), hub.clone(), 1).await;
1068            let x = spawn_node(Some(U256::from(2u64)), hub, 2).await;
1069
1070            b.begin_bootstrap().expect("begin_bootstrap b");
1071            x.begin_bootstrap().expect("begin_bootstrap x");
1072
1073            let b_pid = b.peer_id();
1074            let x_pid = x.peer_id();
1075
1076            let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
1077            let mut consecutive = 0usize;
1078            loop {
1079                assert!(
1080                    tokio::time::Instant::now() < deadline,
1081                    "steady state not reached",
1082                );
1083                tokio::time::sleep(Duration::from_secs(1)).await;
1084                let kad_peers = a.kad_routing_peers().await.expect("kad_routing_peers");
1085                if kad_peers.contains(&b_pid) && !kad_peers.contains(&x_pid) {
1086                    consecutive += 1;
1087                    if consecutive >= 3 {
1088                        break;
1089                    }
1090                } else {
1091                    consecutive = 0;
1092                }
1093            }
1094
1095            x.shutdown().await.expect("shutdown x");
1096            b.shutdown().await.expect("shutdown b");
1097            a.shutdown().await.expect("shutdown a");
1098        })
1099        .await
1100        .expect("test timed out");
1101    }
1102
1103    /// Hub `a` keeps same-network peer `b` and drops foreign peer `x`; proves selective,
1104    /// not wholesale, disconnection.
1105    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1106    async fn drops_cross_network_peer() {
1107        tokio::time::timeout(Duration::from_secs(90), async {
1108            let a = spawn_node(Some(U256::from(1u64)), HashSet::new(), 0).await;
1109
1110            let hub = HashSet::from([(a.peer_id(), a.listen_addr())]);
1111            let b = spawn_node(Some(U256::from(1u64)), hub.clone(), 1).await;
1112            let x = spawn_node(Some(U256::from(2u64)), hub, 2).await;
1113
1114            b.begin_bootstrap().expect("begin_bootstrap b");
1115            x.begin_bootstrap().expect("begin_bootstrap x");
1116
1117            let b_pid = b.peer_id();
1118            let expected = HashSet::from([b_pid]);
1119
1120            let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
1121            // Require N consecutive 1s-apart checks to avoid transient matches.
1122            let mut consecutive = 0usize;
1123            loop {
1124                assert!(
1125                    tokio::time::Instant::now() < deadline,
1126                    "steady state not reached; a.connected_pids()={:?}",
1127                    a.connected_pids().await,
1128                );
1129                tokio::time::sleep(Duration::from_secs(1)).await;
1130                let a_pids = a.connected_pids().await.expect("connected_pids");
1131                let x_conn = x.num_connected().await.expect("num_connected");
1132                if a_pids == expected && x_conn == 0 {
1133                    consecutive += 1;
1134                    if consecutive >= 3 {
1135                        break;
1136                    }
1137                } else {
1138                    consecutive = 0;
1139                }
1140            }
1141
1142            x.shutdown().await.expect("shutdown x");
1143            b.shutdown().await.expect("shutdown b");
1144            a.shutdown().await.expect("shutdown a");
1145        })
1146        .await
1147        .expect("test timed out");
1148    }
1149
1150    #[test]
1151    fn put_quorum_override() {
1152        let nz = |n| NonZeroUsize::new(n).unwrap();
1153
1154        // default path: quorum = replication_factor / 2
1155        assert_eq!(resolve_put_quorum(None, nz(20)), nz(10));
1156        assert_eq!(resolve_put_quorum(None, nz(2)), nz(1));
1157
1158        // override below default
1159        assert_eq!(resolve_put_quorum(Some(nz(1)), nz(20)), nz(1));
1160        // override independent of replication factor
1161        assert_eq!(resolve_put_quorum(Some(nz(7)), nz(20)), nz(7));
1162        // resolver does not clamp; kad caps separately at min(n, rf)
1163        assert_eq!(resolve_put_quorum(Some(nz(50)), nz(20)), nz(50));
1164    }
1165}