Skip to main content

zebra_network/peer_set/set/
tests.rs

1//! Peer set unit tests, and test setup code.
2
3#![allow(clippy::unwrap_in_result)]
4
5#[cfg(test)]
6use std::net::SocketAddr;
7use std::{net::IpAddr, sync::Arc};
8
9use futures::channel::mpsc;
10#[cfg(test)]
11use futures::{stream, Stream, StreamExt};
12#[cfg(test)]
13use proptest::{collection::vec, prelude::*};
14#[cfg(test)]
15use proptest_derive::Arbitrary;
16use tokio::{
17    sync::{broadcast, watch},
18    task::JoinHandle,
19};
20#[cfg(test)]
21use tower::discover::Change;
22use tower::{discover::Discover, BoxError};
23use tracing::Span;
24
25#[cfg(test)]
26use zebra_chain::{block, parameters::NetworkUpgrade};
27use zebra_chain::{chain_tip::ChainTip, parameters::Network};
28
29use crate::{
30    address_book::AddressMetrics,
31    constants::DEFAULT_MAX_CONNS_PER_IP,
32    peer::{LoadTrackedClient, MinimumPeerVersion},
33    peer_set::{set::MorePeers, InventoryChange, PeerSet},
34    AddressBook, Config, PeerSocketAddr,
35};
36#[cfg(test)]
37use crate::{peer::ClientTestHarness, protocol::external::types::Version};
38
39pub mod consumer;
40
41#[cfg(test)]
42mod prop;
43
44#[cfg(test)]
45mod vectors;
46
47/// The maximum number of arbitrary peers to generate in [`PeerVersions`].
48///
49/// This affects the maximum number of peer connections added to the [`PeerSet`] during the tests.
50#[cfg(test)]
51const MAX_PEERS: usize = 20;
52
53/// A helper type to generate arbitrary peer versions which can then become mock peer services.
54#[cfg(test)]
55#[derive(Arbitrary, Debug)]
56struct PeerVersions {
57    #[proptest(strategy = "vec(any::<Version>(), 1..MAX_PEERS)")]
58    peer_versions: Vec<Version>,
59}
60
61#[cfg(test)]
62impl PeerVersions {
63    /// Convert the arbitrary peer versions into mock peer services.
64    ///
65    /// Each peer versions results in a mock peer service, which is returned as a tuple. The first
66    /// element is the [`LeadTrackedClient`], which is the actual service for the peer connection.
67    /// The second element is a [`ClientTestHarness`], which contains the open endpoints of the
68    /// mock channels used by the peer service.
69    ///
70    /// The clients and the harnesses are collected into separate [`Vec`] lists and returned.
71    pub fn mock_peers(&self) -> (Vec<LoadTrackedClient>, Vec<ClientTestHarness>) {
72        let mut clients = Vec::with_capacity(self.peer_versions.len());
73        let mut harnesses = Vec::with_capacity(self.peer_versions.len());
74
75        for peer_version in &self.peer_versions {
76            let (client, harness) = ClientTestHarness::build()
77                .with_version(*peer_version)
78                .finish();
79
80            clients.push(client.into());
81            harnesses.push(harness);
82        }
83
84        (clients, harnesses)
85    }
86
87    /// Convert the arbitrary peer versions into mock peer services available through a
88    /// [`Discover`] compatible stream.
89    ///
90    /// A tuple is returned, where the first item is a stream with the mock peers available through
91    /// a [`Discover`] interface, and the second is a list of harnesses to the mocked services.
92    ///
93    /// The returned stream never finishes, so it is ready to be passed to the [`PeerSet`]
94    /// constructor.
95    ///
96    /// See [`Self::mock_peers`] for details on how the peers are mocked and on what the harnesses
97    /// contain.
98    pub fn mock_peer_discovery(
99        &self,
100    ) -> (
101        impl Stream<Item = Result<Change<PeerSocketAddr, LoadTrackedClient>, BoxError>>,
102        Vec<ClientTestHarness>,
103    ) {
104        let (clients, harnesses) = self.mock_peers();
105        let fake_ports = 1_u16..;
106
107        let discovered_peers_iterator = fake_ports.zip(clients).map(|(port, client)| {
108            let peer_address: PeerSocketAddr = SocketAddr::new([127, 0, 0, 1].into(), port).into();
109
110            Ok(Change::Insert(peer_address, client))
111        });
112
113        let discovered_peers = stream::iter(discovered_peers_iterator).chain(stream::pending());
114
115        (discovered_peers, harnesses)
116    }
117}
118
119/// Discover one peer advertising `start_height`, and return its request harness.
120#[cfg(test)]
121fn mock_peer_discovery_with_start_height(
122    start_height: block::Height,
123) -> (
124    impl Stream<Item = Result<Change<PeerSocketAddr, LoadTrackedClient>, BoxError>>,
125    ClientTestHarness,
126) {
127    let (client, harness) = ClientTestHarness::build()
128        .with_version(crate::constants::CURRENT_NETWORK_PROTOCOL_VERSION)
129        .with_start_height(start_height)
130        .finish();
131    let peer_address: PeerSocketAddr = SocketAddr::new([127, 0, 0, 1].into(), 1).into();
132    let discovery =
133        stream::iter([Ok(Change::Insert(peer_address, client.into()))]).chain(stream::pending());
134
135    (discovery, harness)
136}
137
138/// A helper builder type for creating test [`PeerSet`] instances.
139///
140/// This helps to reduce repeated boilerplate code. Fields that are not set are configured to use
141/// dummy fallbacks.
142#[derive(Default)]
143pub(crate) struct PeerSetBuilder<D, C> {
144    config: Option<Config>,
145    discover: Option<D>,
146    demand_signal: Option<mpsc::Sender<MorePeers>>,
147    handle_rx: Option<tokio::sync::oneshot::Receiver<Vec<JoinHandle<Result<(), BoxError>>>>>,
148    inv_stream: Option<broadcast::Receiver<InventoryChange>>,
149    address_book: Option<Arc<std::sync::Mutex<AddressBook>>>,
150    minimum_peer_version: Option<MinimumPeerVersion<C>>,
151    max_conns_per_ip: Option<usize>,
152    block_gossip_peer_ips: Vec<IpAddr>,
153}
154
155impl PeerSetBuilder<(), ()> {
156    /// Create a new [`PeerSetBuilder`] instance.
157    pub fn new() -> Self {
158        PeerSetBuilder::default()
159    }
160}
161
162impl<D, C> PeerSetBuilder<D, C> {
163    /// Use the provided [`Config`] when constructing the [`PeerSet`] instance.
164    #[cfg(test)]
165    pub fn with_config(self, config: Config) -> PeerSetBuilder<D, C> {
166        PeerSetBuilder {
167            config: Some(config),
168            ..self
169        }
170    }
171
172    /// Always include these inbound peer IPs in block inventory broadcasts.
173    #[cfg(test)]
174    pub fn with_block_gossip_peer_ips(
175        self,
176        block_gossip_peer_ips: Vec<IpAddr>,
177    ) -> PeerSetBuilder<D, C> {
178        PeerSetBuilder {
179            block_gossip_peer_ips,
180            ..self
181        }
182    }
183
184    /// Use the provided `discover` parameter when constructing the [`PeerSet`] instance.
185    pub fn with_discover<NewD>(self, discover: NewD) -> PeerSetBuilder<NewD, C> {
186        PeerSetBuilder {
187            discover: Some(discover),
188            config: self.config,
189            demand_signal: self.demand_signal,
190            handle_rx: self.handle_rx,
191            inv_stream: self.inv_stream,
192            address_book: self.address_book,
193            minimum_peer_version: self.minimum_peer_version,
194            max_conns_per_ip: self.max_conns_per_ip,
195            block_gossip_peer_ips: self.block_gossip_peer_ips,
196        }
197    }
198
199    /// Use the provided [`MinimumPeerVersion`] instance when constructing the [`PeerSet`] instance.
200    pub fn with_minimum_peer_version<NewC>(
201        self,
202        minimum_peer_version: MinimumPeerVersion<NewC>,
203    ) -> PeerSetBuilder<D, NewC> {
204        PeerSetBuilder {
205            config: self.config,
206            discover: self.discover,
207            demand_signal: self.demand_signal,
208            handle_rx: self.handle_rx,
209            inv_stream: self.inv_stream,
210            address_book: self.address_book,
211            minimum_peer_version: Some(minimum_peer_version),
212            max_conns_per_ip: self.max_conns_per_ip,
213            block_gossip_peer_ips: self.block_gossip_peer_ips,
214        }
215    }
216
217    /// Use the provided [`MinimumPeerVersion`] instance when constructing the [`PeerSet`] instance.
218    #[cfg(test)]
219    pub fn max_conns_per_ip(self, max_conns_per_ip: usize) -> PeerSetBuilder<D, C> {
220        assert!(
221            max_conns_per_ip > 0,
222            "max_conns_per_ip must be greater than zero"
223        );
224
225        PeerSetBuilder {
226            config: self.config,
227            discover: self.discover,
228            demand_signal: self.demand_signal,
229            handle_rx: self.handle_rx,
230            inv_stream: self.inv_stream,
231            address_book: self.address_book,
232            minimum_peer_version: self.minimum_peer_version,
233            max_conns_per_ip: Some(max_conns_per_ip),
234            block_gossip_peer_ips: self.block_gossip_peer_ips,
235        }
236    }
237}
238
239impl<D, C> PeerSetBuilder<D, C>
240where
241    D: Discover<Key = PeerSocketAddr, Service = LoadTrackedClient> + Unpin,
242    D::Error: Into<BoxError>,
243    C: ChainTip,
244{
245    /// Finish building the [`PeerSet`] instance.
246    ///
247    /// Returns a tuple with the [`PeerSet`] instance and a [`PeerSetGuard`] to keep track of some
248    /// endpoints of channels created for the [`PeerSet`].
249    pub fn build(self) -> (PeerSet<D, C>, PeerSetGuard) {
250        let mut guard = PeerSetGuard::new();
251
252        let config = self.config.unwrap_or_default();
253        let discover = self.discover.expect("`discover` must be set");
254        let minimum_peer_version = self
255            .minimum_peer_version
256            .expect("`minimum_peer_version` must be set");
257        let max_conns_per_ip = self.max_conns_per_ip;
258
259        let demand_signal = self
260            .demand_signal
261            .unwrap_or_else(|| guard.create_demand_sender());
262        let handle_rx = self
263            .handle_rx
264            .unwrap_or_else(|| guard.create_background_tasks_receiver());
265        let inv_stream = self
266            .inv_stream
267            .unwrap_or_else(|| guard.create_inventory_receiver());
268
269        let address_metrics = guard.prepare_address_book(self.address_book);
270        let (_bans_sender, bans_receiver) = tokio::sync::watch::channel(Default::default());
271
272        let peer_set = PeerSet::new(
273            &config,
274            self.block_gossip_peer_ips,
275            discover,
276            demand_signal,
277            handle_rx,
278            inv_stream,
279            bans_receiver,
280            address_metrics,
281            minimum_peer_version,
282            max_conns_per_ip,
283        );
284
285        (peer_set, guard)
286    }
287}
288
289/// A helper type to keep track of some dummy endpoints sent to a test [`PeerSet`] instance.
290#[derive(Default)]
291pub struct PeerSetGuard {
292    background_tasks_sender:
293        Option<tokio::sync::oneshot::Sender<Vec<JoinHandle<Result<(), BoxError>>>>>,
294    demand_receiver: Option<mpsc::Receiver<MorePeers>>,
295    inventory_sender: Option<broadcast::Sender<InventoryChange>>,
296    address_book: Option<Arc<std::sync::Mutex<AddressBook>>>,
297}
298
299impl PeerSetGuard {
300    /// Create a new empty [`PeerSetGuard`] instance.
301    pub fn new() -> Self {
302        PeerSetGuard::default()
303    }
304
305    /// Return a mutable reference to the background tasks sender, if present.
306    #[allow(dead_code)]
307    pub fn background_tasks_sender(
308        &mut self,
309    ) -> &mut Option<tokio::sync::oneshot::Sender<Vec<JoinHandle<Result<(), BoxError>>>>> {
310        &mut self.background_tasks_sender
311    }
312
313    /// Return a mutable reference to the background tasks sender, if present.
314    #[allow(dead_code)]
315    pub fn demand_receiver(&mut self) -> &mut Option<mpsc::Receiver<MorePeers>> {
316        &mut self.demand_receiver
317    }
318
319    /// Return a mutable reference to the background tasks sender, if present.
320    pub fn inventory_sender(&mut self) -> &mut Option<broadcast::Sender<InventoryChange>> {
321        &mut self.inventory_sender
322    }
323
324    /// Return a mutable reference to the background tasks sender, if present.
325    #[allow(dead_code)]
326    pub fn address_book(&mut self) -> &mut Option<Arc<std::sync::Mutex<AddressBook>>> {
327        &mut self.address_book
328    }
329
330    /// Create a dummy channel for the background tasks sent to the [`PeerSet`].
331    ///
332    /// The sender is stored inside the [`PeerSetGuard`], while the receiver is returned to be
333    /// passed to the [`PeerSet`] constructor.
334    pub fn create_background_tasks_receiver(
335        &mut self,
336    ) -> tokio::sync::oneshot::Receiver<Vec<JoinHandle<Result<(), BoxError>>>> {
337        let (sender, receiver) = tokio::sync::oneshot::channel();
338
339        self.background_tasks_sender = Some(sender);
340
341        receiver
342    }
343
344    /// Create a dummy channel for the [`PeerSet`] to send demand signals for more peers.
345    ///
346    /// The receiver is stored inside the [`PeerSetGuard`], while the sender is returned to be
347    /// passed to the [`PeerSet`] constructor.
348    pub fn create_demand_sender(&mut self) -> mpsc::Sender<MorePeers> {
349        let (sender, receiver) = mpsc::channel(1);
350
351        self.demand_receiver = Some(receiver);
352
353        sender
354    }
355
356    /// Create a dummy channel for the inventory hashes sent to the [`PeerSet`].
357    ///
358    /// The sender is stored inside the [`PeerSetGuard`], while the receiver is returned to be
359    /// passed to the [`PeerSet`] constructor.
360    pub fn create_inventory_receiver(&mut self) -> broadcast::Receiver<InventoryChange> {
361        let (sender, receiver) = broadcast::channel(1);
362
363        self.inventory_sender = Some(sender);
364
365        receiver
366    }
367
368    /// Prepare an [`AddressBook`] instance to send to the [`PeerSet`].
369    ///
370    /// If the `maybe_address_book` parameter contains an [`AddressBook`] instance, it is stored
371    /// inside the [`PeerSetGuard`] to keep track of it. Otherwise, a new instance is created with
372    /// the [`Self::fallback_address_book`] method.
373    ///
374    /// Returns a metrics watch channel for the [`AddressBook`] instance tracked by the [`PeerSetGuard`],
375    /// so it can be passed to the [`PeerSet`] constructor.
376    pub fn prepare_address_book(
377        &mut self,
378        maybe_address_book: Option<Arc<std::sync::Mutex<AddressBook>>>,
379    ) -> watch::Receiver<AddressMetrics> {
380        let address_book = maybe_address_book.unwrap_or_else(Self::fallback_address_book);
381        let metrics_watcher = address_book
382            .lock()
383            .expect("unexpected panic in previous address book mutex guard")
384            .address_metrics_watcher();
385
386        self.address_book = Some(address_book);
387
388        metrics_watcher
389    }
390
391    /// Create an empty [`AddressBook`] instance using a dummy local listener address.
392    fn fallback_address_book() -> Arc<std::sync::Mutex<AddressBook>> {
393        let local_listener = "127.0.0.1:1000"
394            .parse()
395            .expect("Invalid local listener address");
396        let address_book = AddressBook::new(
397            local_listener,
398            &Network::Mainnet,
399            DEFAULT_MAX_CONNS_PER_IP,
400            Span::none(),
401        );
402
403        Arc::new(std::sync::Mutex::new(address_book))
404    }
405}
406
407/// A pair of block height values, where one is before and the other is at or after an arbitrary
408/// network upgrade's activation height.
409#[cfg(test)]
410#[derive(Clone, Debug)]
411pub struct BlockHeightPairAcrossNetworkUpgrades {
412    /// The network for which the block height values represent heights before and after an
413    /// upgrade.
414    pub network: Network,
415
416    /// The block height before the network upgrade activation.
417    pub before_upgrade: block::Height,
418
419    /// The block height at or after the network upgrade activation.
420    pub after_upgrade: block::Height,
421}
422
423#[cfg(test)]
424impl Arbitrary for BlockHeightPairAcrossNetworkUpgrades {
425    type Parameters = ();
426
427    fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
428        any::<(Network, NetworkUpgrade)>()
429            // Filter out genesis upgrade because there is no block height before genesis.
430            .prop_filter("no block height before genesis", |(_, upgrade)| {
431                !matches!(upgrade, NetworkUpgrade::Genesis)
432            })
433            // Filter out network upgrades without activation heights.
434            .prop_filter_map(
435                "missing activation height for network upgrade",
436                |(network, upgrade)| {
437                    upgrade
438                        .activation_height(&network)
439                        .map(|height| (network, height))
440                },
441            )
442            // Obtain random heights before and after (or at) the network upgrade activation.
443            .prop_flat_map(|(network, activation_height)| {
444                let before_upgrade_strategy = 0..activation_height.0;
445                let after_upgrade_strategy = activation_height.0..;
446
447                (
448                    Just(network),
449                    before_upgrade_strategy,
450                    after_upgrade_strategy,
451                )
452            })
453            // Collect the arbitrary values to build the final type.
454            .prop_map(|(network, before_upgrade_height, after_upgrade_height)| {
455                BlockHeightPairAcrossNetworkUpgrades {
456                    network,
457                    before_upgrade: block::Height(before_upgrade_height),
458                    after_upgrade: block::Height(after_upgrade_height),
459                }
460            })
461            .boxed()
462    }
463
464    type Strategy = BoxedStrategy<Self>;
465}