Skip to main content

zebrad/components/mempool/storage/
verified_set.rs

1//! The set of verified transactions in the mempool.
2
3use std::{
4    borrow::Cow,
5    collections::{HashMap, HashSet},
6    hash::Hash,
7};
8
9use zebra_chain::{
10    block::Height,
11    orchard, sapling, sprout,
12    transaction::{self, UnminedTx, UnminedTxId, VerifiedUnminedTx},
13    transparent,
14};
15use zebra_node_services::mempool::TransactionDependencies;
16
17use crate::components::mempool::pending_outputs::PendingOutputs;
18
19use super::super::SameEffectsTipRejectionError;
20
21// Imports for doc links
22#[allow(unused_imports)]
23use zebra_chain::transaction::MEMPOOL_TRANSACTION_COST_THRESHOLD;
24
25/// The set of verified transactions stored in the mempool.
26///
27/// This also caches the all the spent outputs from the transactions in the mempool. The spent
28/// outputs include:
29///
30/// - the dependencies of transactions that spent the outputs of other transactions in the mempool
31/// - the outputs of transactions in the mempool
32/// - the transparent outpoints spent by transactions in the mempool
33/// - the Sprout nullifiers revealed by transactions in the mempool
34/// - the Sapling nullifiers revealed by transactions in the mempool
35/// - the Orchard nullifiers revealed by transactions in the mempool
36#[derive(Default)]
37pub struct VerifiedSet {
38    /// The set of verified transactions in the mempool.
39    transactions: HashMap<transaction::Hash, VerifiedUnminedTx>,
40
41    /// A map of dependencies between transactions in the mempool that
42    /// spend or create outputs of other transactions in the mempool.
43    transaction_dependencies: TransactionDependencies,
44
45    /// The [`transparent::Output`]s created by verified transactions in the mempool.
46    ///
47    /// These outputs may be spent by other transactions in the mempool.
48    created_outputs: HashMap<transparent::OutPoint, transparent::Output>,
49
50    /// The total size of the transactions in the mempool if they were
51    /// serialized.
52    transactions_serialized_size: usize,
53
54    /// The total cost of the verified transactions in the set.
55    total_cost: u64,
56
57    /// The set of spent out points by the verified transactions.
58    spent_outpoints: HashSet<transparent::OutPoint>,
59
60    /// The set of revealed Sprout nullifiers.
61    sprout_nullifiers: HashSet<sprout::Nullifier>,
62
63    /// The set of revealed Sapling nullifiers.
64    sapling_nullifiers: HashSet<sapling::Nullifier>,
65
66    /// The set of revealed Orchard nullifiers.
67    orchard_nullifiers: HashSet<orchard::Nullifier>,
68}
69
70impl Drop for VerifiedSet {
71    fn drop(&mut self) {
72        // zero the metrics on drop
73        self.clear()
74    }
75}
76
77impl VerifiedSet {
78    /// Returns a reference to the [`HashMap`] of [`VerifiedUnminedTx`]s in the set.
79    pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
80        &self.transactions
81    }
82
83    /// Returns a reference to the [`TransactionDependencies`] in the set.
84    pub fn transaction_dependencies(&self) -> &TransactionDependencies {
85        &self.transaction_dependencies
86    }
87
88    /// Returns a [`transparent::Output`] created by a mempool transaction for the provided
89    /// [`transparent::OutPoint`] if one exists, or None otherwise.
90    pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
91        self.created_outputs.get(outpoint).cloned()
92    }
93
94    /// Returns true if a tx in the set has spent the output at the provided outpoint.
95    pub fn has_spent_outpoint(&self, outpoint: &transparent::OutPoint) -> bool {
96        self.spent_outpoints.contains(outpoint)
97    }
98
99    /// Returns the number of verified transactions in the set.
100    pub fn transaction_count(&self) -> usize {
101        self.transactions.len()
102    }
103
104    /// Returns the total cost of the verified transactions in the set.
105    ///
106    /// [ZIP-401]: https://zips.z.cash/zip-0401
107    pub fn total_cost(&self) -> u64 {
108        self.total_cost
109    }
110
111    /// Returns the total serialized size of the verified transactions in the set.
112    ///
113    /// This can be less than the total cost, because the minimum transaction cost
114    /// is based on the [`MEMPOOL_TRANSACTION_COST_THRESHOLD`].
115    pub fn total_serialized_size(&self) -> usize {
116        self.transactions_serialized_size
117    }
118
119    /// Returns `true` if the set of verified transactions contains the transaction with the
120    /// specified [`transaction::Hash`].
121    pub fn contains(&self, id: &transaction::Hash) -> bool {
122        self.transactions.contains_key(id)
123    }
124
125    /// Clear the set of verified transactions.
126    ///
127    /// Also clears all internal caches.
128    pub fn clear(&mut self) {
129        self.transactions.clear();
130        self.transaction_dependencies.clear();
131        self.spent_outpoints.clear();
132        self.sprout_nullifiers.clear();
133        self.sapling_nullifiers.clear();
134        self.orchard_nullifiers.clear();
135        self.created_outputs.clear();
136        self.transactions_serialized_size = 0;
137        self.total_cost = 0;
138        self.update_metrics();
139    }
140
141    /// Insert a `transaction` into the set.
142    ///
143    /// Returns an error if the `transaction` has spend conflicts with any other transaction
144    /// already in the set.
145    ///
146    /// Two transactions have a spend conflict if they spend the same UTXO or if they reveal the
147    /// same nullifier.
148    pub fn insert(
149        &mut self,
150        mut transaction: VerifiedUnminedTx,
151        spent_mempool_outpoints: Vec<transparent::OutPoint>,
152        pending_outputs: &mut PendingOutputs,
153        height: Option<Height>,
154    ) -> Result<(), SameEffectsTipRejectionError> {
155        if self.has_spend_conflicts(&transaction.transaction) {
156            return Err(SameEffectsTipRejectionError::SpendConflict);
157        }
158
159        // This likely only needs to check that the transaction hash of the outpoint is still in the mempool,
160        // but it's likely rare that a transaction spends multiple transparent outputs of
161        // a single transaction in practice.
162        for outpoint in &spent_mempool_outpoints {
163            if !self.created_outputs.contains_key(outpoint) {
164                return Err(SameEffectsTipRejectionError::MissingOutput);
165            }
166        }
167
168        let tx_id = transaction.transaction.id.mined_id();
169        self.transaction_dependencies
170            .add(tx_id, spent_mempool_outpoints);
171
172        // Inserts the transaction's outputs into the internal caches and responds to pending output requests.
173        let tx = &transaction.transaction.transaction;
174        for (index, output) in tx.outputs().iter().cloned().enumerate() {
175            let outpoint = transparent::OutPoint::from_usize(tx_id, index);
176            self.created_outputs.insert(outpoint, output.clone());
177            pending_outputs.respond(&outpoint, output)
178        }
179        self.spent_outpoints.extend(tx.spent_outpoints());
180        self.sprout_nullifiers.extend(tx.sprout_nullifiers());
181        self.sapling_nullifiers.extend(tx.sapling_nullifiers());
182        self.orchard_nullifiers.extend(tx.orchard_nullifiers());
183
184        self.transactions_serialized_size += transaction.transaction.size;
185        self.total_cost += transaction.cost();
186        transaction.time = Some(chrono::Utc::now());
187        transaction.height = height;
188        self.transactions.insert(tx_id, transaction);
189
190        self.update_metrics();
191
192        Ok(())
193    }
194
195    /// Evict one transaction and any transactions that directly or indirectly depend on
196    /// its outputs from the set, returns the victim transaction and any dependent transactions.
197    ///
198    /// Removes a transaction with probability in direct proportion to the
199    /// eviction weight, as per [ZIP-401].
200    ///
201    /// Consensus rule:
202    ///
203    /// > Each transaction also has an eviction weight, which is cost +
204    /// > low_fee_penalty, where low_fee_penalty is 16000 if the transaction pays
205    /// > a fee less than the conventional fee, otherwise 0. The conventional fee
206    /// > is currently defined as 1000 zatoshis
207    ///
208    /// # Note
209    ///
210    /// Collecting and calculating weights is O(n). But in practice n is limited
211    /// to 20,000 (mempooltxcostlimit/min(cost)), so the actual cost shouldn't
212    /// be too bad.
213    ///
214    /// This function is equivalent to `EvictTransaction` in [ZIP-401].
215    ///
216    /// [ZIP-401]: https://zips.z.cash/zip-0401
217    #[allow(clippy::unwrap_in_result)]
218    pub fn evict_one(&mut self) -> Option<VerifiedUnminedTx> {
219        use rand::distributions::{Distribution, WeightedIndex};
220        use rand::prelude::thread_rng;
221
222        let (keys, weights): (Vec<transaction::Hash>, Vec<u64>) = self
223            .transactions
224            .iter()
225            .map(|(&tx_id, tx)| (tx_id, tx.eviction_weight()))
226            .unzip();
227
228        let dist = WeightedIndex::new(weights).expect(
229            "there is at least one weight, all weights are non-negative, and the total is positive",
230        );
231
232        let key_to_remove = keys
233            .get(dist.sample(&mut thread_rng()))
234            .expect("should have a key at every index in the distribution");
235
236        // Removes the randomly selected transaction and all of its dependents from the set,
237        // then returns just the randomly selected transaction
238        self.remove(key_to_remove).pop()
239    }
240
241    /// Clears a list of mined transaction ids from the lists of dependencies for
242    /// any other transactions in the mempool and removes their dependents.
243    pub fn clear_mined_dependencies(&mut self, mined_ids: &HashSet<transaction::Hash>) {
244        self.transaction_dependencies
245            .clear_mined_dependencies(mined_ids);
246    }
247
248    /// Removes all transactions in the set that match the `predicate`.
249    ///
250    /// Returns the amount of transactions removed.
251    pub fn remove_all_that(
252        &mut self,
253        predicate: impl Fn(&VerifiedUnminedTx) -> bool,
254    ) -> HashSet<UnminedTxId> {
255        let keys_to_remove: Vec<_> = self
256            .transactions
257            .iter()
258            .filter_map(|(&tx_id, tx)| predicate(tx).then_some(tx_id))
259            .collect();
260
261        let mut removed_transactions = HashSet::new();
262
263        for key_to_remove in keys_to_remove {
264            if !self.transactions.contains_key(&key_to_remove) {
265                // Skip any keys that may have already been removed as their dependencies were removed.
266                continue;
267            }
268
269            removed_transactions.extend(
270                self.remove(&key_to_remove)
271                    .into_iter()
272                    .map(|tx| tx.transaction.id),
273            );
274        }
275
276        removed_transactions
277    }
278
279    /// Accepts a transaction id for a transaction to remove from the verified set.
280    ///
281    /// Removes the transaction and any transactions that directly or indirectly
282    /// depend on it from the set.
283    ///
284    /// Returns a list of transactions that have been removed with the target transaction
285    /// as the last item.
286    ///
287    /// Also removes the outputs of any removed transactions from the internal caches.
288    fn remove(&mut self, key_to_remove: &transaction::Hash) -> Vec<VerifiedUnminedTx> {
289        let removed_transactions: Vec<_> = self
290            .transaction_dependencies
291            .remove_all(key_to_remove)
292            .iter()
293            .chain(std::iter::once(key_to_remove))
294            .filter_map(|key_to_remove| {
295                let Some(removed_tx) = self.transactions.remove(key_to_remove) else {
296                    tracing::warn!(?key_to_remove, "invalid transaction key");
297                    return None;
298                };
299
300                self.transactions_serialized_size -= removed_tx.transaction.size;
301                self.total_cost -= removed_tx.cost();
302                self.remove_outputs(&removed_tx.transaction);
303
304                Some(removed_tx)
305            })
306            .collect();
307
308        self.update_metrics();
309        removed_transactions
310    }
311
312    /// Returns `true` if the given `transaction` has any spend conflicts with transactions in the
313    /// mempool.
314    ///
315    /// Two transactions have a spend conflict if they spend the same UTXO or if they reveal the
316    /// same nullifier.
317    fn has_spend_conflicts(&self, unmined_tx: &UnminedTx) -> bool {
318        let tx = &unmined_tx.transaction;
319
320        Self::has_conflicts(&self.spent_outpoints, tx.spent_outpoints())
321            || Self::has_conflicts(&self.sprout_nullifiers, tx.sprout_nullifiers().copied())
322            || Self::has_conflicts(&self.sapling_nullifiers, tx.sapling_nullifiers().copied())
323            || Self::has_conflicts(&self.orchard_nullifiers, tx.orchard_nullifiers().copied())
324    }
325
326    /// Removes the tracked transaction outputs from the mempool.
327    fn remove_outputs(&mut self, unmined_tx: &UnminedTx) {
328        let tx = &unmined_tx.transaction;
329
330        for index in 0..tx.outputs().len() {
331            self.created_outputs
332                .remove(&transparent::OutPoint::from_usize(
333                    unmined_tx.id.mined_id(),
334                    index,
335                ));
336        }
337
338        let spent_outpoints = tx.spent_outpoints().map(Cow::Owned);
339        let sprout_nullifiers = tx.sprout_nullifiers().map(Cow::Borrowed);
340        let sapling_nullifiers = tx.sapling_nullifiers().map(Cow::Borrowed);
341        let orchard_nullifiers = tx.orchard_nullifiers().map(Cow::Borrowed);
342
343        Self::remove_from_set(&mut self.spent_outpoints, spent_outpoints);
344        Self::remove_from_set(&mut self.sprout_nullifiers, sprout_nullifiers);
345        Self::remove_from_set(&mut self.sapling_nullifiers, sapling_nullifiers);
346        Self::remove_from_set(&mut self.orchard_nullifiers, orchard_nullifiers);
347    }
348
349    /// Returns `true` if the two sets have common items.
350    fn has_conflicts<T>(set: &HashSet<T>, mut list: impl Iterator<Item = T>) -> bool
351    where
352        T: Eq + Hash,
353    {
354        list.any(|item| set.contains(&item))
355    }
356
357    /// Removes some items from a [`HashSet`].
358    ///
359    /// Each item in the list of `items` should be wrapped in a [`Cow`]. This allows this generic
360    /// method to support both borrowed and owned items.
361    fn remove_from_set<'t, T>(set: &mut HashSet<T>, items: impl IntoIterator<Item = Cow<'t, T>>)
362    where
363        T: Clone + Eq + Hash + 't,
364    {
365        for item in items {
366            set.remove(&item);
367        }
368    }
369
370    fn update_metrics(&mut self) {
371        // Track the sum of unpaid actions within each transaction (as they are subject to the
372        // unpaid action limit). Transactions that have weight >= 1 have no unpaid actions by
373        // definition.
374        let mut unpaid_actions_with_weight_lt20pct = 0;
375        let mut unpaid_actions_with_weight_lt40pct = 0;
376        let mut unpaid_actions_with_weight_lt60pct = 0;
377        let mut unpaid_actions_with_weight_lt80pct = 0;
378        let mut unpaid_actions_with_weight_lt1 = 0;
379
380        // Track the total number of paid actions across all transactions in the mempool. This
381        // added to the bucketed unpaid actions above is equal to the total number of conventional
382        // actions in the mempool.
383        let mut paid_actions = 0;
384
385        // Track the sum of transaction sizes (the metric by which they are mainly limited) across
386        // several buckets.
387        let mut size_with_weight_lt1 = 0;
388        let mut size_with_weight_eq1 = 0;
389        let mut size_with_weight_gt1 = 0;
390        let mut size_with_weight_gt2 = 0;
391        let mut size_with_weight_gt3 = 0;
392
393        for entry in self.transactions().values() {
394            paid_actions += entry.conventional_actions - entry.unpaid_actions;
395
396            if entry.fee_weight_ratio > 3.0 {
397                size_with_weight_gt3 += entry.transaction.size;
398            } else if entry.fee_weight_ratio > 2.0 {
399                size_with_weight_gt2 += entry.transaction.size;
400            } else if entry.fee_weight_ratio > 1.0 {
401                size_with_weight_gt1 += entry.transaction.size;
402            } else if entry.fee_weight_ratio == 1.0 {
403                size_with_weight_eq1 += entry.transaction.size;
404            } else {
405                size_with_weight_lt1 += entry.transaction.size;
406                if entry.fee_weight_ratio < 0.2 {
407                    unpaid_actions_with_weight_lt20pct += entry.unpaid_actions;
408                } else if entry.fee_weight_ratio < 0.4 {
409                    unpaid_actions_with_weight_lt40pct += entry.unpaid_actions;
410                } else if entry.fee_weight_ratio < 0.6 {
411                    unpaid_actions_with_weight_lt60pct += entry.unpaid_actions;
412                } else if entry.fee_weight_ratio < 0.8 {
413                    unpaid_actions_with_weight_lt80pct += entry.unpaid_actions;
414                } else {
415                    unpaid_actions_with_weight_lt1 += entry.unpaid_actions;
416                }
417            }
418        }
419
420        metrics::gauge!(
421            "zcash.mempool.actions.unpaid",
422            "bk" => "< 0.2",
423        )
424        .set(unpaid_actions_with_weight_lt20pct as f64);
425        metrics::gauge!(
426            "zcash.mempool.actions.unpaid",
427            "bk" => "< 0.4",
428        )
429        .set(unpaid_actions_with_weight_lt40pct as f64);
430        metrics::gauge!(
431            "zcash.mempool.actions.unpaid",
432            "bk" => "< 0.6",
433        )
434        .set(unpaid_actions_with_weight_lt60pct as f64);
435        metrics::gauge!(
436            "zcash.mempool.actions.unpaid",
437            "bk" => "< 0.8",
438        )
439        .set(unpaid_actions_with_weight_lt80pct as f64);
440        metrics::gauge!(
441            "zcash.mempool.actions.unpaid",
442            "bk" => "< 1",
443        )
444        .set(unpaid_actions_with_weight_lt1 as f64);
445        metrics::gauge!("zcash.mempool.actions.paid").set(paid_actions as f64);
446        metrics::gauge!("zcash.mempool.size.transactions",).set(self.transaction_count() as f64);
447        metrics::gauge!(
448            "zcash.mempool.size.weighted",
449            "bk" => "< 1",
450        )
451        .set(size_with_weight_lt1 as f64);
452        metrics::gauge!(
453            "zcash.mempool.size.weighted",
454            "bk" => "1",
455        )
456        .set(size_with_weight_eq1 as f64);
457        metrics::gauge!(
458            "zcash.mempool.size.weighted",
459            "bk" => "> 1",
460        )
461        .set(size_with_weight_gt1 as f64);
462        metrics::gauge!(
463            "zcash.mempool.size.weighted",
464            "bk" => "> 2",
465        )
466        .set(size_with_weight_gt2 as f64);
467        metrics::gauge!(
468            "zcash.mempool.size.weighted",
469            "bk" => "> 3",
470        )
471        .set(size_with_weight_gt3 as f64);
472        metrics::gauge!("zcash.mempool.size.bytes",).set(self.transactions_serialized_size as f64);
473        metrics::gauge!("zcash.mempool.cost.bytes").set(self.total_cost as f64);
474    }
475}