From 7103088adc0e38af072bbc327f324ae43600c6ef Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 25 Feb 2026 14:32:21 +0100 Subject: [PATCH] feat(txpool): support additional custom validation checks in EthTransactionValidator (#22559) Co-authored-by: Amp --- .changelog/cool-suns-rest.md | 5 + crates/transaction-pool/src/validate/eth.rs | 140 +++++++++++++++++++- 2 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 .changelog/cool-suns-rest.md diff --git a/.changelog/cool-suns-rest.md b/.changelog/cool-suns-rest.md new file mode 100644 index 0000000000..be77e453a9 --- /dev/null +++ b/.changelog/cool-suns-rest.md @@ -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. diff --git a/crates/transaction-pool/src/validate/eth.rs b/crates/transaction-pool/src/validate/eth.rs index e470562f5f..1640ce77b1 100644 --- a/crates/transaction-pool/src/validate/eth.rs +++ b/crates/transaction-pool/src/validate/eth.rs @@ -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 = + Arc 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 = 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 { /// This type fetches account info from the db client: Client, @@ -103,6 +120,39 @@ pub struct EthTransactionValidator { /// 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>, + /// Optional additional stateful validation check applied at the end of + /// [`validate_stateful`](Self::validate_stateful). + additional_stateful_validation: Option>, +} + +impl fmt::Debug for EthTransactionValidator { + 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 EthTransactionValidator { @@ -182,6 +232,78 @@ impl EthTransactionValidator { 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(&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(&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 EthTransactionValidator @@ -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 EthTransactionValidatorBuilder { validation_metrics: TxPoolValidationMetrics::default(), other_tx_types, eip7594, + additional_stateless_validation: None, + additional_stateful_validation: None, } }