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