use std::{net::SocketAddr, sync::Arc}; use futures::{channel::mpsc, stream, Stream, StreamExt}; use proptest::{collection::vec, prelude::*}; use proptest_derive::Arbitrary; use tokio::{ sync::{broadcast, watch}, task::JoinHandle, }; use tower::{ discover::{Change, Discover}, BoxError, }; use tracing::Span; use zebra_chain::{ block, chain_tip::ChainTip, parameters::{Network, NetworkUpgrade}, }; use super::MorePeers; use crate::{ address_book::AddressMetrics, peer::{ClientTestHarness, LoadTrackedClient, MinimumPeerVersion}, peer_set::PeerSet, protocol::external::{types::Version, InventoryHash}, AddressBook, Config, }; #[cfg(test)] mod prop; /// The maximum number of arbitrary peers to generate in [`PeerVersions`]. /// /// This affects the maximum number of peer connections added to the [`PeerSet`] during the tests. const MAX_PEERS: usize = 20; /// A helper type to generate arbitrary peer versions which can then become mock peer services. #[derive(Arbitrary, Debug)] struct PeerVersions { #[proptest(strategy = "vec(any::(), 1..MAX_PEERS)")] peer_versions: Vec, } impl PeerVersions { /// Convert the arbitrary peer versions into mock peer services. /// /// Each peer versions results in a mock peer service, which is returned as a tuple. The first /// element is the [`LeadTrackedClient`], which is the actual service for the peer connection. /// The second element is a [`ClientTestHarness`], which contains the open endpoints of the /// mock channels used by the peer service. /// /// The clients and the harnesses are collected into separate [`Vec`] lists and returned. pub fn mock_peers(&self) -> (Vec, Vec) { let mut clients = Vec::with_capacity(self.peer_versions.len()); let mut harnesses = Vec::with_capacity(self.peer_versions.len()); for peer_version in &self.peer_versions { let (client, harness) = ClientTestHarness::build() .with_version(*peer_version) .finish(); clients.push(client.into()); harnesses.push(harness); } (clients, harnesses) } /// Convert the arbitrary peer versions into mock peer services available through a /// [`Discover`] compatible stream. /// /// A tuple is returned, where the first item is a stream with the mock peers available through /// a [`Discover`] interface, and the second is a list of harnesses to the mocked services. /// /// The returned stream never finishes, so it is ready to be passed to the [`PeerSet`] /// constructor. /// /// See [`Self::mock_peers`] for details on how the peers are mocked and on what the harnesses /// contain. pub fn mock_peer_discovery( &self, ) -> ( impl Stream, BoxError>>, Vec, ) { let (clients, harnesses) = self.mock_peers(); let fake_ports = 1_u16..; let discovered_peers_iterator = fake_ports.zip(clients).map(|(port, client)| { let peer_address = SocketAddr::new([127, 0, 0, 1].into(), port); Ok(Change::Insert(peer_address, client)) }); let discovered_peers = stream::iter(discovered_peers_iterator).chain(stream::pending()); (discovered_peers, harnesses) } } /// A helper builder type for creating test [`PeerSet`] instances. /// /// This helps to reduce repeated boilerplate code. Fields that are not set are configured to use /// dummy fallbacks. #[derive(Default)] struct PeerSetBuilder { config: Option, discover: Option, demand_signal: Option>, handle_rx: Option>>>>, inv_stream: Option>, address_book: Option>>, minimum_peer_version: Option>, } impl PeerSetBuilder<(), ()> { /// Create a new [`PeerSetBuilder`] instance. pub fn new() -> Self { PeerSetBuilder::default() } } impl PeerSetBuilder { /// Use the provided `discover` parameter when constructing the [`PeerSet`] instance. pub fn with_discover(self, discover: NewD) -> PeerSetBuilder { PeerSetBuilder { discover: Some(discover), config: self.config, demand_signal: self.demand_signal, handle_rx: self.handle_rx, inv_stream: self.inv_stream, address_book: self.address_book, minimum_peer_version: self.minimum_peer_version, } } /// Use the provided [`MinimumPeerVersion`] instance when constructing the [`PeerSet`] instance. pub fn with_minimum_peer_version( self, minimum_peer_version: MinimumPeerVersion, ) -> PeerSetBuilder { PeerSetBuilder { minimum_peer_version: Some(minimum_peer_version), config: self.config, discover: self.discover, demand_signal: self.demand_signal, handle_rx: self.handle_rx, inv_stream: self.inv_stream, address_book: self.address_book, } } } impl PeerSetBuilder where D: Discover + Unpin, D::Error: Into, C: ChainTip, { /// Finish building the [`PeerSet`] instance. /// /// Returns a tuple with the [`PeerSet`] instance and a [`PeerSetGuard`] to keep track of some /// endpoints of channels created for the [`PeerSet`]. pub fn build(self) -> (PeerSet, PeerSetGuard) { let mut guard = PeerSetGuard::new(); let config = self.config.unwrap_or_default(); let discover = self.discover.expect("`discover` must be set"); let minimum_peer_version = self .minimum_peer_version .expect("`minimum_peer_version` must be set"); let demand_signal = self .demand_signal .unwrap_or_else(|| guard.create_demand_sender()); let handle_rx = self .handle_rx .unwrap_or_else(|| guard.create_background_tasks_receiver()); let inv_stream = self .inv_stream .unwrap_or_else(|| guard.create_inventory_receiver()); let address_metrics = guard.prepare_address_book(self.address_book); let peer_set = PeerSet::new( &config, discover, demand_signal, handle_rx, inv_stream, address_metrics, minimum_peer_version, ); (peer_set, guard) } } /// A helper type to keep track of some dummy endpoints sent to a test [`PeerSet`] instance. #[derive(Default)] pub struct PeerSetGuard { background_tasks_sender: Option>>>>, demand_receiver: Option>, inventory_sender: Option>, address_book: Option>>, } impl PeerSetGuard { /// Create a new empty [`PeerSetGuard`] instance. pub fn new() -> Self { PeerSetGuard::default() } /// Create a dummy channel for the background tasks sent to the [`PeerSet`]. /// /// The sender is stored inside the [`PeerSetGuard`], while the receiver is returned to be /// passed to the [`PeerSet`] constructor. pub fn create_background_tasks_receiver( &mut self, ) -> tokio::sync::oneshot::Receiver>>> { let (sender, receiver) = tokio::sync::oneshot::channel(); self.background_tasks_sender = Some(sender); receiver } /// Create a dummy channel for the [`PeerSet`] to send demand signals for more peers. /// /// The receiver is stored inside the [`PeerSetGuard`], while the sender is returned to be /// passed to the [`PeerSet`] constructor. pub fn create_demand_sender(&mut self) -> mpsc::Sender { let (sender, receiver) = mpsc::channel(1); self.demand_receiver = Some(receiver); sender } /// Create a dummy channel for the inventory hashes sent to the [`PeerSet`]. /// /// The sender is stored inside the [`PeerSetGuard`], while the receiver is returned to be /// passed to the [`PeerSet`] constructor. pub fn create_inventory_receiver( &mut self, ) -> broadcast::Receiver<(InventoryHash, SocketAddr)> { let (sender, receiver) = broadcast::channel(1); self.inventory_sender = Some(sender); receiver } /// Prepare an [`AddressBook`] instance to send to the [`PeerSet`]. /// /// If the `maybe_address_book` parameter contains an [`AddressBook`] instance, it is stored /// inside the [`PeerSetGuard`] to keep track of it. Otherwise, a new instance is created with /// the [`Self::fallback_address_book`] method. /// /// Returns a metrics watch channel for the [`AddressBook`] instance tracked by the [`PeerSetGuard`], /// so it can be passed to the [`PeerSet`] constructor. pub fn prepare_address_book( &mut self, maybe_address_book: Option>>, ) -> watch::Receiver { let address_book = maybe_address_book.unwrap_or_else(Self::fallback_address_book); let metrics_watcher = address_book .lock() .expect("unexpected panic in previous address book mutex guard") .address_metrics_watcher(); self.address_book = Some(address_book); metrics_watcher } /// Create an empty [`AddressBook`] instance using a dummy local listener address. fn fallback_address_book() -> Arc> { let local_listener = "127.0.0.1:1000" .parse() .expect("Invalid local listener address"); let address_book = AddressBook::new(local_listener, Span::none()); Arc::new(std::sync::Mutex::new(address_book)) } } /// A pair of block height values, where one is before and the other is at or after an arbitrary /// network upgrade's activation height. #[derive(Clone, Copy, Debug)] pub struct BlockHeightPairAcrossNetworkUpgrades { /// The network for which the block height values represent heights before and after an /// upgrade. pub network: Network, /// The block height before the network upgrade activation. pub before_upgrade: block::Height, /// The block height at or after the network upgrade activation. pub after_upgrade: block::Height, } impl Arbitrary for BlockHeightPairAcrossNetworkUpgrades { type Parameters = (); fn arbitrary_with((): Self::Parameters) -> Self::Strategy { any::<(Network, NetworkUpgrade)>() // Filter out genesis upgrade because there is no block height before genesis. .prop_filter("no block height before genesis", |(_, upgrade)| { !matches!(upgrade, NetworkUpgrade::Genesis) }) // Filter out network upgrades without activation heights. .prop_filter_map( "missing activation height for network upgrade", |(network, upgrade)| { upgrade .activation_height(network) .map(|height| (network, height)) }, ) // Obtain random heights before and after (or at) the network upgrade activation. .prop_flat_map(|(network, activation_height)| { let before_upgrade_strategy = 0..activation_height.0; let after_upgrade_strategy = activation_height.0..; ( Just(network), before_upgrade_strategy, after_upgrade_strategy, ) }) // Collect the arbitrary values to build the final type. .prop_map(|(network, before_upgrade_height, after_upgrade_height)| { BlockHeightPairAcrossNetworkUpgrades { network, before_upgrade: block::Height(before_upgrade_height), after_upgrade: block::Height(after_upgrade_height), } }) .boxed() } type Strategy = BoxedStrategy; }