Remove transactions in newly committed blocks from the mempool (#2827)

* Add a mempool transaction removal method for mined IDs

And use this method to remove expired transactions,
because all transactions with the same mined ID expire at the same height.

* Remove mined transaction IDs from the mempool

Co-authored-by: Conrado Gouvea <conrado@zfnd.org>
This commit is contained in:
teor 2021-10-07 09:45:14 +10:00 committed by GitHub
parent f1718f5c92
commit d1ce8e3e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 29 deletions

View File

@ -210,11 +210,12 @@ impl Service<Request> for Mempool {
storage.clear(); storage.clear();
tx_downloads.cancel_all(); tx_downloads.cancel_all();
} }
// Cancel downloads/verifications of transactions with the same // Cancel downloads/verifications/storage of transactions
// IDs as recently mined transactions. // with the same mined IDs as recently mined transactions.
TipAction::Grow { block } => { TipAction::Grow { block } => {
let txid_set = block.transaction_hashes.iter().collect(); let mined_ids = block.transaction_hashes.iter().cloned().collect();
tx_downloads.cancel(txid_set); tx_downloads.cancel(&mined_ids);
storage.remove_same_effects(&mined_ids);
} }
} }
} }
@ -309,14 +310,16 @@ fn remove_expired_transactions(
storage: &mut storage::Storage, storage: &mut storage::Storage,
tip_height: zebra_chain::block::Height, tip_height: zebra_chain::block::Height,
) { ) {
let ids = storage.tx_ids().iter().copied().collect(); let mut txid_set = HashSet::new();
let transactions = storage.transactions(ids);
for t in transactions { for t in storage.transactions_all() {
if let Some(expiry_height) = t.transaction.expiry_height() { if let Some(expiry_height) = t.transaction.expiry_height() {
if tip_height >= expiry_height { if tip_height >= expiry_height {
storage.remove(&t.id); txid_set.insert(t.id.mined_id());
} }
} }
} }
// expiry height is effecting data, so we match by non-malleable TXID
storage.remove_same_effects(&txid_set);
} }

View File

