Remove unused mempool errors (#2831)

* Remove unused mempool storage errors

Preparation for ticket #2819.

Removing these errors means that we don't have to decide
which type of transaction ID match we want for them.

* Remove unused mempool errors, and deduplicate storage errors

* rustfmt
This commit is contained in:
teor 2021-10-07 11:20:38 +10:00 committed by GitHub
parent d1ce8e3e6d
commit a3a4773047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 67 deletions

View File

@ -12,7 +12,7 @@ use crate::BoxError;
#[cfg(any(test, feature = "proptest-impl"))] #[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary; use proptest_derive::Arbitrary;
#[derive(Error, Copy, Clone, Debug, PartialEq)] #[derive(Error, Copy, Clone, Debug, PartialEq, Eq)]
pub enum SubsidyError { pub enum SubsidyError {
#[error("no coinbase transaction in block")] #[error("no coinbase transaction in block")]
NoCoinbase, NoCoinbase,
@ -21,7 +21,7 @@ pub enum SubsidyError {
FoundersRewardNotFound, FoundersRewardNotFound,
} }
#[derive(Error, Clone, Debug, PartialEq)] #[derive(Error, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
pub enum TransactionError { pub enum TransactionError {
#[error("first transaction must be coinbase")] #[error("first transaction must be coinbase")]
@ -128,7 +128,7 @@ impl From<SubsidyError> for BlockError {
} }
} }
#[derive(Error, Clone, Debug, PartialEq)] #[derive(Error, Clone, Debug, PartialEq, Eq)]
pub enum BlockError { pub enum BlockError {
#[error("block contains invalid transactions")] #[error("block contains invalid transactions")]
Transaction(#[from] TransactionError), Transaction(#[from] TransactionError),

View File

@ -24,7 +24,7 @@ use zebra_chain::{
transparent, transparent,
}; };
#[derive(Copy, Clone, Debug, Display, Error, PartialEq)] #[derive(Copy, Clone, Debug, Display, Error, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
/// An Error type representing the error codes returned from zcash_script. /// An Error type representing the error codes returned from zcash_script.
pub enum Error { pub enum Error {

View File

@ -33,6 +33,8 @@ mod tests;
pub use self::crawler::Crawler; pub use self::crawler::Crawler;
pub use self::error::MempoolError; pub use self::error::MempoolError;
pub use self::storage::StorageRejectionError;
#[cfg(test)] #[cfg(test)]
pub use self::storage::tests::unmined_transactions_in_blocks; pub use self::storage::tests::unmined_transactions_in_blocks;
@ -182,8 +184,8 @@ impl Mempool {
if storage.contains(&txid) { if storage.contains(&txid) {
return Err(MempoolError::InMempool); return Err(MempoolError::InMempool);
} }
if storage.contains_rejected(&txid) { if let Some(error) = storage.rejection_error(&txid) {
return Err(MempoolError::Rejected); return Err(error.into());
} }
Ok(()) Ok(())
} }

View File

@ -5,34 +5,24 @@ use thiserror::Error;
#[cfg(any(test, feature = "proptest-impl"))] #[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary; use proptest_derive::Arbitrary;
#[derive(Error, Clone, Debug, PartialEq)] use super::storage::StorageRejectionError;
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
#[allow(dead_code)] #[allow(dead_code)]
pub enum MempoolError { pub enum MempoolError {
#[error("mempool storage has a cached rejection for this transaction")]
Storage(#[from] StorageRejectionError),
#[error("transaction already exists in mempool")] #[error("transaction already exists in mempool")]
InMempool, InMempool,
#[error("transaction did not pass consensus validation")]
Invalid(#[from] zebra_consensus::error::TransactionError),
#[error("transaction is committed in block {0:?}")] #[error("transaction is committed in block {0:?}")]
InBlock(zebra_chain::block::Hash), InBlock(zebra_chain::block::Hash),
#[error("transaction has expired")]
Expired,
#[error("transaction fee is too low for the current mempool state")]
LowFee,
#[error("transaction was not found in mempool")] #[error("transaction was not found in mempool")]
NotInMempool, NotInMempool,
#[error("transaction evicted from the mempool due to size restrictions")]
Excess,
#[error("transaction is in the mempool rejected list")]
Rejected,
/// The transaction hash is already queued, so this request was ignored. /// The transaction hash is already queued, so this request was ignored.
/// ///
/// Another peer has already gossiped the same hash to us, or the mempool crawler has fetched it. /// Another peer has already gossiped the same hash to us, or the mempool crawler has fetched it.
@ -48,13 +38,6 @@ pub enum MempoolError {
#[error("transaction dropped because the queue is full")] #[error("transaction dropped because the queue is full")]
FullQueue, FullQueue,
/// The transaction has a spend conflict with another transaction already in the mempool.
#[error(
"transaction rejected because another transaction in the mempool has already spent some of \
its inputs"
)]
SpendConflict,
#[error("mempool is disabled since synchronization is behind the chain tip")] #[error("mempool is disabled since synchronization is behind the chain tip")]
Disabled, Disabled,
} }

View File

@ -3,38 +3,44 @@ use std::{
hash::Hash, hash::Hash,
}; };
use zebra_chain::{ use thiserror::Error;
block,
transaction::{self, Transaction, UnminedTx, UnminedTxId}, use zebra_chain::transaction::{self, Transaction, UnminedTx, UnminedTxId};
};
use zebra_consensus::error::TransactionError;
use super::MempoolError; use super::MempoolError;
#[cfg(any(test, feature = "proptest-impl"))]
use proptest_derive::Arbitrary;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
const MEMPOOL_SIZE: usize = 2; const MEMPOOL_SIZE: usize = 2;
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Clone, Debug)] pub enum StorageRejectionError {
pub enum State { #[error(
/// Rejected because verification failed. "transaction rejected because another transaction in the mempool has already spent some of \
Invalid(TransactionError), its inputs"
/// An otherwise valid mempool transaction was mined into a block, therefore )]
/// no longer belongs in the mempool.
Confirmed(block::Hash),
/// Rejected because it has a spend conflict with another transaction already in the mempool.
SpendConflict, SpendConflict,
/// Stayed in mempool for too long without being mined.
// TODO(2021-09-09): Implement ZIP-203: Validate Transaction Expiry Height. #[error("best chain tip has reached transaction expiry height")]
// TODO(2021-09-09): https://github.com/ZcashFoundation/zebra/issues/2387
Expired, Expired,
/// Transaction fee is too low for the current mempool state.
LowFee, /// Otherwise valid transaction removed from mempool due to ZIP-401 random eviction.
/// Otherwise valid transaction removed from mempool, say because of FIFO ///
/// (first in, first out) policy. /// Consensus rule:
Excess, /// > The txid (rather than the wtxid ...) is used even for version 5 transactions
///
/// https://zips.z.cash/zip-0401#specification
#[error("transaction evicted from the mempool due to ZIP-401 denial of service limits")]
RandomlyEvicted,
#[error("transaction did not pass consensus validation")]
FailedVerification(#[from] zebra_consensus::error::TransactionError),
} }
#[derive(Default)] #[derive(Default)]
@ -43,27 +49,20 @@ pub struct Storage {
/// cache of size [`MEMPOOL_SIZE`]. /// cache of size [`MEMPOOL_SIZE`].
verified: VecDeque<UnminedTx>, verified: VecDeque<UnminedTx>,
/// The set of rejected transactions by id, and their rejection reasons. /// The set of rejected transactions by id, and their rejection reasons.
rejected: HashMap<UnminedTxId, State>, rejected: HashMap<UnminedTxId, StorageRejectionError>,
} }
impl Storage { impl Storage {
/// Insert a [`UnminedTx`] into the mempool. /// Insert a [`UnminedTx`] into the mempool.
/// ///
/// If its insertion results in evicting other transactions, they will be tracked /// If its insertion results in evicting other transactions, they will be tracked
/// as [`State::Excess`]. /// as [`StorageRejectionError::RandomlyEvicted`].
pub fn insert(&mut self, tx: UnminedTx) -> Result<UnminedTxId, MempoolError> { pub fn insert(&mut self, tx: UnminedTx) -> Result<UnminedTxId, MempoolError> {
let tx_id = tx.id; let tx_id = tx.id;
// First, check if we should reject this transaction. // First, check if we have a cached rejection for this transaction.
if self.rejected.contains_key(&tx.id) { if let Some(error) = self.rejection_error(&tx.id) {
return Err(match self.rejected.get(&tx.id).unwrap() { return Err(error.into());
State::Invalid(e) => MempoolError::Invalid(e.clone()),
State::Expired => MempoolError::Expired,
State::Confirmed(block_hash) => MempoolError::InBlock(*block_hash),
State::Excess => MempoolError::Excess,
State::LowFee => MempoolError::LowFee,
State::SpendConflict => MempoolError::SpendConflict,
});
} }
// If `tx` is already in the mempool, we don't change anything. // If `tx` is already in the mempool, we don't change anything.
@ -78,8 +77,9 @@ impl Storage {
// nullifier already revealed by another transaction in the mempool, reject that // nullifier already revealed by another transaction in the mempool, reject that
// transaction. // transaction.
if self.check_spend_conflicts(&tx) { if self.check_spend_conflicts(&tx) {
self.rejected.insert(tx.id, State::SpendConflict); self.rejected
return Err(MempoolError::Rejected); .insert(tx.id, StorageRejectionError::SpendConflict);
return Err(StorageRejectionError::SpendConflict.into());
} }
// Then, we insert into the pool. // Then, we insert into the pool.
@ -87,9 +87,13 @@ impl Storage {
// Once inserted, we evict transactions over the pool size limit in FIFO // Once inserted, we evict transactions over the pool size limit in FIFO
// order. // order.
//
// TODO: use random weighted eviction as specified in ZIP-401 (#2780)
if self.verified.len() > MEMPOOL_SIZE { if self.verified.len() > MEMPOOL_SIZE {
for evicted_tx in self.verified.drain(MEMPOOL_SIZE..) { for evicted_tx in self.verified.drain(MEMPOOL_SIZE..) {
let _ = self.rejected.insert(evicted_tx.id, State::Excess); let _ = self
.rejected
.insert(evicted_tx.id, StorageRejectionError::RandomlyEvicted);
} }
assert_eq!(self.verified.len(), MEMPOOL_SIZE); assert_eq!(self.verified.len(), MEMPOOL_SIZE);
@ -170,10 +174,17 @@ impl Storage {
/// 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.
#[allow(dead_code)]
pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool { pub fn contains_rejected(&self, txid: &UnminedTxId) -> bool {
self.rejected.contains_key(txid) self.rejected.contains_key(txid)
} }
/// Returns `true` if a [`UnminedTx`] matching an [`UnminedTxId`] is in
/// the mempool rejected list.
pub fn rejection_error(&self, txid: &UnminedTxId) -> Option<StorageRejectionError> {
self.rejected.get(txid).cloned()
}
/// Returns the set of [`UnminedTxId`]s matching ids in the rejected list. /// Returns the set of [`UnminedTxId`]s matching ids in the rejected list.
pub fn rejected_transactions(&self, tx_ids: HashSet<UnminedTxId>) -> Vec<UnminedTxId> { pub fn rejected_transactions(&self, tx_ids: HashSet<UnminedTxId>) -> Vec<UnminedTxId> {
tx_ids tx_ids

View File

@ -11,6 +11,8 @@ use zebra_chain::{
transparent, LedgerState, transparent, LedgerState,
}; };
use crate::components::mempool::storage::StorageRejectionError;
use super::super::{MempoolError, Storage}; use super::super::{MempoolError, Storage};
proptest! { proptest! {
@ -40,7 +42,7 @@ proptest! {
assert_eq!( assert_eq!(
storage.insert(transaction_to_reject), storage.insert(transaction_to_reject),
Err(MempoolError::Rejected) Err(MempoolError::Storage(StorageRejectionError::SpendConflict))
); );
assert!(storage.contains_rejected(&id_to_reject)); assert!(storage.contains_rejected(&id_to_reject));

View File

@ -229,7 +229,12 @@ async fn mempool_queue() -> Result<(), Report> {
_ => unreachable!("will never happen in this test"), _ => unreachable!("will never happen in this test"),
}; };
assert_eq!(queued_responses.len(), 1); assert_eq!(queued_responses.len(), 1);
assert_eq!(queued_responses[0], Err(MempoolError::Rejected)); assert_eq!(
queued_responses[0],
Err(MempoolError::Storage(
StorageRejectionError::RandomlyEvicted
))
);
Ok(()) Ok(())
} }