feat(txpool): enforce account tx capacity (#88)

This commit is contained in:
Matthias Seitz
2022-10-17 20:42:51 +02:00
committed by GitHub
parent 9056b8cbf2
commit 6bc09809f3
3 changed files with 110 additions and 6 deletions

View File

@ -1,6 +1,6 @@
//! Transaction pool errors //! Transaction pool errors
use reth_primitives::{BlockID, TxHash, U256}; use reth_primitives::{Address, BlockID, TxHash, U256};
/// Transaction pool result type. /// Transaction pool result type.
pub type PoolResult<T> = Result<T, PoolError>; pub type PoolResult<T> = Result<T, PoolError>;
@ -14,4 +14,7 @@ pub enum PoolError {
/// Encountered a transaction that was already added into the poll /// Encountered a transaction that was already added into the poll
#[error("[{0:?}] Transaction feeCap {1} below chain minimum.")] #[error("[{0:?}] Transaction feeCap {1} below chain minimum.")]
ProtocolFeeCapTooLow(TxHash, U256), ProtocolFeeCapTooLow(TxHash, U256),
/// Thrown when the number of unique transactions of a sender exceeded the slot capacity.
#[error("{0:?} identified as spammer. Transaction {1:?} rejected.")]
SpammerExceededCapacity(Address, TxHash),
} }

View File

