1use std::{
29 collections::{HashMap, HashSet},
30 net::SocketAddr,
31 pin::Pin,
32 task::{Context, Poll},
33 time::Duration,
34};
35
36use futures::{
37 future::TryFutureExt,
38 ready,
39 stream::{FuturesUnordered, Stream},
40 FutureExt,
41};
42use pin_project::{pin_project, pinned_drop};
43use thiserror::Error;
44use tokio::{sync::oneshot, task::JoinHandle};
45use tower::{Service, ServiceExt};
46use tracing_futures::Instrument;
47
48use zebra_chain::{
49 block::Height,
50 transaction::{self, UnminedTxId, VerifiedUnminedTx},
51 transparent,
52};
53use zebra_consensus::transaction as tx;
54use zebra_network::{self as zn, PeerSocketAddr};
55use zebra_node_services::mempool::Gossip;
56use zebra_state::{self as zs, CloneError};
57
58use crate::components::{
59 mempool::crawler::RATE_LIMIT_DELAY,
60 sync::{BLOCK_DOWNLOAD_TIMEOUT, BLOCK_VERIFY_TIMEOUT},
61};
62
63use super::MempoolError;
64
65type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
66
67pub(crate) const TRANSACTION_DOWNLOAD_TIMEOUT: Duration = BLOCK_DOWNLOAD_TIMEOUT;
73
74pub(crate) const TRANSACTION_VERIFY_TIMEOUT: Duration = BLOCK_VERIFY_TIMEOUT;
82
83pub const MAX_INBOUND_CONCURRENCY: usize = 500;
107
108pub const MAX_INBOUND_CONCURRENCY_PER_PEER: usize = 5;
117
118#[derive(Copy, Clone, Debug, Eq, PartialEq)]
120struct CancelDownloadAndVerify;
121
122#[derive(Error, Debug, Clone)]
124#[allow(dead_code)]
125pub enum TransactionDownloadVerifyError {
126 #[error("transaction is already in state")]
127 InState,
128
129 #[error("error in state service: {0}")]
130 StateError(#[source] CloneError),
131
132 #[error("error downloading transaction: {0}")]
133 DownloadFailed(#[source] CloneError),
134
135 #[error("transaction download / verification was cancelled")]
136 Cancelled,
137
138 #[error("transaction did not pass consensus validation: {error}")]
139 Invalid {
140 error: zebra_consensus::error::TransactionError,
141 advertiser_addr: Option<PeerSocketAddr>,
142 },
143}
144
145#[pin_project(PinnedDrop)]
147#[derive(Debug)]
148pub struct Downloads<ZN, ZV, ZS>
149where
150 ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Clone + 'static,
151 ZN::Future: Send,
152 ZV: Service<tx::Request, Response = tx::Response, Error = BoxError> + Send + Clone + 'static,
153 ZV::Future: Send,
154 ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
155 ZS::Future: Send,
156{
157 network: ZN,
161
162 verifier: ZV,
164
165 state: ZS,
167
168 #[pin]
171 pending: FuturesUnordered<
172 JoinHandle<
173 Result<
174 Result<
175 (
176 VerifiedUnminedTx,
177 Vec<transparent::OutPoint>,
178 Option<Height>,
179 Option<oneshot::Sender<Result<(), BoxError>>>,
180 ),
181 Box<(TransactionDownloadVerifyError, UnminedTxId)>,
182 >,
183 (UnminedTxId, tokio::time::error::Elapsed),
184 >,
185 >,
186 >,
187
188 cancel_handles: HashMap<
193 UnminedTxId,
194 (
195 oneshot::Sender<CancelDownloadAndVerify>,
196 Gossip,
197 Option<SocketAddr>,
198 ),
199 >,
200
201 pending_per_peer: HashMap<SocketAddr, usize>,
207}
208
209impl<ZN, ZV, ZS> Stream for Downloads<ZN, ZV, ZS>
210where
211 ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Clone + 'static,
212 ZN::Future: Send,
213 ZV: Service<tx::Request, Response = tx::Response, Error = BoxError> + Send + Clone + 'static,
214 ZV::Future: Send,
215 ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
216 ZS::Future: Send,
217{
218 type Item = Result<
219 Result<
220 (
221 VerifiedUnminedTx,
222 Vec<transparent::OutPoint>,
223 Option<Height>,
224 Option<oneshot::Sender<Result<(), BoxError>>>,
225 ),
226 Box<(UnminedTxId, TransactionDownloadVerifyError)>,
227 >,
228 (UnminedTxId, tokio::time::error::Elapsed),
229 >;
230
231 fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
232 let this = self.project();
233 let item = if let Some(join_result) = ready!(this.pending.poll_next(cx)) {
243 let result = join_result.expect("transaction download and verify tasks must not panic");
244 let (result, completed_txid) = match result {
245 Ok(Ok((tx, spent_mempool_outpoints, tip_height, rsp_tx))) => {
246 let hash = tx.transaction.id;
247 (
248 Ok(Ok((tx, spent_mempool_outpoints, tip_height, rsp_tx))),
249 Some(hash),
250 )
251 }
252 Ok(Err(boxed_err)) => {
253 let (e, hash) = *boxed_err;
254 (Ok(Err(Box::new((hash, e)))), Some(hash))
255 }
256 Err((txid, elapsed)) => {
257 this.cancel_handles.remove(&txid);
262 (Err((txid, elapsed)), None)
263 }
264 };
265
266 if let Some(hash) = completed_txid {
267 if let Some((_, _gossip, Some(source))) = this.cancel_handles.remove(&hash) {
268 Self::release_peer_slot(this.pending_per_peer, source);
269 }
270 }
271
272 Some(result)
273 } else {
274 None
275 };
276
277 Poll::Ready(item)
278 }
279
280 fn size_hint(&self) -> (usize, Option<usize>) {
281 self.pending.size_hint()
282 }
283}
284
285impl<ZN, ZV, ZS> Downloads<ZN, ZV, ZS>
286where
287 ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Clone + 'static,
288 ZN::Future: Send,
289 ZV: Service<tx::Request, Response = tx::Response, Error = BoxError> + Send + Clone + 'static,
290 ZV::Future: Send,
291 ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
292 ZS::Future: Send,
293{
294 pub fn new(network: ZN, verifier: ZV, state: ZS) -> Self {
304 Self {
305 network,
306 verifier,
307 state,
308 pending: FuturesUnordered::new(),
309 cancel_handles: HashMap::new(),
310 pending_per_peer: HashMap::new(),
311 }
312 }
313
314 #[instrument(skip(self, gossiped_tx), fields(txid = %gossiped_tx.id()))]
322 #[allow(clippy::unwrap_in_result)]
323 pub fn download_if_needed_and_verify(
324 &mut self,
325 gossiped_tx: Gossip,
326 source: Option<SocketAddr>,
327 mut rsp_tx: Option<oneshot::Sender<Result<(), BoxError>>>,
328 ) -> Result<(), MempoolError> {
329 let txid = gossiped_tx.id();
330
331 if self.cancel_handles.contains_key(&txid) {
332 debug!(
333 ?txid,
334 queue_len = self.pending.len(),
335 ?MAX_INBOUND_CONCURRENCY,
336 "transaction id already queued for inbound download: ignored transaction"
337 );
338 metrics::gauge!("mempool.currently.queued.transactions",)
339 .set(self.pending.len() as f64);
340
341 return Err(MempoolError::AlreadyQueued);
342 }
343
344 if self.pending.len() >= MAX_INBOUND_CONCURRENCY {
345 debug!(
346 ?txid,
347 queue_len = self.pending.len(),
348 ?MAX_INBOUND_CONCURRENCY,
349 "too many transactions queued for inbound download: ignored transaction"
350 );
351 metrics::gauge!("mempool.currently.queued.transactions",)
352 .set(self.pending.len() as f64);
353
354 return Err(MempoolError::FullQueue);
355 }
356
357 if let Some(source) = source {
360 let count = self.pending_per_peer.get(&source).copied().unwrap_or(0);
361 if count >= MAX_INBOUND_CONCURRENCY_PER_PEER {
362 debug!(
363 ?txid,
364 peer_queue_len = count,
365 ?MAX_INBOUND_CONCURRENCY_PER_PEER,
366 "too many transactions queued for this peer: ignored transaction"
367 );
368 metrics::counter!("mempool.full_queue.per_peer.total").increment(1);
369 return Err(MempoolError::FullQueue);
370 }
371 }
372
373 let (cancel_tx, mut cancel_rx) = oneshot::channel::<CancelDownloadAndVerify>();
375
376 let network = self.network.clone();
377 let verifier = self.verifier.clone();
378 let mut state = self.state.clone();
379 let pushed_advertiser_addr = source.map(PeerSocketAddr::from);
380
381 let gossiped_tx_req = gossiped_tx.clone();
382
383 let fut = async move {
384 Self::transaction_in_best_chain(&mut state, txid).await?;
386
387 trace!(?txid, "transaction is not in best chain");
388
389 let (tip_height, next_height) = match state.oneshot(zs::Request::Tip).await {
390 Ok(zs::Response::Tip(None)) => Ok((None, Height(0))),
391 Ok(zs::Response::Tip(Some((height, _hash)))) => {
392 let next_height =
393 (height + 1).expect("valid heights are far below the maximum");
394 Ok((Some(height), next_height))
395 }
396 Ok(_) => unreachable!("wrong response"),
397 Err(e) => Err(TransactionDownloadVerifyError::StateError(e.into())),
398 }?;
399
400 trace!(?txid, ?next_height, "got next height");
401
402 let (tx, advertiser_addr) = match gossiped_tx {
403 Gossip::Id(txid) => {
404 let req = zn::Request::TransactionsById(std::iter::once(txid).collect());
405
406 let tx = match network
407 .oneshot(req)
408 .await
409 .map_err(CloneError::from)
410 .map_err(TransactionDownloadVerifyError::DownloadFailed)?
411 {
412 zn::Response::Transactions(mut txs) => txs.pop().ok_or_else(|| {
413 TransactionDownloadVerifyError::DownloadFailed(
414 BoxError::from("no transactions returned").into(),
415 )
416 })?,
417 _ => unreachable!("wrong response to transaction request"),
418 };
419
420 let (tx, advertiser_addr) = tx.available().expect(
421 "unexpected missing tx status: single tx failures should be errors",
422 );
423
424 metrics::counter!(
425 "mempool.downloaded.transactions.total",
426 "version" => format!("{}",tx.transaction.version()),
427 ).increment(1);
428 (tx, advertiser_addr)
429 }
430 Gossip::Tx(tx) => {
431 metrics::counter!(
432 "mempool.pushed.transactions.total",
433 "version" => format!("{}",tx.transaction.version()),
434 ).increment(1);
435 (tx, pushed_advertiser_addr)
436 }
437 };
438
439 trace!(?txid, "got tx");
440
441 let result = verifier
442 .oneshot(tx::Request::Mempool {
443 transaction: tx.clone(),
444 height: next_height,
445 })
446 .map_ok(|rsp| {
447 let tx::Response::Mempool { transaction, spent_mempool_outpoints } = rsp else {
448 panic!("unexpected non-mempool response to mempool request")
449 };
450
451 (transaction, spent_mempool_outpoints, tip_height)
452 })
453 .await;
454
455 trace!(?txid, result = ?result.as_ref().map(|_tx| ()), "verified transaction for the mempool");
457
458 result.map_err(|e| TransactionDownloadVerifyError::Invalid { error: e.into(), advertiser_addr } )
459 }
460 .map_ok(|(tx, spent_mempool_outpoints, tip_height)| {
461 metrics::counter!(
462 "mempool.verified.transactions.total",
463 "version" => format!("{}", tx.transaction.transaction.version()),
464 ).increment(1);
465 (tx, spent_mempool_outpoints, tip_height)
466 })
467 .map_err(move |e| Box::new((e, txid)))
470 .inspect(move |result| {
471 let result = result.as_ref().map(|_tx| txid);
473 debug!("mempool transaction result: {result:?}");
474 })
475 .in_current_span();
476
477 let task = tokio::spawn(async move {
478 let fut = tokio::time::timeout(RATE_LIMIT_DELAY, fut);
479
480 let result = tokio::select! {
482 biased;
483 _ = &mut cancel_rx => {
484 trace!("task cancelled prior to completion");
485 metrics::counter!("mempool.cancelled.verify.tasks.total").increment(1);
486 if let Some(rsp_tx) = rsp_tx.take() {
487 let _ = rsp_tx.send(Err("verification cancelled".into()));
488 }
489
490 Ok(Err(Box::new((TransactionDownloadVerifyError::Cancelled, txid))))
491 }
492 verification = fut => {
493 verification
494 .inspect_err(|_elapsed| {
495 if let Some(rsp_tx) = rsp_tx.take() {
496 let _ = rsp_tx.send(Err("timeout waiting for verification result".into()));
497 }
498 })
499 .map_err(|elapsed| (txid, elapsed))
500 .map(|inner_result| {
501 match inner_result {
502 Ok((transaction, spent_mempool_outpoints, tip_height)) => Ok((transaction, spent_mempool_outpoints, tip_height, rsp_tx)),
503 Err(boxed_err) => {
504 let (tx_verifier_error, tx_id) = *boxed_err;
505 if let Some(rsp_tx) = rsp_tx.take() {
506 let error_msg = format!(
507 "failed to validate tx: {tx_id}, error: {tx_verifier_error}"
508 );
509 let _ = rsp_tx.send(Err(error_msg.into()));
510 };
511
512 Err(Box::new((tx_verifier_error, tx_id)))
513 }
514 }
515 })
516 },
517 };
518
519 result
520 });
521
522 self.pending.push(task);
523 assert!(
524 self.cancel_handles
525 .insert(txid, (cancel_tx, gossiped_tx_req, source))
526 .is_none(),
527 "transactions are only queued once"
528 );
529 if let Some(source) = source {
530 *self.pending_per_peer.entry(source).or_insert(0) += 1;
533 }
534
535 debug!(
536 ?txid,
537 queue_len = self.pending.len(),
538 ?MAX_INBOUND_CONCURRENCY,
539 "queued transaction hash for download"
540 );
541 metrics::gauge!("mempool.currently.queued.transactions",).set(self.pending.len() as f64);
542 metrics::counter!("mempool.queued.transactions.total").increment(1);
543
544 Ok(())
545 }
546
547 pub fn cancel(&mut self, mined_ids: &HashSet<transaction::Hash>) {
550 let removed_txids: Vec<UnminedTxId> = self
553 .cancel_handles
554 .keys()
555 .filter(|txid| mined_ids.contains(&txid.mined_id()))
556 .cloned()
557 .collect();
558
559 for txid in removed_txids {
560 if let Some((cancel_tx, _gossip, source)) = self.cancel_handles.remove(&txid) {
561 let _ = cancel_tx.send(CancelDownloadAndVerify);
562 if let Some(source) = source {
563 Self::release_peer_slot(&mut self.pending_per_peer, source);
564 }
565 }
566 }
567 }
568
569 pub fn cancel_all(&mut self) {
572 let _ = std::mem::take(&mut self.pending);
574 for (_hash, (cancel_tx, _gossip, _source)) in self.cancel_handles.drain() {
578 let _ = cancel_tx.send(CancelDownloadAndVerify);
579 }
580 self.pending_per_peer.clear();
581 assert!(self.pending.is_empty());
582 assert!(self.cancel_handles.is_empty());
583 metrics::gauge!("mempool.currently.queued.transactions",).set(self.pending.len() as f64);
584 }
585
586 fn release_peer_slot(pending_per_peer: &mut HashMap<SocketAddr, usize>, source: SocketAddr) {
589 if let Some(count) = pending_per_peer.get_mut(&source) {
590 *count = count.saturating_sub(1);
591 if *count == 0 {
592 pending_per_peer.remove(&source);
593 }
594 }
595 }
596
597 #[allow(dead_code)]
599 pub fn in_flight(&self) -> usize {
600 self.pending.len()
601 }
602
603 pub fn transaction_requests(&self) -> impl Iterator<Item = &Gossip> {
605 self.cancel_handles
606 .iter()
607 .map(|(_tx_id, (_handle, tx, _source))| tx)
608 }
609
610 async fn transaction_in_best_chain(
612 state: &mut ZS,
613 txid: UnminedTxId,
614 ) -> Result<(), TransactionDownloadVerifyError> {
615 match state
616 .ready()
617 .await
618 .map_err(CloneError::from)
619 .map_err(TransactionDownloadVerifyError::StateError)?
620 .call(zs::Request::Transaction(txid.mined_id()))
621 .await
622 {
623 Ok(zs::Response::Transaction(None)) => Ok(()),
624 Ok(zs::Response::Transaction(Some(_))) => Err(TransactionDownloadVerifyError::InState),
625 Ok(_) => unreachable!("wrong response"),
626 Err(e) => Err(TransactionDownloadVerifyError::StateError(e.into())),
627 }?;
628
629 Ok(())
630 }
631}
632
633#[pinned_drop]
634impl<ZN, ZV, ZS> PinnedDrop for Downloads<ZN, ZV, ZS>
635where
636 ZN: Service<zn::Request, Response = zn::Response, Error = BoxError> + Send + Clone + 'static,
637 ZN::Future: Send,
638 ZV: Service<tx::Request, Response = tx::Response, Error = BoxError> + Send + Clone + 'static,
639 ZV::Future: Send,
640 ZS: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
641 ZS::Future: Send,
642{
643 fn drop(mut self: Pin<&mut Self>) {
644 self.cancel_all();
645
646 metrics::gauge!("mempool.currently.queued.transactions").set(0 as f64);
647 }
648}
649
650#[cfg(test)]
651mod tests;