@ -317,7 +317,7 @@ where
/// Cancel download/verification tasks of transactions with the /// Cancel download/verification tasks of transactions with the
/// given transaction hash (see [`UnminedTxId::mined_id`]). /// given transaction hash (see [`UnminedTxId::mined_id`]).
pub fn cancel(&mut self, mined_ids: HashSet<&transaction::Hash>) { pub fn cancel(&mut self, mined_ids: &HashSet<transaction::Hash>) {
// TODO: this can be simplified with [`HashMap::drain_filter`] which // TODO: this can be simplified with [`HashMap::drain_filter`] which
// is currently nightly-only experimental API. // is currently nightly-only experimental API.
let removed_txids: Vec<UnminedTxId> = self let removed_txids: Vec<UnminedTxId> = self

View File

@ -5,7 +5,7 @@ use std::{
use zebra_chain::{ use zebra_chain::{
block, block,
transaction::{Transaction, UnminedTx, UnminedTxId}, transaction::{self, Transaction, UnminedTx, UnminedTxId},
}; };
use zebra_consensus::error::TransactionError; use zebra_consensus::error::TransactionError;
@ -104,24 +104,49 @@ impl Storage {
self.verified.iter().any(|tx| &tx.id == txid) self.verified.iter().any(|tx| &tx.id == txid)
} }
/// Remove a [`UnminedTx`] from the mempool via [`UnminedTxId`]. Returns /// Remove [`UnminedTx`]es from the mempool via exact [`UnminedTxId`].
/// whether the transaction was present.
/// ///
/// Removes from the 'verified' set, does not remove from the 'rejected' /// For v5 transactions, transactions are matched by WTXID, using both the:
/// tracking set, if present. Maintains the order in which the other unmined /// - non-malleable transaction ID, and
/// transactions have been inserted into the mempool. /// - authorizing data hash.
pub fn remove(&mut self, txid: &UnminedTxId) -> Option<UnminedTx> { ///
// If the txid exists in the verified set and is then deleted, /// This matches the exact transaction, with identical blockchain effects, signatures, and proofs.
// `retain()` removes it and returns `Some(UnminedTx)`. If it's not ///
// present and nothing changes, returns `None`. /// Returns the number of transactions which were removed.
///
/// Removes from the 'verified' set, if present.
/// Maintains the order in which the other unmined transactions have been inserted into the mempool.
///
/// Does not add or remove from the 'rejected' tracking set.
#[allow(dead_code)]
pub fn remove_exact(&mut self, exact_wtxids: &HashSet<UnminedTxId>) -> usize {
let original_size = self.verified.len();
match self.verified.clone().iter().find(|tx| &tx.id == txid) { self.verified.retain(|tx| !exact_wtxids.contains(&tx.id));
Some(tx) => {
self.verified.retain(|tx| &tx.id != txid); original_size - self.verified.len()
Some(tx.clone()) }
}
None => None, /// Remove [`UnminedTx`]es from the mempool via non-malleable [`transaction::Hash`].
} ///
/// For v5 transactions, transactions are matched by TXID,
/// using only the non-malleable transaction ID.
/// This matches any transaction with the same effect on the blockchain state,
/// even if its signatures and proofs are different.
///
/// Returns the number of transactions which were removed.
///
/// Removes from the 'verified' set, if present.
/// Maintains the order in which the other unmined transactions have been inserted into the mempool.
///
/// Does not add or remove from the 'rejected' tracking set.
pub fn remove_same_effects(&mut self, mined_ids: &HashSet<transaction::Hash>) -> usize {
let original_size = self.verified.len();
self.verified
.retain(|tx| !mined_ids.contains(&tx.id.mined_id()));
original_size - self.verified.len()
} }
/// Returns the set of [`UnminedTxId`]s in the mempool. /// Returns the set of [`UnminedTxId`]s in the mempool.
@ -129,7 +154,7 @@ impl Storage {
self.verified.iter().map(|tx| tx.id).collect() self.verified.iter().map(|tx| tx.id).collect()
} }
/// Returns the set of [`Transaction`]s matching ids in the mempool. /// Returns the set of [`Transaction`]s matching `tx_ids` in the mempool.
pub fn transactions(&self, tx_ids: HashSet<UnminedTxId>) -> Vec<UnminedTx> { pub fn transactions(&self, tx_ids: HashSet<UnminedTxId>) -> Vec<UnminedTx> {
self.verified self.verified
.iter() .iter()
@ -138,6 +163,11 @@ impl Storage {
.collect() .collect()
} }
/// Returns the set of [`Transaction`]s in the mempool.
pub fn transactions_all(&self) -> Vec<UnminedTx> {
self.verified.iter().cloned().collect()
}
/// Returns `true` if a [`UnminedTx`] matching an [`UnminedTxId`] is in /// Returns `true` if a [`UnminedTx`] matching an [`UnminedTxId`] is in
/// the mempool rejected list. /// the mempool rejected list.
pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool { pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool {

View File

@ -1,3 +1,5 @@
use std::iter;
use super::{super::*, unmined_transactions_in_blocks}; use super::{super::*, unmined_transactions_in_blocks};
use zebra_chain::parameters::Network; use zebra_chain::parameters::Network;
@ -5,7 +7,7 @@ use zebra_chain::parameters::Network;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
#[test] #[test]
fn mempool_storage_crud_mainnet() { fn mempool_storage_crud_exact_mainnet() {
zebra_test::init(); zebra_test::init();
let network = Network::Mainnet; let network = Network::Mainnet;
@ -25,9 +27,39 @@ fn mempool_storage_crud_mainnet() {
assert!(storage.contains(&unmined_tx.id)); assert!(storage.contains(&unmined_tx.id));
// Remove tx // Remove tx
let _ = storage.remove(&unmined_tx.id); let removal_count = storage.remove_exact(&iter::once(unmined_tx.id).collect());
// Check that it is /not/ in the mempool. // Check that it is /not/ in the mempool.
assert_eq!(removal_count, 1);
assert!(!storage.contains(&unmined_tx.id));
}
#[test]
fn mempool_storage_crud_same_effects_mainnet() {
zebra_test::init();
let network = Network::Mainnet;
// Create an empty storage instance
let mut storage: Storage = Default::default();
// Get one (1) unmined transaction
let unmined_tx = unmined_transactions_in_blocks(.., network)
.next()
.expect("at least one unmined transaction");
// Insert unmined tx into the mempool.
let _ = storage.insert(unmined_tx.clone());
// Check that it is in the mempool, and not rejected.
assert!(storage.contains(&unmined_tx.id));
// Remove tx
let removal_count =
storage.remove_same_effects(&iter::once(unmined_tx.id.mined_id()).collect());
// Check that it is /not/ in the mempool.
assert_eq!(removal_count, 1);
assert!(!storage.contains(&unmined_tx.id)); assert!(!storage.contains(&unmined_tx.id));
} }