Skip to main content

zebrad/components/
mempool.rs

1//! Zebra mempool.
2//!
3//! A zebrad application component that manages the active collection, reception,
4//! gossip, verification, in-memory storage, eviction, and rejection of unmined Zcash
5//! transactions (those that have not been confirmed in a mined block on the
6//! blockchain).
7//!
8//! Major parts of the mempool include:
9//!  * [Mempool Service][`Mempool`]
10//!    * activates when the syncer is near the chain tip
11//!    * spawns [download and verify tasks][`downloads::Downloads`] for each crawled or gossiped transaction
12//!    * handles in-memory [storage][`storage::Storage`] of unmined transactions
13//!  * [Crawler][`crawler::Crawler`]
14//!    * runs in the background to periodically poll peers for fresh unmined transactions
15//!  * [Queue Checker][`queue_checker::QueueChecker`]
16//!    * runs in the background, polling the mempool to store newly verified transactions
17//!  * [Transaction Gossip Task][`gossip::gossip_mempool_transaction_id`]
18//!    * runs in the background and gossips newly added mempool transactions
19//!      to peers
20
21use std::{
22    collections::HashSet,
23    future::Future,
24    iter,
25    ops::Bound,
26    pin::{pin, Pin},
27    task::{Context, Poll},
28};
29
30use chrono::Duration;
31use futures::{future::FutureExt, stream::Stream};
32use tokio::sync::{broadcast, mpsc, oneshot};
33use tower::{buffer::Buffer, timeout::Timeout, util::BoxService, Service};
34
35use zebra_chain::{
36    block::{self, Height},
37    chain_sync_status::ChainSyncStatus,
38    chain_tip::ChainTip,
39    parameters::{Network, NetworkUpgrade},
40    transaction::UnminedTxId,
41};
42use zebra_consensus::{error::TransactionError, transaction};
43use zebra_network as zn;
44use zebra_node_services::mempool::{
45    CreatedOrSpent, Gossip, MempoolChange, MempoolTxSubscriber, Request, Response,
46};
47use zebra_state as zs;
48use zebra_state::{ChainTipChange, TipAction};
49
50use crate::components::sync::SyncStatus;
51
52pub mod config;
53mod crawler;
54pub mod downloads;
55mod error;
56pub mod gossip;
57mod pending_outputs;
58mod queue_checker;
59mod storage;
60
61#[cfg(test)]
62mod tests;
63
64pub use crate::BoxError;
65
66pub use config::Config;
67pub use crawler::Crawler;
68pub use error::MempoolError;
69pub use gossip::gossip_mempool_transaction_id;
70pub use queue_checker::QueueChecker;
71pub use storage::{
72    ExactTipRejectionError, SameEffectsChainRejectionError, SameEffectsTipRejectionError, Storage,
73};
74
75#[cfg(test)]
76pub use self::tests::UnboxMempoolError;
77
78use downloads::{
79    Downloads as TxDownloads, TransactionDownloadVerifyError, TRANSACTION_DOWNLOAD_TIMEOUT,
80    TRANSACTION_VERIFY_TIMEOUT,
81};
82
83type Outbound = Buffer<BoxService<zn::Request, zn::Response, zn::BoxError>, zn::Request>;
84type State = Buffer<BoxService<zs::Request, zs::Response, zs::BoxError>, zs::Request>;
85type TxVerifier = Buffer<
86    BoxService<transaction::MempoolRequest, transaction::MempoolResponse, TransactionError>,
87    transaction::MempoolRequest,
88>;
89type InboundTxDownloads = TxDownloads<Timeout<Outbound>, Timeout<TxVerifier>, State>;
90
91/// The wall-clock window around a network upgrade activation where an adjacent upgrade's branch ID
92/// receives no peer score: 40 blocks at 75 seconds, 120 blocks at ZIP 218's 25 seconds.
93const BRANCH_ID_GRACE_PERIOD: Duration = Duration::minutes(50);
94
95/// Returns the mempool peer score after applying branch ID activation policy.
96fn adjusted_mempool_misbehavior_score(
97    error: &TransactionError,
98    transaction_upgrade: Option<NetworkUpgrade>,
99    verification_height: Height,
100    network: &Network,
101) -> u32 {
102    let is_grace_mismatch = matches!(error, TransactionError::WrongConsensusBranchId)
103        && transaction_upgrade.is_some_and(|transaction_upgrade| {
104            is_in_branch_id_grace_period(transaction_upgrade, verification_height, network)
105        });
106
107    // Lock times are checked against our own tip, so a peer whose tip is ahead can honestly
108    // relay a transaction we reject.
109    let is_tip_relative_lock_time = matches!(
110        error,
111        TransactionError::LockedUntilAfterBlockHeight(_)
112            | TransactionError::LockedUntilAfterBlockTime(_)
113    );
114
115    if is_grace_mismatch || is_tip_relative_lock_time {
116        0
117    } else {
118        error.mempool_misbehavior_score()
119    }
120}
121
122/// Returns `true` if `transaction_upgrade` activates immediately before or after the upgrade
123/// current at `height` on `network`, and `height` is within the grace window of that boundary.
124fn is_in_branch_id_grace_period(
125    transaction_upgrade: NetworkUpgrade,
126    height: Height,
127    network: &Network,
128) -> bool {
129    let activations = network.activation_list();
130    let Some((&current_height, &current)) = activations.range(..=height).next_back() else {
131        return false;
132    };
133    let grace_blocks =
134        BRANCH_ID_GRACE_PERIOD.num_seconds() / current.target_spacing().num_seconds();
135
136    let previous = activations.range(..current_height).next_back();
137    let next = activations
138        .range((Bound::Excluded(height), Bound::Unbounded))
139        .next();
140
141    previous.is_some_and(|(_, &previous)| {
142        previous == transaction_upgrade && height - current_height < grace_blocks
143    }) || next.is_some_and(|(&next_height, &next)| {
144        next == transaction_upgrade && next_height - height <= grace_blocks
145    })
146}
147
148/// The state of the mempool.
149///
150/// Indicates whether it is enabled or disabled and, if enabled, contains
151/// the necessary data to run it.
152//
153// Zebra only has one mempool, so the enum variant size difference doesn't matter.
154#[allow(clippy::large_enum_variant)]
155#[derive(Default)]
156enum ActiveState {
157    /// The Mempool is disabled.
158    #[default]
159    Disabled,
160
161    /// The Mempool is enabled.
162    Enabled {
163        /// The Mempool storage itself.
164        ///
165        /// # Correctness
166        ///
167        /// Only components internal to the [`Mempool`] struct are allowed to
168        /// inject transactions into `storage`, as transactions must be verified beforehand.
169        storage: Storage,
170
171        /// The transaction download and verify stream.
172        tx_downloads: Pin<Box<InboundTxDownloads>>,
173
174        /// Last seen chain tip hash that mempool transactions have been verified against.
175        ///
176        /// In some tests, this is initialized to the latest chain tip, then updated in `poll_ready()` before each request.
177        last_seen_tip_hash: block::Hash,
178    },
179}
180
181impl ActiveState {
182    /// Returns the current state, leaving [`Self::Disabled`] in its place.
183    fn take(&mut self) -> Self {
184        std::mem::take(self)
185    }
186
187    /// Returns a list of requests that will retry every stored and pending transaction.
188    fn transaction_retry_requests(&self) -> Vec<Gossip> {
189        match self {
190            ActiveState::Disabled => Vec::new(),
191            ActiveState::Enabled {
192                storage,
193                tx_downloads,
194                ..
195            } => {
196                let mut transactions = Vec::new();
197
198                let storage = storage
199                    .transactions()
200                    .values()
201                    .map(|tx| tx.transaction.clone().into());
202                transactions.extend(storage);
203
204                let pending = tx_downloads.transaction_requests().cloned();
205                transactions.extend(pending);
206
207                transactions
208            }
209        }
210    }
211
212    /// Returns the number of pending transactions waiting for download or verify,
213    /// or zero if the mempool is disabled.
214    #[cfg(feature = "progress-bar")]
215    fn queued_transaction_count(&self) -> usize {
216        match self {
217            ActiveState::Disabled => 0,
218            ActiveState::Enabled { tx_downloads, .. } => tx_downloads.in_flight(),
219        }
220    }
221
222    /// Returns the number of transactions in storage, or zero if the mempool is disabled.
223    #[cfg(feature = "progress-bar")]
224    fn transaction_count(&self) -> usize {
225        match self {
226            ActiveState::Disabled => 0,
227            ActiveState::Enabled { storage, .. } => storage.transaction_count(),
228        }
229    }
230
231    /// Returns the cost of the transactions in the mempool, according to ZIP-401.
232    /// Returns zero if the mempool is disabled.
233    #[cfg(feature = "progress-bar")]
234    fn total_cost(&self) -> u64 {
235        match self {
236            ActiveState::Disabled => 0,
237            ActiveState::Enabled { storage, .. } => storage.total_cost(),
238        }
239    }
240
241    /// Returns the total serialized size of the verified transactions in the set,
242    /// or zero if the mempool is disabled.
243    ///
244    /// See [`Storage::total_serialized_size()`] for details.
245    #[cfg(feature = "progress-bar")]
246    pub fn total_serialized_size(&self) -> usize {
247        match self {
248            ActiveState::Disabled => 0,
249            ActiveState::Enabled { storage, .. } => storage.total_serialized_size(),
250        }
251    }
252
253    /// Returns the number of rejected transaction hashes in storage,
254    /// or zero if the mempool is disabled.
255    #[cfg(feature = "progress-bar")]
256    fn rejected_transaction_count(&mut self) -> usize {
257        match self {
258            ActiveState::Disabled => 0,
259            ActiveState::Enabled { storage, .. } => storage.rejected_transaction_count(),
260        }
261    }
262}
263
264/// Mempool async management and query service.
265///
266/// The mempool is the set of all verified transactions that this node is aware
267/// of that have yet to be confirmed by the Zcash network. A transaction is
268/// confirmed when it has been included in a block ('mined').
269pub struct Mempool {
270    /// The configured Zcash network.
271    network: Network,
272
273    /// The configurable options for the mempool, persisted between states.
274    config: Config,
275
276    /// The state of the mempool.
277    active_state: ActiveState,
278
279    /// Allows checking if we are near the tip to enable/disable the mempool.
280    sync_status: SyncStatus,
281
282    /// If the state's best chain tip has reached this height, always enable the mempool.
283    debug_enable_at_height: Option<Height>,
284
285    /// Allows efficient access to the best tip of the blockchain.
286    latest_chain_tip: zs::LatestChainTip,
287
288    /// Allows the detection of newly added chain tip blocks,
289    /// and chain tip resets.
290    chain_tip_change: ChainTipChange,
291
292    /// Handle to the outbound service.
293    /// Used to construct the transaction downloader.
294    outbound: Outbound,
295
296    /// Handle to the state service.
297    /// Used to construct the transaction downloader.
298    state: State,
299
300    /// Handle to the transaction verifier service.
301    /// Used to construct the transaction downloader.
302    tx_verifier: TxVerifier,
303
304    /// Sender part of a gossip transactions channel.
305    /// Used to broadcast transaction ids to peers.
306    transaction_sender: broadcast::Sender<MempoolChange>,
307
308    /// Sender for reporting peer addresses that advertised unexpectedly invalid transactions.
309    misbehavior_sender: mpsc::Sender<zn::MisbehaviorReport>,
310
311    // Diagnostics
312    //
313    /// Queued transactions pending download or verification transmitter.
314    /// Only displayed after the mempool's first activation.
315    #[cfg(feature = "progress-bar")]
316    queued_count_bar: Option<howudoin::Tx>,
317
318    /// Number of mempool transactions transmitter.
319    /// Only displayed after the mempool's first activation.
320    #[cfg(feature = "progress-bar")]
321    transaction_count_bar: Option<howudoin::Tx>,
322
323    /// Mempool transaction cost transmitter.
324    /// Only displayed after the mempool's first activation.
325    #[cfg(feature = "progress-bar")]
326    transaction_cost_bar: Option<howudoin::Tx>,
327
328    /// Rejected transactions transmitter.
329    /// Only displayed after the mempool's first activation.
330    #[cfg(feature = "progress-bar")]
331    rejected_count_bar: Option<howudoin::Tx>,
332}
333
334impl Mempool {
335    /// Builds the mempool using a non-owning outbound network handle.
336    ///
337    /// Pass [`zn::NetworkService::background_service`] because the network's inbound handler
338    /// retains the mempool and must not keep its own network alive.
339    #[allow(clippy::too_many_arguments)]
340    pub(crate) fn new(
341        network: &Network,
342        config: &Config,
343        outbound: Outbound,
344        state: State,
345        tx_verifier: TxVerifier,
346        sync_status: SyncStatus,
347        latest_chain_tip: zs::LatestChainTip,
348        chain_tip_change: ChainTipChange,
349        misbehavior_sender: mpsc::Sender<zn::MisbehaviorReport>,
350    ) -> (Self, MempoolTxSubscriber) {
351        let (transaction_sender, _) =
352            tokio::sync::broadcast::channel(gossip::MAX_CHANGES_BEFORE_SEND * 2);
353        let transaction_subscriber = MempoolTxSubscriber::new(transaction_sender.clone());
354
355        let mut service = Mempool {
356            network: network.clone(),
357            config: config.clone(),
358            active_state: ActiveState::Disabled,
359            sync_status,
360            debug_enable_at_height: config.debug_enable_at_height.map(Height),
361            latest_chain_tip,
362            chain_tip_change,
363            outbound,
364            state,
365            tx_verifier,
366            transaction_sender,
367            misbehavior_sender,
368            #[cfg(feature = "progress-bar")]
369            queued_count_bar: None,
370            #[cfg(feature = "progress-bar")]
371            transaction_count_bar: None,
372            #[cfg(feature = "progress-bar")]
373            transaction_cost_bar: None,
374            #[cfg(feature = "progress-bar")]
375            rejected_count_bar: None,
376        };
377
378        // Make sure `is_enabled` is accurate.
379        // Otherwise, it is only updated in `poll_ready`, right before each service call.
380        let is_caught_up_to_start = service.is_caught_up_to_start();
381        service.update_state(None, is_caught_up_to_start);
382
383        (service, transaction_subscriber)
384    }
385
386    /// Is the mempool enabled by a debug config option?
387    fn is_enabled_by_debug(&self) -> bool {
388        let mut is_debug_enabled = false;
389
390        // optimise non-debug performance
391        if self.debug_enable_at_height.is_none() {
392            return is_debug_enabled;
393        }
394
395        let enable_at_height = self
396            .debug_enable_at_height
397            .expect("unexpected debug_enable_at_height: just checked for None");
398
399        if let Some(best_tip_height) = self.latest_chain_tip.best_tip_height() {
400            is_debug_enabled = best_tip_height >= enable_at_height;
401
402            if is_debug_enabled && !self.is_enabled() {
403                info!(
404                    ?best_tip_height,
405                    ?enable_at_height,
406                    "enabling mempool for debugging"
407                );
408            }
409        }
410
411        is_debug_enabled
412    }
413
414    /// Returns `true` if Zebra is caught up enough to start the mempool.
415    fn is_caught_up_to_start(&self) -> bool {
416        self.sync_status.is_close_to_tip() || self.is_enabled_by_debug()
417    }
418
419    /// Replaces the active state with a freshly-initialised [`ActiveState::Enabled`],
420    /// using `tip_action`'s best tip hash as the `last_seen_tip_hash`.
421    fn enable_at_tip(&mut self, tip_action: &TipAction) {
422        let (last_seen_tip_hash, _) = tip_action.best_tip_hash_and_height();
423
424        let tx_downloads = Box::pin(TxDownloads::new(
425            Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT),
426            Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT),
427            self.state.clone(),
428        ));
429        self.active_state = ActiveState::Enabled {
430            storage: storage::Storage::new(&self.config),
431            tx_downloads,
432            last_seen_tip_hash,
433        };
434    }
435
436    /// Activate the mempool once Zebra is close enough to the tip.
437    ///
438    /// Sync status only gates initial activation. Once the mempool is active,
439    /// this method does not disable it.
440    ///
441    /// Accepts an optional [`TipAction`] for setting the `last_seen_tip_hash`
442    /// field when enabling the mempool state. It will not enable the mempool if
443    /// this is [`None`]. `is_caught_up_to_start` is supplied by the caller, which
444    /// already computes it, to avoid evaluating the sync-status predicate twice.
445    ///
446    /// Returns `true` if the state changed.
447    fn update_state(
448        &mut self,
449        tip_action: Option<&TipAction>,
450        is_caught_up_to_start: bool,
451    ) -> bool {
452        // TODO: revisit these state transitions when sync status can prove
453        // whether Zebra is behind the network tip.
454        match (is_caught_up_to_start, self.is_enabled(), tip_action) {
455            // the active state is up to date, or there is no tip action to activate the mempool
456            (false, false, _) | (true, true, _) | (true, false, None) => return false,
457
458            // Enable state - there should be a chain tip when Zebra is close to the network tip
459            (true, false, Some(tip_action)) => {
460                info!(
461                    tip_height = ?tip_action.best_tip_height(),
462                    "activating mempool: Zebra is close to the tip"
463                );
464
465                self.enable_at_tip(tip_action);
466            }
467
468            // TODO: only disable an already-active mempool when validated sync
469            // state proves Zebra is behind a higher-work chain that follows
470            // this node's consensus rules.
471            //
472            // The sync status can be triggered by lower-work forks,
473            // stale peers, or peers on incompatible consensus rules, so
474            // it is strong enough to delay initial activation but not to shut
475            // down a working mempool.
476            (false, true, _) => {
477                return false;
478            }
479        };
480
481        true
482    }
483
484    /// Return whether the mempool is enabled or not.
485    pub fn is_enabled(&self) -> bool {
486        match self.active_state {
487            ActiveState::Disabled => false,
488            ActiveState::Enabled { .. } => true,
489        }
490    }
491
492    /// Remove expired transaction ids from a given list of inserted ones.
493    fn remove_expired_from_peer_list(
494        send_to_peers_ids: &HashSet<UnminedTxId>,
495        expired_transactions: &HashSet<UnminedTxId>,
496    ) -> HashSet<UnminedTxId> {
497        send_to_peers_ids
498            .iter()
499            .filter(|id| !expired_transactions.contains(id))
500            .copied()
501            .collect()
502    }
503
504    /// Update metrics for the mempool.
505    fn update_metrics(&mut self) {
506        // Shutdown if needed
507        #[cfg(feature = "progress-bar")]
508        if matches!(howudoin::cancelled(), Some(true)) {
509            self.disable_metrics();
510            return;
511        }
512
513        // Initialize if just activated
514        #[cfg(feature = "progress-bar")]
515        if self.is_enabled()
516            && (self.queued_count_bar.is_none()
517                || self.transaction_count_bar.is_none()
518                || self.transaction_cost_bar.is_none()
519                || self.rejected_count_bar.is_none())
520        {
521            let _max_transaction_count = self.config.tx_cost_limit
522                / zebra_chain::transaction::MEMPOOL_TRANSACTION_COST_THRESHOLD;
523
524            let transaction_count_bar = *howudoin::new_root()
525                .label("Mempool Transactions")
526                .set_pos(0u64);
527            // .set_len(max_transaction_count);
528
529            let transaction_cost_bar = howudoin::new_with_parent(transaction_count_bar.id())
530                .label("Mempool Cost")
531                .set_pos(0u64)
532                // .set_len(self.config.tx_cost_limit)
533                .fmt_as_bytes(true);
534
535            let queued_count_bar = *howudoin::new_with_parent(transaction_cost_bar.id())
536                .label("Mempool Queue")
537                .set_pos(0u64);
538            // .set_len(
539            //     u64::try_from(downloads::MAX_INBOUND_CONCURRENCY).expect("fits in u64"),
540            // );
541
542            let rejected_count_bar = *howudoin::new_with_parent(queued_count_bar.id())
543                .label("Mempool Rejects")
544                .set_pos(0u64);
545            // .set_len(
546            //     u64::try_from(storage::MAX_EVICTION_MEMORY_ENTRIES).expect("fits in u64"),
547            // );
548
549            self.transaction_count_bar = Some(transaction_count_bar);
550            self.transaction_cost_bar = Some(transaction_cost_bar);
551            self.queued_count_bar = Some(queued_count_bar);
552            self.rejected_count_bar = Some(rejected_count_bar);
553        }
554
555        // Update if the mempool has ever been active
556        #[cfg(feature = "progress-bar")]
557        if let (
558            Some(queued_count_bar),
559            Some(transaction_count_bar),
560            Some(transaction_cost_bar),
561            Some(rejected_count_bar),
562        ) = (
563            self.queued_count_bar,
564            self.transaction_count_bar,
565            self.transaction_cost_bar,
566            self.rejected_count_bar,
567        ) {
568            let queued_count = self.active_state.queued_transaction_count();
569            let transaction_count = self.active_state.transaction_count();
570
571            let transaction_cost = self.active_state.total_cost();
572            let transaction_size = self.active_state.total_serialized_size();
573            let transaction_size =
574                indicatif::HumanBytes(transaction_size.try_into().expect("fits in u64"));
575
576            let rejected_count = self.active_state.rejected_transaction_count();
577
578            queued_count_bar.set_pos(u64::try_from(queued_count).expect("fits in u64"));
579
580            transaction_count_bar.set_pos(u64::try_from(transaction_count).expect("fits in u64"));
581
582            // Display the cost and cost limit, with the actual size as a description.
583            //
584            // Costs can be much higher than the transaction size due to the
585            // MEMPOOL_TRANSACTION_COST_THRESHOLD minimum cost.
586            transaction_cost_bar
587                .set_pos(transaction_cost)
588                .desc(format!("Actual size {transaction_size}"));
589
590            rejected_count_bar.set_pos(u64::try_from(rejected_count).expect("fits in u64"));
591        }
592    }
593
594    /// Disable metrics for the mempool.
595    fn disable_metrics(&self) {
596        #[cfg(feature = "progress-bar")]
597        {
598            if let Some(bar) = self.queued_count_bar {
599                bar.close()
600            }
601            if let Some(bar) = self.transaction_count_bar {
602                bar.close()
603            }
604            if let Some(bar) = self.transaction_cost_bar {
605                bar.close()
606            }
607            if let Some(bar) = self.rejected_count_bar {
608                bar.close()
609            }
610        }
611    }
612}
613
614impl Service<Request> for Mempool {
615    type Response = Response;
616    type Error = BoxError;
617    type Future =
618        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
619
620    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
621        let is_caught_up_to_start = self.is_caught_up_to_start();
622        let should_check_tip = self.is_enabled() || is_caught_up_to_start;
623        let tip_action = should_check_tip
624            .then(|| self.chain_tip_change.last_tip_change())
625            .flatten();
626
627        // TODO: Consider broadcasting a `MempoolChange` when the mempool is disabled.
628        let is_state_changed = self.update_state(tip_action.as_ref(), is_caught_up_to_start);
629
630        tracing::trace!(is_enabled = ?self.is_enabled(), ?is_state_changed, "started polling the mempool...");
631
632        // When the mempool is disabled we still return that the service is ready.
633        // Otherwise, callers could block waiting for the mempool to be enabled.
634        if !self.is_enabled() {
635            self.update_metrics();
636
637            return Poll::Ready(Ok(()));
638        }
639
640        // Clear the mempool and cancel downloads if there has been a chain tip reset.
641        //
642        // But if the mempool was just freshly enabled,
643        // skip resetting and removing mined transactions for this tip.
644        let reset_tip_action = match tip_action.as_ref() {
645            Some(reset_tip_action @ TipAction::Reset { .. }) if !is_state_changed => {
646                Some(reset_tip_action)
647            }
648            _ => None,
649        };
650
651        if let Some(reset_tip_action) = reset_tip_action {
652            info!(
653                tip_height = ?reset_tip_action.best_tip_height(),
654                "resetting mempool: switched best chain, skipped blocks, or activated network upgrade"
655            );
656
657            let previous_state = self.active_state.take();
658            let tx_retries = previous_state.transaction_retry_requests();
659
660            // Use the same code for dropping and resetting the mempool,
661            // to avoid subtle bugs.
662            //
663            // Drop the current contents of the state,
664            // cancelling any pending download tasks,
665            // and dropping completed verification results.
666            std::mem::drop(previous_state);
667
668            // Re-initialise an empty state.
669            //
670            // This deliberately bypasses the initial-activation gate in `update_state()`:
671            // the mempool was already active when the reset arrived, and a
672            // far-from-tip sync status must not disable an already-active mempool
673            // (it can be triggered by lower-work forks, stale peers, or peers on
674            // incompatible consensus rules).
675            self.enable_at_tip(reset_tip_action);
676
677            // Re-verify the transactions that were pending or valid at the previous tip.
678            // This saves us the time and data needed to re-download them.
679            if let ActiveState::Enabled { tx_downloads, .. } = &mut self.active_state {
680                info!(
681                    transactions = tx_retries.len(),
682                    "re-verifying mempool transactions after a chain fork"
683                );
684
685                for tx in tx_retries {
686                    // This is just an efficiency optimisation, so we don't care if queueing
687                    // transaction requests fails.
688                    let _result = tx_downloads.download_if_needed_and_verify(tx, None, None);
689                }
690            }
691
692            self.update_metrics();
693
694            return Poll::Ready(Ok(()));
695        }
696
697        if let ActiveState::Enabled {
698            storage,
699            tx_downloads,
700            last_seen_tip_hash,
701        } = &mut self.active_state
702        {
703            // Collect inserted transaction ids.
704            let mut send_to_peers_ids = HashSet::<_>::new();
705            let mut invalidated_ids = HashSet::<_>::new();
706            let mut mined_mempool_ids = HashSet::<_>::new();
707
708            let best_tip_height = self.latest_chain_tip.best_tip_height();
709
710            // Clean up completed download tasks and add to mempool if successful.
711            while let Poll::Ready(Some(result)) = pin!(&mut *tx_downloads).poll_next(cx) {
712                match result {
713                    Ok(Ok((tx, spent_mempool_outpoints, expected_tip_height, rsp_tx))) => {
714                        // # Correctness:
715                        //
716                        // It's okay to use tip height here instead of the tip hash since
717                        // chain_tip_change.last_tip_change() returns a `TipAction::Reset` when
718                        // the best chain changes (which is the only way to stay at the same height), and the
719                        // mempool re-verifies all pending tx_downloads when there's a `TipAction::Reset`.
720                        if best_tip_height == expected_tip_height {
721                            let tx_id = tx.transaction.id;
722                            let insert_result =
723                                storage.insert(tx, spent_mempool_outpoints, best_tip_height);
724
725                            tracing::trace!(
726                                ?insert_result,
727                                "got Ok(_) transaction verify, tried to store",
728                            );
729
730                            if let Ok(inserted_id) = insert_result {
731                                // Save transaction ids that we will send to peers
732                                send_to_peers_ids.insert(inserted_id);
733                            } else {
734                                invalidated_ids.insert(tx_id);
735                            }
736
737                            // Send the result to responder channel if one was provided.
738                            if let Some(rsp_tx) = rsp_tx {
739                                let _ = rsp_tx
740                                    .send(insert_result.map(|_| ()).map_err(|err| err.into()));
741                            }
742                        } else {
743                            tracing::trace!("chain grew during tx verification, retrying ..",);
744
745                            // We don't care if re-queueing the transaction request fails.
746                            let _result = tx_downloads.download_if_needed_and_verify(
747                                tx.transaction.into(),
748                                None,
749                                rsp_tx,
750                            );
751                        }
752                    }
753                    Ok(Err(boxed_err)) => {
754                        let (tx_id, error) = *boxed_err;
755                        if let TransactionDownloadVerifyError::Invalid {
756                            error,
757                            advertiser_addr: Some(advertiser_addr),
758                            transaction_upgrade,
759                            verification_height,
760                        } = &error
761                        {
762                            let score = adjusted_mempool_misbehavior_score(
763                                error,
764                                *transaction_upgrade,
765                                *verification_height,
766                                &self.network,
767                            );
768
769                            if score != 0 {
770                                let _ =
771                                    self.misbehavior_sender.try_send(zn::MisbehaviorReport::new(
772                                        *advertiser_addr,
773                                        score,
774                                        format!("invalid mempool transaction {tx_id}: {error}"),
775                                    ));
776                            }
777                        };
778
779                        tracing::debug!(?tx_id, ?error, "mempool transaction failed to verify");
780
781                        metrics::counter!("mempool.failed.verify.tasks.total", "reason" => error.to_string()).increment(1);
782
783                        invalidated_ids.insert(tx_id);
784                        storage.reject_if_needed(tx_id, error);
785                    }
786                    Err((tx_id, _elapsed)) => {
787                        tracing::info!(
788                            ?tx_id,
789                            "mempool transaction failed to verify due to timeout"
790                        );
791
792                        invalidated_ids.insert(tx_id);
793
794                        metrics::counter!("mempool.failed.verify.tasks.total", "reason" => "timeout").increment(1);
795                    }
796                };
797            }
798
799            // Handle best chain tip changes
800            if let Some(TipAction::Grow { block }) = tip_action {
801                tracing::trace!(block_height = ?block.height, "handling blocks added to tip");
802                *last_seen_tip_hash = block.hash;
803
804                // Cancel downloads/verifications/storage of transactions
805                // with the same mined IDs as recently mined transactions.
806                let mined_ids = block.transaction_hashes.iter().cloned().collect();
807                tx_downloads.cancel(&mined_ids);
808                storage.clear_mined_dependencies(&mined_ids);
809
810                let storage::RemovedTransactionIds { mined, invalidated } =
811                    storage.reject_and_remove_same_effects(&mined_ids, block.transactions);
812
813                // Clear any transaction rejections if they might have become valid after
814                // the new block was added to the tip.
815                storage.clear_tip_rejections();
816
817                mined_mempool_ids.extend(mined);
818                invalidated_ids.extend(invalidated);
819            }
820
821            // Remove expired transactions from the mempool.
822            //
823            // Lock times never expire, because block times are strictly increasing.
824            // So we don't need to check them here.
825            if let Some(tip_height) = best_tip_height {
826                let expired_transactions = storage.remove_expired_transactions(tip_height);
827                // Remove transactions that are expired from the peers list
828                send_to_peers_ids =
829                    Self::remove_expired_from_peer_list(&send_to_peers_ids, &expired_transactions);
830
831                if !expired_transactions.is_empty() {
832                    tracing::debug!(
833                        ?expired_transactions,
834                        "removed expired transactions from the mempool",
835                    );
836
837                    invalidated_ids.extend(expired_transactions);
838                }
839            }
840
841            // Send transactions that were not rejected nor expired to peers and RPC listeners.
842            if !send_to_peers_ids.is_empty() {
843                tracing::trace!(
844                    ?send_to_peers_ids,
845                    "sending new transactions to peers and RPC listeners"
846                );
847
848                // A send fails only when the broadcast channel has no
849                // subscribers, which is a normal state the mempool must
850                // tolerate: the gossip and indexer tasks are downstream
851                // consumers of this feed, and mempool liveness must not depend
852                // on them staying subscribed. Discard the result so a momentary
853                // absence of subscribers does not make `poll_ready()` return a
854                // service-fatal error. See #10689.
855                let _ = self
856                    .transaction_sender
857                    .send(MempoolChange::added(send_to_peers_ids));
858            }
859
860            // Send transactions that were rejected to RPC listeners.
861            if !invalidated_ids.is_empty() {
862                tracing::trace!(
863                    ?invalidated_ids,
864                    "sending invalidated transactions to RPC listeners"
865                );
866
867                // See the `MempoolChange::added` send above: a missing
868                // subscriber is not a fatal condition. See #10689.
869                let _ = self
870                    .transaction_sender
871                    .send(MempoolChange::invalidated(invalidated_ids));
872            }
873
874            // Send transactions that were mined onto the best chain to RPC listeners.
875            if !mined_mempool_ids.is_empty() {
876                tracing::trace!(
877                    ?mined_mempool_ids,
878                    "sending mined transactions to RPC listeners"
879                );
880
881                // See the `MempoolChange::added` send above: a missing
882                // subscriber is not a fatal condition. See #10689.
883                let _ = self
884                    .transaction_sender
885                    .send(MempoolChange::mined(mined_mempool_ids));
886            }
887        }
888
889        self.update_metrics();
890
891        Poll::Ready(Ok(()))
892    }
893
894    /// Call the mempool service.
895    ///
896    /// Errors indicate that the peer has done something wrong or unexpected,
897    /// and will cause callers to disconnect from the remote peer.
898    #[instrument(name = "mempool", skip(self, req))]
899    fn call(&mut self, req: Request) -> Self::Future {
900        match &mut self.active_state {
901            ActiveState::Enabled {
902                storage,
903                tx_downloads,
904                last_seen_tip_hash,
905            } => match req {
906                // Queries
907                Request::TransactionIds => {
908                    trace!(?req, "got mempool request");
909
910                    let res: HashSet<_> = storage.tx_ids().collect();
911
912                    trace!(?req, res_count = ?res.len(), "answered mempool request");
913
914                    async move { Ok(Response::TransactionIds(res)) }.boxed()
915                }
916
917                Request::TransactionsById(ref ids) => {
918                    trace!(?req, "got mempool request");
919
920                    let res: Vec<_> = storage.transactions_exact(ids.clone()).cloned().collect();
921
922                    trace!(?req, res_count = ?res.len(), "answered mempool request");
923
924                    async move { Ok(Response::Transactions(res)) }.boxed()
925                }
926                Request::TransactionsByMinedId(ref ids) => {
927                    trace!(?req, "got mempool request");
928
929                    let res: Vec<_> = storage
930                        .transactions_same_effects(ids.clone())
931                        .cloned()
932                        .collect();
933
934                    trace!(?req, res_count = ?res.len(), "answered mempool request");
935
936                    async move { Ok(Response::Transactions(res)) }.boxed()
937                }
938                Request::TransactionWithDepsByMinedId(tx_id) => {
939                    trace!(?req, "got mempool request");
940
941                    let res = if let Some((transaction, dependencies)) =
942                        storage.transaction_with_deps(tx_id)
943                    {
944                        Ok(Response::TransactionWithDeps {
945                            transaction,
946                            dependencies,
947                        })
948                    } else {
949                        Err("transaction not found in mempool".into())
950                    };
951
952                    trace!(?req, ?res, "answered mempool request");
953
954                    async move { res }.boxed()
955                }
956
957                Request::AwaitOutput(outpoint) => {
958                    trace!(?req, "got mempool request");
959
960                    let response_fut = storage.pending_outputs.queue(outpoint);
961
962                    if let Some(output) = storage.created_output(&outpoint) {
963                        storage.pending_outputs.respond(&outpoint, output)
964                    }
965
966                    trace!("answered mempool request");
967
968                    response_fut.boxed()
969                }
970
971                Request::FullTransactions => {
972                    trace!(?req, "got mempool request");
973
974                    let transactions: Vec<_> = storage.transactions().values().cloned().collect();
975                    let transaction_dependencies = storage.transaction_dependencies().clone();
976
977                    trace!(?req, transactions_count = ?transactions.len(), "answered mempool request");
978
979                    let response = Response::FullTransactions {
980                        transactions,
981                        transaction_dependencies,
982                        last_seen_tip_hash: *last_seen_tip_hash,
983                    };
984
985                    async move { Ok(response) }.boxed()
986                }
987
988                Request::RejectedTransactionIds(ref ids) => {
989                    trace!(?req, "got mempool request");
990
991                    let res = storage.rejected_transactions(ids.clone()).collect();
992
993                    trace!(?req, ?res, "answered mempool request");
994
995                    async move { Ok(Response::RejectedTransactionIds(res)) }.boxed()
996                }
997
998                // Queue mempool candidates
999                Request::Queue(gossiped_txs) => {
1000                    trace!(req_count = ?gossiped_txs.len(), "got mempool Queue request");
1001
1002                    let rsp: Vec<Result<oneshot::Receiver<Result<(), BoxError>>, BoxError>> =
1003                        gossiped_txs
1004                            .into_iter()
1005                            .map(
1006                                |gossiped_tx| -> Result<
1007                                    oneshot::Receiver<Result<(), BoxError>>,
1008                                    MempoolError,
1009                                > {
1010                                    let (rsp_tx, rsp_rx) = oneshot::channel();
1011                                    storage.should_download_or_verify(gossiped_tx.id())?;
1012                                    tx_downloads.download_if_needed_and_verify(
1013                                        gossiped_tx,
1014                                        None,
1015                                        Some(rsp_tx),
1016                                    )?;
1017
1018                                    Ok(rsp_rx)
1019                                },
1020                            )
1021                            .map(|result| result.map_err(BoxError::from))
1022                            .collect();
1023
1024                    // We've added transactions to the queue
1025                    self.update_metrics();
1026
1027                    async move { Ok(Response::Queued(rsp)) }.boxed()
1028                }
1029
1030                // Queue candidates received from a specific peer (advertised IDs
1031                // or a directly pushed transaction). Per-peer accounting is
1032                // enforced inside the downloader.
1033                Request::QueueFromPeer { candidates, source } => {
1034                    trace!(req_count = ?candidates.len(), ?source, "got mempool QueueFromPeer request");
1035
1036                    for candidate in candidates {
1037                        if storage.should_download_or_verify(candidate.id()).is_err() {
1038                            continue;
1039                        }
1040                        let _ = tx_downloads.download_if_needed_and_verify(
1041                            candidate,
1042                            Some(source),
1043                            None,
1044                        );
1045                    }
1046
1047                    self.update_metrics();
1048
1049                    async move { Ok(Response::Queued(Vec::new())) }.boxed()
1050                }
1051
1052                // Store successfully downloaded and verified transactions in the mempool
1053                Request::CheckForVerifiedTransactions => {
1054                    trace!(?req, "got mempool request");
1055
1056                    // all the work for this request is done in poll_ready
1057                    async move { Ok(Response::CheckedForVerifiedTransactions) }.boxed()
1058                }
1059
1060                // Summary statistics for the mempool: count, total size, and memory usage.
1061                //
1062                // Used by the `getmempoolinfo` RPC method
1063                Request::QueueStats => {
1064                    trace!(?req, "got mempool request");
1065
1066                    let size = storage.transaction_count();
1067
1068                    let bytes = storage.total_serialized_size();
1069
1070                    let usage = bytes; // TODO: Placeholder, should be fixed later
1071
1072                    // TODO: Set to Some(true) on regtest once network info is available.
1073                    let fully_notified = None;
1074
1075                    trace!(size, bytes, usage, "answered mempool request");
1076
1077                    async move {
1078                        Ok(Response::QueueStats {
1079                            size,
1080                            bytes,
1081                            usage,
1082                            fully_notified,
1083                        })
1084                    }
1085                    .boxed()
1086                }
1087                Request::UnspentOutput(outpoint) => {
1088                    trace!(?req, "got mempool request");
1089
1090                    if storage.has_spent_outpoint(&outpoint) {
1091                        trace!(?req, "answered mempool request");
1092
1093                        return async move {
1094                            Ok(Response::TransparentOutput(Some(CreatedOrSpent::Spent)))
1095                        }
1096                        .boxed();
1097                    }
1098
1099                    if let Some((tx_version, output)) = storage
1100                        .transactions()
1101                        .get(&outpoint.hash)
1102                        .map(|tx| tx.transaction.transaction.clone())
1103                        .and_then(|tx| {
1104                            tx.outputs()
1105                                .get(outpoint.index as usize)
1106                                .map(|output| (tx.version(), output.clone()))
1107                        })
1108                    {
1109                        trace!(?req, "answered mempool request");
1110
1111                        let last_seen_hash = *last_seen_tip_hash;
1112                        return async move {
1113                            Ok(Response::TransparentOutput(Some(CreatedOrSpent::Created {
1114                                output,
1115                                tx_version,
1116                                last_seen_hash,
1117                            })))
1118                        }
1119                        .boxed();
1120                    }
1121
1122                    trace!(?req, "answered mempool request");
1123
1124                    async move { Ok(Response::TransparentOutput(None)) }.boxed()
1125                }
1126            },
1127            ActiveState::Disabled => {
1128                // TODO: add the name of the request, but not the content,
1129                //       like the command() or Display impls of network requests
1130                trace!("got mempool request while mempool is disabled");
1131
1132                // We can't return an error since that will cause a disconnection
1133                // by the peer connection handler. Therefore, return successful
1134                // empty responses.
1135
1136                let resp = match req {
1137                    // Return empty responses for queries.
1138                    Request::TransactionIds => Response::TransactionIds(Default::default()),
1139
1140                    Request::TransactionsById(_) => Response::Transactions(Default::default()),
1141                    Request::TransactionsByMinedId(_) => Response::Transactions(Default::default()),
1142                    Request::TransactionWithDepsByMinedId(_)
1143                    | Request::AwaitOutput(_)
1144                    | Request::UnspentOutput(_) => {
1145                        return async move {
1146                            Err("mempool is not active: wait for Zebra to sync to the tip".into())
1147                        }
1148                        .boxed()
1149                    }
1150
1151                    Request::FullTransactions => {
1152                        return async move {
1153                            Err("mempool is not active: wait for Zebra to sync to the tip".into())
1154                        }
1155                        .boxed()
1156                    }
1157
1158                    Request::RejectedTransactionIds(_) => {
1159                        Response::RejectedTransactionIds(Default::default())
1160                    }
1161
1162                    // Don't queue mempool candidates, because there is no queue.
1163                    Request::Queue(gossiped_txs) => Response::Queued(
1164                        // Special case; we can signal the error inside the response,
1165                        // because the inbound service ignores inner errors.
1166                        iter::repeat_n(MempoolError::Disabled, gossiped_txs.len())
1167                            .map(BoxError::from)
1168                            .map(Err)
1169                            .collect(),
1170                    ),
1171
1172                    // Drop peer-advertised txids when the mempool is disabled.
1173                    Request::QueueFromPeer { .. } => Response::Queued(Vec::new()),
1174
1175                    // Check if the mempool should be enabled.
1176                    // This request makes sure mempools are debug-enabled in the acceptance tests.
1177                    Request::CheckForVerifiedTransactions => {
1178                        // all the work for this request is done in poll_ready
1179                        Response::CheckedForVerifiedTransactions
1180                    }
1181
1182                    // Return empty mempool stats
1183                    Request::QueueStats => Response::QueueStats {
1184                        size: 0,
1185                        bytes: 0,
1186                        usage: 0,
1187                        fully_notified: None,
1188                    },
1189                };
1190
1191                async move { Ok(resp) }.boxed()
1192            }
1193        }
1194    }
1195}
1196
1197impl Drop for Mempool {
1198    fn drop(&mut self) {
1199        self.disable_metrics();
1200    }
1201}