Skip to main content

hotshot/traits/networking/
libp2p_network.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//! Libp2p based/production networking implementation
8//! This module provides a libp2p based networking implementation where each node in the
9//! network forms a tcp or udp connection to a subset of other nodes in the network
10use std::{
11    cmp::min,
12    collections::{BTreeSet, HashSet},
13    fmt::Debug,
14    net::{IpAddr, ToSocketAddrs},
15    num::NonZeroUsize,
16    sync::{
17        Arc,
18        atomic::{AtomicBool, AtomicU64, Ordering},
19    },
20    time::Duration,
21};
22#[cfg(feature = "hotshot-testing")]
23use std::{collections::HashMap, str::FromStr};
24
25use alloy::primitives::U256;
26use anyhow::{Context, anyhow};
27use async_lock::RwLock;
28use async_trait::async_trait;
29use bimap::BiMap;
30use futures::future::join_all;
31#[cfg(feature = "hotshot-testing")]
32use hotshot_libp2p_networking::network::behaviours::dht::store::persistent::DhtNoPersistence;
33pub use hotshot_libp2p_networking::network::{GossipConfig, RequestResponseConfig};
34use hotshot_libp2p_networking::{
35    network::{
36        DEFAULT_REPLICATION_FACTOR,
37        NetworkEvent::{self, DirectRequest, DirectResponse, GossipMsg},
38        NetworkNodeConfig, NetworkNodeConfigBuilder, NetworkNodeHandle, NetworkNodeReceiver,
39        behaviours::dht::{
40            record::{Namespace, RecordKey, RecordValue},
41            store::persistent::DhtPersistentStorage,
42        },
43        log_summary::LogEvent,
44        spawn_network_node,
45        transport::construct_auth_message,
46    },
47    reexport::Multiaddr,
48};
49use hotshot_types::{
50    BoxSyncFuture, boxed_sync,
51    constants::LOOK_AHEAD,
52    data::{EpochNumber, ViewNumber},
53    network::NetworkConfig,
54    traits::{
55        metrics::{Counter, Gauge, Metrics, NoMetrics},
56        network::{ConnectedNetwork, NetworkError, Topic},
57        node_implementation::NodeType,
58        signature_key::{PrivateSignatureKey, SignatureKey},
59    },
60};
61#[cfg(feature = "hotshot-testing")]
62use hotshot_types::{
63    PeerConnectInfo,
64    traits::network::{AsyncGenerator, NetworkReliability, TestableNetworkingImplementation},
65};
66use libp2p_identity::{
67    Keypair, PeerId,
68    ed25519::{self, SecretKey},
69};
70use serde::Serialize;
71use tokio::{
72    select, spawn,
73    sync::{
74        Mutex,
75        mpsc::{Receiver, Sender, channel, error::TrySendError},
76    },
77    time::sleep,
78};
79use tracing::{debug, error, info, instrument, trace, warn};
80
81use crate::{BroadcastDelay, EpochMembershipCoordinator};
82
83/// Libp2p-specific metrics
84#[derive(Clone, Debug)]
85pub struct Libp2pMetricsValue {
86    /// The number of currently connected peers
87    pub num_connected_peers: Box<dyn Gauge>,
88    /// The number of failed messages
89    pub num_failed_messages: Box<dyn Counter>,
90    /// Whether or not the network is considered ready
91    pub is_ready: Box<dyn Gauge>,
92}
93
94impl Libp2pMetricsValue {
95    /// Populate the metrics with Libp2p-specific metrics
96    pub fn new(metrics: &dyn Metrics) -> Self {
97        // Create a `libp2p subgroup
98        let subgroup = metrics.subgroup("libp2p".into());
99
100        // Create the metrics
101        Self {
102            num_connected_peers: subgroup.create_gauge("num_connected_peers".into(), None),
103            num_failed_messages: subgroup.create_counter("num_failed_messages".into(), None),
104            is_ready: subgroup.create_gauge("is_ready".into(), None),
105        }
106    }
107}
108
109impl Default for Libp2pMetricsValue {
110    /// Initialize with empty metrics
111    fn default() -> Self {
112        Self::new(&*NoMetrics::boxed())
113    }
114}
115
116/// convenience alias for the type for bootstrap addresses
117/// concurrency primitives are needed for having tests
118pub type BootstrapAddrs = Arc<RwLock<Vec<(PeerId, Multiaddr)>>>;
119
120/// hardcoded topic of QC used
121pub const QC_TOPIC: &str = "global";
122
123/// Stubbed out Ack
124///
125/// Note: as part of versioning for upgradability,
126/// all network messages must begin with a 4-byte version number.
127///
128/// Hence:
129///   * `Empty` *must* be a struct (enums are serialized with a leading byte for the variant), and
130///   * we must have an explicit version field.
131#[derive(Serialize)]
132pub struct Empty {
133    /// This should not be required, but it is. Version automatically gets prepended.
134    /// Perhaps this could be replaced with something zero-sized and serializable.
135    byte: u8,
136}
137
138impl<T: NodeType> Debug for Libp2pNetwork<T> {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("Libp2p").field("inner", &"inner").finish()
141    }
142}
143
144/// Type alias for a shared collection of peerid, multiaddrs
145pub type PeerInfoVec = Arc<RwLock<Vec<(PeerId, Multiaddr)>>>;
146
147/// The underlying state of the libp2p network
148#[derive(Debug)]
149struct Libp2pNetworkInner<T: NodeType> {
150    /// this node's public key
151    pk: T::SignatureKey,
152    /// handle to control the network
153    handle: Arc<NetworkNodeHandle<T>>,
154    /// Message Receiver
155    receiver: Mutex<Receiver<Vec<u8>>>,
156    /// Sender for broadcast messages
157    sender: Sender<Vec<u8>>,
158    /// Sender for node lookup (relevant view number, key of node) (None for shutdown)
159    node_lookup_send: Sender<Option<(ViewNumber, T::SignatureKey)>>,
160    /// this is really cheating to enable local tests
161    /// hashset of (bootstrap_addr, peer_id)
162    bootstrap_addrs: PeerInfoVec,
163    /// whether or not the network is ready to send
164    is_ready: Arc<AtomicBool>,
165    /// max time before dropping message due to DHT error
166    dht_timeout: Duration,
167    /// whether or not we've bootstrapped into the DHT yet
168    is_bootstrapped: Arc<AtomicBool>,
169    /// The Libp2p metrics we're managing
170    metrics: Libp2pMetricsValue,
171    /// The list of topics we're subscribed to
172    subscribed_topics: HashSet<String>,
173    /// the latest view number (for node lookup purposes)
174    /// NOTE: supposed to represent a ViewNumber but we
175    /// haven't made that atomic yet and we prefer lock-free
176    latest_seen_view: Arc<AtomicU64>,
177    #[cfg(feature = "hotshot-testing")]
178    /// reliability_config
179    reliability_config: Option<Box<dyn NetworkReliability>>,
180    /// Killswitch sender
181    kill_switch: Sender<()>,
182}
183
184/// Networking implementation that uses libp2p
185/// generic over `M` which is the message type
186#[derive(Clone)]
187pub struct Libp2pNetwork<T: NodeType> {
188    /// holds the state of the libp2p network
189    inner: Arc<Libp2pNetworkInner<T>>,
190}
191
192#[cfg(feature = "hotshot-testing")]
193impl<T: NodeType> TestableNetworkingImplementation<T> for Libp2pNetwork<T> {
194    /// Returns a boxed function `f(node_id, public_key) -> Libp2pNetwork`
195    /// with the purpose of generating libp2p networks.
196    /// Generates `num_bootstrap` bootstrap nodes. The remainder of nodes are normal
197    /// nodes with sane defaults.
198    /// # Panics
199    /// Returned function may panic either:
200    /// - An invalid configuration
201    ///   (probably an issue with the defaults of this function)
202    /// - An inability to spin up the replica's network
203    #[allow(clippy::panic, clippy::too_many_lines)]
204    fn generator(
205        expected_node_count: usize,
206        num_bootstrap: usize,
207        _network_id: usize,
208        da_committee_size: usize,
209        reliability_config: Option<Box<dyn NetworkReliability>>,
210        _secondary_network_delay: Duration,
211        _connect_infos: &mut HashMap<T::SignatureKey, PeerConnectInfo>,
212    ) -> AsyncGenerator<Arc<Self>> {
213        assert!(
214            da_committee_size <= expected_node_count,
215            "DA committee size must be less than or equal to total # nodes"
216        );
217        let bootstrap_addrs: PeerInfoVec = Arc::default();
218        let node_ids: Arc<RwLock<HashSet<u64>>> = Arc::default();
219
220        // NOTE uncomment this for easier debugging
221        // let start_port = 5000;
222        Box::pin({
223            move |node_id| {
224                info!(
225                    "GENERATOR: Node id {:?}, is bootstrap: {:?}",
226                    node_id,
227                    node_id < num_bootstrap as u64
228                );
229
230                // UDP has no TIME_WAIT, so there's a tiny race before libp2p binds.
231                let port = std::net::UdpSocket::bind("127.0.0.1:0")
232                    .expect("UDP socket should bind")
233                    .local_addr()
234                    .expect("UDP socket should have local addr")
235                    .port();
236
237                let addr =
238                    Multiaddr::from_str(&format!("/ip4/127.0.0.1/udp/{port}/quic-v1")).unwrap();
239
240                // We assign node's public key and stake value rather than read from config file since it's a test
241                let privkey = T::SignatureKey::generated_from_seed_indexed([0u8; 32], node_id).1;
242                let pubkey = T::SignatureKey::from_private(&privkey);
243
244                // Derive the Libp2p keypair from the private key
245                let libp2p_keypair = derive_libp2p_keypair::<T::SignatureKey>(&privkey)
246                    .expect("Failed to derive libp2p keypair");
247
248                // Sign the lookup record
249                let lookup_record_value = RecordValue::new_signed(
250                    &RecordKey::new(Namespace::Lookup, pubkey.to_bytes()),
251                    libp2p_keypair.public().to_peer_id().to_bytes(),
252                    &privkey,
253                )
254                .expect("Failed to sign DHT lookup record");
255
256                // We want at least 2/3 of the nodes to have any given record in the DHT
257                let replication_factor =
258                    NonZeroUsize::new((2 * expected_node_count).div_ceil(3)).unwrap();
259
260                // Build the network node configuration
261                let config = NetworkNodeConfigBuilder::default()
262                    .keypair(libp2p_keypair)
263                    .replication_factor(replication_factor)
264                    .bind_address(Some(addr))
265                    .to_connect_addrs(HashSet::default())
266                    .republication_interval(None)
267                    .build()
268                    .expect("Failed to build network node config");
269
270                let bootstrap_addrs_ref = Arc::clone(&bootstrap_addrs);
271                let node_ids_ref = Arc::clone(&node_ids);
272                let reliability_config_dup = reliability_config.clone();
273
274                Box::pin(async move {
275                    // If it's the second time we are starting this network, clear the bootstrap info
276                    let mut write_ids = node_ids_ref.write().await;
277                    if write_ids.contains(&node_id) {
278                        write_ids.clear();
279                    }
280                    write_ids.insert(node_id);
281                    drop(write_ids);
282                    Arc::new(
283                        match Libp2pNetwork::new(
284                            Libp2pMetricsValue::default(),
285                            DhtNoPersistence,
286                            config,
287                            pubkey.clone(),
288                            lookup_record_value,
289                            bootstrap_addrs_ref,
290                            usize::try_from(node_id).unwrap(),
291                            #[cfg(feature = "hotshot-testing")]
292                            reliability_config_dup,
293                        )
294                        .await
295                        {
296                            Ok(network) => network,
297                            Err(err) => {
298                                panic!("Failed to create libp2p network: {err:?}");
299                            },
300                        },
301                    )
302                })
303            }
304        })
305    }
306
307    fn in_flight_message_count(&self) -> Option<usize> {
308        None
309    }
310}
311
312/// Derive a Libp2p keypair from a given private key
313///
314/// # Errors
315/// If we are unable to derive a new `SecretKey` from the `blake3`-derived
316/// bytes.
317pub fn derive_libp2p_keypair<K: SignatureKey>(
318    private_key: &K::PrivateKey,
319) -> anyhow::Result<Keypair> {
320    // Derive a secondary key from our primary private key
321    let derived_key = blake3::derive_key("libp2p key", &private_key.to_bytes());
322    let derived_key = SecretKey::try_from_bytes(derived_key)?;
323
324    // Create an `ed25519` keypair from the derived key
325    Ok(ed25519::Keypair::from(derived_key).into())
326}
327
328/// Derive a Libp2p Peer ID from a given private key
329///
330/// # Errors
331/// If we are unable to derive a Libp2p keypair
332pub fn derive_libp2p_peer_id<K: SignatureKey>(
333    private_key: &K::PrivateKey,
334) -> anyhow::Result<PeerId> {
335    // Get the derived keypair
336    let keypair = derive_libp2p_keypair::<K>(private_key)?;
337
338    // Return the PeerID derived from the public key
339    Ok(PeerId::from_public_key(&keypair.public()))
340}
341
342/// Parse a Libp2p Multiaddr from a string. The input string should be in the format
343/// `hostname:port` or `ip:port`. This function derives a `Multiaddr` from the input string.
344///
345/// This borrows from Rust's implementation of `to_socket_addrs` but will only warn if the domain
346/// does not yet resolve.
347///
348/// # Errors
349/// - If the input string is not in the correct format
350pub fn derive_libp2p_multiaddr(addr: &String) -> anyhow::Result<Multiaddr> {
351    // Split the address into the host and port parts
352    let (host, port) = match addr.rfind(':') {
353        Some(idx) => (&addr[..idx], &addr[idx + 1..]),
354        None => return Err(anyhow!("Invalid address format, no port supplied")),
355    };
356
357    // Try parsing the host as an IP address
358    let ip = host.parse::<IpAddr>();
359
360    // Conditionally build the multiaddr string
361    let multiaddr_string = match ip {
362        Ok(IpAddr::V4(ip)) => format!("/ip4/{ip}/udp/{port}/quic-v1"),
363        Ok(IpAddr::V6(ip)) => format!("/ip6/{ip}/udp/{port}/quic-v1"),
364        Err(_) => {
365            // Try resolving the host. If it fails, continue but warn the user
366            let lookup_result = addr.to_socket_addrs();
367
368            // See if the lookup failed
369            let failed = lookup_result
370                .map(|result| result.collect::<Vec<_>>().is_empty())
371                .unwrap_or(true);
372
373            // If it did, warn the user
374            if failed {
375                warn!(
376                    "Failed to resolve domain name {host}, assuming it has not yet been \
377                     provisioned"
378                );
379            }
380
381            format!("/dns/{host}/udp/{port}/quic-v1")
382        },
383    };
384
385    // Convert the multiaddr string to a `Multiaddr`
386    multiaddr_string.parse().with_context(|| {
387        format!("Failed to convert Multiaddr string to Multiaddr: {multiaddr_string}")
388    })
389}
390
391impl<T: NodeType> Libp2pNetwork<T> {
392    /// Create and return a Libp2p network from a network config file
393    /// and various other configuration-specific values.
394    ///
395    /// # Errors
396    /// If we are unable to parse a Multiaddress
397    ///
398    /// # Panics
399    /// If we are unable to calculate the replication factor
400    #[allow(clippy::too_many_arguments)]
401    pub async fn from_config<D: DhtPersistentStorage>(
402        mut config: NetworkConfig<T>,
403        dht_persistent_storage: D,
404        gossip_config: GossipConfig,
405        request_response_config: RequestResponseConfig,
406        bind_address: Multiaddr,
407        announce_addresses: Vec<Multiaddr>,
408        pub_key: &T::SignatureKey,
409        priv_key: &<T::SignatureKey as SignatureKey>::PrivateKey,
410        metrics: Libp2pMetricsValue,
411        network_discriminator: Option<U256>,
412        dht_put_quorum: Option<NonZeroUsize>,
413    ) -> anyhow::Result<Self> {
414        // Try to take our Libp2p config from our broader network config
415        let libp2p_config = config
416            .libp2p_config
417            .take()
418            .ok_or(anyhow!("Libp2p config not supplied"))?;
419
420        // Derive our Libp2p keypair from our supplied private key
421        let keypair = derive_libp2p_keypair::<T::SignatureKey>(priv_key)?;
422
423        // Build our libp2p configuration
424        let mut config_builder = NetworkNodeConfigBuilder::default();
425
426        // Set the gossip configuration
427        config_builder.gossip_config(gossip_config.clone());
428        config_builder.request_response_config(request_response_config);
429
430        // Construct the auth message
431        let auth_message =
432            construct_auth_message(pub_key, &keypair.public().to_peer_id(), priv_key)
433                .with_context(|| "Failed to construct auth message")?;
434
435        // Set the auth message and stake table
436        config_builder.auth_message(Some(auth_message));
437
438        // The replication factor is the minimum of [the default and 2/3 the number of nodes]
439        let Some(default_replication_factor) = DEFAULT_REPLICATION_FACTOR else {
440            return Err(anyhow!("Default replication factor not supplied"));
441        };
442
443        let replication_factor = NonZeroUsize::new(min(
444            default_replication_factor.get(),
445            config.config.num_nodes_with_stake.get() / 2,
446        ))
447        .with_context(|| "Failed to calculate replication factor")?;
448
449        // Sign our DHT lookup record
450        let lookup_record_value = RecordValue::new_signed(
451            &RecordKey::new(Namespace::Lookup, pub_key.to_bytes()),
452            // The value is our Libp2p Peer ID
453            keypair.public().to_peer_id().to_bytes(),
454            priv_key,
455        )
456        .with_context(|| "Failed to sign DHT lookup record")?;
457
458        config_builder
459            .keypair(keypair)
460            .replication_factor(replication_factor)
461            .bind_address(Some(bind_address.clone()))
462            .announce_addresses(announce_addresses)
463            .network_discriminator(network_discriminator)
464            .dht_put_quorum(dht_put_quorum);
465
466        // Connect to the provided bootstrap nodes
467        config_builder.to_connect_addrs(HashSet::from_iter(libp2p_config.bootstrap_nodes.clone()));
468
469        // Build the node's configuration
470        let node_config = config_builder.build()?;
471
472        // Calculate all keys so we can keep track of direct message recipients
473        let mut all_keys = BTreeSet::new();
474
475        // Insert all known nodes into the set of all keys
476        for node in config.config.known_nodes_with_stake {
477            all_keys.insert(T::SignatureKey::public_key(&node.stake_table_entry));
478        }
479
480        Ok(Libp2pNetwork::new(
481            metrics,
482            dht_persistent_storage,
483            node_config,
484            pub_key.clone(),
485            lookup_record_value,
486            Arc::new(RwLock::new(libp2p_config.bootstrap_nodes)),
487            usize::try_from(config.node_index)?,
488            #[cfg(feature = "hotshot-testing")]
489            None,
490        )
491        .await?)
492    }
493
494    /// Returns whether or not the network has any peers.
495    #[must_use]
496    pub fn has_peers(&self) -> bool {
497        self.inner.is_ready.load(Ordering::Relaxed)
498    }
499
500    /// Returns only when the network is ready.
501    pub async fn wait_for_peers(&self) {
502        loop {
503            if self.has_peers() {
504                break;
505            }
506            sleep(Duration::from_secs(1)).await;
507        }
508    }
509
510    /// Constructs new network for a node. Note that this network is unconnected.
511    /// One must call `connect` in order to connect.
512    /// * `config`: the configuration of the node
513    /// * `pk`: public key associated with the node
514    /// * `bootstrap_addrs`: rwlock containing the bootstrap addrs
515    /// # Errors
516    /// Returns error in the event that the underlying libp2p network
517    /// is unable to create a network.
518    ///
519    /// # Panics
520    ///
521    /// This will panic if there are less than 5 bootstrap nodes
522    #[allow(clippy::too_many_arguments)]
523    pub async fn new<D: DhtPersistentStorage>(
524        metrics: Libp2pMetricsValue,
525        dht_persistent_storage: D,
526        config: NetworkNodeConfig,
527        pk: T::SignatureKey,
528        lookup_record_value: RecordValue<T::SignatureKey>,
529        bootstrap_addrs: BootstrapAddrs,
530        id: usize,
531        #[cfg(feature = "hotshot-testing")] reliability_config: Option<Box<dyn NetworkReliability>>,
532    ) -> Result<Libp2pNetwork<T>, NetworkError> {
533        // Create a map from consensus keys to Libp2p peer IDs
534        let consensus_key_to_pid_map = Arc::new(parking_lot::Mutex::new(BiMap::new()));
535
536        let (mut rx, network_handle) = spawn_network_node::<T, D>(
537            config.clone(),
538            dht_persistent_storage,
539            Arc::clone(&consensus_key_to_pid_map),
540            id,
541        )
542        .await
543        .map_err(|e| NetworkError::ConfigError(format!("failed to spawn network node: {e}")))?;
544
545        // Add our own address to the bootstrap addresses
546        let addr = network_handle.listen_addr();
547        let pid = network_handle.peer_id();
548        bootstrap_addrs.write().await.push((pid, addr));
549
550        // Subscribe to the relevant topics
551        let subscribed_topics = HashSet::from_iter(vec![QC_TOPIC.to_string()]);
552
553        // unbounded channels may not be the best choice (spammed?)
554        // if bounded figure out a way to log dropped msgs
555        let (sender, receiver) = channel(1000);
556        let (node_lookup_send, node_lookup_recv) = channel(10);
557        let (kill_tx, kill_rx) = channel(1);
558        rx.set_kill_switch(kill_rx);
559
560        let mut result = Libp2pNetwork {
561            inner: Arc::new(Libp2pNetworkInner {
562                handle: Arc::new(network_handle),
563                receiver: Mutex::new(receiver),
564                sender: sender.clone(),
565                pk,
566                bootstrap_addrs,
567                is_ready: Arc::new(AtomicBool::new(false)),
568                // This is optimal for 10-30 nodes. TODO: parameterize this for both tests and examples
569                dht_timeout: config.dht_timeout.unwrap_or(Duration::from_secs(120)),
570                is_bootstrapped: Arc::new(AtomicBool::new(false)),
571                metrics,
572                subscribed_topics,
573                node_lookup_send,
574                // Start the latest view from 0. "Latest" refers to "most recent view we are polling for
575                // proposals on". We need this because to have consensus info injected we need a working
576                // network already. In the worst case, we send a few lookups we don't need.
577                latest_seen_view: Arc::new(AtomicU64::new(0)),
578                #[cfg(feature = "hotshot-testing")]
579                reliability_config,
580                kill_switch: kill_tx,
581            }),
582        };
583
584        // Set the network as not ready
585        result.inner.metrics.is_ready.set(0);
586
587        result.handle_event_generator(sender, rx);
588        result.spawn_node_lookup(node_lookup_recv);
589        result.spawn_connect(id, lookup_record_value);
590
591        Ok(result)
592    }
593
594    /// Spawns task for looking up nodes pre-emptively
595    #[allow(clippy::cast_sign_loss, clippy::cast_precision_loss)]
596    fn spawn_node_lookup(
597        &self,
598        mut node_lookup_recv: Receiver<Option<(ViewNumber, T::SignatureKey)>>,
599    ) {
600        let handle = Arc::clone(&self.inner.handle);
601        let dht_timeout = self.inner.dht_timeout;
602        let latest_seen_view = Arc::clone(&self.inner.latest_seen_view);
603
604        // deals with handling lookup queue. should be infallible
605        spawn(async move {
606            // cancels on shutdown
607            while let Some(Some((view_number, pk))) = node_lookup_recv.recv().await {
608                /// defines lookahead threshold based on the constant
609                #[allow(clippy::cast_possible_truncation)]
610                const THRESHOLD: u64 = (LOOK_AHEAD as f64 * 0.8) as u64;
611
612                trace!("Performing lookup for peer {pk}");
613
614                // only run if we are not too close to the next view number
615                if latest_seen_view.load(Ordering::Relaxed) + THRESHOLD <= *view_number {
616                    // look up
617                    if let Err(err) = handle.lookup_node(&pk, dht_timeout).await {
618                        LogEvent::DhtLookupFailure.record();
619                        debug!("Failed to perform lookup for key {pk}: {err}");
620                    };
621                }
622            }
623        });
624    }
625
626    /// Initiates connection to the outside world
627    fn spawn_connect(&mut self, id: usize, lookup_record_value: RecordValue<T::SignatureKey>) {
628        let pk = self.inner.pk.clone();
629        let bootstrap_ref = Arc::clone(&self.inner.bootstrap_addrs);
630        let handle = Arc::clone(&self.inner.handle);
631        let is_bootstrapped = Arc::clone(&self.inner.is_bootstrapped);
632        let inner = Arc::clone(&self.inner);
633
634        spawn({
635            let is_ready = Arc::clone(&self.inner.is_ready);
636            async move {
637                let bs_addrs = bootstrap_ref.read().await.clone();
638
639                // Add known peers to the network
640                handle.add_known_peers(bs_addrs).unwrap();
641
642                // Begin the bootstrap process
643                handle.begin_bootstrap()?;
644                while !is_bootstrapped.load(Ordering::Relaxed) {
645                    sleep(Duration::from_secs(1)).await;
646                    handle.begin_bootstrap()?;
647                }
648
649                // Subscribe to the QC topic
650                handle.subscribe(QC_TOPIC.to_string()).await.unwrap();
651
652                // Map our staking key to our Libp2p Peer ID so we can properly
653                // route direct messages
654                while handle
655                    .put_record(
656                        RecordKey::new(Namespace::Lookup, pk.to_bytes()),
657                        lookup_record_value.clone(),
658                    )
659                    .await
660                    .is_err()
661                {
662                    sleep(Duration::from_secs(1)).await;
663                }
664
665                // Wait for the network to connect to at least 1 peer
666                if let Err(e) = handle.wait_to_connect(1, id).await {
667                    error!("Failed to connect to peers: {e:?}");
668                    return Err::<(), NetworkError>(e);
669                }
670                info!("Connected to required number of peers");
671
672                // Set the network as ready
673                is_ready.store(true, Ordering::Relaxed);
674                inner.metrics.is_ready.set(1);
675
676                Ok::<(), NetworkError>(())
677            }
678        });
679    }
680
681    /// Handle events
682    fn handle_recvd_events(
683        &self,
684        msg: NetworkEvent,
685        sender: &Sender<Vec<u8>>,
686    ) -> Result<(), NetworkError> {
687        match msg {
688            GossipMsg(msg) => {
689                sender.try_send(msg).map_err(|err| {
690                    NetworkError::ChannelSendError(format!("failed to send gossip message: {err}"))
691                })?;
692            },
693            DirectRequest(msg, _pid, chan) => {
694                sender.try_send(msg).map_err(|err| {
695                    NetworkError::ChannelSendError(format!(
696                        "failed to send direct request message: {err}"
697                    ))
698                })?;
699                if self
700                    .inner
701                    .handle
702                    .direct_response(
703                        chan,
704                        &bincode::serialize(&Empty { byte: 0u8 }).map_err(|e| {
705                            NetworkError::FailedToSerialize(format!(
706                                "failed to serialize acknowledgement: {e}"
707                            ))
708                        })?,
709                    )
710                    .is_err()
711                {
712                    error!("failed to ack!");
713                };
714            },
715            DirectResponse(_msg, _) => {},
716            NetworkEvent::IsBootstrapped => {
717                error!(
718                    "handle_recvd_events received `NetworkEvent::IsBootstrapped`, which should be \
719                     impossible."
720                );
721            },
722            NetworkEvent::ConnectedPeersUpdate(_) => {},
723        }
724        Ok::<(), NetworkError>(())
725    }
726
727    /// task to propagate messages to handlers
728    /// terminates on shut down of network
729    fn handle_event_generator(&self, sender: Sender<Vec<u8>>, mut network_rx: NetworkNodeReceiver) {
730        let handle = self.clone();
731        let is_bootstrapped = Arc::clone(&self.inner.is_bootstrapped);
732        spawn(async move {
733            let Some(mut kill_switch) = network_rx.take_kill_switch() else {
734                tracing::error!(
735                    "`spawn_handle` was called on a network handle that was already closed"
736                );
737                return;
738            };
739
740            loop {
741                select! {
742                    msg = network_rx.recv() => {
743                        let Ok(message) = msg else {
744                            warn!("Network receiver shut down!");
745                            return;
746                        };
747
748                        match message {
749                            NetworkEvent::IsBootstrapped => {
750                                is_bootstrapped.store(true, Ordering::Relaxed);
751                            }
752                            GossipMsg(_) | DirectRequest(_, _, _) | DirectResponse(_, _) => {
753                                let _ = handle.handle_recvd_events(message, &sender);
754                            }
755                            NetworkEvent::ConnectedPeersUpdate(num_peers) => {
756                                handle.inner.metrics.num_connected_peers.set(num_peers);
757                            }
758                        }
759                    }
760
761                    _kill_switch = kill_switch.recv() => {
762                        warn!("Event Handler shutdown");
763                        return;
764                    }
765                }
766            }
767        });
768    }
769}
770
771#[async_trait]
772impl<T: NodeType> ConnectedNetwork<T::SignatureKey> for Libp2pNetwork<T> {
773    #[instrument(name = "Libp2pNetwork::ready_blocking", skip_all)]
774    async fn wait_for_ready(&self) {
775        self.wait_for_peers().await;
776    }
777
778    fn pause(&self) {
779        unimplemented!("Pausing not implemented for the Libp2p network");
780    }
781
782    fn resume(&self) {
783        unimplemented!("Resuming not implemented for the Libp2p network");
784    }
785
786    #[instrument(name = "Libp2pNetwork::shut_down", skip_all)]
787    fn shut_down<'a, 'b>(&'a self) -> BoxSyncFuture<'b, ()>
788    where
789        'a: 'b,
790        Self: 'b,
791    {
792        let closure = async move {
793            let _ = self.inner.handle.shutdown().await;
794            let _ = self.inner.node_lookup_send.send(None).await;
795            let _ = self.inner.kill_switch.send(()).await;
796        };
797        boxed_sync(closure)
798    }
799
800    #[instrument(name = "Libp2pNetwork::broadcast_message", skip_all)]
801    async fn broadcast_message(
802        &self,
803        _: ViewNumber,
804        message: Vec<u8>,
805        topic: Topic,
806        _broadcast_delay: BroadcastDelay,
807    ) -> Result<(), NetworkError> {
808        // If we're not ready yet (we don't have any peers, error)
809        if !self.has_peers() {
810            self.inner.metrics.num_failed_messages.add(1);
811            return Err(NetworkError::NoPeersYet);
812        };
813
814        // If we are subscribed to the topic,
815        let topic = topic.to_string();
816        if self.inner.subscribed_topics.contains(&topic) {
817            // Short-circuit-send the message to ourselves
818            self.inner.sender.try_send(message.clone()).map_err(|_| {
819                self.inner.metrics.num_failed_messages.add(1);
820                NetworkError::ShutDown
821            })?;
822        }
823
824        // NOTE: metrics is threadsafe, so clone is fine (and lightweight)
825        #[cfg(feature = "hotshot-testing")]
826        {
827            let metrics = self.inner.metrics.clone();
828            if let Some(config) = &self.inner.reliability_config {
829                let handle = Arc::clone(&self.inner.handle);
830
831                let fut = config.clone().chaos_send_msg(
832                    message,
833                    Arc::new(move |msg: Vec<u8>| {
834                        let topic_2 = topic.clone();
835                        let handle_2 = Arc::clone(&handle);
836                        let metrics_2 = metrics.clone();
837                        boxed_sync(async move {
838                            if let Err(e) = handle_2.gossip_no_serialize(topic_2, msg) {
839                                metrics_2.num_failed_messages.add(1);
840                                warn!("Failed to broadcast to libp2p: {e:?}");
841                            }
842                        })
843                    }),
844                );
845                spawn(fut);
846                return Ok(());
847            }
848        }
849
850        if let Err(e) = self.inner.handle.gossip(topic, &message) {
851            self.inner.metrics.num_failed_messages.add(1);
852            return Err(e);
853        }
854
855        Ok(())
856    }
857
858    #[instrument(name = "Libp2pNetwork::da_broadcast_message", skip_all)]
859    async fn da_broadcast_message(
860        &self,
861        view: ViewNumber,
862        message: Vec<u8>,
863        recipients: Vec<T::SignatureKey>,
864        _broadcast_delay: BroadcastDelay,
865    ) -> Result<(), NetworkError> {
866        // If we're not ready yet (we don't have any peers, error)
867        if !self.has_peers() {
868            self.inner.metrics.num_failed_messages.add(1);
869            return Err(NetworkError::NoPeersYet);
870        };
871
872        // If we are subscribed to the DA topic, send the message to ourselves first
873        let topic = Topic::Da.to_string();
874        if self.inner.subscribed_topics.contains(&topic) {
875            self.inner.sender.try_send(message.clone()).map_err(|_| {
876                self.inner.metrics.num_failed_messages.add(1);
877                NetworkError::ShutDown
878            })?;
879        }
880
881        let future_results = recipients
882            .into_iter()
883            .map(|r| self.direct_message(view, message.clone(), r));
884        let results = join_all(future_results).await;
885
886        let errors: Vec<_> = results.into_iter().filter_map(|r| r.err()).collect();
887
888        if errors.is_empty() {
889            Ok(())
890        } else {
891            Err(NetworkError::Multiple(errors))
892        }
893    }
894
895    #[instrument(name = "Libp2pNetwork::direct_message", skip_all)]
896    async fn direct_message(
897        &self,
898        _: ViewNumber,
899        message: Vec<u8>,
900        recipient: T::SignatureKey,
901    ) -> Result<(), NetworkError> {
902        // If we're not ready yet (we don't have any peers, error)
903        if !self.has_peers() {
904            self.inner.metrics.num_failed_messages.add(1);
905            return Err(NetworkError::NoPeersYet);
906        };
907
908        // short circuit if we're dming ourselves
909        if recipient == self.inner.pk {
910            // panic if we already shut down?
911            self.inner.sender.try_send(message).map_err(|_x| {
912                self.inner.metrics.num_failed_messages.add(1);
913                NetworkError::ShutDown
914            })?;
915            return Ok(());
916        }
917
918        let pid = match self
919            .inner
920            .handle
921            .lookup_node(&recipient, Duration::from_secs(2))
922            .await
923        {
924            Ok(pid) => pid,
925            Err(err) => {
926                self.inner.metrics.num_failed_messages.add(1);
927                return Err(NetworkError::LookupError(format!(
928                    "failed to look up node for direct message: {err}"
929                )));
930            },
931        };
932
933        #[cfg(feature = "hotshot-testing")]
934        {
935            let metrics = self.inner.metrics.clone();
936            if let Some(config) = &self.inner.reliability_config {
937                let handle = Arc::clone(&self.inner.handle);
938
939                let fut = config.clone().chaos_send_msg(
940                    message,
941                    Arc::new(move |msg: Vec<u8>| {
942                        let handle_2 = Arc::clone(&handle);
943                        let metrics_2 = metrics.clone();
944                        boxed_sync(async move {
945                            if let Err(e) = handle_2.direct_request_no_serialize(pid, msg) {
946                                metrics_2.num_failed_messages.add(1);
947                                warn!("Failed to broadcast to libp2p: {e:?}");
948                            }
949                        })
950                    }),
951                );
952                spawn(fut);
953                return Ok(());
954            }
955        }
956
957        match self.inner.handle.direct_request(pid, &message) {
958            Ok(()) => Ok(()),
959            Err(e) => {
960                self.inner.metrics.num_failed_messages.add(1);
961                Err(e)
962            },
963        }
964    }
965
966    /// Receive one or many messages from the underlying network.
967    ///
968    /// # Errors
969    /// If there is a network-related failure.
970    #[instrument(name = "Libp2pNetwork::recv_message", skip_all)]
971    async fn recv_message(&self) -> Result<Vec<u8>, NetworkError> {
972        let result = self
973            .inner
974            .receiver
975            .lock()
976            .await
977            .recv()
978            .await
979            .ok_or(NetworkError::ShutDown)?;
980
981        Ok(result)
982    }
983
984    #[instrument(name = "Libp2pNetwork::queue_node_lookup", skip_all)]
985    #[allow(clippy::type_complexity)]
986    fn queue_node_lookup(
987        &self,
988        view_number: ViewNumber,
989        pk: T::SignatureKey,
990    ) -> Result<(), TrySendError<Option<(ViewNumber, T::SignatureKey)>>> {
991        self.inner
992            .node_lookup_send
993            .try_send(Some((view_number, pk)))
994    }
995
996    /// The libp2p view update is a special operation intrinsic to its internal behavior.
997    ///
998    /// Libp2p needs to do a lookup because a libp2p address is not related to
999    /// hotshot keys. So in libp2p we store a mapping of HotShot key to libp2p address
1000    /// in a distributed hash table.
1001    ///
1002    /// This means to directly message someone on libp2p we need to lookup in the hash
1003    /// table what their libp2p address is, using their HotShot public key as the key.
1004    ///
1005    /// So the logic with libp2p is to prefetch upcoming leaders libp2p address to
1006    /// save time when we later need to direct message the leader our vote. Hence the
1007    /// use of the future view and leader to queue the lookups.
1008    async fn update_view<TYPES>(
1009        &self,
1010        view: ViewNumber,
1011        epoch: Option<EpochNumber>,
1012        membership_coordinator: EpochMembershipCoordinator<TYPES>,
1013    ) where
1014        TYPES: NodeType<SignatureKey = T::SignatureKey>,
1015    {
1016        let future_view = ViewNumber::new(*view) + LOOK_AHEAD;
1017        let epoch = epoch.map(|e| EpochNumber::new(*e));
1018
1019        let membership = match membership_coordinator.membership_for_epoch(epoch) {
1020            Ok(m) => m,
1021            Err(e) => {
1022                return tracing::warn!(e.message);
1023            },
1024        };
1025        let future_leader = match membership.leader(future_view) {
1026            Ok(l) => l,
1027            Err(e) => {
1028                return tracing::info!("Failed to calculate leader for view {future_view}: {e}");
1029            },
1030        };
1031
1032        let _ = self
1033            .queue_node_lookup(ViewNumber::new(*future_view), future_leader)
1034            .map_err(|err| tracing::warn!("failed to process node lookup request: {err}"));
1035    }
1036}
1037
1038#[cfg(test)]
1039mod test {
1040    mod derive_multiaddr {
1041        use std::net::Ipv6Addr;
1042
1043        use super::super::*;
1044
1045        /// Test derivation of a valid IPv4 address -> Multiaddr
1046        #[test]
1047        fn test_v4_valid() {
1048            // Derive a multiaddr from a valid IPv4 address
1049            let addr = "1.1.1.1:8080".to_string();
1050            let multiaddr =
1051                derive_libp2p_multiaddr(&addr).expect("Failed to derive valid multiaddr, {}");
1052
1053            // Make sure it's the correct (quic) multiaddr
1054            assert_eq!(multiaddr.to_string(), "/ip4/1.1.1.1/udp/8080/quic-v1");
1055        }
1056
1057        /// Test derivation of a valid IPv6 address -> Multiaddr
1058        #[test]
1059        fn test_v6_valid() {
1060            // Derive a multiaddr from a valid IPv6 address
1061            let ipv6_addr = Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8);
1062            let addr = format!("{ipv6_addr}:8080");
1063            let multiaddr =
1064                derive_libp2p_multiaddr(&addr).expect("Failed to derive valid multiaddr, {}");
1065
1066            // Make sure it's the correct (quic) multiaddr
1067            assert_eq!(
1068                multiaddr.to_string(),
1069                format!("/ip6/{ipv6_addr}/udp/8080/quic-v1")
1070            );
1071        }
1072
1073        /// Test that an invalid address fails to derive to a Multiaddr
1074        #[test]
1075        fn test_no_port() {
1076            // Derive a multiaddr from an invalid port
1077            let addr = "1.1.1.1".to_string();
1078            let multiaddr = derive_libp2p_multiaddr(&addr);
1079
1080            // Make sure it fails
1081            assert!(multiaddr.is_err());
1082        }
1083
1084        /// Test that an existing domain name resolves to a Multiaddr
1085        #[test]
1086        fn test_fqdn_exists() {
1087            // Derive a multiaddr from a valid FQDN
1088            let addr = "example.com:8080".to_string();
1089            let multiaddr =
1090                derive_libp2p_multiaddr(&addr).expect("Failed to derive valid multiaddr, {}");
1091
1092            // Make sure it's the correct (quic) multiaddr
1093            assert_eq!(multiaddr.to_string(), "/dns/example.com/udp/8080/quic-v1");
1094        }
1095
1096        /// Test that a non-existent domain name still resolves to a Multiaddr
1097        #[test]
1098        fn test_fqdn_does_not_exist() {
1099            // Derive a multiaddr from an invalid FQDN
1100            let addr = "libp2p.example.com:8080".to_string();
1101            let multiaddr =
1102                derive_libp2p_multiaddr(&addr).expect("Failed to derive valid multiaddr, {}");
1103
1104            // Make sure it still worked
1105            assert_eq!(
1106                multiaddr.to_string(),
1107                "/dns/libp2p.example.com/udp/8080/quic-v1"
1108            );
1109        }
1110
1111        /// Test that a domain name without a port fails to derive to a Multiaddr
1112        #[test]
1113        fn test_fqdn_no_port() {
1114            // Derive a multiaddr from an invalid port
1115            let addr = "example.com".to_string();
1116            let multiaddr = derive_libp2p_multiaddr(&addr);
1117
1118            // Make sure it fails
1119            assert!(multiaddr.is_err());
1120        }
1121    }
1122}