feat(txpool): support additional custom validation checks in EthTransactionValidator (#22559)

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Matthias Seitz
2026-02-25 14:32:21 +01:00
committed by GitHub
parent 663765af5c
commit 7103088adc
2 changed files with 144 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
reth-transaction-pool: minor
---
Added support for optional custom stateless and stateful validation hooks in `EthTransactionValidator` via `set_additional_stateless_validation` and `set_additional_stateful_validation` methods. Also implemented a manual `Debug` impl to handle the non-`Debug` function pointer fields.

View File

@@ -36,6 +36,7 @@ use reth_tasks::Runtime;
use revm::context_interface::Cfg;
use revm_primitives::U256;
use std::{
fmt,
marker::PhantomData,
sync::{
atomic::{AtomicBool, AtomicU64, AtomicUsize},
@@ -45,6 +46,23 @@ use std::{
};
use tokio::sync::Mutex;
/// Additional stateless validation function signature.
///
/// Receives the transaction origin and a reference to the transaction. Returns `Ok(())` if the
/// transaction passes or `Err` to reject it.
type StatelessValidationFn<T> =
Arc<dyn Fn(TransactionOrigin, &T) -> Result<(), InvalidPoolTransactionError> + Send + Sync>;
/// Additional stateful validation function signature.
///
/// Receives the transaction origin, a reference to the transaction, and an account state reader.
/// Returns `Ok(())` if the transaction passes or `Err` to reject it.
type StatefulValidationFn<T> = Arc<
dyn Fn(TransactionOrigin, &T, &dyn AccountInfoReader) -> Result<(), InvalidPoolTransactionError>
+ Send
+ Sync,
>;
/// A [`TransactionValidator`] implementation that validates ethereum transaction.
///
/// It supports all known ethereum transaction types:
@@ -59,7 +77,6 @@ use tokio::sync::Mutex;
/// - Maximum gas limit
///
/// And adheres to the configured [`LocalTransactionConfig`].
#[derive(Debug)]
pub struct EthTransactionValidator<Client, T, Evm> {
/// This type fetches account info from the db
client: Client,
@@ -103,6 +120,39 @@ pub struct EthTransactionValidator<Client, T, Evm> {
/// When false, EIP-7594 (v1) sidecars are always rejected and EIP-4844 (v0) sidecars
/// are always accepted, regardless of Osaka fork activation.
eip7594: bool,
/// Optional additional stateless validation check applied at the end of
/// [`validate_stateless`](Self::validate_stateless).
additional_stateless_validation: Option<StatelessValidationFn<T>>,
/// Optional additional stateful validation check applied at the end of
/// [`validate_stateful`](Self::validate_stateful).
additional_stateful_validation: Option<StatefulValidationFn<T>>,
}
impl<Client, Tx, Evm> fmt::Debug for EthTransactionValidator<Client, Tx, Evm> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EthTransactionValidator")
.field("fork_tracker", &self.fork_tracker)
.field("eip2718", &self.eip2718)
.field("eip1559", &self.eip1559)
.field("eip4844", &self.eip4844)
.field("eip7702", &self.eip7702)
.field("block_gas_limit", &self.block_gas_limit)
.field("tx_fee_cap", &self.tx_fee_cap)
.field("minimum_priority_fee", &self.minimum_priority_fee)
.field("max_tx_input_bytes", &self.max_tx_input_bytes)
.field("max_tx_gas_limit", &self.max_tx_gas_limit)
.field("disable_balance_check", &self.disable_balance_check)
.field("eip7594", &self.eip7594)
.field(
"additional_stateless_validation",
&self.additional_stateless_validation.as_ref().map(|_| "..."),
)
.field(
"additional_stateful_validation",
&self.additional_stateful_validation.as_ref().map(|_| "..."),
)
.finish()
}
}
impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm> {
@@ -182,6 +232,78 @@ impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm> {
pub const fn disable_balance_check(&self) -> bool {
self.disable_balance_check
}
/// Sets an additional stateless validation check that is applied at the end of
/// [`validate_stateless`](Self::validate_stateless).
///
/// The check receives the transaction origin and a reference to the transaction, and
/// should return `Ok(())` if the transaction is valid or
/// `Err(InvalidPoolTransactionError)` to reject it.
///
/// # Example
///
/// ```ignore
/// use reth_transaction_pool::{error::InvalidPoolTransactionError, TransactionOrigin};
///
/// let mut validator = builder.build(blob_store);
/// // Reject external transactions with input data exceeding 1KB
/// validator.set_additional_stateless_validation(|origin, tx| {
/// if origin.is_external() && tx.input().len() > 1024 {
/// return Err(InvalidPoolTransactionError::OversizedData {
/// size: tx.input().len(),
/// limit: 1024,
/// });
/// }
/// Ok(())
/// });
/// ```
pub fn set_additional_stateless_validation<F>(&mut self, f: F)
where
F: Fn(TransactionOrigin, &Tx) -> Result<(), InvalidPoolTransactionError>
+ Send
+ Sync
+ 'static,
{
self.additional_stateless_validation = Some(Arc::new(f));
}
/// Sets an additional stateful validation check that is applied at the end of
/// [`validate_stateful`](Self::validate_stateful).
///
/// The check receives the transaction origin, a reference to the transaction, and the
/// account state reader, and should return `Ok(())` if the transaction is valid or
/// `Err(InvalidPoolTransactionError)` to reject it.
///
/// # Example
///
/// ```ignore
/// use reth_transaction_pool::{error::InvalidPoolTransactionError, TransactionOrigin};
///
/// let mut validator = builder.build(blob_store);
/// // Reject transactions from accounts with zero balance
/// validator.set_additional_stateful_validation(|origin, tx, state| {
/// let account = state.basic_account(tx.sender_ref())?.unwrap_or_default();
/// if account.balance.is_zero() {
/// return Err(InvalidPoolTransactionError::Other(Box::new(
/// std::io::Error::new(std::io::ErrorKind::Other, "zero balance"),
/// )));
/// }
/// Ok(())
/// });
/// ```
pub fn set_additional_stateful_validation<F>(&mut self, f: F)
where
F: Fn(
TransactionOrigin,
&Tx,
&dyn AccountInfoReader,
) -> Result<(), InvalidPoolTransactionError>
+ Send
+ Sync
+ 'static,
{
self.additional_stateful_validation = Some(Arc::new(f));
}
}
impl<Client, Tx, Evm> EthTransactionValidator<Client, Tx, Evm>
@@ -530,6 +652,13 @@ where
))
}
// Run additional stateless validation if configured
if let Some(check) = &self.additional_stateless_validation &&
let Err(err) = check(origin, &transaction)
{
return Err(TransactionValidationOutcome::Invalid(transaction, err))
}
Ok(transaction)
}
@@ -579,6 +708,13 @@ where
Ok(sidecar) => sidecar,
};
// Run additional stateful validation if configured
if let Some(check) = &self.additional_stateful_validation &&
let Err(err) = check(origin, &transaction, &state)
{
return TransactionValidationOutcome::Invalid(transaction, err)
}
let authorities = self.recover_authorities(&transaction);
// Return the valid transaction
TransactionValidationOutcome::Valid {
@@ -1243,6 +1379,8 @@ impl<Client, Evm> EthTransactionValidatorBuilder<Client, Evm> {
validation_metrics: TxPoolValidationMetrics::default(),
other_tx_types,
eip7594,
additional_stateless_validation: None,
additional_stateful_validation: None,
}
}