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 // TODO: `spent_outputs` may not align with `tx.inputs()` when a transaction
284 // spends both chain and mempool UTXOs (mempool outputs are appended last by
285 // `spent_utxos()`), causing `are_inputs_standard` and `p2sh_sigop_count`
286 // to pair the wrong input with the wrong spent output.
287 // https://github.com/ZcashFoundation/zebra/issues/10346
288 if transaction.inputs().len() != spent_outputs.len() {
289 tracing::warn!(
290 inputs = transaction.inputs().len(),
291 spent_outputs = spent_outputs.len(),
292 "spent_outputs length mismatch, rejecting as non-standard"
293 );
294 return self
295 .reject_non_standard(tx, NonStandardTransactionError::NonStandardInputs);
296 }
297
298 // Rule: all transparent inputs must pass `AreInputsStandard()` checks:
299 // https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L137
300 if !policy::are_inputs_standard(transaction, spent_outputs) {
301 return self
302 .reject_non_standard(tx, NonStandardTransactionError::NonStandardInputs);
303 }
304
305 // Rule: per-transaction sigops (legacy + P2SH) must not exceed the limit.
306 // zcashd sums GetLegacySigOpCount + GetP2SHSigOpCount for AcceptToMemoryPool:
307 // https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L1819
308 let total_sigops =
309 tx.legacy_sigop_count + policy::p2sh_sigop_count(transaction, spent_outputs);
310 if total_sigops > policy::MAX_STANDARD_TX_SIGOPS {
311 return self.reject_non_standard(tx, NonStandardTransactionError::TooManySigops);
312 }
313 } else {
314 // No spent outputs available (e.g. shielded-only transaction).
315 // Only check legacy sigops.
316 if tx.legacy_sigop_count > policy::MAX_STANDARD_TX_SIGOPS {
317 return self.reject_non_standard(tx, NonStandardTransactionError::TooManySigops);
318 }
319 }
320
321 // Rule: outputs must be standard script kinds, with special handling for OP_RETURN.
322 let mut data_out_count = 0u32;
323
324 for output in transaction.outputs() {
325 let lock_script = &output.lock_script;
326 let script_len = lock_script.as_raw_bytes().len();
327 let script_kind = policy::standard_script_kind(lock_script);
328
329 match script_kind {
330 None => {
331 // Rule: output script must be standard (P2PKH/P2SH/P2PK/multisig/OP_RETURN).
332 return self.reject_non_standard(
333 tx,
334 NonStandardTransactionError::ScriptPubKeyNonStandard,
335 );
336 }
337 Some(solver::ScriptKind::NullData { .. }) => {
338 // Rule: OP_RETURN script size is limited.
339 if script_len > self.max_datacarrier_bytes as usize {
340 return self.reject_non_standard(
341 tx,
342 NonStandardTransactionError::DataCarrierTooLarge,
343 );
344 }
345 // Rule: count OP_RETURN outputs to enforce the one-output limit.
346 data_out_count += 1;
347 }
348 Some(solver::ScriptKind::MultiSig { pubkeys, .. }) => {
349 // Rule: multisig must be at most 3-of-3 for standardness.
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.
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 fn reject_non_standard(
378 &mut self,
379 tx: &VerifiedUnminedTx,
380 rejection_error: NonStandardTransactionError,
381 ) -> Result<(), MempoolError> {
382 self.reject(tx.transaction.id, rejection_error.clone().into());
383 Err(MempoolError::NonStandardTransaction(rejection_error))
384 }
385
386 /// Insert a [`VerifiedUnminedTx`] into the mempool, caching any rejections.
387 ///
388 /// Accepts the [`VerifiedUnminedTx`] being inserted and `spent_mempool_outpoints`,
389 /// a list of transparent inputs of the provided [`VerifiedUnminedTx`] that were found
390 /// as newly created transparent outputs in the mempool during transaction verification.
391 ///
392 /// Returns an error if the mempool's verified transactions or rejection caches
393 /// prevent this transaction from being inserted.
394 /// These errors should not be propagated to peers, because the transactions are valid.
395 ///
396 /// If inserting this transaction evicts other transactions, they will be tracked
397 /// as [`SameEffectsChainRejectionError::RandomlyEvicted`].
398 #[allow(clippy::unwrap_in_result)]
399 pub fn insert(
400 &mut self,
401 tx: VerifiedUnminedTx,
402 spent_mempool_outpoints: Vec<transparent::OutPoint>,
403 height: Option<Height>,
404 ) -> Result<UnminedTxId, MempoolError> {
405 // # Security
406 //
407 // This method must call `reject`, rather than modifying the rejection lists directly.
408 let unmined_tx_id = tx.transaction.id;
409 let tx_id = unmined_tx_id.mined_id();
410
411 // First, check if we have a cached rejection for this transaction.
412 if let Some(error) = self.rejection_error(&unmined_tx_id) {
413 tracing::trace!(
414 ?tx_id,
415 ?error,
416 stored_transaction_count = ?self.verified.transaction_count(),
417 "returning cached error for transaction",
418 );
419
420 return Err(error);
421 }
422
423 // If `tx` is already in the mempool, we don't change anything.
424 //
425 // Security: transactions must not get refreshed by new queries,
426 // because that allows malicious peers to keep transactions live forever.
427 if self.verified.contains(&tx_id) {
428 tracing::trace!(
429 ?tx_id,
430 stored_transaction_count = ?self.verified.transaction_count(),
431 "returning InMempool error for transaction that is already in the mempool",
432 );
433
434 return Err(MempoolError::InMempool);
435 }
436
437 // Check that the transaction is standard.
438 self.reject_if_non_standard_tx(&tx)?;
439
440 // Then, we try to insert into the pool. If this fails the transaction is rejected.
441 let mut result = Ok(unmined_tx_id);
442 if let Err(rejection_error) = self.verified.insert(
443 tx,
444 spent_mempool_outpoints,
445 &mut self.pending_outputs,
446 height,
447 ) {
448 tracing::debug!(
449 ?tx_id,
450 ?rejection_error,
451 stored_transaction_count = ?self.verified.transaction_count(),
452 "insertion error for transaction",
453 );
454
455 // We could return here, but we still want to check the mempool size
456 self.reject(unmined_tx_id, rejection_error.clone().into());
457 result = Err(rejection_error.into());
458 }
459
460 // Once inserted, we evict transactions over the pool size limit per [ZIP-401];
461 //
462 // > On receiving a transaction: (...)
463 // > Calculate its cost. If the total cost of transactions in the mempool including this
464 // > one would `exceed mempooltxcostlimit`, then the node MUST repeatedly call
465 // > EvictTransaction (with the new transaction included as a candidate to evict) until the
466 // > total cost does not exceed `mempooltxcostlimit`.
467 //
468 // 'EvictTransaction' is equivalent to [`VerifiedSet::evict_one()`] in
469 // our implementation.
470 //
471 // [ZIP-401]: https://zips.z.cash/zip-0401
472 while self.verified.total_cost() > self.tx_cost_limit {
473 // > EvictTransaction MUST do the following:
474 // > Select a random transaction to evict, with probability in direct proportion to
475 // > eviction weight. (...) Remove it from the mempool.
476 let victim_tx = self
477 .verified
478 .evict_one()
479 .expect("mempool is empty, but was expected to be full");
480
481 // > Add the txid and the current time to RecentlyEvicted, dropping the oldest entry in
482 // > RecentlyEvicted if necessary to keep it to at most `eviction_memory_entries entries`.
483 self.reject(
484 victim_tx.transaction.id,
485 SameEffectsChainRejectionError::RandomlyEvicted.into(),
486 );
487
488 // If this transaction gets evicted, set its result to the same error
489 if victim_tx.transaction.id == unmined_tx_id {
490 result = Err(SameEffectsChainRejectionError::RandomlyEvicted.into());
491 }
492 }
493
494 result
495 }
496
497 /// Remove transactions from the mempool via exact [`UnminedTxId`].
498 ///
499 /// For v5 transactions, transactions are matched by WTXID, using both the:
500 /// - non-malleable transaction ID, and
501 /// - authorizing data hash.
502 ///
503 /// This matches the exact transaction, with identical blockchain effects, signatures, and proofs.
504 ///
505 /// Returns the number of transactions which were removed.
506 ///
507 /// Removes from the 'verified' set, if present.
508 /// Maintains the order in which the other unmined transactions have been inserted into the mempool.
509 ///
510 /// Does not add or remove from the 'rejected' tracking set.
511 #[allow(dead_code)]
512 pub fn remove_exact(&mut self, exact_wtxids: &HashSet<UnminedTxId>) -> usize {
513 self.verified
514 .remove_all_that(|tx| exact_wtxids.contains(&tx.transaction.id))
515 .len()
516 }
517
518 /// Clears a list of mined transaction ids from the verified set's tracked transaction dependencies.
519 pub fn clear_mined_dependencies(&mut self, mined_ids: &HashSet<transaction::Hash>) {
520 self.verified.clear_mined_dependencies(mined_ids);
521 }
522
523 /// Reject and remove transactions from the mempool via non-malleable [`transaction::Hash`].
524 /// - For v5 transactions, transactions are matched by TXID,
525 /// using only the non-malleable transaction ID.
526 /// This matches any transaction with the same effect on the blockchain state,
527 /// even if its signatures and proofs are different.
528 /// - Returns the number of transactions which were removed.
529 /// - Removes from the 'verified' set, if present.
530 /// Maintains the order in which the other unmined transactions have been inserted into the mempool.
531 /// - Prunes `pending_outputs` of any closed channels.
532 ///
533 /// Reject and remove transactions from the mempool that contain any spent outpoints or revealed
534 /// nullifiers from the passed in `transactions`.
535 ///
536 /// Returns the number of transactions that were removed.
537 pub fn reject_and_remove_same_effects(
538 &mut self,
539 mined_ids: &HashSet<transaction::Hash>,
540 transactions: Vec<Arc<Transaction>>,
541 ) -> RemovedTransactionIds {
542 let removed_mined = self
543 .verified
544 .remove_all_that(|tx| mined_ids.contains(&tx.transaction.id.mined_id()));
545
546 let spent_outpoints: HashSet<_> = transactions
547 .iter()
548 .flat_map(|tx| tx.spent_outpoints())
549 .collect();
550 let sprout_nullifiers: HashSet<_> = transactions
551 .iter()
552 .flat_map(|transaction| transaction.sprout_nullifiers())
553 .collect();
554 let sapling_nullifiers: HashSet<_> = transactions
555 .iter()
556 .flat_map(|transaction| transaction.sapling_nullifiers())
557 .collect();
558 let orchard_nullifiers: HashSet<_> = transactions
559 .iter()
560 .flat_map(|transaction| transaction.orchard_nullifiers())
561 .collect();
562
563 let duplicate_spend_ids: HashSet<_> = self
564 .verified
565 .transactions()
566 .values()
567 .map(|tx| (tx.transaction.id, &tx.transaction.transaction))
568 .filter_map(|(tx_id, tx)| {
569 (tx.spent_outpoints()
570 .any(|outpoint| spent_outpoints.contains(&outpoint))
571 || tx
572 .sprout_nullifiers()
573 .any(|nullifier| sprout_nullifiers.contains(nullifier))
574 || tx
575 .sapling_nullifiers()
576 .any(|nullifier| sapling_nullifiers.contains(nullifier))
577 || tx
578 .orchard_nullifiers()
579 .any(|nullifier| orchard_nullifiers.contains(nullifier)))
580 .then_some(tx_id)
581 })
582 .collect();
583
584 let removed_duplicate_spend = self
585 .verified
586 .remove_all_that(|tx| duplicate_spend_ids.contains(&tx.transaction.id));
587
588 for &mined_id in mined_ids {
589 self.reject(
590 // the reject and rejection_error fns that store and check `SameEffectsChainRejectionError`s
591 // only use the mined id, so using `Legacy` ids will apply to v5 transactions as well.
592 UnminedTxId::Legacy(mined_id),
593 SameEffectsChainRejectionError::Mined.into(),
594 );
595 }
596
597 for duplicate_spend_id in duplicate_spend_ids {
598 self.reject(
599 duplicate_spend_id,
600 SameEffectsChainRejectionError::DuplicateSpend.into(),
601 );
602 }
603
604 self.pending_outputs.prune();
605
606 RemovedTransactionIds {
607 mined: removed_mined,
608 invalidated: removed_duplicate_spend,
609 }
610 }
611
612 /// Clears the whole mempool storage.
613 #[allow(dead_code)]
614 pub fn clear(&mut self) {
615 self.verified.clear();
616 self.tip_rejected_exact.clear();
617 self.pending_outputs.clear();
618 self.tip_rejected_same_effects.clear();
619 self.chain_rejected_same_effects.clear();
620 self.update_rejected_metrics();
621 }
622
623 /// Clears rejections that only apply to the current tip.
624 pub fn clear_tip_rejections(&mut self) {
625 self.tip_rejected_exact.clear();
626 self.tip_rejected_same_effects.clear();
627 self.update_rejected_metrics();
628 }
629
630 /// Clears rejections that only apply to the current tip.
631 ///
632 /// # Security
633 ///
634 /// This method must be called at the end of every method that adds rejections.
635 /// Otherwise, peers could make our reject lists use a lot of RAM.
636 fn limit_rejection_list_memory(&mut self) {
637 // These lists are an optimisation - it's ok to totally clear them as needed.
638 if self.tip_rejected_exact.len() > MAX_EVICTION_MEMORY_ENTRIES {
639 self.tip_rejected_exact.clear();
640 }
641 if self.tip_rejected_same_effects.len() > MAX_EVICTION_MEMORY_ENTRIES {
642 self.tip_rejected_same_effects.clear();
643 }
644 // `chain_rejected_same_effects` limits its size by itself
645 self.update_rejected_metrics();
646 }
647
648 /// Returns the set of [`UnminedTxId`]s in the mempool.
649 pub fn tx_ids(&self) -> impl Iterator<Item = UnminedTxId> + '_ {
650 self.transactions().values().map(|tx| tx.transaction.id)
651 }
652
653 /// Returns a reference to the [`HashMap`] of [`VerifiedUnminedTx`]s in the verified set.
654 ///
655 /// Each [`VerifiedUnminedTx`] contains an [`UnminedTx`],
656 /// and adds extra fields from the transaction verifier result.
657 pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
658 self.verified.transactions()
659 }
660
661 /// Returns a reference to the [`TransactionDependencies`] in the verified set.
662 pub fn transaction_dependencies(&self) -> &TransactionDependencies {
663 self.verified.transaction_dependencies()
664 }
665
666 /// Returns a [`transparent::Output`] created by a mempool transaction for the provided
667 /// [`transparent::OutPoint`] if one exists, or None otherwise.
668 pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
669 self.verified.created_output(outpoint)
670 }
671
672 /// Returns true if a tx in the set has spent the output at the provided outpoint.
673 pub fn has_spent_outpoint(&self, outpoint: &transparent::OutPoint) -> bool {
674 self.verified.has_spent_outpoint(outpoint)
675 }
676
677 /// Returns the number of transactions in the mempool.
678 #[allow(dead_code)]
679 pub fn transaction_count(&self) -> usize {
680 self.verified.transaction_count()
681 }
682
683 /// Returns the cost of the transactions in the mempool, according to ZIP-401.
684 #[allow(dead_code)]
685 pub fn total_cost(&self) -> u64 {
686 self.verified.total_cost()
687 }
688
689 /// Returns the total serialized size of the verified transactions in the set.
690 ///
691 /// See [`VerifiedSet::total_serialized_size()`] for details.
692 pub fn total_serialized_size(&self) -> usize {
693 self.verified.total_serialized_size()
694 }
695
696 /// Returns the set of [`UnminedTx`]es with exactly matching `tx_ids` in the
697 /// mempool.
698 ///
699 /// This matches the exact transaction, with identical blockchain effects,
700 /// signatures, and proofs.
701 pub fn transactions_exact(
702 &self,
703 tx_ids: HashSet<UnminedTxId>,
704 ) -> impl Iterator<Item = &UnminedTx> {
705 tx_ids.into_iter().filter_map(|tx_id| {
706 self.transactions()
707 .get(&tx_id.mined_id())
708 .map(|tx| &tx.transaction)
709 })
710 }
711
712 /// Returns the set of [`UnminedTx`]es with matching [`transaction::Hash`]es
713 /// in the mempool.
714 ///
715 /// This matches transactions with the same effects, regardless of
716 /// [`transaction::AuthDigest`].
717 pub fn transactions_same_effects(
718 &self,
719 tx_ids: HashSet<Hash>,
720 ) -> impl Iterator<Item = &UnminedTx> {
721 self.verified
722 .transactions()
723 .iter()
724 .filter(move |(tx_id, _)| tx_ids.contains(tx_id))
725 .map(|(_, tx)| &tx.transaction)
726 }
727
728 /// Returns a transaction and the transaction ids of its dependencies, if it is in the verified set.
729 pub fn transaction_with_deps(
730 &self,
731 tx_id: transaction::Hash,
732 ) -> Option<(VerifiedUnminedTx, HashSet<transaction::Hash>)> {
733 let tx = self.verified.transactions().get(&tx_id).cloned()?;
734 let deps = self
735 .verified
736 .transaction_dependencies()
737 .dependencies()
738 .get(&tx_id)
739 .cloned()
740 .unwrap_or_default();
741
742 Some((tx, deps))
743 }
744
745 /// Returns `true` if a transaction exactly matching an [`UnminedTxId`] is in
746 /// the mempool.
747 ///
748 /// This matches the exact transaction, with identical blockchain effects,
749 /// signatures, and proofs.
750 pub fn contains_transaction_exact(&self, tx_id: &transaction::Hash) -> bool {
751 self.verified.contains(tx_id)
752 }
753
754 /// Returns the number of rejected [`UnminedTxId`]s or [`transaction::Hash`]es.
755 ///
756 /// Transactions on multiple rejected lists are counted multiple times.
757 #[allow(dead_code)]
758 pub fn rejected_transaction_count(&mut self) -> usize {
759 self.tip_rejected_exact.len()
760 + self.tip_rejected_same_effects.len()
761 + self
762 .chain_rejected_same_effects
763 .iter_mut()
764 .map(|(_, map)| map.len())
765 .sum::<usize>()
766 }
767
768 /// Add a transaction to the rejected list for the given reason.
769 pub fn reject(&mut self, tx_id: UnminedTxId, reason: RejectionError) {
770 match reason {
771 RejectionError::ExactTip(e) => {
772 self.tip_rejected_exact.insert(tx_id, e);
773 }
774 RejectionError::SameEffectsTip(e) => {
775 self.tip_rejected_same_effects.insert(tx_id.mined_id(), e);
776 }
777 RejectionError::SameEffectsChain(e) => {
778 let eviction_memory_time = self.eviction_memory_time;
779 self.chain_rejected_same_effects
780 .entry(e)
781 .or_insert_with(|| {
782 EvictionList::new(MAX_EVICTION_MEMORY_ENTRIES, eviction_memory_time)
783 })
784 .insert(tx_id.mined_id());
785 }
786 RejectionError::NonStandardTransaction(e) => {
787 // Non-standard transactions are rejected based on their exact
788 // transaction data.
789 self.tip_rejected_exact
790 .insert(tx_id, ExactTipRejectionError::from(e));
791 }
792 }
793 self.limit_rejection_list_memory();
794 }
795
796 /// Returns the rejection error if a transaction matching an [`UnminedTxId`]
797 /// is in any mempool rejected list.
798 ///
799 /// This matches transactions based on each rejection list's matching rule.
800 ///
801 /// Returns an arbitrary error if the transaction is in multiple lists.
802 pub fn rejection_error(&self, txid: &UnminedTxId) -> Option<MempoolError> {
803 if let Some(error) = self.tip_rejected_exact.get(txid) {
804 return Some(error.clone().into());
805 }
806
807 if let Some(error) = self.tip_rejected_same_effects.get(&txid.mined_id()) {
808 return Some(error.clone().into());
809 }
810
811 for (error, set) in self.chain_rejected_same_effects.iter() {
812 if set.contains_key(&txid.mined_id()) {
813 return Some(error.clone().into());
814 }
815 }
816
817 None
818 }
819
820 /// Returns the set of [`UnminedTxId`]s matching `tx_ids` in the rejected list.
821 ///
822 /// This matches transactions based on each rejection list's matching rule.
823 pub fn rejected_transactions(
824 &self,
825 tx_ids: HashSet<UnminedTxId>,
826 ) -> impl Iterator<Item = UnminedTxId> + '_ {
827 tx_ids
828 .into_iter()
829 .filter(move |txid| self.contains_rejected(txid))
830 }
831
832 /// Returns `true` if a transaction matching the supplied [`UnminedTxId`] is in
833 /// the mempool rejected list.
834 ///
835 /// This matches transactions based on each rejection list's matching rule.
836 pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool {
837 self.rejection_error(txid).is_some()
838 }
839
840 /// Add a transaction that failed download and verification to the rejected list
841 /// if needed, depending on the reason for the failure.
842 pub fn reject_if_needed(&mut self, tx_id: UnminedTxId, e: TransactionDownloadVerifyError) {
843 match e {
844 // Rejecting a transaction already in state would speed up further
845 // download attempts without checking the state. However it would
846 // make the reject list grow forever.
847 //
848 // TODO: revisit after reviewing the rejected list cleanup criteria?
849 // TODO: if we decide to reject it, then we need to pass the block hash
850 // to State::Confirmed. This would require the zs::Response::Transaction
851 // to include the hash, which would need to be implemented.
852 TransactionDownloadVerifyError::InState |
853 // An unknown error in the state service, better do nothing
854 TransactionDownloadVerifyError::StateError(_) |
855 // If download failed, do nothing; the crawler will end up trying to
856 // download it again.
857 TransactionDownloadVerifyError::DownloadFailed(_) |
858 // If it was cancelled then a block was mined, or there was a network
859 // upgrade, etc. No reason to reject it.
860 TransactionDownloadVerifyError::Cancelled => {}
861
862 // Consensus verification failed. Reject transaction to avoid
863 // having to download and verify it again just for it to fail again.
864 TransactionDownloadVerifyError::Invalid { error, .. } => {
865 self.reject(tx_id, ExactTipRejectionError::FailedVerification(error).into())
866 }
867 }
868 }
869
870 /// Remove transactions from the mempool if they have not been mined after a
871 /// specified height, per [ZIP-203].
872 ///
873 /// > Transactions will have a new field, nExpiryHeight, which will set the
874 /// > block height after which transactions will be removed from the mempool
875 /// > if they have not been mined.
876 ///
877 ///
878 /// [ZIP-203]: https://zips.z.cash/zip-0203#specification
879 pub fn remove_expired_transactions(
880 &mut self,
881 tip_height: zebra_chain::block::Height,
882 ) -> HashSet<UnminedTxId> {
883 let mut tx_ids = HashSet::new();
884 let mut unmined_tx_ids = HashSet::new();
885
886 for (&tx_id, tx) in self.transactions() {
887 if let Some(expiry_height) = tx.transaction.transaction.expiry_height() {
888 if tip_height >= expiry_height {
889 tx_ids.insert(tx_id);
890 unmined_tx_ids.insert(tx.transaction.id);
891 }
892 }
893 }
894
895 // expiry height is effecting data, so we match by non-malleable TXID
896 self.verified
897 .remove_all_that(|tx| tx_ids.contains(&tx.transaction.id.mined_id()));
898
899 // also reject it
900 for id in tx_ids {
901 self.reject(
902 // It's okay to omit the auth digest here as we know that `reject()` will always
903 // use mined ids for `SameEffectsChainRejectionError`s.
904 UnminedTxId::Legacy(id),
905 SameEffectsChainRejectionError::Expired.into(),
906 );
907 }
908
909 unmined_tx_ids
910 }
911
912 /// Check if transaction should be downloaded and/or verified.
913 ///
914 /// If it is already in the mempool (or in its rejected list)
915 /// then it shouldn't be downloaded/verified.
916 pub fn should_download_or_verify(&mut self, txid: UnminedTxId) -> Result<(), MempoolError> {
917 // Check if the transaction is already in the mempool.
918 if self.contains_transaction_exact(&txid.mined_id()) {
919 return Err(MempoolError::InMempool);
920 }
921 if let Some(error) = self.rejection_error(&txid) {
922 return Err(error);
923 }
924 Ok(())
925 }
926
927 /// Update metrics related to the rejected lists.
928 ///
929 /// Must be called every time the rejected lists change.
930 fn update_rejected_metrics(&mut self) {
931 metrics::gauge!("mempool.rejected.transaction.ids",)
932 .set(self.rejected_transaction_count() as f64);
933 // This is just an approximation.
934 // TODO: make it more accurate #2869
935 let item_size = size_of::<(transaction::Hash, SameEffectsTipRejectionError)>();
936 metrics::gauge!("mempool.rejected.transaction.ids.bytes",)
937 .set((self.rejected_transaction_count() * item_size) as f64);
938 }
939}