@ -187,6 +187,9 @@ impl<T: TransactionOrdering> TxPool<T> {
Err(InsertErr::ProtocolFeeCapTooLow { transaction, fee_cap }) => { Err(InsertErr::ProtocolFeeCapTooLow { transaction, fee_cap }) => {
Err(PoolError::ProtocolFeeCapTooLow(*transaction.hash(), fee_cap)) Err(PoolError::ProtocolFeeCapTooLow(*transaction.hash(), fee_cap))
} }
Err(InsertErr::ExceededSenderTransactionsCapacity { transaction }) => {
Err(PoolError::SpammerExceededCapacity(*transaction.sender(), *transaction.hash()))
}
} }
} }
@ -447,6 +450,27 @@ impl<T: PoolTransaction> AllTransactions<T> {
self.by_hash.remove(tx.transaction.hash()) self.by_hash.remove(tx.transaction.hash())
} }
/// Additional checks for a new transaction.
///
/// This will enforce all additional rules in the context of this pool, such as:
/// - Spam protection: reject new non-local transaction from a sender that exhausted its slot
/// capacity.
fn ensure_valid(
&self,
transaction: ValidPoolTransaction<T>,
) -> Result<ValidPoolTransaction<T>, InsertErr<T>> {
if !transaction.origin.is_local() {
let current_txs =
self.tx_counter.get(&transaction.sender_id()).copied().unwrap_or_default();
if current_txs >= self.max_account_slots {
return Err(InsertErr::ExceededSenderTransactionsCapacity {
transaction: Arc::new(transaction),
})
}
}
Ok(transaction)
}
/// Inserts a new transaction into the pool. /// Inserts a new transaction into the pool.
/// ///
/// If the transaction already exists, it will be replaced if not underpriced. /// If the transaction already exists, it will be replaced if not underpriced.
@ -465,8 +489,8 @@ impl<T: PoolTransaction> AllTransactions<T> {
) -> InsertResult<T> { ) -> InsertResult<T> {
assert!(on_chain_nonce <= transaction.nonce(), "Invalid transaction"); assert!(on_chain_nonce <= transaction.nonce(), "Invalid transaction");
let transaction = Arc::new(self.ensure_valid(transaction)?);
let tx_id = *transaction.id(); let tx_id = *transaction.id();
let transaction = Arc::new(transaction);
let mut state = TxState::default(); let mut state = TxState::default();
let mut cumulative_cost = U256::zero(); let mut cumulative_cost = U256::zero();
let mut updates = Vec::new(); let mut updates = Vec::new();
@ -634,6 +658,14 @@ impl<T: PoolTransaction> AllTransactions<T> {
} }
} }
#[cfg(test)]
#[allow(missing_docs)]
impl<T: PoolTransaction> AllTransactions<T> {
pub(crate) fn tx_count(&self, sender: SenderId) -> usize {
self.tx_counter.get(&sender).copied().unwrap_or_default()
}
}
impl<T: PoolTransaction> Default for AllTransactions<T> { impl<T: PoolTransaction> Default for AllTransactions<T> {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -682,6 +714,10 @@ pub(crate) enum InsertErr<T: PoolTransaction> {
/// ///
/// See also [`MIN_PROTOCOL_BASE_FEE`] /// See also [`MIN_PROTOCOL_BASE_FEE`]
ProtocolFeeCapTooLow { transaction: Arc<ValidPoolTransaction<T>>, fee_cap: U256 }, ProtocolFeeCapTooLow { transaction: Arc<ValidPoolTransaction<T>>, fee_cap: U256 },
/// Sender currently exceeds the configured limit for max account slots.
///
/// The sender can be considered a spammer at this point.
ExceededSenderTransactionsCapacity { transaction: Arc<ValidPoolTransaction<T>> },
} }
/// Transaction was successfully inserted into the pool /// Transaction was successfully inserted into the pool
@ -796,7 +832,10 @@ impl SenderInfo {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test_util::{MockTransaction, MockTransactionFactory}; use crate::{
test_util::{MockTransaction, MockTransactionFactory},
traits::TransactionOrigin,
};
#[test] #[test]
fn test_simple_insert() { fn test_simple_insert() {
@ -958,4 +997,58 @@ mod tests {
// has non nonce gap // has non nonce gap
assert!(first_in_pool.state.contains(TxState::NO_NONCE_GAPS)); assert!(first_in_pool.state.contains(TxState::NO_NONCE_GAPS));
} }
#[test]
fn rejects_spammer() {
let on_chain_balance = U256::from(1_000);
let on_chain_nonce = 0;
let mut f = MockTransactionFactory::default();
let mut pool = AllTransactions::default();
let mut tx = MockTransaction::eip1559();
for _ in 0..pool.max_account_slots {
tx = tx.next();
pool.insert_tx(f.validated(tx.clone()), on_chain_balance, on_chain_nonce).unwrap();
}
assert_eq!(
pool.max_account_slots,
pool.tx_count(f.ids.sender_id(&tx.get_sender()).unwrap())
);
let err =
pool.insert_tx(f.validated(tx.next()), on_chain_balance, on_chain_nonce).unwrap_err();
assert!(matches!(err, InsertErr::ExceededSenderTransactionsCapacity { .. }));
}
#[test]
fn allow_local_spamming() {
let on_chain_balance = U256::from(1_000);
let on_chain_nonce = 0;
let mut f = MockTransactionFactory::default();
let mut pool = AllTransactions::default();
let mut tx = MockTransaction::eip1559();
for _ in 0..pool.max_account_slots {
tx = tx.next();
pool.insert_tx(
f.validated_with_origin(TransactionOrigin::Local, tx.clone()),
on_chain_balance,
on_chain_nonce,
)
.unwrap();
}
assert_eq!(
pool.max_account_slots,
pool.tx_count(f.ids.sender_id(&tx.get_sender()).unwrap())
);
pool.insert_tx(
f.validated_with_origin(TransactionOrigin::Local, tx.next()),
on_chain_balance,
on_chain_nonce,
)
.unwrap();
}
} }

View File

@ -331,7 +331,7 @@ impl PoolTransaction for MockTransaction {
#[derive(Default)] #[derive(Default)]
pub struct MockTransactionFactory { pub struct MockTransactionFactory {
ids: SenderIdentifiers, pub ids: SenderIdentifiers,
} }
// === impl MockTransactionFactory === // === impl MockTransactionFactory ===
@ -342,8 +342,16 @@ impl MockTransactionFactory {
TransactionId::new(sender, tx.get_nonce()) TransactionId::new(sender, tx.get_nonce())
} }
/// Converts the transaction into a validated transaction
pub fn validated(&mut self, transaction: MockTransaction) -> MockValidTx { pub fn validated(&mut self, transaction: MockTransaction) -> MockValidTx {
self.validated_with_origin(TransactionOrigin::External, transaction)
}
/// Converts the transaction into a validated transaction
pub fn validated_with_origin(
&mut self,
origin: TransactionOrigin,
transaction: MockTransaction,
) -> MockValidTx {
let transaction_id = self.tx_id(&transaction); let transaction_id = self.tx_id(&transaction);
MockValidTx { MockValidTx {
propagate: false, propagate: false,
@ -352,7 +360,7 @@ impl MockTransactionFactory {
cost: transaction.cost(), cost: transaction.cost(),
transaction, transaction,
timestamp: Instant::now(), timestamp: Instant::now(),
origin: TransactionOrigin::External, origin,
} }
} }