Skip to main content

zebrad/components/mempool/
storage.rs

1//! Mempool transaction storage.
2//!
3//! The main struct [`Storage`] holds verified and rejected transactions.
4//! [`Storage`] is effectively the data structure of the mempool. Convenient methods to
5//! manage it are included.
6//!
7//! [`Storage`] does not expose a service so it can only be used by other code directly.
8//! Only code inside the [`crate::components::mempool`] module has access to it.
9
10use std::{
11    collections::{HashMap, HashSet},
12    mem::size_of,
13    sync::Arc,
14    time::Duration,
15};
16
17use thiserror::Error;
18
19use zcash_script::solver;
20use zebra_chain::{
21    block::Height,
22    transaction::{self, Hash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx},
23    transparent,
24};
25use zebra_node_services::mempool::TransactionDependencies;
26
27use self::{eviction_list::EvictionList, verified_set::VerifiedSet};
28use super::{
29    config, downloads::TransactionDownloadVerifyError, pending_outputs::PendingOutputs,
30    MempoolError,
31};
32
33#[cfg(any(test, feature = "proptest-impl"))]
34use proptest_derive::Arbitrary;
35
36#[cfg(test)]
37pub mod tests;
38
39mod eviction_list;
40mod policy;
41mod verified_set;
42
43/// The size limit for mempool transaction rejection lists per [ZIP-401].
44///
45/// > The size of RecentlyEvicted SHOULD never exceed `eviction_memory_entries`
46/// > entries, which is the constant 40000.
47///
48/// We use the specified value for all lists for consistency.
49///
50/// [ZIP-401]: https://zips.z.cash/zip-0401#specification
51pub(crate) const MAX_EVICTION_MEMORY_ENTRIES: usize = 40_000;
52
53/// Transactions rejected based on transaction authorizing data (scripts, proofs, signatures),
54/// or lock times. These rejections are only valid for the current tip.
55///
56/// Each committed block clears these rejections, because new blocks can supply missing inputs.
57#[derive(Error, Clone, Debug, PartialEq, Eq)]
58#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
59#[allow(dead_code)]
60pub enum ExactTipRejectionError {
61    /// Skip this variant in proptest because `TransactionError` is a large enum
62    /// that causes stack overflow during arbitrary value generation.
63    #[error("transaction did not pass consensus validation: {0}")]
64    #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))]
65    FailedVerification(#[from] zebra_consensus::error::TransactionError),
66    #[error("transaction did not pass standard validation: {0}")]
67    FailedStandard(#[from] NonStandardTransactionError),
68}
69
70/// Transactions rejected based only on their effects (spends, outputs, transaction header).
71/// These rejections are only valid for the current tip.
72///
73/// Each committed block clears these rejections, because new blocks can evict other transactions.
74#[derive(Error, Clone, Debug, PartialEq, Eq)]
75#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
76#[allow(dead_code)]
77pub enum SameEffectsTipRejectionError {
78    #[error(
79        "transaction rejected because another transaction in the mempool has already spent some of \
80        its inputs"
81    )]
82    SpendConflict,
83
84    #[error(
85        "transaction rejected because it spends missing outputs from \
86        another transaction in the mempool"
87    )]
88    MissingOutput,
89}
90
91/// Transactions rejected based only on their effects (spends, outputs, transaction header).
92/// These rejections are valid while the current chain continues to grow.
93///
94/// Rollbacks and network upgrades clear these rejections, because they can lower the tip height,
95/// or change the consensus rules.
96#[derive(Error, Clone, Debug, PartialEq, Eq, Hash)]
97#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
98#[allow(dead_code)]
99pub enum SameEffectsChainRejectionError {
100    #[error("best chain tip has reached transaction expiry height")]
101    Expired,
102
103    #[error("transaction inputs were spent, or nullifiers were revealed, in the best chain")]
104    DuplicateSpend,
105
106    #[error("transaction was committed to the best chain")]
107    Mined,
108
109    /// Otherwise valid transaction removed from mempool due to [ZIP-401] random
110    /// eviction.
111    ///
112    /// Consensus rule:
113    /// > The txid (rather than the wtxid ...) is used even for version 5 transactions
114    ///
115    /// [ZIP-401]: https://zips.z.cash/zip-0401#specification
116    #[error("transaction evicted from the mempool due to ZIP-401 denial of service limits")]
117    RandomlyEvicted,
118}
119
120/// Storage error that combines all other specific error types.
121#[derive(Error, Clone, Debug, PartialEq, Eq)]
122#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
123#[allow(dead_code)]
124pub enum RejectionError {
125    #[error(transparent)]
126    ExactTip(#[from] ExactTipRejectionError),
127    #[error(transparent)]
128    SameEffectsTip(#[from] SameEffectsTipRejectionError),
129    #[error(transparent)]
130    SameEffectsChain(#[from] SameEffectsChainRejectionError),
131    #[error(transparent)]
132    NonStandardTransaction(#[from] NonStandardTransactionError),
133}
134
135/// Non-standard transaction error.
136#[derive(Error, Clone, Debug, PartialEq, Eq)]
137#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
138pub enum NonStandardTransactionError {
139    #[error("transaction is dust")]
140    IsDust,
141    #[error("transaction scriptSig is too large")]
142    ScriptSigTooLarge,
143    #[error("transaction scriptSig is not push-only")]
144    ScriptSigNotPushOnly,
145    #[error("transaction scriptPubKey is non-standard")]
146    ScriptPubKeyNonStandard,
147    #[error("transaction has a bare multisig output")]
148    BareMultiSig,
149    #[error("transaction has multiple OP_RETURN outputs")]
150    MultiOpReturn,
151    #[error("transaction has an OP_RETURN output that exceeds the size limit")]
152    DataCarrierTooLarge,
153    #[error("transaction has too many signature operations")]
154    TooManySigops,
155    #[error("transaction has non-standard inputs")]
156    NonStandardInputs,
157}
158
159/// Represents a set of transactions that have been removed from the mempool, either because
160/// they were mined, or because they were invalidated by another transaction that was mined.
161#[derive(Clone, Debug, PartialEq, Eq)]
162pub struct RemovedTransactionIds {
163    /// A list of ids for transactions that were removed mined onto the best chain.
164    pub mined: HashSet<UnminedTxId>,
165    /// A list of ids for transactions that were invalidated by other transactions
166    /// that were mined onto the best chain.
167    pub invalidated: HashSet<UnminedTxId>,
168}
169
170impl RemovedTransactionIds {
171    /// Returns the total number of transactions that were removed from the mempool.
172    pub fn total_len(&self) -> usize {
173        self.mined.len() + self.invalidated.len()
174    }
175}
176
177/// Hold mempool verified and rejected mempool transactions.
178pub struct Storage {
179    /// The set of verified transactions in the mempool.
180    verified: VerifiedSet,
181
182    /// The set of outpoints with pending requests for their associated transparent::Output.
183    pub(super) pending_outputs: PendingOutputs,
184
185    /// The set of transactions rejected due to bad authorizations, or for other
186    /// reasons, and their rejection reasons. These rejections only apply to the
187    /// current tip.
188    ///
189    /// Only transactions with the exact [`UnminedTxId`] are invalid.
190    tip_rejected_exact: HashMap<UnminedTxId, ExactTipRejectionError>,
191
192    /// A set of transactions rejected for their effects, and their rejection
193    /// reasons. These rejections only apply to the current tip.
194    ///
195    /// Any transaction with the same [`transaction::Hash`] is invalid.
196    tip_rejected_same_effects: HashMap<transaction::Hash, SameEffectsTipRejectionError>,
197
198    /// Sets of transactions rejected for their effects, keyed by rejection reason.
199    /// These rejections apply until a rollback or network upgrade.
200    ///
201    /// Any transaction with the same [`transaction::Hash`] is invalid.
202    ///
203    /// An [`EvictionList`] is used for both randomly evicted and expired
204    /// transactions, even if it is only needed for the evicted ones. This was
205    /// done just to simplify the existing code; there is no harm in having a
206    /// timeout for expired transactions too since re-checking expired
207    /// transactions is cheap.
208    // If this code is ever refactored and the lists are split in different
209    // fields, then we can use an `EvictionList` just for the evicted list.
210    chain_rejected_same_effects: HashMap<SameEffectsChainRejectionError, EvictionList>,
211
212    /// The mempool transaction eviction age limit.
213    /// Same as [`config::Config::eviction_memory_time`].
214    eviction_memory_time: Duration,
215
216    /// Max total cost of the verified mempool set, beyond which transactions
217    /// are evicted to make room.
218    tx_cost_limit: u64,
219
220    /// Maximum allowed size of OP_RETURN scripts, in bytes.
221    max_datacarrier_bytes: u32,
222}
223
224impl Drop for Storage {
225    fn drop(&mut self) {
226        self.clear();
227    }
228}
229
230impl Storage {
231    #[allow(clippy::field_reassign_with_default)]
232    pub(crate) fn new(config: &config::Config) -> Self {
233        Self {
234            tx_cost_limit: config.tx_cost_limit,
235            eviction_memory_time: config.eviction_memory_time,
236            max_datacarrier_bytes: config
237                .max_datacarrier_bytes
238                .unwrap_or(config::DEFAULT_MAX_DATACARRIER_BYTES),
239            verified: Default::default(),
240            pending_outputs: Default::default(),
241            tip_rejected_exact: Default::default(),
242            tip_rejected_same_effects: Default::default(),
243            chain_rejected_same_effects: Default::default(),
244        }
245    }
246
247    /// Check and reject non-standard transaction.
248    ///
249    /// Zcashd defines non-consensus standard transaction checks in
250    /// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L58-L135>
251    ///
252    /// This checks are applied before inserting a transaction in `AcceptToMemoryPool`:
253    /// <https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L1819>
254    ///
255    /// Currently, we implement: per-transaction sigops limit, standard input script checks,
256    /// input scriptSig size/push-only checks, standard output script checks (including OP_RETURN
257    /// limits), and dust checks.
258    fn reject_if_non_standard_tx(&mut self, tx: &VerifiedUnminedTx) -> Result<(), MempoolError> {
259        use zcash_script::script::{self, Evaluable as _};
260
261        let transaction = tx.transaction.transaction.as_ref();
262        let spent_outputs = &tx.spent_outputs;
263
264        for input in transaction.inputs() {
265            let unlock_script = match input {
266                transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
267                transparent::Input::Coinbase { .. } => continue,
268            };
269
270            // Rule: scriptSig size must be within the standard limit.
271            if unlock_script.as_raw_bytes().len() > policy::MAX_STANDARD_SCRIPTSIG_SIZE {
272                return self
273                    .reject_non_standard(tx, NonStandardTransactionError::ScriptSigTooLarge);
274            }
275
276            let code = script::Code(unlock_script.as_raw_bytes().to_vec());
277            // Rule: scriptSig must be push-only.
278            if !code.is_push_only() {
279                return self
280                    .reject_non_standard(tx, NonStandardTransactionError::ScriptSigNotPushOnly);
281            }
282        }
283
284        if !spent_outputs.is_empty() {
285            // Validate that spent_outputs aligns with transparent inputs.
286            if transaction.inputs().len() != spent_outputs.len() {
287                tracing::warn!(
288                    inputs = transaction.inputs().len(),
289                    spent_outputs = spent_outputs.len(),
290                    "spent_outputs length mismatch, rejecting as non-standard"
291                );
292                return self
293                    .reject_non_standard(tx, NonStandardTransactionError::NonStandardInputs);
294            }
295
296            // Rule: all transparent inputs must pass `AreInputsStandard()` checks:
297            // https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L137
298            if !policy::are_inputs_standard(transaction, spent_outputs) {
299                return self
300                    .reject_non_standard(tx, NonStandardTransactionError::NonStandardInputs);
301            }
302
303            // Rule: per-transaction sigops (legacy + P2SH) must not exceed the limit.
304            // zcashd sums GetLegacySigOpCount + GetP2SHSigOpCount for AcceptToMemoryPool:
305            // https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L1819
306            let total_sigops = tx.block_sigop_count();
307            if total_sigops > policy::MAX_STANDARD_TX_SIGOPS {
308                return self.reject_non_standard(tx, NonStandardTransactionError::TooManySigops);
309            }
310        } else {
311            // No spent outputs available (e.g. shielded-only transaction).
312            // Only check legacy sigops.
313            if tx.legacy_sigop_count > policy::MAX_STANDARD_TX_SIGOPS {
314                return self.reject_non_standard(tx, NonStandardTransactionError::TooManySigops);
315            }
316        }
317
318        // Rule: outputs must be standard script kinds, with special handling for OP_RETURN.
319        let mut data_out_count = 0u32;
320
321        for output in transaction.outputs() {
322            let lock_script = &output.lock_script;
323            let script_len = lock_script.as_raw_bytes().len();
324            let script_kind = policy::standard_script_kind(lock_script);
325
326            match script_kind {
327                None => {
328                    // Rule: output script must be standard (P2PKH/P2SH/P2PK/multisig/OP_RETURN).
329                    return self.reject_non_standard(
330                        tx,
331                        NonStandardTransactionError::ScriptPubKeyNonStandard,
332                    );
333                }
334                Some(solver::ScriptKind::NullData { .. }) => {
335                    // Rule: OP_RETURN script size is limited.
336                    // Cast is safe: u32 always fits in usize on 32-bit and 64-bit platforms.
337                    if script_len > self.max_datacarrier_bytes as usize {
338                        return self.reject_non_standard(
339                            tx,
340                            NonStandardTransactionError::DataCarrierTooLarge,
341                        );
342                    }
343                    // Rule: count OP_RETURN outputs to enforce the one-output limit.
344                    data_out_count += 1;
345                }
346                Some(solver::ScriptKind::MultiSig { pubkeys, .. }) => {
347                    // Rule: multisig must be at most 3-of-3 for standardness.
348                    // Note: This check is technically subsumed by the unconditional BareMultiSig
349                    // rejection below, but we keep it to distinguish the two error reasons
350                    // (ScriptPubKeyNonStandard for >3 keys vs BareMultiSig for valid multisig).
351                    if pubkeys.len() > policy::MAX_STANDARD_MULTISIG_PUBKEYS {
352                        return self.reject_non_standard(
353                            tx,
354                            NonStandardTransactionError::ScriptPubKeyNonStandard,
355                        );
356                    }
357                    // Rule: bare multisig outputs are non-standard (fIsBareMultisigStd = false).
358                    return self.reject_non_standard(tx, NonStandardTransactionError::BareMultiSig);
359                }
360                Some(_) => {
361                    // Rule: non-OP_RETURN outputs must not be dust.
362                    if output.is_dust() {
363                        return self.reject_non_standard(tx, NonStandardTransactionError::IsDust);
364                    }
365                }
366            }
367        }
368
369        // Rule: only one OP_RETURN output is permitted.
370        if data_out_count > 1 {
371            return self.reject_non_standard(tx, NonStandardTransactionError::MultiOpReturn);
372        }
373
374        Ok(())
375    }
376
377    /// Rejects a transaction as non-standard, caches the rejection, and returns the mempool error.
378    ///
379    /// Note: The returned error is `MempoolError::NonStandardTransaction`, while the cached
380    /// rejection (via `reject()`) is stored as
381    /// `ExactTipRejectionError::FailedStandard`. Callers that later check
382    /// `rejection_error()` will get `MempoolError::StorageExactTip(FailedStandard(...))`.
383    fn reject_non_standard(
384        &mut self,
385        tx: &VerifiedUnminedTx,
386        rejection_error: NonStandardTransactionError,
387    ) -> Result<(), MempoolError> {
388        self.reject(tx.transaction.id, rejection_error.clone().into());
389        Err(MempoolError::NonStandardTransaction(rejection_error))
390    }
391
392    /// Insert a [`VerifiedUnminedTx`] into the mempool, caching any rejections.
393    ///
394    /// Accepts the [`VerifiedUnminedTx`] being inserted and `spent_mempool_outpoints`,
395    /// a list of transparent inputs of the provided [`VerifiedUnminedTx`] that were found
396    /// as newly created transparent outputs in the mempool during transaction verification.
397    ///
398    /// Returns an error if the mempool's verified transactions or rejection caches
399    /// prevent this transaction from being inserted.
400    /// These errors should not be propagated to peers, because the transactions are valid.
401    ///
402    /// If inserting this transaction evicts other transactions, they will be tracked
403    /// as [`SameEffectsChainRejectionError::RandomlyEvicted`].
404    #[allow(clippy::unwrap_in_result)]
405    pub fn insert(
406        &mut self,
407        tx: VerifiedUnminedTx,
408        spent_mempool_outpoints: Vec<transparent::OutPoint>,
409        height: Option<Height>,
410    ) -> Result<UnminedTxId, MempoolError> {
411        // # Security
412        //
413        // This method must call `reject`, rather than modifying the rejection lists directly.
414        let unmined_tx_id = tx.transaction.id;
415        let tx_id = unmined_tx_id.mined_id();
416
417        // First, check if we have a cached rejection for this transaction.
418        if let Some(error) = self.rejection_error(&unmined_tx_id) {
419            tracing::trace!(
420                ?tx_id,
421                ?error,
422                stored_transaction_count = ?self.verified.transaction_count(),
423                "returning cached error for transaction",
424            );
425
426            return Err(error);
427        }
428
429        // If `tx` is already in the mempool, we don't change anything.
430        //
431        // Security: transactions must not get refreshed by new queries,
432        // because that allows malicious peers to keep transactions live forever.
433        if self.verified.contains(&tx_id) {
434            tracing::trace!(
435                ?tx_id,
436                stored_transaction_count = ?self.verified.transaction_count(),
437                "returning InMempool error for transaction that is already in the mempool",
438            );
439
440            return Err(MempoolError::InMempool);
441        }
442
443        // Check that the transaction is standard.
444        self.reject_if_non_standard_tx(&tx)?;
445
446        // Then, we try to insert into the pool. If this fails the transaction is rejected.
447        let mut result = Ok(unmined_tx_id);
448        if let Err(rejection_error) = self.verified.insert(
449            tx,
450            spent_mempool_outpoints,
451            &mut self.pending_outputs,
452            height,
453        ) {
454            tracing::debug!(
455                ?tx_id,
456                ?rejection_error,
457                stored_transaction_count = ?self.verified.transaction_count(),
458                "insertion error for transaction",
459            );
460
461            // We could return here, but we still want to check the mempool size
462            self.reject(unmined_tx_id, rejection_error.clone().into());
463            result = Err(rejection_error.into());
464        }
465
466        // Once inserted, we evict transactions over the pool size limit per [ZIP-401];
467        //
468        // > On receiving a transaction: (...)
469        // > Calculate its cost. If the total cost of transactions in the mempool including this
470        // > one would `exceed mempooltxcostlimit`, then the node MUST repeatedly call
471        // > EvictTransaction (with the new transaction included as a candidate to evict) until the
472        // > total cost does not exceed `mempooltxcostlimit`.
473        //
474        // 'EvictTransaction' is equivalent to [`VerifiedSet::evict_one()`] in
475        // our implementation.
476        //
477        // [ZIP-401]: https://zips.z.cash/zip-0401
478        while self.verified.total_cost() > self.tx_cost_limit {
479            // > EvictTransaction MUST do the following:
480            // > Select a random transaction to evict, with probability in direct proportion to
481            // > eviction weight. (...) Remove it from the mempool.
482            let victim_tx = self
483                .verified
484                .evict_one()
485                .expect("mempool is empty, but was expected to be full");
486
487            // > Add the txid and the current time to RecentlyEvicted, dropping the oldest entry in
488            // > RecentlyEvicted if necessary to keep it to at most `eviction_memory_entries entries`.
489            self.reject(
490                victim_tx.transaction.id,
491                SameEffectsChainRejectionError::RandomlyEvicted.into(),
492            );
493
494            // If this transaction gets evicted, set its result to the same error
495            if victim_tx.transaction.id == unmined_tx_id {
496                result = Err(SameEffectsChainRejectionError::RandomlyEvicted.into());
497            }
498        }
499
500        result
501    }
502
503    /// Remove transactions from the mempool via exact [`UnminedTxId`].
504    ///
505    /// For v5 transactions, transactions are matched by WTXID, using both the:
506    /// - non-malleable transaction ID, and
507    /// - authorizing data hash.
508    ///
509    /// This matches the exact transaction, with identical blockchain effects, signatures, and proofs.
510    ///
511    /// Returns the number of transactions which were removed.
512    ///
513    /// Removes from the 'verified' set, if present.
514    /// Maintains the order in which the other unmined transactions have been inserted into the mempool.
515    ///
516    /// Does not add or remove from the 'rejected' tracking set.
517    #[allow(dead_code)]
518    pub fn remove_exact(&mut self, exact_wtxids: &HashSet<UnminedTxId>) -> usize {
519        self.verified
520            .remove_all_that(|tx| exact_wtxids.contains(&tx.transaction.id))
521            .len()
522    }
523
524    /// Clears a list of mined transaction ids from the verified set's tracked transaction dependencies.
525    pub fn clear_mined_dependencies(&mut self, mined_ids: &HashSet<transaction::Hash>) {
526        self.verified.clear_mined_dependencies(mined_ids);
527    }
528
529    /// Reject and remove transactions from the mempool via non-malleable [`transaction::Hash`].
530    /// - For v5 transactions, transactions are matched by TXID,
531    ///   using only the non-malleable transaction ID.
532    ///   This matches any transaction with the same effect on the blockchain state,
533    ///   even if its signatures and proofs are different.
534    /// - Returns the number of transactions which were removed.
535    /// - Removes from the 'verified' set, if present.
536    ///   Maintains the order in which the other unmined transactions have been inserted into the mempool.
537    /// - Prunes `pending_outputs` of any closed channels.
538    ///
539    /// Reject and remove transactions from the mempool that contain any spent outpoints or revealed
540    /// nullifiers from the passed in `transactions`.
541    ///
542    /// Returns the number of transactions that were removed.
543    pub fn reject_and_remove_same_effects(
544        &mut self,
545        mined_ids: &HashSet<transaction::Hash>,
546        transactions: Vec<Arc<Transaction>>,
547    ) -> RemovedTransactionIds {
548        let removed_mined = self
549            .verified
550            .remove_all_that(|tx| mined_ids.contains(&tx.transaction.id.mined_id()));
551
552        let spent_outpoints: HashSet<_> = transactions
553            .iter()
554            .flat_map(|tx| tx.spent_outpoints())
555            .collect();
556        let sprout_nullifiers: HashSet<_> = transactions
557            .iter()
558            .flat_map(|transaction| transaction.sprout_nullifiers())
559            .collect();
560        let sapling_nullifiers: HashSet<_> = transactions
561            .iter()
562            .flat_map(|transaction| transaction.sapling_nullifiers())
563            .collect();
564        let orchard_nullifiers: HashSet<_> = transactions
565            .iter()
566            .flat_map(|transaction| transaction.orchard_nullifiers())
567            .collect();
568        let ironwood_nullifiers: HashSet<_> = transactions
569            .iter()
570            .flat_map(|transaction| transaction.ironwood_nullifiers())
571            .collect();
572
573        let duplicate_spend_ids: HashSet<_> = self
574            .verified
575            .transactions()
576            .values()
577            .map(|tx| (tx.transaction.id, &tx.transaction.transaction))
578            .filter_map(|(tx_id, tx)| {
579                (tx.spent_outpoints()
580                    .any(|outpoint| spent_outpoints.contains(&outpoint))
581                    || tx
582                        .sprout_nullifiers()
583                        .any(|nullifier| sprout_nullifiers.contains(nullifier))
584                    || tx
585                        .sapling_nullifiers()
586                        .any(|nullifier| sapling_nullifiers.contains(nullifier))
587                    || tx
588                        .orchard_nullifiers()
589                        .any(|nullifier| orchard_nullifiers.contains(nullifier))
590                    // `ironwood_nullifiers()` yields owned `ironwood::Nullifier`s.
591                    || tx
592                        .ironwood_nullifiers()
593                        .any(|nullifier| ironwood_nullifiers.contains(&nullifier)))
594                .then_some(tx_id)
595            })
596            .collect();
597
598        let removed_duplicate_spend = self
599            .verified
600            .remove_all_that(|tx| duplicate_spend_ids.contains(&tx.transaction.id));
601
602        for &mined_id in mined_ids {
603            self.reject(
604                // the reject and rejection_error fns that store and check `SameEffectsChainRejectionError`s
605                // only use the mined id, so using `Legacy` ids will apply to v5 transactions as well.
606                UnminedTxId::Legacy(mined_id),
607                SameEffectsChainRejectionError::Mined.into(),
608            );
609        }
610
611        for duplicate_spend_id in duplicate_spend_ids {
612            self.reject(
613                duplicate_spend_id,
614                SameEffectsChainRejectionError::DuplicateSpend.into(),
615            );
616        }
617
618        self.pending_outputs.prune();
619
620        RemovedTransactionIds {
621            mined: removed_mined,
622            invalidated: removed_duplicate_spend,
623        }
624    }
625
626    /// Clears the whole mempool storage.
627    #[allow(dead_code)]
628    pub fn clear(&mut self) {
629        self.verified.clear();
630        self.tip_rejected_exact.clear();
631        self.pending_outputs.clear();
632        self.tip_rejected_same_effects.clear();
633        self.chain_rejected_same_effects.clear();
634        self.update_rejected_metrics();
635    }
636
637    /// Clears rejections that only apply to the current tip.
638    pub fn clear_tip_rejections(&mut self) {
639        self.tip_rejected_exact.clear();
640        self.tip_rejected_same_effects.clear();
641        self.update_rejected_metrics();
642    }
643
644    /// Clears rejections that only apply to the current tip.
645    ///
646    /// # Security
647    ///
648    /// This method must be called at the end of every method that adds rejections.
649    /// Otherwise, peers could make our reject lists use a lot of RAM.
650    fn limit_rejection_list_memory(&mut self) {
651        // These lists are an optimisation - it's ok to totally clear them as needed.
652        if self.tip_rejected_exact.len() > MAX_EVICTION_MEMORY_ENTRIES {
653            self.tip_rejected_exact.clear();
654        }
655        if self.tip_rejected_same_effects.len() > MAX_EVICTION_MEMORY_ENTRIES {
656            self.tip_rejected_same_effects.clear();
657        }
658        // `chain_rejected_same_effects` limits its size by itself
659        self.update_rejected_metrics();
660    }
661
662    /// Returns the set of [`UnminedTxId`]s in the mempool.
663    pub fn tx_ids(&self) -> impl Iterator<Item = UnminedTxId> + '_ {
664        self.transactions().values().map(|tx| tx.transaction.id)
665    }
666
667    /// Returns a reference to the [`HashMap`] of [`VerifiedUnminedTx`]s in the verified set.
668    ///
669    /// Each [`VerifiedUnminedTx`] contains an [`UnminedTx`],
670    /// and adds extra fields from the transaction verifier result.
671    pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
672        self.verified.transactions()
673    }
674
675    /// Returns a reference to the [`TransactionDependencies`] in the verified set.
676    pub fn transaction_dependencies(&self) -> &TransactionDependencies {
677        self.verified.transaction_dependencies()
678    }
679
680    /// Returns a [`transparent::Output`] created by a mempool transaction for the provided
681    /// [`transparent::OutPoint`] if one exists, or None otherwise.
682    pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
683        self.verified.created_output(outpoint)
684    }
685
686    /// Returns true if a tx in the set has spent the output at the provided outpoint.
687    pub fn has_spent_outpoint(&self, outpoint: &transparent::OutPoint) -> bool {
688        self.verified.has_spent_outpoint(outpoint)
689    }
690
691    /// Returns the number of transactions in the mempool.
692    #[allow(dead_code)]
693    pub fn transaction_count(&self) -> usize {
694        self.verified.transaction_count()
695    }
696
697    /// Returns the cost of the transactions in the mempool, according to ZIP-401.
698    #[allow(dead_code)]
699    pub fn total_cost(&self) -> u64 {
700        self.verified.total_cost()
701    }
702
703    /// Returns the total serialized size of the verified transactions in the set.
704    ///
705    /// See [`VerifiedSet::total_serialized_size()`] for details.
706    pub fn total_serialized_size(&self) -> usize {
707        self.verified.total_serialized_size()
708    }
709
710    /// Returns the set of [`UnminedTx`]es with exactly matching `tx_ids` in the
711    /// mempool.
712    ///
713    /// This matches the exact transaction, with identical blockchain effects,
714    /// signatures, and proofs.
715    pub fn transactions_exact(
716        &self,
717        tx_ids: HashSet<UnminedTxId>,
718    ) -> impl Iterator<Item = &UnminedTx> {
719        tx_ids.into_iter().filter_map(|tx_id| {
720            self.transactions()
721                .get(&tx_id.mined_id())
722                .map(|tx| &tx.transaction)
723        })
724    }
725
726    /// Returns the set of [`UnminedTx`]es with matching [`transaction::Hash`]es
727    /// in the mempool.
728    ///
729    /// This matches transactions with the same effects, regardless of
730    /// [`transaction::AuthDigest`].
731    pub fn transactions_same_effects(
732        &self,
733        tx_ids: HashSet<Hash>,
734    ) -> impl Iterator<Item = &UnminedTx> {
735        self.verified
736            .transactions()
737            .iter()
738            .filter(move |(tx_id, _)| tx_ids.contains(tx_id))
739            .map(|(_, tx)| &tx.transaction)
740    }
741
742    /// Returns a transaction and the transaction ids of its dependencies, if it is in the verified set.
743    pub fn transaction_with_deps(
744        &self,
745        tx_id: transaction::Hash,
746    ) -> Option<(VerifiedUnminedTx, HashSet<transaction::Hash>)> {
747        let tx = self.verified.transactions().get(&tx_id).cloned()?;
748        let deps = self
749            .verified
750            .transaction_dependencies()
751            .dependencies()
752            .get(&tx_id)
753            .cloned()
754            .unwrap_or_default();
755
756        Some((tx, deps))
757    }
758
759    /// Returns `true` if a transaction exactly matching an [`UnminedTxId`] is in
760    /// the mempool.
761    ///
762    /// This matches the exact transaction, with identical blockchain effects,
763    /// signatures, and proofs.
764    pub fn contains_transaction_exact(&self, tx_id: &transaction::Hash) -> bool {
765        self.verified.contains(tx_id)
766    }
767
768    /// Returns the number of rejected [`UnminedTxId`]s or [`transaction::Hash`]es.
769    ///
770    /// Transactions on multiple rejected lists are counted multiple times.
771    #[allow(dead_code)]
772    pub fn rejected_transaction_count(&mut self) -> usize {
773        self.tip_rejected_exact.len()
774            + self.tip_rejected_same_effects.len()
775            + self
776                .chain_rejected_same_effects
777                .iter_mut()
778                .map(|(_, map)| map.len())
779                .sum::<usize>()
780    }
781
782    /// Add a transaction to the rejected list for the given reason.
783    pub fn reject(&mut self, tx_id: UnminedTxId, reason: RejectionError) {
784        match reason {
785            RejectionError::ExactTip(e) => {
786                self.tip_rejected_exact.insert(tx_id, e);
787            }
788            RejectionError::SameEffectsTip(e) => {
789                self.tip_rejected_same_effects.insert(tx_id.mined_id(), e);
790            }
791            RejectionError::SameEffectsChain(e) => {
792                let eviction_memory_time = self.eviction_memory_time;
793                self.chain_rejected_same_effects
794                    .entry(e)
795                    .or_insert_with(|| {
796                        EvictionList::new(MAX_EVICTION_MEMORY_ENTRIES, eviction_memory_time)
797                    })
798                    .insert(tx_id.mined_id());
799            }
800            RejectionError::NonStandardTransaction(e) => {
801                // Non-standard transactions are rejected based on their exact
802                // transaction data.
803                self.tip_rejected_exact
804                    .insert(tx_id, ExactTipRejectionError::from(e));
805            }
806        }
807        self.limit_rejection_list_memory();
808    }
809
810    /// Returns the rejection error if a transaction matching an [`UnminedTxId`]
811    /// is in any mempool rejected list.
812    ///
813    /// This matches transactions based on each rejection list's matching rule.
814    ///
815    /// Returns an arbitrary error if the transaction is in multiple lists.
816    pub fn rejection_error(&self, txid: &UnminedTxId) -> Option<MempoolError> {
817        if let Some(error) = self.tip_rejected_exact.get(txid) {
818            return Some(error.clone().into());
819        }
820
821        if let Some(error) = self.tip_rejected_same_effects.get(&txid.mined_id()) {
822            return Some(error.clone().into());
823        }
824
825        for (error, set) in self.chain_rejected_same_effects.iter() {
826            if set.contains_key(&txid.mined_id()) {
827                return Some(error.clone().into());
828            }
829        }
830
831        None
832    }
833
834    /// Returns the set of [`UnminedTxId`]s matching `tx_ids` in the rejected list.
835    ///
836    /// This matches transactions based on each rejection list's matching rule.
837    pub fn rejected_transactions(
838        &self,
839        tx_ids: HashSet<UnminedTxId>,
840    ) -> impl Iterator<Item = UnminedTxId> + '_ {
841        tx_ids
842            .into_iter()
843            .filter(move |txid| self.contains_rejected(txid))
844    }
845
846    /// Returns `true` if a transaction matching the supplied [`UnminedTxId`] is in
847    /// the mempool rejected list.
848    ///
849    /// This matches transactions based on each rejection list's matching rule.
850    pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool {
851        self.rejection_error(txid).is_some()
852    }
853
854    /// Add a transaction that failed download and verification to the rejected list
855    /// if needed, depending on the reason for the failure.
856    pub fn reject_if_needed(&mut self, tx_id: UnminedTxId, e: TransactionDownloadVerifyError) {
857        match e {
858            // Rejecting a transaction already in state would speed up further
859            // download attempts without checking the state. However it would
860            // make the reject list grow forever.
861            //
862            // TODO: revisit after reviewing the rejected list cleanup criteria?
863            // TODO: if we decide to reject it, then we need to pass the block hash
864            // to State::Confirmed. This would require the zs::Response::Transaction
865            // to include the hash, which would need to be implemented.
866            TransactionDownloadVerifyError::InState |
867            // An unknown error in the state service, better do nothing
868            TransactionDownloadVerifyError::StateError(_) |
869            // If download failed, do nothing; the crawler will end up trying to
870            // download it again.
871            TransactionDownloadVerifyError::DownloadFailed(_) |
872            // If it was cancelled then a block was mined, or there was a network
873            // upgrade, etc. No reason to reject it.
874            TransactionDownloadVerifyError::Cancelled => {}
875
876            // Consensus verification failed. Reject transaction to avoid
877            // having to download and verify it again just for it to fail again.
878            TransactionDownloadVerifyError::Invalid { error, .. }  => {
879                self.reject(tx_id, ExactTipRejectionError::FailedVerification(error).into())
880            }
881        }
882    }
883
884    /// Remove transactions from the mempool if they have not been mined after a
885    /// specified height, per [ZIP-203].
886    ///
887    /// > Transactions will have a new field, nExpiryHeight, which will set the
888    /// > block height after which transactions will be removed from the mempool
889    /// > if they have not been mined.
890    ///
891    ///
892    /// [ZIP-203]: https://zips.z.cash/zip-0203#specification
893    pub fn remove_expired_transactions(
894        &mut self,
895        tip_height: zebra_chain::block::Height,
896    ) -> HashSet<UnminedTxId> {
897        let mut tx_ids = HashSet::new();
898        let mut unmined_tx_ids = HashSet::new();
899
900        for (&tx_id, tx) in self.transactions() {
901            if let Some(expiry_height) = tx.transaction.transaction.expiry_height() {
902                if tip_height >= expiry_height {
903                    tx_ids.insert(tx_id);
904                    unmined_tx_ids.insert(tx.transaction.id);
905                }
906            }
907        }
908
909        // expiry height is effecting data, so we match by non-malleable TXID
910        self.verified
911            .remove_all_that(|tx| tx_ids.contains(&tx.transaction.id.mined_id()));
912
913        // also reject it
914        for id in tx_ids {
915            self.reject(
916                // It's okay to omit the auth digest here as we know that `reject()` will always
917                // use mined ids for `SameEffectsChainRejectionError`s.
918                UnminedTxId::Legacy(id),
919                SameEffectsChainRejectionError::Expired.into(),
920            );
921        }
922
923        unmined_tx_ids
924    }
925
926    /// Check if transaction should be downloaded and/or verified.
927    ///
928    /// If it is already in the mempool (or in its rejected list)
929    /// then it shouldn't be downloaded/verified.
930    pub fn should_download_or_verify(&mut self, txid: UnminedTxId) -> Result<(), MempoolError> {
931        // Check if the transaction is already in the mempool.
932        if self.contains_transaction_exact(&txid.mined_id()) {
933            return Err(MempoolError::InMempool);
934        }
935        if let Some(error) = self.rejection_error(&txid) {
936            return Err(error);
937        }
938        Ok(())
939    }
940
941    /// Update metrics related to the rejected lists.
942    ///
943    /// Must be called every time the rejected lists change.
944    fn update_rejected_metrics(&mut self) {
945        metrics::gauge!("mempool.rejected.transaction.ids",)
946            .set(self.rejected_transaction_count() as f64);
947        // This is just an approximation.
948        // TODO: make it more accurate #2869
949        let item_size = size_of::<(transaction::Hash, SameEffectsTipRejectionError)>();
950        metrics::gauge!("mempool.rejected.transaction.ids.bytes",)
951            .set((self.rejected_transaction_count() * item_size) as f64);
952    }
953}