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