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}