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 has_spent_outpoint(&self, outpoint: &transparent::OutPoint) -> bool {
96 self.spent_outpoints.contains(outpoint)
97 }
98
99 pub fn transaction_count(&self) -> usize {
101 self.transactions.len()
102 }
103
104 pub fn total_cost(&self) -> u64 {
108 self.total_cost
109 }
110
111 pub fn total_serialized_size(&self) -> usize {
116 self.transactions_serialized_size
117 }
118
119 pub fn contains(&self, id: &transaction::Hash) -> bool {
122 self.transactions.contains_key(id)
123 }
124
125 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 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 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 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 #[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 self.remove(key_to_remove).pop()
239 }
240
241 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 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 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 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 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 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 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 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 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 let mut paid_actions = 0;
384
385 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}