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 zebra_chain::{
20 block::Height,
21 transaction::{self, Hash, Transaction, UnminedTx, UnminedTxId, VerifiedUnminedTx},
22 transparent,
23};
24use zebra_node_services::mempool::TransactionDependencies;
25
26use self::{eviction_list::EvictionList, verified_set::VerifiedSet};
27use super::{
28 config, downloads::TransactionDownloadVerifyError, pending_outputs::PendingOutputs,
29 MempoolError,
30};
31
32#[cfg(any(test, feature = "proptest-impl"))]
33use proptest_derive::Arbitrary;
34
35#[cfg(test)]
36pub mod tests;
37
38mod eviction_list;
39mod verified_set;
40
41/// The size limit for mempool transaction rejection lists per [ZIP-401].
42///
43/// > The size of RecentlyEvicted SHOULD never exceed `eviction_memory_entries`
44/// > entries, which is the constant 40000.
45///
46/// We use the specified value for all lists for consistency.
47///
48/// [ZIP-401]: https://zips.z.cash/zip-0401#specification
49pub(crate) const MAX_EVICTION_MEMORY_ENTRIES: usize = 40_000;
50
51/// Transactions rejected based on transaction authorizing data (scripts, proofs, signatures),
52/// or lock times. These rejections are only valid for the current tip.
53///
54/// Each committed block clears these rejections, because new blocks can supply missing inputs.
55#[derive(Error, Clone, Debug, PartialEq, Eq)]
56#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
57#[allow(dead_code)]
58pub enum ExactTipRejectionError {
59 #[error("transaction did not pass consensus validation: {0}")]
60 FailedVerification(#[from] zebra_consensus::error::TransactionError),
61}
62
63/// Transactions rejected based only on their effects (spends, outputs, transaction header).
64/// These rejections are only valid for the current tip.
65///
66/// Each committed block clears these rejections, because new blocks can evict other transactions.
67#[derive(Error, Clone, Debug, PartialEq, Eq)]
68#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
69#[allow(dead_code)]
70pub enum SameEffectsTipRejectionError {
71 #[error(
72 "transaction rejected because another transaction in the mempool has already spent some of \
73 its inputs"
74 )]
75 SpendConflict,
76
77 #[error(
78 "transaction rejected because it spends missing outputs from \
79 another transaction in the mempool"
80 )]
81 MissingOutput,
82}
83
84/// Transactions rejected based only on their effects (spends, outputs, transaction header).
85/// These rejections are valid while the current chain continues to grow.
86///
87/// Rollbacks and network upgrades clear these rejections, because they can lower the tip height,
88/// or change the consensus rules.
89#[derive(Error, Clone, Debug, PartialEq, Eq, Hash)]
90#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
91#[allow(dead_code)]
92pub enum SameEffectsChainRejectionError {
93 #[error("best chain tip has reached transaction expiry height")]
94 Expired,
95
96 #[error("transaction inputs were spent, or nullifiers were revealed, in the best chain")]
97 DuplicateSpend,
98
99 #[error("transaction was committed to the best chain")]
100 Mined,
101
102 /// Otherwise valid transaction removed from mempool due to [ZIP-401] random
103 /// eviction.
104 ///
105 /// Consensus rule:
106 /// > The txid (rather than the wtxid ...) is used even for version 5 transactions
107 ///
108 /// [ZIP-401]: https://zips.z.cash/zip-0401#specification
109 #[error("transaction evicted from the mempool due to ZIP-401 denial of service limits")]
110 RandomlyEvicted,
111}
112
113/// Storage error that combines all other specific error types.
114#[derive(Error, Clone, Debug, PartialEq, Eq)]
115#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
116#[allow(dead_code)]
117pub enum RejectionError {
118 #[error(transparent)]
119 ExactTip(#[from] ExactTipRejectionError),
120 #[error(transparent)]
121 SameEffectsTip(#[from] SameEffectsTipRejectionError),
122 #[error(transparent)]
123 SameEffectsChain(#[from] SameEffectsChainRejectionError),
124}
125
126/// Hold mempool verified and rejected mempool transactions.
127pub struct Storage {
128 /// The set of verified transactions in the mempool.
129 verified: VerifiedSet,
130
131 /// The set of outpoints with pending requests for their associated transparent::Output.
132 pub(super) pending_outputs: PendingOutputs,
133
134 /// The set of transactions rejected due to bad authorizations, or for other
135 /// reasons, and their rejection reasons. These rejections only apply to the
136 /// current tip.
137 ///
138 /// Only transactions with the exact [`UnminedTxId`] are invalid.
139 tip_rejected_exact: HashMap<UnminedTxId, ExactTipRejectionError>,
140
141 /// A set of transactions rejected for their effects, and their rejection
142 /// reasons. These rejections only apply to the current tip.
143 ///
144 /// Any transaction with the same [`transaction::Hash`] is invalid.
145 tip_rejected_same_effects: HashMap<transaction::Hash, SameEffectsTipRejectionError>,
146
147 /// Sets of transactions rejected for their effects, keyed by rejection reason.
148 /// These rejections apply until a rollback or network upgrade.
149 ///
150 /// Any transaction with the same [`transaction::Hash`] is invalid.
151 ///
152 /// An [`EvictionList`] is used for both randomly evicted and expired
153 /// transactions, even if it is only needed for the evicted ones. This was
154 /// done just to simplify the existing code; there is no harm in having a
155 /// timeout for expired transactions too since re-checking expired
156 /// transactions is cheap.
157 // If this code is ever refactored and the lists are split in different
158 // fields, then we can use an `EvictionList` just for the evicted list.
159 chain_rejected_same_effects: HashMap<SameEffectsChainRejectionError, EvictionList>,
160
161 /// The mempool transaction eviction age limit.
162 /// Same as [`config::Config::eviction_memory_time`].
163 eviction_memory_time: Duration,
164
165 /// Max total cost of the verified mempool set, beyond which transactions
166 /// are evicted to make room.
167 tx_cost_limit: u64,
168}
169
170impl Drop for Storage {
171 fn drop(&mut self) {
172 self.clear();
173 }
174}
175
176impl Storage {
177 #[allow(clippy::field_reassign_with_default)]
178 pub(crate) fn new(config: &config::Config) -> Self {
179 Self {
180 tx_cost_limit: config.tx_cost_limit,
181 eviction_memory_time: config.eviction_memory_time,
182 verified: Default::default(),
183 pending_outputs: Default::default(),
184 tip_rejected_exact: Default::default(),
185 tip_rejected_same_effects: Default::default(),
186 chain_rejected_same_effects: Default::default(),
187 }
188 }
189
190 /// Insert a [`VerifiedUnminedTx`] into the mempool, caching any rejections.
191 ///
192 /// Accepts the [`VerifiedUnminedTx`] being inserted and `spent_mempool_outpoints`,
193 /// a list of transparent inputs of the provided [`VerifiedUnminedTx`] that were found
194 /// as newly created transparent outputs in the mempool during transaction verification.
195 ///
196 /// Returns an error if the mempool's verified transactions or rejection caches
197 /// prevent this transaction from being inserted.
198 /// These errors should not be propagated to peers, because the transactions are valid.
199 ///
200 /// If inserting this transaction evicts other transactions, they will be tracked
201 /// as [`SameEffectsChainRejectionError::RandomlyEvicted`].
202 #[allow(clippy::unwrap_in_result)]
203 pub fn insert(
204 &mut self,
205 tx: VerifiedUnminedTx,
206 spent_mempool_outpoints: Vec<transparent::OutPoint>,
207 height: Option<Height>,
208 ) -> Result<UnminedTxId, MempoolError> {
209 // # Security
210 //
211 // This method must call `reject`, rather than modifying the rejection lists directly.
212 let unmined_tx_id = tx.transaction.id;
213 let tx_id = unmined_tx_id.mined_id();
214
215 // First, check if we have a cached rejection for this transaction.
216 if let Some(error) = self.rejection_error(&unmined_tx_id) {
217 tracing::trace!(
218 ?tx_id,
219 ?error,
220 stored_transaction_count = ?self.verified.transaction_count(),
221 "returning cached error for transaction",
222 );
223
224 return Err(error);
225 }
226
227 // If `tx` is already in the mempool, we don't change anything.
228 //
229 // Security: transactions must not get refreshed by new queries,
230 // because that allows malicious peers to keep transactions live forever.
231 if self.verified.contains(&tx_id) {
232 tracing::trace!(
233 ?tx_id,
234 stored_transaction_count = ?self.verified.transaction_count(),
235 "returning InMempool error for transaction that is already in the mempool",
236 );
237
238 return Err(MempoolError::InMempool);
239 }
240
241 // Then, we try to insert into the pool. If this fails the transaction is rejected.
242 let mut result = Ok(unmined_tx_id);
243 if let Err(rejection_error) = self.verified.insert(
244 tx,
245 spent_mempool_outpoints,
246 &mut self.pending_outputs,
247 height,
248 ) {
249 tracing::debug!(
250 ?tx_id,
251 ?rejection_error,
252 stored_transaction_count = ?self.verified.transaction_count(),
253 "insertion error for transaction",
254 );
255
256 // We could return here, but we still want to check the mempool size
257 self.reject(unmined_tx_id, rejection_error.clone().into());
258 result = Err(rejection_error.into());
259 }
260
261 // Once inserted, we evict transactions over the pool size limit per [ZIP-401];
262 //
263 // > On receiving a transaction: (...)
264 // > Calculate its cost. If the total cost of transactions in the mempool including this
265 // > one would `exceed mempooltxcostlimit`, then the node MUST repeatedly call
266 // > EvictTransaction (with the new transaction included as a candidate to evict) until the
267 // > total cost does not exceed `mempooltxcostlimit`.
268 //
269 // 'EvictTransaction' is equivalent to [`VerifiedSet::evict_one()`] in
270 // our implementation.
271 //
272 // [ZIP-401]: https://zips.z.cash/zip-0401
273 while self.verified.total_cost() > self.tx_cost_limit {
274 // > EvictTransaction MUST do the following:
275 // > Select a random transaction to evict, with probability in direct proportion to
276 // > eviction weight. (...) Remove it from the mempool.
277 let victim_tx = self
278 .verified
279 .evict_one()
280 .expect("mempool is empty, but was expected to be full");
281
282 // > Add the txid and the current time to RecentlyEvicted, dropping the oldest entry in
283 // > RecentlyEvicted if necessary to keep it to at most `eviction_memory_entries entries`.
284 self.reject(
285 victim_tx.transaction.id,
286 SameEffectsChainRejectionError::RandomlyEvicted.into(),
287 );
288
289 // If this transaction gets evicted, set its result to the same error
290 if victim_tx.transaction.id == unmined_tx_id {
291 result = Err(SameEffectsChainRejectionError::RandomlyEvicted.into());
292 }
293 }
294
295 result
296 }
297
298 /// Remove transactions from the mempool via exact [`UnminedTxId`].
299 ///
300 /// For v5 transactions, transactions are matched by WTXID, using both the:
301 /// - non-malleable transaction ID, and
302 /// - authorizing data hash.
303 ///
304 /// This matches the exact transaction, with identical blockchain effects, signatures, and proofs.
305 ///
306 /// Returns the number of transactions which were removed.
307 ///
308 /// Removes from the 'verified' set, if present.
309 /// Maintains the order in which the other unmined transactions have been inserted into the mempool.
310 ///
311 /// Does not add or remove from the 'rejected' tracking set.
312 #[allow(dead_code)]
313 pub fn remove_exact(&mut self, exact_wtxids: &HashSet<UnminedTxId>) -> usize {
314 self.verified
315 .remove_all_that(|tx| exact_wtxids.contains(&tx.transaction.id))
316 }
317
318 /// Clears a list of mined transaction ids from the verified set's tracked transaction dependencies.
319 pub fn clear_mined_dependencies(&mut self, mined_ids: &HashSet<transaction::Hash>) {
320 self.verified.clear_mined_dependencies(mined_ids);
321 }
322
323 /// Reject and remove transactions from the mempool via non-malleable [`transaction::Hash`].
324 /// - For v5 transactions, transactions are matched by TXID,
325 /// using only the non-malleable transaction ID.
326 /// This matches any transaction with the same effect on the blockchain state,
327 /// even if its signatures and proofs are different.
328 /// - Returns the number of transactions which were removed.
329 /// - Removes from the 'verified' set, if present.
330 /// Maintains the order in which the other unmined transactions have been inserted into the mempool.
331 /// - Prunes `pending_outputs` of any closed channels.
332 ///
333 /// Reject and remove transactions from the mempool that contain any spent outpoints or revealed
334 /// nullifiers from the passed in `transactions`.
335 ///
336 /// Returns the number of transactions that were removed.
337 pub fn reject_and_remove_same_effects(
338 &mut self,
339 mined_ids: &HashSet<transaction::Hash>,
340 transactions: Vec<Arc<Transaction>>,
341 ) -> usize {
342 let num_removed_mined = self
343 .verified
344 .remove_all_that(|tx| mined_ids.contains(&tx.transaction.id.mined_id()));
345
346 let spent_outpoints: HashSet<_> = transactions
347 .iter()
348 .flat_map(|tx| tx.spent_outpoints())
349 .collect();
350 let sprout_nullifiers: HashSet<_> = transactions
351 .iter()
352 .flat_map(|transaction| transaction.sprout_nullifiers())
353 .collect();
354 let sapling_nullifiers: HashSet<_> = transactions
355 .iter()
356 .flat_map(|transaction| transaction.sapling_nullifiers())
357 .collect();
358 let orchard_nullifiers: HashSet<_> = transactions
359 .iter()
360 .flat_map(|transaction| transaction.orchard_nullifiers())
361 .collect();
362
363 let duplicate_spend_ids: HashSet<_> = self
364 .verified
365 .transactions()
366 .values()
367 .map(|tx| (tx.transaction.id, &tx.transaction.transaction))
368 .filter_map(|(tx_id, tx)| {
369 (tx.spent_outpoints()
370 .any(|outpoint| spent_outpoints.contains(&outpoint))
371 || tx
372 .sprout_nullifiers()
373 .any(|nullifier| sprout_nullifiers.contains(nullifier))
374 || tx
375 .sapling_nullifiers()
376 .any(|nullifier| sapling_nullifiers.contains(nullifier))
377 || tx
378 .orchard_nullifiers()
379 .any(|nullifier| orchard_nullifiers.contains(nullifier)))
380 .then_some(tx_id)
381 })
382 .collect();
383
384 let num_removed_duplicate_spend = self
385 .verified
386 .remove_all_that(|tx| duplicate_spend_ids.contains(&tx.transaction.id));
387
388 for &mined_id in mined_ids {
389 self.reject(
390 // the reject and rejection_error fns that store and check `SameEffectsChainRejectionError`s
391 // only use the mined id, so using `Legacy` ids will apply to v5 transactions as well.
392 UnminedTxId::Legacy(mined_id),
393 SameEffectsChainRejectionError::Mined.into(),
394 );
395 }
396
397 for duplicate_spend_id in duplicate_spend_ids {
398 self.reject(
399 duplicate_spend_id,
400 SameEffectsChainRejectionError::DuplicateSpend.into(),
401 );
402 }
403
404 self.pending_outputs.prune();
405
406 num_removed_mined + num_removed_duplicate_spend
407 }
408
409 /// Clears the whole mempool storage.
410 #[allow(dead_code)]
411 pub fn clear(&mut self) {
412 self.verified.clear();
413 self.tip_rejected_exact.clear();
414 self.pending_outputs.clear();
415 self.tip_rejected_same_effects.clear();
416 self.chain_rejected_same_effects.clear();
417 self.update_rejected_metrics();
418 }
419
420 /// Clears rejections that only apply to the current tip.
421 pub fn clear_tip_rejections(&mut self) {
422 self.tip_rejected_exact.clear();
423 self.tip_rejected_same_effects.clear();
424 self.update_rejected_metrics();
425 }
426
427 /// Clears rejections that only apply to the current tip.
428 ///
429 /// # Security
430 ///
431 /// This method must be called at the end of every method that adds rejections.
432 /// Otherwise, peers could make our reject lists use a lot of RAM.
433 fn limit_rejection_list_memory(&mut self) {
434 // These lists are an optimisation - it's ok to totally clear them as needed.
435 if self.tip_rejected_exact.len() > MAX_EVICTION_MEMORY_ENTRIES {
436 self.tip_rejected_exact.clear();
437 }
438 if self.tip_rejected_same_effects.len() > MAX_EVICTION_MEMORY_ENTRIES {
439 self.tip_rejected_same_effects.clear();
440 }
441 // `chain_rejected_same_effects` limits its size by itself
442 self.update_rejected_metrics();
443 }
444
445 /// Returns the set of [`UnminedTxId`]s in the mempool.
446 pub fn tx_ids(&self) -> impl Iterator<Item = UnminedTxId> + '_ {
447 self.transactions().values().map(|tx| tx.transaction.id)
448 }
449
450 /// Returns a reference to the [`HashMap`] of [`VerifiedUnminedTx`]s in the verified set.
451 ///
452 /// Each [`VerifiedUnminedTx`] contains an [`UnminedTx`],
453 /// and adds extra fields from the transaction verifier result.
454 pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
455 self.verified.transactions()
456 }
457
458 /// Returns a reference to the [`TransactionDependencies`] in the verified set.
459 pub fn transaction_dependencies(&self) -> &TransactionDependencies {
460 self.verified.transaction_dependencies()
461 }
462
463 /// Returns a [`transparent::Output`] created by a mempool transaction for the provided
464 /// [`transparent::OutPoint`] if one exists, or None otherwise.
465 pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
466 self.verified.created_output(outpoint)
467 }
468
469 /// Returns the number of transactions in the mempool.
470 #[allow(dead_code)]
471 pub fn transaction_count(&self) -> usize {
472 self.verified.transaction_count()
473 }
474
475 /// Returns the cost of the transactions in the mempool, according to ZIP-401.
476 #[allow(dead_code)]
477 pub fn total_cost(&self) -> u64 {
478 self.verified.total_cost()
479 }
480
481 /// Returns the total serialized size of the verified transactions in the set.
482 ///
483 /// See [`VerifiedSet::total_serialized_size()`] for details.
484 pub fn total_serialized_size(&self) -> usize {
485 self.verified.total_serialized_size()
486 }
487
488 /// Returns the set of [`UnminedTx`]es with exactly matching `tx_ids` in the
489 /// mempool.
490 ///
491 /// This matches the exact transaction, with identical blockchain effects,
492 /// signatures, and proofs.
493 pub fn transactions_exact(
494 &self,
495 tx_ids: HashSet<UnminedTxId>,
496 ) -> impl Iterator<Item = &UnminedTx> {
497 tx_ids.into_iter().filter_map(|tx_id| {
498 self.transactions()
499 .get(&tx_id.mined_id())
500 .map(|tx| &tx.transaction)
501 })
502 }
503
504 /// Returns the set of [`UnminedTx`]es with matching [`transaction::Hash`]es
505 /// in the mempool.
506 ///
507 /// This matches transactions with the same effects, regardless of
508 /// [`transaction::AuthDigest`].
509 pub fn transactions_same_effects(
510 &self,
511 tx_ids: HashSet<Hash>,
512 ) -> impl Iterator<Item = &UnminedTx> {
513 self.verified
514 .transactions()
515 .iter()
516 .filter(move |(tx_id, _)| tx_ids.contains(tx_id))
517 .map(|(_, tx)| &tx.transaction)
518 }
519
520 /// Returns a transaction and the transaction ids of its dependencies, if it is in the verified set.
521 pub fn transaction_with_deps(
522 &self,
523 tx_id: transaction::Hash,
524 ) -> Option<(VerifiedUnminedTx, HashSet<transaction::Hash>)> {
525 let tx = self.verified.transactions().get(&tx_id).cloned()?;
526 let deps = self
527 .verified
528 .transaction_dependencies()
529 .dependencies()
530 .get(&tx_id)
531 .cloned()
532 .unwrap_or_default();
533
534 Some((tx, deps))
535 }
536
537 /// Returns `true` if a transaction exactly matching an [`UnminedTxId`] is in
538 /// the mempool.
539 ///
540 /// This matches the exact transaction, with identical blockchain effects,
541 /// signatures, and proofs.
542 pub fn contains_transaction_exact(&self, tx_id: &transaction::Hash) -> bool {
543 self.verified.contains(tx_id)
544 }
545
546 /// Returns the number of rejected [`UnminedTxId`]s or [`transaction::Hash`]es.
547 ///
548 /// Transactions on multiple rejected lists are counted multiple times.
549 #[allow(dead_code)]
550 pub fn rejected_transaction_count(&mut self) -> usize {
551 self.tip_rejected_exact.len()
552 + self.tip_rejected_same_effects.len()
553 + self
554 .chain_rejected_same_effects
555 .iter_mut()
556 .map(|(_, map)| map.len())
557 .sum::<usize>()
558 }
559
560 /// Add a transaction to the rejected list for the given reason.
561 pub fn reject(&mut self, tx_id: UnminedTxId, reason: RejectionError) {
562 match reason {
563 RejectionError::ExactTip(e) => {
564 self.tip_rejected_exact.insert(tx_id, e);
565 }
566 RejectionError::SameEffectsTip(e) => {
567 self.tip_rejected_same_effects.insert(tx_id.mined_id(), e);
568 }
569 RejectionError::SameEffectsChain(e) => {
570 let eviction_memory_time = self.eviction_memory_time;
571 self.chain_rejected_same_effects
572 .entry(e)
573 .or_insert_with(|| {
574 EvictionList::new(MAX_EVICTION_MEMORY_ENTRIES, eviction_memory_time)
575 })
576 .insert(tx_id.mined_id());
577 }
578 }
579 self.limit_rejection_list_memory();
580 }
581
582 /// Returns the rejection error if a transaction matching an [`UnminedTxId`]
583 /// is in any mempool rejected list.
584 ///
585 /// This matches transactions based on each rejection list's matching rule.
586 ///
587 /// Returns an arbitrary error if the transaction is in multiple lists.
588 pub fn rejection_error(&self, txid: &UnminedTxId) -> Option<MempoolError> {
589 if let Some(error) = self.tip_rejected_exact.get(txid) {
590 return Some(error.clone().into());
591 }
592
593 if let Some(error) = self.tip_rejected_same_effects.get(&txid.mined_id()) {
594 return Some(error.clone().into());
595 }
596
597 for (error, set) in self.chain_rejected_same_effects.iter() {
598 if set.contains_key(&txid.mined_id()) {
599 return Some(error.clone().into());
600 }
601 }
602
603 None
604 }
605
606 /// Returns the set of [`UnminedTxId`]s matching `tx_ids` in the rejected list.
607 ///
608 /// This matches transactions based on each rejection list's matching rule.
609 pub fn rejected_transactions(
610 &self,
611 tx_ids: HashSet<UnminedTxId>,
612 ) -> impl Iterator<Item = UnminedTxId> + '_ {
613 tx_ids
614 .into_iter()
615 .filter(move |txid| self.contains_rejected(txid))
616 }
617
618 /// Returns `true` if a transaction matching the supplied [`UnminedTxId`] is in
619 /// the mempool rejected list.
620 ///
621 /// This matches transactions based on each rejection list's matching rule.
622 pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool {
623 self.rejection_error(txid).is_some()
624 }
625
626 /// Add a transaction that failed download and verification to the rejected list
627 /// if needed, depending on the reason for the failure.
628 pub fn reject_if_needed(&mut self, tx_id: UnminedTxId, e: TransactionDownloadVerifyError) {
629 match e {
630 // Rejecting a transaction already in state would speed up further
631 // download attempts without checking the state. However it would
632 // make the reject list grow forever.
633 //
634 // TODO: revisit after reviewing the rejected list cleanup criteria?
635 // TODO: if we decide to reject it, then we need to pass the block hash
636 // to State::Confirmed. This would require the zs::Response::Transaction
637 // to include the hash, which would need to be implemented.
638 TransactionDownloadVerifyError::InState |
639 // An unknown error in the state service, better do nothing
640 TransactionDownloadVerifyError::StateError(_) |
641 // If download failed, do nothing; the crawler will end up trying to
642 // download it again.
643 TransactionDownloadVerifyError::DownloadFailed(_) |
644 // If it was cancelled then a block was mined, or there was a network
645 // upgrade, etc. No reason to reject it.
646 TransactionDownloadVerifyError::Cancelled => {}
647
648 // Consensus verification failed. Reject transaction to avoid
649 // having to download and verify it again just for it to fail again.
650 TransactionDownloadVerifyError::Invalid { error, .. } => {
651 self.reject(tx_id, ExactTipRejectionError::FailedVerification(error).into())
652 }
653 }
654 }
655
656 /// Remove transactions from the mempool if they have not been mined after a
657 /// specified height, per [ZIP-203].
658 ///
659 /// > Transactions will have a new field, nExpiryHeight, which will set the
660 /// > block height after which transactions will be removed from the mempool
661 /// > if they have not been mined.
662 ///
663 ///
664 /// [ZIP-203]: https://zips.z.cash/zip-0203#specification
665 pub fn remove_expired_transactions(
666 &mut self,
667 tip_height: zebra_chain::block::Height,
668 ) -> HashSet<transaction::Hash> {
669 let mut tx_ids = HashSet::new();
670
671 for (&tx_id, tx) in self.transactions() {
672 if let Some(expiry_height) = tx.transaction.transaction.expiry_height() {
673 if tip_height >= expiry_height {
674 tx_ids.insert(tx_id);
675 }
676 }
677 }
678
679 // expiry height is effecting data, so we match by non-malleable TXID
680 self.verified
681 .remove_all_that(|tx| tx_ids.contains(&tx.transaction.id.mined_id()));
682
683 // also reject it
684 for &id in &tx_ids {
685 self.reject(
686 // It's okay to omit the auth digest here as we know that `reject()` will always
687 // use mined ids for `SameEffectsChainRejectionError`s.
688 UnminedTxId::Legacy(id),
689 SameEffectsChainRejectionError::Expired.into(),
690 );
691 }
692
693 tx_ids
694 }
695
696 /// Check if transaction should be downloaded and/or verified.
697 ///
698 /// If it is already in the mempool (or in its rejected list)
699 /// then it shouldn't be downloaded/verified.
700 pub fn should_download_or_verify(&mut self, txid: UnminedTxId) -> Result<(), MempoolError> {
701 // Check if the transaction is already in the mempool.
702 if self.contains_transaction_exact(&txid.mined_id()) {
703 return Err(MempoolError::InMempool);
704 }
705 if let Some(error) = self.rejection_error(&txid) {
706 return Err(error);
707 }
708 Ok(())
709 }
710
711 /// Update metrics related to the rejected lists.
712 ///
713 /// Must be called every time the rejected lists change.
714 fn update_rejected_metrics(&mut self) {
715 metrics::gauge!("mempool.rejected.transaction.ids",)
716 .set(self.rejected_transaction_count() as f64);
717 // This is just an approximation.
718 // TODO: make it more accurate #2869
719 let item_size = size_of::<(transaction::Hash, SameEffectsTipRejectionError)>();
720 metrics::gauge!("mempool.rejected.transaction.ids.bytes",)
721 .set((self.rejected_transaction_count() * item_size) as f64);
722 }
723}