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
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 #[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 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())
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 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 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 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 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 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 let mut paid_actions = 0;
408
409 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}