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
569        let duplicate_spend_ids: HashSet<_> = self
570            .verified
571            .transactions()
572            .values()
573            .map(|tx| (tx.transaction.id, &tx.transaction.transaction))
574            .filter_map(|(tx_id, tx)| {
575                (tx.spent_outpoints()
576                    .any(|outpoint| spent_outpoints.contains(&outpoint))
577                    || tx
578                        .sprout_nullifiers()
579                        .any(|nullifier| sprout_nullifiers.contains(nullifier))
580                    || tx
581                        .sapling_nullifiers()
582                        .any(|nullifier| sapling_nullifiers.contains(nullifier))
583                    || tx
584                        .orchard_nullifiers()
585                        .any(|nullifier| orchard_nullifiers.contains(nullifier)))
586                .then_some(tx_id)
587            })
588            .collect();
589
590        let removed_duplicate_spend = self
591            .verified
592            .remove_all_that(|tx| duplicate_spend_ids.contains(&tx.transaction.id));
593
594        for &mined_id in mined_ids {
595            self.reject(
596                // the reject and rejection_error fns that store and check `SameEffectsChainRejectionError`s
597                // only use the mined id, so using `Legacy` ids will apply to v5 transactions as well.
598                UnminedTxId::Legacy(mined_id),
599                SameEffectsChainRejectionError::Mined.into(),
600            );
601        }
602
603        for duplicate_spend_id in duplicate_spend_ids {
604            self.reject(
605                duplicate_spend_id,
606                SameEffectsChainRejectionError::DuplicateSpend.into(),
607            );
608        }
609
610        self.pending_outputs.prune();
611
612        RemovedTransactionIds {
613            mined: removed_mined,
614            invalidated: removed_duplicate_spend,
615        }
616    }
617
618    /// Clears the whole mempool storage.
619    #[allow(dead_code)]
620    pub fn clear(&mut self) {
621        self.verified.clear();
622        self.tip_rejected_exact.clear();
623        self.pending_outputs.clear();
624        self.tip_rejected_same_effects.clear();
625        self.chain_rejected_same_effects.clear();
626        self.update_rejected_metrics();
627    }
628
629    /// Clears rejections that only apply to the current tip.
630    pub fn clear_tip_rejections(&mut self) {
631        self.tip_rejected_exact.clear();
632        self.tip_rejected_same_effects.clear();
633        self.update_rejected_metrics();
634    }
635
636    /// Clears rejections that only apply to the current tip.
637    ///
638    /// # Security
639    ///
640    /// This method must be called at the end of every method that adds rejections.
641    /// Otherwise, peers could make our reject lists use a lot of RAM.
642    fn limit_rejection_list_memory(&mut self) {
643        // These lists are an optimisation - it's ok to totally clear them as needed.
644        if self.tip_rejected_exact.len() > MAX_EVICTION_MEMORY_ENTRIES {
645            self.tip_rejected_exact.clear();
646        }
647        if self.tip_rejected_same_effects.len() > MAX_EVICTION_MEMORY_ENTRIES {
648            self.tip_rejected_same_effects.clear();
649        }
650        // `chain_rejected_same_effects` limits its size by itself
651        self.update_rejected_metrics();
652    }
653
654    /// Returns the set of [`UnminedTxId`]s in the mempool.
655    pub fn tx_ids(&self) -> impl Iterator<Item = UnminedTxId> + '_ {
656        self.transactions().values().map(|tx| tx.transaction.id)
657    }
658
659    /// Returns a reference to the [`HashMap`] of [`VerifiedUnminedTx`]s in the verified set.
660    ///
661    /// Each [`VerifiedUnminedTx`] contains an [`UnminedTx`],
662    /// and adds extra fields from the transaction verifier result.
663    pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
664        self.verified.transactions()
665    }
666
667    /// Returns a reference to the [`TransactionDependencies`] in the verified set.
668    pub fn transaction_dependencies(&self) -> &TransactionDependencies {
669        self.verified.transaction_dependencies()
670    }
671
672    /// Returns a [`transparent::Output`] created by a mempool transaction for the provided
673    /// [`transparent::OutPoint`] if one exists, or None otherwise.
674    pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
675        self.verified.created_output(outpoint)
676    }
677
678    /// Returns true if a tx in the set has spent the output at the provided outpoint.
679    pub fn has_spent_outpoint(&self, outpoint: &transparent::OutPoint) -> bool {
680        self.verified.has_spent_outpoint(outpoint)
681    }
682
683    /// Returns the number of transactions in the mempool.
684    #[allow(dead_code)]
685    pub fn transaction_count(&self) -> usize {
686        self.verified.transaction_count()
687    }
688
689    /// Returns the cost of the transactions in the mempool, according to ZIP-401.
690    #[allow(dead_code)]
691    pub fn total_cost(&self) -> u64 {
692        self.verified.total_cost()
693    }
694
695    /// Returns the total serialized size of the verified transactions in the set.
696    ///
697    /// See [`VerifiedSet::total_serialized_size()`] for details.
698    pub fn total_serialized_size(&self) -> usize {
699        self.verified.total_serialized_size()
700    }
701
702    /// Returns the set of [`UnminedTx`]es with exactly matching `tx_ids` in the
703    /// mempool.
704    ///
705    /// This matches the exact transaction, with identical blockchain effects,
706    /// signatures, and proofs.
707    pub fn transactions_exact(
708        &self,
709        tx_ids: HashSet<UnminedTxId>,
710    ) -> impl Iterator<Item = &UnminedTx> {
711        tx_ids.into_iter().filter_map(|tx_id| {
712            self.transactions()
713                .get(&tx_id.mined_id())
714                .map(|tx| &tx.transaction)
715        })
716    }
717
718    /// Returns the set of [`UnminedTx`]es with matching [`transaction::Hash`]es
719    /// in the mempool.
720    ///
721    /// This matches transactions with the same effects, regardless of
722    /// [`transaction::AuthDigest`].
723    pub fn transactions_same_effects(
724        &self,
725        tx_ids: HashSet<Hash>,
726    ) -> impl Iterator<Item = &UnminedTx> {
727        self.verified
728            .transactions()
729            .iter()
730            .filter(move |(tx_id, _)| tx_ids.contains(tx_id))
731            .map(|(_, tx)| &tx.transaction)
732    }
733
734    /// Returns a transaction and the transaction ids of its dependencies, if it is in the verified set.
735    pub fn transaction_with_deps(
736        &self,
737        tx_id: transaction::Hash,
738    ) -> Option<(VerifiedUnminedTx, HashSet<transaction::Hash>)> {
739        let tx = self.verified.transactions().get(&tx_id).cloned()?;
740        let deps = self
741            .verified
742            .transaction_dependencies()
743            .dependencies()
744            .get(&tx_id)
745            .cloned()
746            .unwrap_or_default();
747
748        Some((tx, deps))
749    }
750
751    /// Returns `true` if a transaction exactly matching an [`UnminedTxId`] is in
752    /// the mempool.
753    ///
754    /// This matches the exact transaction, with identical blockchain effects,
755    /// signatures, and proofs.
756    pub fn contains_transaction_exact(&self, tx_id: &transaction::Hash) -> bool {
757        self.verified.contains(tx_id)
758    }
759
760    /// Returns the number of rejected [`UnminedTxId`]s or [`transaction::Hash`]es.
761    ///
762    /// Transactions on multiple rejected lists are counted multiple times.
763    #[allow(dead_code)]
764    pub fn rejected_transaction_count(&mut self) -> usize {
765        self.tip_rejected_exact.len()
766            + self.tip_rejected_same_effects.len()
767            + self
768                .chain_rejected_same_effects
769                .iter_mut()
770                .map(|(_, map)| map.len())
771                .sum::<usize>()
772    }
773
774    /// Add a transaction to the rejected list for the given reason.
775    pub fn reject(&mut self, tx_id: UnminedTxId, reason: RejectionError) {
776        match reason {
777            RejectionError::ExactTip(e) => {
778                self.tip_rejected_exact.insert(tx_id, e);
779            }
780            RejectionError::SameEffectsTip(e) => {
781                self.tip_rejected_same_effects.insert(tx_id.mined_id(), e);
782            }
783            RejectionError::SameEffectsChain(e) => {
784                let eviction_memory_time = self.eviction_memory_time;
785                self.chain_rejected_same_effects
786                    .entry(e)
787                    .or_insert_with(|| {
788                        EvictionList::new(MAX_EVICTION_MEMORY_ENTRIES, eviction_memory_time)
789                    })
790                    .insert(tx_id.mined_id());
791            }
792            RejectionError::NonStandardTransaction(e) => {
793                // Non-standard transactions are rejected based on their exact
794                // transaction data.
795                self.tip_rejected_exact
796                    .insert(tx_id, ExactTipRejectionError::from(e));
797            }
798        }
799        self.limit_rejection_list_memory();
800    }
801
802    /// Returns the rejection error if a transaction matching an [`UnminedTxId`]
803    /// is in any mempool rejected list.
804    ///
805    /// This matches transactions based on each rejection list's matching rule.
806    ///
807    /// Returns an arbitrary error if the transaction is in multiple lists.
808    pub fn rejection_error(&self, txid: &UnminedTxId) -> Option<MempoolError> {
809        if let Some(error) = self.tip_rejected_exact.get(txid) {
810            return Some(error.clone().into());
811        }
812
813        if let Some(error) = self.tip_rejected_same_effects.get(&txid.mined_id()) {
814            return Some(error.clone().into());
815        }
816
817        for (error, set) in self.chain_rejected_same_effects.iter() {
818            if set.contains_key(&txid.mined_id()) {
819                return Some(error.clone().into());
820            }
821        }
822
823        None
824    }
825
826    /// Returns the set of [`UnminedTxId`]s matching `tx_ids` in the rejected list.
827    ///
828    /// This matches transactions based on each rejection list's matching rule.
829    pub fn rejected_transactions(
830        &self,
831        tx_ids: HashSet<UnminedTxId>,
832    ) -> impl Iterator<Item = UnminedTxId> + '_ {
833        tx_ids
834            .into_iter()
835            .filter(move |txid| self.contains_rejected(txid))
836    }
837
838    /// Returns `true` if a transaction matching the supplied [`UnminedTxId`] is in
839    /// the mempool rejected list.
840    ///
841    /// This matches transactions based on each rejection list's matching rule.
842    pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool {
843        self.rejection_error(txid).is_some()
844    }
845
846    /// Add a transaction that failed download and verification to the rejected list
847    /// if needed, depending on the reason for the failure.
848    pub fn reject_if_needed(&mut self, tx_id: UnminedTxId, e: TransactionDownloadVerifyError) {
849        match e {
850            // Rejecting a transaction already in state would speed up further
851            // download attempts without checking the state. However it would
852            // make the reject list grow forever.
853            //
854            // TODO: revisit after reviewing the rejected list cleanup criteria?
855            // TODO: if we decide to reject it, then we need to pass the block hash
856            // to State::Confirmed. This would require the zs::Response::Transaction
857            // to include the hash, which would need to be implemented.
858            TransactionDownloadVerifyError::InState |
859            // An unknown error in the state service, better do nothing
860            TransactionDownloadVerifyError::StateError(_) |
861            // If download failed, do nothing; the crawler will end up trying to
862            // download it again.
863            TransactionDownloadVerifyError::DownloadFailed(_) |
864            // If it was cancelled then a block was mined, or there was a network
865            // upgrade, etc. No reason to reject it.
866            TransactionDownloadVerifyError::Cancelled => {}
867
868            // Consensus verification failed. Reject transaction to avoid
869            // having to download and verify it again just for it to fail again.
870            TransactionDownloadVerifyError::Invalid { error, .. }  => {
871                self.reject(tx_id, ExactTipRejectionError::FailedVerification(error).into())
872            }
873        }
874    }
875
876    /// Remove transactions from the mempool if they have not been mined after a
877    /// specified height, per [ZIP-203].
878    ///
879    /// > Transactions will have a new field, nExpiryHeight, which will set the
880    /// > block height after which transactions will be removed from the mempool
881    /// > if they have not been mined.
882    ///
883    ///
884    /// [ZIP-203]: https://zips.z.cash/zip-0203#specification
885    pub fn remove_expired_transactions(
886        &mut self,
887        tip_height: zebra_chain::block::Height,
888    ) -> HashSet<UnminedTxId> {
889        let mut tx_ids = HashSet::new();
890        let mut unmined_tx_ids = HashSet::new();
891
892        for (&tx_id, tx) in self.transactions() {
893            if let Some(expiry_height) = tx.transaction.transaction.expiry_height() {
894                if tip_height >= expiry_height {
895                    tx_ids.insert(tx_id);
896                    unmined_tx_ids.insert(tx.transaction.id);
897                }
898            }
899        }
900
901        // expiry height is effecting data, so we match by non-malleable TXID
902        self.verified
903            .remove_all_that(|tx| tx_ids.contains(&tx.transaction.id.mined_id()));
904
905        // also reject it
906        for id in tx_ids {
907            self.reject(
908                // It's okay to omit the auth digest here as we know that `reject()` will always
909                // use mined ids for `SameEffectsChainRejectionError`s.
910                UnminedTxId::Legacy(id),
911                SameEffectsChainRejectionError::Expired.into(),
912            );
913        }
914
915        unmined_tx_ids
916    }
917
918    /// Check if transaction should be downloaded and/or verified.
919    ///
920    /// If it is already in the mempool (or in its rejected list)
921    /// then it shouldn't be downloaded/verified.
922    pub fn should_download_or_verify(&mut self, txid: UnminedTxId) -> Result<(), MempoolError> {
923        // Check if the transaction is already in the mempool.
924        if self.contains_transaction_exact(&txid.mined_id()) {
925            return Err(MempoolError::InMempool);
926        }
927        if let Some(error) = self.rejection_error(&txid) {
928            return Err(error);
929        }
930        Ok(())
931    }
932
933    /// Update metrics related to the rejected lists.
934    ///
935    /// Must be called every time the rejected lists change.
936    fn update_rejected_metrics(&mut self) {
937        metrics::gauge!("mempool.rejected.transaction.ids",)
938            .set(self.rejected_transaction_count() as f64);
939        // This is just an approximation.
940        // TODO: make it more accurate #2869
941        let item_size = size_of::<(transaction::Hash, SameEffectsTipRejectionError)>();
942        metrics::gauge!("mempool.rejected.transaction.ids.bytes",)
943            .set((self.rejected_transaction_count() * item_size) as f64);
944    }
945}