zebrad/components/mempool/storage/
verified_set.rs1use 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#[allow(unused_imports)]
23use zebra_chain::transaction::MEMPOOL_TRANSACTION_COST_THRESHOLD;
24
25#[derive(Default)]
37pub struct VerifiedSet {
38 transactions: HashMap<transaction::Hash, VerifiedUnminedTx>,
40
41 transaction_dependencies: TransactionDependencies,
44
45 created_outputs: HashMap<transparent::OutPoint, transparent::Output>,
49
50 transactions_serialized_size: usize,
53
54 total_cost: u64,
56
57 spent_outpoints: HashSet<transparent::OutPoint>,
59
60 sprout_nullifiers: HashSet<sprout::Nullifier>,
62
63 sapling_nullifiers: HashSet<sapling::Nullifier>,
65
66 orchard_nullifiers: HashSet<orchard::Nullifier>,
68}
69
70impl Drop for VerifiedSet {
71 fn drop(&mut self) {
72 self.clear()
74 }
75}
76
77impl VerifiedSet {
78 pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
80 &self.transactions
81 }
82
83 pub fn transaction_dependencies(&self) -> &TransactionDependencies {
85 &self.transaction_dependencies
86 }
87
88 pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
91 self.created_outputs.get(outpoint).cloned()
92 }
93
94 pub fn transaction_count(&self) -> usize {
96 self.transactions.len()
97 }
98
99 pub fn total_cost(&self) -> u64 {
103 self.total_cost
104 }
105
106 pub fn total_serialized_size(&self) -> usize {
111 self.transactions_serialized_size
112 }
113
114 pub fn contains(&self, id: &transaction::Hash) -> bool {
117 self.transactions.contains_key(id)
118 }
119
120 pub fn clear(&mut self) {
124 self.transactions.clear();
125 self.transaction_dependencies.clear();
126 self.spent_outpoints.clear();
127 self.sprout_nullifiers.clear();
128 self.sapling_nullifiers.clear();
129 self.orchard_nullifiers.clear();
130 self.created_outputs.clear();
131 self.transactions_serialized_size = 0;
132 self.total_cost = 0;
133 self.update_metrics();
134 }
135
136 pub fn insert(
144 &mut self,
145 mut transaction: VerifiedUnminedTx,
146 spent_mempool_outpoints: Vec<transparent::OutPoint>,
147 pending_outputs: &mut PendingOutputs,
148 height: Option<Height>,
149 ) -> Result<(), SameEffectsTipRejectionError> {
150 if self.has_spend_conflicts(&transaction.transaction) {
151 return Err(SameEffectsTipRejectionError::SpendConflict);
152 }
153
154 for outpoint in &spent_mempool_outpoints {
158 if !self.created_outputs.contains_key(outpoint) {
159 return Err(SameEffectsTipRejectionError::MissingOutput);
160 }
161 }
162
163 let tx_id = transaction.transaction.id.mined_id();
164 self.transaction_dependencies
165 .add(tx_id, spent_mempool_outpoints);
166
167 let tx = &transaction.transaction.transaction;
169 for (index, output) in tx.outputs().iter().cloned().enumerate() {
170 let outpoint = transparent::OutPoint::from_usize(tx_id, index);
171 self.created_outputs.insert(outpoint, output.clone());
172 pending_outputs.respond(&outpoint, output)
173 }
174 self.spent_outpoints.extend(tx.spent_outpoints());
175 self.sprout_nullifiers.extend(tx.sprout_nullifiers());
176 self.sapling_nullifiers.extend(tx.sapling_nullifiers());
177 self.orchard_nullifiers.extend(tx.orchard_nullifiers());
178
179 self.transactions_serialized_size += transaction.transaction.size;
180 self.total_cost += transaction.cost();
181 transaction.time = Some(chrono::Utc::now());
182 transaction.height = height;
183 self.transactions.insert(tx_id, transaction);
184
185 self.update_metrics();
186
187 Ok(())
188 }
189
190 #[allow(clippy::unwrap_in_result)]
213 pub fn evict_one(&mut self) -> Option<VerifiedUnminedTx> {
214 use rand::distributions::{Distribution, WeightedIndex};
215 use rand::prelude::thread_rng;
216
217 let (keys, weights): (Vec<transaction::Hash>, Vec<u64>) = self
218 .transactions
219 .iter()
220 .map(|(&tx_id, tx)| (tx_id, tx.eviction_weight()))
221 .unzip();
222
223 let dist = WeightedIndex::new(weights).expect(
224 "there is at least one weight, all weights are non-negative, and the total is positive",
225 );
226
227 let key_to_remove = keys
228 .get(dist.sample(&mut thread_rng()))
229 .expect("should have a key at every index in the distribution");
230
231 self.remove(key_to_remove).pop()
234 }
235
236 pub fn clear_mined_dependencies(&mut self, mined_ids: &HashSet<transaction::Hash>) {
239 self.transaction_dependencies
240 .clear_mined_dependencies(mined_ids);
241 }
242
243 pub fn remove_all_that(
247 &mut self,
248 predicate: impl Fn(&VerifiedUnminedTx) -> bool,
249 ) -> HashSet<UnminedTxId> {
250 let keys_to_remove: Vec<_> = self
251 .transactions
252 .iter()
253 .filter_map(|(&tx_id, tx)| predicate(tx).then_some(tx_id))
254 .collect();
255
256 let mut removed_transactions = HashSet::new();
257
258 for key_to_remove in keys_to_remove {
259 removed_transactions.extend(
260 self.remove(&key_to_remove)
261 .into_iter()
262 .map(|tx| tx.transaction.id),
263 );
264 }
265
266 removed_transactions
267 }
268
269 fn remove(&mut self, key_to_remove: &transaction::Hash) -> Vec<VerifiedUnminedTx> {
279 let removed_transactions: Vec<_> = self
280 .transaction_dependencies
281 .remove_all(key_to_remove)
282 .iter()
283 .chain(std::iter::once(key_to_remove))
284 .map(|key_to_remove| {
285 let removed_tx = self
286 .transactions
287 .remove(key_to_remove)
288 .expect("invalid transaction key");
289
290 self.transactions_serialized_size -= removed_tx.transaction.size;
291 self.total_cost -= removed_tx.cost();
292 self.remove_outputs(&removed_tx.transaction);
293
294 removed_tx
295 })
296 .collect();
297
298 self.update_metrics();
299 removed_transactions
300 }
301
302 fn has_spend_conflicts(&self, unmined_tx: &UnminedTx) -> bool {
308 let tx = &unmined_tx.transaction;
309
310 Self::has_conflicts(&self.spent_outpoints, tx.spent_outpoints())
311 || Self::has_conflicts(&self.sprout_nullifiers, tx.sprout_nullifiers().copied())
312 || Self::has_conflicts(&self.sapling_nullifiers, tx.sapling_nullifiers().copied())
313 || Self::has_conflicts(&self.orchard_nullifiers, tx.orchard_nullifiers().copied())
314 }
315
316 fn remove_outputs(&mut self, unmined_tx: &UnminedTx) {
318 let tx = &unmined_tx.transaction;
319
320 for index in 0..tx.outputs().len() {
321 self.created_outputs
322 .remove(&transparent::OutPoint::from_usize(
323 unmined_tx.id.mined_id(),
324 index,
325 ));
326 }
327
328 let spent_outpoints = tx.spent_outpoints().map(Cow::Owned);
329 let sprout_nullifiers = tx.sprout_nullifiers().map(Cow::Borrowed);
330 let sapling_nullifiers = tx.sapling_nullifiers().map(Cow::Borrowed);
331 let orchard_nullifiers = tx.orchard_nullifiers().map(Cow::Borrowed);
332
333 Self::remove_from_set(&mut self.spent_outpoints, spent_outpoints);
334 Self::remove_from_set(&mut self.sprout_nullifiers, sprout_nullifiers);
335 Self::remove_from_set(&mut self.sapling_nullifiers, sapling_nullifiers);
336 Self::remove_from_set(&mut self.orchard_nullifiers, orchard_nullifiers);
337 }
338
339 fn has_conflicts<T>(set: &HashSet<T>, mut list: impl Iterator<Item = T>) -> bool
341 where
342 T: Eq + Hash,
343 {
344 list.any(|item| set.contains(&item))
345 }
346
347 fn remove_from_set<'t, T>(set: &mut HashSet<T>, items: impl IntoIterator<Item = Cow<'t, T>>)
352 where
353 T: Clone + Eq + Hash + 't,
354 {
355 for item in items {
356 set.remove(&item);
357 }
358 }
359
360 fn update_metrics(&mut self) {
361 let mut unpaid_actions_with_weight_lt20pct = 0;
365 let mut unpaid_actions_with_weight_lt40pct = 0;
366 let mut unpaid_actions_with_weight_lt60pct = 0;
367 let mut unpaid_actions_with_weight_lt80pct = 0;
368 let mut unpaid_actions_with_weight_lt1 = 0;
369
370 let mut paid_actions = 0;
374
375 let mut size_with_weight_lt1 = 0;
378 let mut size_with_weight_eq1 = 0;
379 let mut size_with_weight_gt1 = 0;
380 let mut size_with_weight_gt2 = 0;
381 let mut size_with_weight_gt3 = 0;
382
383 for entry in self.transactions().values() {
384 paid_actions += entry.conventional_actions - entry.unpaid_actions;
385
386 if entry.fee_weight_ratio > 3.0 {
387 size_with_weight_gt3 += entry.transaction.size;
388 } else if entry.fee_weight_ratio > 2.0 {
389 size_with_weight_gt2 += entry.transaction.size;
390 } else if entry.fee_weight_ratio > 1.0 {
391 size_with_weight_gt1 += entry.transaction.size;
392 } else if entry.fee_weight_ratio == 1.0 {
393 size_with_weight_eq1 += entry.transaction.size;
394 } else {
395 size_with_weight_lt1 += entry.transaction.size;
396 if entry.fee_weight_ratio < 0.2 {
397 unpaid_actions_with_weight_lt20pct += entry.unpaid_actions;
398 } else if entry.fee_weight_ratio < 0.4 {
399 unpaid_actions_with_weight_lt40pct += entry.unpaid_actions;
400 } else if entry.fee_weight_ratio < 0.6 {
401 unpaid_actions_with_weight_lt60pct += entry.unpaid_actions;
402 } else if entry.fee_weight_ratio < 0.8 {
403 unpaid_actions_with_weight_lt80pct += entry.unpaid_actions;
404 } else {
405 unpaid_actions_with_weight_lt1 += entry.unpaid_actions;
406 }
407 }
408 }
409
410 metrics::gauge!(
411 "zcash.mempool.actions.unpaid",
412 "bk" => "< 0.2",
413 )
414 .set(unpaid_actions_with_weight_lt20pct as f64);
415 metrics::gauge!(
416 "zcash.mempool.actions.unpaid",
417 "bk" => "< 0.4",
418 )
419 .set(unpaid_actions_with_weight_lt40pct as f64);
420 metrics::gauge!(
421 "zcash.mempool.actions.unpaid",
422 "bk" => "< 0.6",
423 )
424 .set(unpaid_actions_with_weight_lt60pct as f64);
425 metrics::gauge!(
426 "zcash.mempool.actions.unpaid",
427 "bk" => "< 0.8",
428 )
429 .set(unpaid_actions_with_weight_lt80pct as f64);
430 metrics::gauge!(
431 "zcash.mempool.actions.unpaid",
432 "bk" => "< 1",
433 )
434 .set(unpaid_actions_with_weight_lt1 as f64);
435 metrics::gauge!("zcash.mempool.actions.paid").set(paid_actions as f64);
436 metrics::gauge!("zcash.mempool.size.transactions",).set(self.transaction_count() as f64);
437 metrics::gauge!(
438 "zcash.mempool.size.weighted",
439 "bk" => "< 1",
440 )
441 .set(size_with_weight_lt1 as f64);
442 metrics::gauge!(
443 "zcash.mempool.size.weighted",
444 "bk" => "1",
445 )
446 .set(size_with_weight_eq1 as f64);
447 metrics::gauge!(
448 "zcash.mempool.size.weighted",
449 "bk" => "> 1",
450 )
451 .set(size_with_weight_gt1 as f64);
452 metrics::gauge!(
453 "zcash.mempool.size.weighted",
454 "bk" => "> 2",
455 )
456 .set(size_with_weight_gt2 as f64);
457 metrics::gauge!(
458 "zcash.mempool.size.weighted",
459 "bk" => "> 3",
460 )
461 .set(size_with_weight_gt3 as f64);
462 metrics::gauge!("zcash.mempool.size.bytes",).set(self.transactions_serialized_size as f64);
463 metrics::gauge!("zcash.mempool.cost.bytes").set(self.total_cost as f64);
464 }
465}