Skip to main content

zebrad/components/mempool/
crawler.rs

1//! Zebra Mempool crawler.
2//!
3//! The [`Crawler`] periodically requests transactions from peers in order to populate the mempool.
4//!
5//! Crawling only happens when the local node has synchronized the chain to be close to its tip. If
6//! synchronization is still happening at a fast rate, the crawler will stay disabled until it
7//! slows down.
8//!
9//! Once enabled, the crawler will periodically request [`FANOUT`] number of peers for transactions
10//! from the `peer_set` specified when it started. These crawl iterations occur at most once per
11//! [`RATE_LIMIT_DELAY`]. The received transaction IDs are forwarded to the `mempool` service so
12//! that they can be downloaded and included in the mempool.
13//!
14//! # Example
15//!
16//! ```
17//! use zebrad::components::mempool;
18//! #
19//! # use zebra_chain::parameters::Network;
20//! # use zebra_state::ChainTipSender;
21//! # use zebra_test::mock_service::MockService;
22//! # use zebrad::components::sync::SyncStatus;
23//! #
24//! # let runtime = tokio::runtime::Builder::new_current_thread()
25//! #     .enable_all()
26//! #     .build()
27//! #     .expect("Failed to create Tokio runtime");
28//! # let _guard = runtime.enter();
29//! #
30//! # let peer_set_service = MockService::build().for_unit_tests();
31//! # let mempool_service = MockService::build().for_unit_tests();
32//! # let (sync_status, _) = SyncStatus::new();
33//! # let (_, _, chain_tip_change) = ChainTipSender::new(None, &Network::Mainnet);
34//!
35//! let crawler_task = mempool::Crawler::spawn(
36//!     &mempool::Config::default(),
37//!     peer_set_service,
38//!     mempool_service,
39//!     sync_status,
40//!     chain_tip_change,
41//! );
42//!
43//! # // Won't actually crawl because the sender endpoint of `sync_status` was dropped immediately
44//! # // when it was created.
45//! # runtime.block_on(async move {
46//! crawler_task.await;
47//! # });
48//! ```
49
50use std::{collections::HashSet, time::Duration};
51
52use futures::{future, pin_mut, stream::FuturesUnordered, StreamExt};
53use tokio::{
54    sync::watch,
55    task::JoinHandle,
56    time::{sleep, timeout},
57};
58use tower::{timeout::Timeout, BoxError, Service, ServiceExt};
59use tracing_futures::Instrument;
60
61use zebra_chain::{block::Height, transaction::UnminedTxId};
62use zebra_network as zn;
63use zebra_node_services::mempool::Gossip;
64use zebra_state::ChainTipChange;
65
66use crate::components::{
67    mempool::{self, Config},
68    sync::SyncStatus,
69};
70
71#[cfg(test)]
72mod tests;
73
74/// The number of peers to request transactions from per crawl event.
75const FANOUT: usize = 3;
76
77/// The delay between crawl events.
78///
79/// This must stay above `zebra_network`'s `DEFAULT_CRAWL_NEW_PEER_INTERVAL` plus `HANDSHAKE_TIMEOUT`,
80/// so address crawls, new connections, and mempool crawls stay ordered. It does not track the
81/// block interval.
82///
83/// Using a prime number makes sure that mempool crawler fanouts
84/// don't synchronise with other crawls.
85pub const RATE_LIMIT_DELAY: Duration = Duration::from_secs(73);
86
87/// The time to wait for a peer response.
88///
89/// # Correctness
90///
91/// If this timeout is removed or set too high, the crawler may hang waiting for a peer to respond.
92///
93/// If this timeout is set too low, the crawler may fail to populate the mempool.
94const PEER_RESPONSE_TIMEOUT: Duration = Duration::from_secs(6);
95
96/// The mempool transaction crawler.
97pub struct Crawler<PeerSet, Mempool> {
98    /// The network peer set to crawl.
99    peer_set: Timeout<PeerSet>,
100
101    /// The mempool service that receives crawled transaction IDs.
102    mempool: Mempool,
103
104    /// Allows checking if we are near the tip to enable/disable the mempool crawler.
105    sync_status: SyncStatus,
106
107    /// Notifies the crawler when the best chain tip height changes.
108    chain_tip_change: ChainTipChange,
109
110    /// If the state's best chain tip has reached this height, always enable the mempool crawler.
111    debug_enable_at_height: Option<Height>,
112}
113
114impl<PeerSet, Mempool> Crawler<PeerSet, Mempool>
115where
116    PeerSet:
117        Service<zn::Request, Response = zn::Response, Error = BoxError> + Clone + Send + 'static,
118    PeerSet::Future: Send,
119    Mempool:
120        Service<mempool::Request, Response = mempool::Response, Error = BoxError> + Send + 'static,
121    Mempool::Future: Send,
122{
123    /// Spawn an asynchronous task to run the mempool crawler.
124    pub fn spawn(
125        config: &Config,
126        peer_set: PeerSet,
127        mempool: Mempool,
128        sync_status: SyncStatus,
129        chain_tip_change: ChainTipChange,
130    ) -> JoinHandle<Result<(), BoxError>> {
131        let crawler = Crawler {
132            peer_set: Timeout::new(peer_set, PEER_RESPONSE_TIMEOUT),
133            mempool,
134            sync_status,
135            chain_tip_change,
136            debug_enable_at_height: config.debug_enable_at_height.map(Height),
137        };
138
139        tokio::spawn(crawler.run().in_current_span())
140    }
141
142    /// Waits until the mempool crawler is enabled by a debug config option.
143    ///
144    /// Returns an error if communication with the state is lost.
145    async fn wait_until_enabled_by_debug(&mut self) -> Result<(), watch::error::RecvError> {
146        // optimise non-debug performance
147        if self.debug_enable_at_height.is_none() {
148            return future::pending().await;
149        }
150
151        let enable_at_height = self
152            .debug_enable_at_height
153            .expect("unexpected debug_enable_at_height: just checked for None");
154
155        loop {
156            let best_tip_height = self
157                .chain_tip_change
158                .wait_for_tip_change()
159                .await?
160                .best_tip_height();
161
162            if best_tip_height >= enable_at_height {
163                return Ok(());
164            }
165        }
166    }
167
168    /// Waits until the mempool crawler is enabled.
169    ///
170    /// Returns an error if communication with the syncer or state is lost.
171    async fn wait_until_enabled(&mut self) -> Result<(), watch::error::RecvError> {
172        let mut sync_status = self.sync_status.clone();
173        let tip_future = sync_status.wait_until_close_to_tip();
174        let debug_future = self.wait_until_enabled_by_debug();
175
176        pin_mut!(tip_future);
177        pin_mut!(debug_future);
178
179        let (result, _unready_future) = future::select(tip_future, debug_future)
180            .await
181            .factor_first();
182
183        result
184    }
185
186    /// Periodically crawl peers for transactions to include in the mempool.
187    ///
188    /// Runs until the [`SyncStatus`] loses its connection to the chain syncer, which happens when
189    /// Zebra is shutting down.
190    pub async fn run(mut self) -> Result<(), BoxError> {
191        // This log is verbose during tests.
192        #[cfg(not(test))]
193        info!("initializing mempool crawler task");
194        #[cfg(test)]
195        debug!("initializing mempool crawler task");
196
197        loop {
198            self.wait_until_enabled().await?;
199            // Avoid hangs when the peer service is not ready, or due to bugs in async code.
200            timeout(RATE_LIMIT_DELAY, self.crawl_transactions())
201                .await
202                .unwrap_or_else(|timeout| {
203                    // Temporary errors just get logged and ignored.
204                    info!("mempool crawl timed out: {timeout:?}");
205                    Ok(())
206                })?;
207            sleep(RATE_LIMIT_DELAY).await;
208        }
209    }
210
211    /// Crawl peers for transactions.
212    ///
213    /// Concurrently request [`FANOUT`] peers for transactions to include in the mempool.
214    async fn crawl_transactions(&mut self) -> Result<(), BoxError> {
215        let peer_set = self.peer_set.clone();
216
217        trace!("Crawling for mempool transactions");
218
219        let mut requests = FuturesUnordered::new();
220        // get readiness for one peer at a time, to avoid peer set contention
221        for attempt in 0..FANOUT {
222            if attempt > 0 {
223                // Let other tasks run, so we're more likely to choose a different peer.
224                //
225                // TODO: move fanouts into the PeerSet, so we always choose different peers (#2214)
226                tokio::task::yield_now().await;
227            }
228
229            let mut peer_set = peer_set.clone();
230            // end the task on permanent peer set errors
231            let peer_set = peer_set.ready().await?;
232
233            requests.push(peer_set.call(zn::Request::MempoolTransactionIds));
234        }
235
236        while let Some(result) = requests.next().await {
237            // log individual response errors
238            match result {
239                Ok(response) => self.handle_response(response).await?,
240                Err(error) => debug!("Failed to crawl peer for mempool transactions: {}", error),
241            }
242        }
243
244        Ok(())
245    }
246
247    /// Handle a peer's response to the crawler's request for transactions.
248    async fn handle_response(&mut self, response: zn::Response) -> Result<(), BoxError> {
249        let transaction_ids: HashSet<_> = match response {
250            zn::Response::TransactionIds(ids) => ids.into_iter().collect(),
251            _ => unreachable!("Peer set did not respond with transaction IDs to mempool crawler"),
252        };
253
254        trace!(
255            "Mempool crawler received {} transaction IDs",
256            transaction_ids.len()
257        );
258
259        if !transaction_ids.is_empty() {
260            self.queue_transactions(transaction_ids).await?;
261        }
262
263        Ok(())
264    }
265
266    /// Forward the crawled transactions IDs to the mempool transaction downloader.
267    async fn queue_transactions(
268        &mut self,
269        transaction_ids: HashSet<UnminedTxId>,
270    ) -> Result<(), BoxError> {
271        let transaction_ids = transaction_ids.into_iter().map(Gossip::Id).collect();
272
273        let call_result = self
274            .mempool
275            .ready()
276            .await?
277            .call(mempool::Request::Queue(transaction_ids))
278            .await;
279
280        let queue_errors = match call_result {
281            Ok(mempool::Response::Queued(queue_results)) => {
282                queue_results.into_iter().filter_map(Result::err)
283            }
284            Ok(_) => unreachable!("Mempool did not respond with queue results to mempool crawler"),
285            Err(call_error) => {
286                debug!("Ignoring unexpected peer behavior: {}", call_error);
287                return Ok(());
288            }
289        };
290
291        for error in queue_errors {
292            debug!("Failed to download a crawled transaction: {}", error);
293        }
294
295        Ok(())
296    }
297}