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 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#[allow(unused_imports)]
23use zebra_chain::transaction::MEMPOOL_TRANSACTION_COST_THRESHOLD;
24
25#[derive(Default)]
38pub struct VerifiedSet {
39 transactions: HashMap<transaction::Hash, VerifiedUnminedTx>,
41
42 transaction_dependencies: TransactionDependencies,
45
46 created_outputs: HashMap<transparent::OutPoint, transparent::Output>,
50
51 transactions_serialized_size: usize,
54
55 total_cost: u64,
57
58 spent_outpoints: HashSet<transparent::OutPoint>,
60
61 sprout_nullifiers: HashSet<sprout::Nullifier>,
63
64 sapling_nullifiers: HashSet<sapling::Nullifier>,
66
67 orchard_nullifiers: HashSet<orchard::Nullifier>,
69
70 ironwood_nullifiers: HashSet<ironwood::Nullifier>,
72}
73
74impl Drop for VerifiedSet {
75 fn drop(&mut self) {
76 self.clear()
78 }
79}
80
81impl VerifiedSet {
82 pub fn transactions(&self) -> &HashMap<transaction::Hash, VerifiedUnminedTx> {
84 &self.transactions
85 }
86
87 pub fn transaction_dependencies(&self) -> &TransactionDependencies {
89 &self.transaction_dependencies
90 }
91
92 pub fn created_output(&self, outpoint: &transparent::OutPoint) -> Option<transparent::Output> {
95 self.created_outputs.get(outpoint).cloned()
96 }
97
98 pub fn has_spent_outpoint(&self, outpoint: &transparent::OutPoint) -> bool {
100 self.spent_outpoints.contains(outpoint)
101 }
102
103 pub fn transaction_count(&self) -> usize {
105 self.transactions.len()
106 }
107
108 pub fn total_cost(&self) -> u64 {
112 self.total_cost
113 }
114
115 pub fn total_serialized_size(&self) -> usize {
120 self.transactions_serialized_size
121 }
122
123 pub fn contains(&self, id: &transaction::Hash) -> bool {
126 self.transactions.contains_key(id)
127 }
128
129 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 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 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 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 #[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 self.remove(key_to_remove).pop()
245 }
246
247 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 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 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 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 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 || Self::has_conflicts(&self.ironwood_nullifiers, tx.ironwood_nullifiers())
332 }
333
334 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 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 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 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 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 let mut paid_actions = 0;
395
396 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}