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}