zebrad/components/mempool/storage/
eviction_list.rs

1//! [`EvictionList`] represents the transaction eviction list with
2//! efficient operations.
3use std::{
4    collections::{HashMap, VecDeque},
5    time::{Duration, Instant},
6};
7
8use zebra_chain::transaction;
9
10/// An eviction list that allows Zebra to efficiently add entries, get entries,
11/// and remove older entries in the order they were inserted.
12pub struct EvictionList {
13    // Maps each TXID in the list to the most recent instant they were added.
14    unique_entries: HashMap<transaction::Hash, Instant>,
15    // The same as `unique_entries` but in the order they were inserted.
16    ordered_entries: VecDeque<transaction::Hash>,
17    // The maximum size of `unique_entries`.
18    max_size: usize,
19    /// The mempool transaction eviction age limit.
20    /// Same as [`Config::eviction_memory_time`][1].
21    ///
22    /// [1]: super::super::Config::eviction_memory_time
23    eviction_memory_time: Duration,
24}
25
26impl EvictionList {
27    /// Create a new [`EvictionList`] with the given maximum size and
28    /// eviction time.
29    pub fn new(max_size: usize, eviction_memory_time: Duration) -> Self {
30        Self {
31            unique_entries: Default::default(),
32            ordered_entries: Default::default(),
33            max_size,
34            eviction_memory_time,
35        }
36    }
37
38    /// Inserts a TXID in the list, keeping track of the time it was inserted.
39    ///
40    /// All entries older than [`EvictionList::eviction_memory_time`] will be removed.
41    ///
42    /// # Panics
43    ///
44    /// If the TXID is already in the list.
45    ///
46    pub fn insert(&mut self, key: transaction::Hash) {
47        // From https://zips.z.cash/zip-0401#specification:
48        // > Nodes SHOULD remove transactions from RecentlyEvicted that were evicted more than
49        // > mempoolevictionmemoryminutes minutes ago. This MAY be done periodically,
50        // > and/or just before RecentlyEvicted is accessed when receiving a transaction.
51        self.prune_old();
52        // > Add the txid and the current time to RecentlyEvicted, dropping the oldest entry
53        // > in RecentlyEvicted if necessary to keep it to at most eviction_memory_entries entries.
54        if self.len() >= self.max_size {
55            self.pop_front();
56        }
57        let value = Instant::now();
58        let old_value = self.unique_entries.insert(key, value);
59        // It should be impossible for an already-evicted transaction to be evicted
60        // again since transactions are not added to the mempool if they are evicted,
61        // and the mempool doesn't allow inserting two transactions with the same
62        // hash (they would conflict).
63        assert_eq!(
64            old_value, None,
65            "an already-evicted transaction should not be evicted again"
66        );
67        self.ordered_entries.push_back(key)
68    }
69
70    /// Checks if the given TXID is in the list.
71    pub fn contains_key(&self, txid: &transaction::Hash) -> bool {
72        if let Some(evicted_at) = self.unique_entries.get(txid) {
73            // Since the list is pruned only in mutable functions, make sure
74            // we take expired items into account.
75            if !self.has_expired(evicted_at) {
76                return true;
77            }
78        }
79        false
80    }
81
82    /// Get the size of the list.
83    //
84    // Note: if this method being mutable becomes an issue, it's possible
85    // to compute the number of expired transactions and subtract,
86    // at the cost of `O(len + expired)` performance each time the method is called.
87    //
88    // Currently the performance is `O(expired)` for the first call, then `O(1)` until the next expiry.
89    pub fn len(&mut self) -> usize {
90        self.prune_old();
91        self.unique_entries.len()
92    }
93
94    /// Clear the list.
95    #[allow(dead_code)]
96    pub fn clear(&mut self) {
97        self.unique_entries.clear();
98        self.ordered_entries.clear();
99    }
100
101    /// Prune TXIDs that are older than `eviction_time` ago.
102    ///
103    // This method is public because ZIP-401 states about pruning:
104    // > This MAY be done periodically,
105    pub fn prune_old(&mut self) {
106        while let Some(txid) = self.front() {
107            let evicted_at = self
108                .unique_entries
109                .get(txid)
110                .unwrap_or_else(|| panic!("all entries should exist in both ordered_entries and unique_entries, missing {txid:?} in unique_entries"));
111            if self.has_expired(evicted_at) {
112                self.pop_front();
113            } else {
114                break;
115            }
116        }
117    }
118
119    /// Get the oldest TXID in the list.
120    fn front(&self) -> Option<&transaction::Hash> {
121        self.ordered_entries.front()
122    }
123
124    /// Removes the first element and returns it, or `None` if the `EvictionList`
125    /// is empty.
126    fn pop_front(&mut self) -> Option<transaction::Hash> {
127        if let Some(key) = self.ordered_entries.pop_front() {
128            let removed = self.unique_entries.remove(&key);
129            assert!(
130                removed.is_some(),
131                "all entries should exist in both ordered_entries and unique_entries, missing {key:?} in unique_entries"
132            );
133            Some(key)
134        } else {
135            None
136        }
137    }
138
139    /// Returns if `evicted_at` is considered expired considering the current
140    /// time and the configured eviction time.
141    fn has_expired(&self, evicted_at: &Instant) -> bool {
142        let now = Instant::now();
143        (now - *evicted_at) > self.eviction_memory_time
144    }
145}