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        self.spent_outpoints.extend(tx.spent_outpoints());
185        self.sprout_nullifiers.extend(tx.sprout_nullifiers());
186        self.sapling_nullifiers.extend(tx.sapling_nullifiers());
187        self.orchard_nullifiers.extend(tx.orchard_nullifiers());
188        self.ironwood_nullifiers.extend(tx.ironwood_nullifiers());
189
190        self.transactions_serialized_size += transaction.transaction.size;
191        self.total_cost += transaction.cost();
192        transaction.time = Some(chrono::Utc::now());
193        transaction.height = height;
194        self.transactions.insert(tx_id, transaction);
195
196        self.update_metrics();
197
198        Ok(())
199    }
200
201    /// Evict one transaction and any transactions that directly or indirectly depend on
202    /// its outputs from the set, returns the victim transaction and any dependent transactions.
203    ///
204    /// Removes a transaction with probability in direct proportion to the
205    /// eviction weight, as per [ZIP-401].
206    ///
207    /// Consensus rule:
208    ///
209    /// > Each transaction also has an eviction weight, which is cost +
210    /// > low_fee_penalty, where low_fee_penalty is 16000 if the transaction pays
211    /// > a fee less than the conventional fee, otherwise 0. The conventional fee
212    /// > is currently defined as 1000 zatoshis
213    ///
214    /// # Note
215    ///
216    /// Collecting and calculating weights is O(n). But in practice n is limited
217    /// to 20,000 (mempooltxcostlimit/min(cost)), so the actual cost shouldn't
218    /// be too bad.
219    ///
220    /// This function is equivalent to `EvictTransaction` in [ZIP-401].
221    ///
222    /// [ZIP-401]: https://zips.z.cash/zip-0401
223    #[allow(clippy::unwrap_in_result)]
224    pub fn evict_one(&mut self) -> Option<VerifiedUnminedTx> {
225        use rand::distributions::{Distribution, WeightedIndex};
226        use rand::prelude::thread_rng;
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 thread_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().copied())
328            || Self::has_conflicts(&self.sapling_nullifiers, tx.sapling_nullifiers().copied())
329            || Self::has_conflicts(&self.orchard_nullifiers, tx.orchard_nullifiers().copied())
330            // `ironwood_nullifiers()` already yields owned `ironwood::Nullifier`s.
331            || Self::has_conflicts(&self.ironwood_nullifiers, tx.ironwood_nullifiers())
332    }
333
334    /// Removes the tracked transaction outputs from the mempool.
335    fn remove_outputs(&mut self, unmined_tx: &UnminedTx) {
336        let tx = &unmined_tx.transaction;
337
338        for index in 0..tx.outputs().len() {
339            self.created_outputs
340                .remove(&transparent::OutPoint::from_usize(
341                    unmined_tx.id.mined_id(),
342                    index,
343                ));
344        }
345
346        let spent_outpoints = tx.spent_outpoints().map(Cow::Owned);
347        let sprout_nullifiers = tx.sprout_nullifiers().map(Cow::Borrowed);
348        let sapling_nullifiers = tx.sapling_nullifiers().map(Cow::Borrowed);
349        let orchard_nullifiers = tx.orchard_nullifiers().map(Cow::Borrowed);
350        // `ironwood_nullifiers()` yields owned `ironwood::Nullifier`s, so wrap them as `Cow::Owned`.
351        let ironwood_nullifiers = tx.ironwood_nullifiers().map(Cow::Owned);
352
353        Self::remove_from_set(&mut self.spent_outpoints, spent_outpoints);
354        Self::remove_from_set(&mut self.sprout_nullifiers, sprout_nullifiers);
355        Self::remove_from_set(&mut self.sapling_nullifiers, sapling_nullifiers);
356        Self::remove_from_set(&mut self.orchard_nullifiers, orchard_nullifiers);
357        Self::remove_from_set(&mut self.ironwood_nullifiers, ironwood_nullifiers);
358    }
359
360    /// Returns `true` if the two sets have common items.
361    fn has_conflicts<T>(set: &HashSet<T>, mut list: impl Iterator<Item = T>) -> bool
362    where
363        T: Eq + Hash,
364    {
365        list.any(|item| set.contains(&item))
366    }
367
368    /// Removes some items from a [`HashSet`].
369    ///
370    /// Each item in the list of `items` should be wrapped in a [`Cow`]. This allows this generic
371    /// method to support both borrowed and owned items.
372    fn remove_from_set<'t, T>(set: &mut HashSet<T>, items: impl IntoIterator<Item = Cow<'t, T>>)
373    where
374        T: Clone + Eq + Hash + 't,
375    {
376        for item in items {
377            set.remove(&item);
378        }
379    }
380
381    fn update_metrics(&mut self) {
382        // Track the sum of unpaid actions within each transaction (as they are subject to the
383        // unpaid action limit). Transactions that have weight >= 1 have no unpaid actions by
384        // definition.
385        let mut unpaid_actions_with_weight_lt20pct = 0;
386        let mut unpaid_actions_with_weight_lt40pct = 0;
387        let mut unpaid_actions_with_weight_lt60pct = 0;
388        let mut unpaid_actions_with_weight_lt80pct = 0;
389        let mut unpaid_actions_with_weight_lt1 = 0;
390
391        // Track the total number of paid actions across all transactions in the mempool. This
392        // added to the bucketed unpaid actions above is equal to the total number of conventional
393        // actions in the mempool.
394        let mut paid_actions = 0;
395
396        // Track the sum of transaction sizes (the metric by which they are mainly limited) across
397        // several buckets.
398        let mut size_with_weight_lt1 = 0;
399        let mut size_with_weight_eq1 = 0;
400        let mut size_with_weight_gt1 = 0;
401        let mut size_with_weight_gt2 = 0;
402        let mut size_with_weight_gt3 = 0;
403
404        for entry in self.transactions().values() {
405            paid_actions += entry.conventional_actions - entry.unpaid_actions;
406
407            if entry.fee_weight_ratio > 3.0 {
408                size_with_weight_gt3 += entry.transaction.size;
409            } else if entry.fee_weight_ratio > 2.0 {
410                size_with_weight_gt2 += entry.transaction.size;
411            } else if entry.fee_weight_ratio > 1.0 {
412                size_with_weight_gt1 += entry.transaction.size;
413            } else if entry.fee_weight_ratio == 1.0 {
414                size_with_weight_eq1 += entry.transaction.size;
415            } else {
416                size_with_weight_lt1 += entry.transaction.size;
417                if entry.fee_weight_ratio < 0.2 {
418                    unpaid_actions_with_weight_lt20pct += entry.unpaid_actions;
419                } else if entry.fee_weight_ratio < 0.4 {
420                    unpaid_actions_with_weight_lt40pct += entry.unpaid_actions;
421                } else if entry.fee_weight_ratio < 0.6 {
422                    unpaid_actions_with_weight_lt60pct += entry.unpaid_actions;
423                } else if entry.fee_weight_ratio < 0.8 {
424                    unpaid_actions_with_weight_lt80pct += entry.unpaid_actions;
425                } else {
426                    unpaid_actions_with_weight_lt1 += entry.unpaid_actions;
427                }
428            }
429        }
430
431        metrics::gauge!(
432            "zcash.mempool.actions.unpaid",
433            "bk" => "< 0.2",
434        )
435        .set(unpaid_actions_with_weight_lt20pct as f64);
436        metrics::gauge!(
437            "zcash.mempool.actions.unpaid",
438            "bk" => "< 0.4",
439        )
440        .set(unpaid_actions_with_weight_lt40pct as f64);
441        metrics::gauge!(
442            "zcash.mempool.actions.unpaid",
443            "bk" => "< 0.6",
444        )
445        .set(unpaid_actions_with_weight_lt60pct as f64);
446        metrics::gauge!(
447            "zcash.mempool.actions.unpaid",
448            "bk" => "< 0.8",
449        )
450        .set(unpaid_actions_with_weight_lt80pct as f64);
451        metrics::gauge!(
452            "zcash.mempool.actions.unpaid",
453            "bk" => "< 1",
454        )
455        .set(unpaid_actions_with_weight_lt1 as f64);
456        metrics::gauge!("zcash.mempool.actions.paid").set(paid_actions as f64);
457        metrics::gauge!("zcash.mempool.size.transactions",).set(self.transaction_count() as f64);
458        metrics::gauge!(
459            "zcash.mempool.size.weighted",
460            "bk" => "< 1",
461        )
462        .set(size_with_weight_lt1 as f64);
463        metrics::gauge!(
464            "zcash.mempool.size.weighted",
465            "bk" => "1",
466        )
467        .set(size_with_weight_eq1 as f64);
468        metrics::gauge!(
469            "zcash.mempool.size.weighted",
470            "bk" => "> 1",
471        )
472        .set(size_with_weight_gt1 as f64);
473        metrics::gauge!(
474            "zcash.mempool.size.weighted",
475            "bk" => "> 2",
476        )
477        .set(size_with_weight_gt2 as f64);
478        metrics::gauge!(
479            "zcash.mempool.size.weighted",
480            "bk" => "> 3",
481        )
482        .set(size_with_weight_gt3 as f64);
483        metrics::gauge!("zcash.mempool.size.bytes",).set(self.transactions_serialized_size as f64);
484        metrics::gauge!("zcash.mempool.cost.bytes").set(self.total_cost as f64);
485    }
486}