348 lines
12 KiB
Rust
348 lines
12 KiB
Rust
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::<Version>(), 1..MAX_PEERS)")]
|
|
peer_versions: Vec<Version>,
|
|
}
|
|
|
|
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<LoadTrackedClient>, Vec<ClientTestHarness>) {
|
|
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<Item = Result<Change<SocketAddr, LoadTrackedClient>, BoxError>>,
|
|
Vec<ClientTestHarness>,
|
|
) {
|
|
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<D, C> {
|
|
config: Option<Config>,
|
|
discover: Option<D>,
|
|
demand_signal: Option<mpsc::Sender<MorePeers>>,
|
|
handle_rx: Option<tokio::sync::oneshot::Receiver<Vec<JoinHandle<Result<(), BoxError>>>>>,
|
|
inv_stream: Option<broadcast::Receiver<(InventoryHash, SocketAddr)>>,
|
|
address_book: Option<Arc<std::sync::Mutex<AddressBook>>>,
|
|
minimum_peer_version: Option<MinimumPeerVersion<C>>,
|
|
}
|
|
|
|
impl PeerSetBuilder<(), ()> {
|
|
/// Create a new [`PeerSetBuilder`] instance.
|
|
pub fn new() -> Self {
|
|
PeerSetBuilder::default()
|
|
}
|
|
}
|
|
|
|
impl<D, C> PeerSetBuilder<D, C> {
|
|
/// Use the provided `discover` parameter when constructing the [`PeerSet`] instance.
|
|
pub fn with_discover<NewD>(self, discover: NewD) -> PeerSetBuilder<NewD, C> {
|
|
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<NewC>(
|
|
self,
|
|
minimum_peer_version: MinimumPeerVersion<NewC>,
|
|
) -> PeerSetBuilder<D, NewC> {
|
|
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<D, C> PeerSetBuilder<D, C>
|
|
where
|
|
D: Discover<Key = SocketAddr, Service = LoadTrackedClient> + Unpin,
|
|
D::Error: Into<BoxError>,
|
|
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<D, C>, 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<tokio::sync::oneshot::Sender<Vec<JoinHandle<Result<(), BoxError>>>>>,
|
|
demand_receiver: Option<mpsc::Receiver<MorePeers>>,
|
|
inventory_sender: Option<broadcast::Sender<(InventoryHash, SocketAddr)>>,
|
|
address_book: Option<Arc<std::sync::Mutex<AddressBook>>>,
|
|
}
|
|
|
|
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<Vec<JoinHandle<Result<(), BoxError>>>> {
|
|
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<MorePeers> {
|
|
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<Arc<std::sync::Mutex<AddressBook>>>,
|
|
) -> watch::Receiver<AddressMetrics> {
|
|
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<std::sync::Mutex<AddressBook>> {
|
|
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<Self>;
|
|
}
|