reth_transaction_pool/validate/
mod.rs

1//! Transaction validation abstractions.
2
3use crate::{
4    error::InvalidPoolTransactionError,
5    identifier::{SenderId, TransactionId},
6    traits::{PoolTransaction, TransactionOrigin},
7    PriceBumpConfig,
8};
9use alloy_eips::{eip7594::BlobTransactionSidecarVariant, eip7702::SignedAuthorization};
10use alloy_primitives::{Address, TxHash, B256, U256};
11use futures_util::future::Either;
12use reth_primitives_traits::{Recovered, SealedBlock};
13use std::{fmt, fmt::Debug, future::Future, time::Instant};
14
15mod constants;
16mod eth;
17mod task;
18
19pub use eth::*;
20
21pub use task::{TransactionValidationTaskExecutor, ValidationTask};
22
23/// Validation constants.
24pub use constants::{
25    DEFAULT_MAX_TX_INPUT_BYTES, MAX_CODE_BYTE_SIZE, MAX_INIT_CODE_BYTE_SIZE, TX_SLOT_BYTE_SIZE,
26};
27use reth_primitives_traits::Block;
28
29/// A Result type returned after checking a transaction's validity.
30#[derive(Debug)]
31pub enum TransactionValidationOutcome<T: PoolTransaction> {
32    /// The transaction is considered _currently_ valid and can be inserted into the pool.
33    Valid {
34        /// Balance of the sender at the current point.
35        balance: U256,
36        /// Current nonce of the sender.
37        state_nonce: u64,
38        /// Code hash of the sender.
39        bytecode_hash: Option<B256>,
40        /// The validated transaction.
41        ///
42        /// See also [`ValidTransaction`].
43        ///
44        /// If this is a _new_ EIP-4844 blob transaction, then this must contain the extracted
45        /// sidecar.
46        transaction: ValidTransaction<T>,
47        /// Whether to propagate the transaction to the network.
48        propagate: bool,
49        /// The authorities of EIP-7702 transaction.
50        authorities: Option<Vec<Address>>,
51    },
52    /// The transaction is considered invalid indefinitely: It violates constraints that prevent
53    /// this transaction from ever becoming valid.
54    Invalid(T, InvalidPoolTransactionError),
55    /// An error occurred while trying to validate the transaction
56    Error(TxHash, Box<dyn core::error::Error + Send + Sync>),
57}
58
59impl<T: PoolTransaction> TransactionValidationOutcome<T> {
60    /// Returns the hash of the transactions
61    pub fn tx_hash(&self) -> TxHash {
62        match self {
63            Self::Valid { transaction, .. } => *transaction.hash(),
64            Self::Invalid(transaction, ..) => *transaction.hash(),
65            Self::Error(hash, ..) => *hash,
66        }
67    }
68
69    /// Returns the [`InvalidPoolTransactionError`] if this is an invalid variant.
70    pub const fn as_invalid(&self) -> Option<&InvalidPoolTransactionError> {
71        match self {
72            Self::Invalid(_, err) => Some(err),
73            _ => None,
74        }
75    }
76
77    /// Returns the [`ValidTransaction`] if this is a [`TransactionValidationOutcome::Valid`].
78    pub const fn as_valid_transaction(&self) -> Option<&ValidTransaction<T>> {
79        match self {
80            Self::Valid { transaction, .. } => Some(transaction),
81            _ => None,
82        }
83    }
84
85    /// Returns true if the transaction is valid.
86    pub const fn is_valid(&self) -> bool {
87        matches!(self, Self::Valid { .. })
88    }
89
90    /// Returns true if the transaction is invalid.
91    pub const fn is_invalid(&self) -> bool {
92        matches!(self, Self::Invalid(_, _))
93    }
94
95    /// Returns true if validation resulted in an error.
96    pub const fn is_error(&self) -> bool {
97        matches!(self, Self::Error(_, _))
98    }
99}
100
101/// A wrapper type for a transaction that is valid and has an optional extracted EIP-4844 blob
102/// transaction sidecar.
103///
104/// If this is provided, then the sidecar will be temporarily stored in the blob store until the
105/// transaction is finalized.
106///
107/// Note: Since blob transactions can be re-injected without their sidecar (after reorg), the
108/// validator can omit the sidecar if it is still in the blob store and return a
109/// [`ValidTransaction::Valid`] instead.
110#[derive(Debug)]
111pub enum ValidTransaction<T> {
112    /// A valid transaction without a sidecar.
113    Valid(T),
114    /// A valid transaction for which a sidecar should be stored.
115    ///
116    /// Caution: The [`TransactionValidator`] must ensure that this is only returned for EIP-4844
117    /// transactions.
118    ValidWithSidecar {
119        /// The valid EIP-4844 transaction.
120        transaction: T,
121        /// The extracted sidecar of that transaction
122        sidecar: BlobTransactionSidecarVariant,
123    },
124}
125
126impl<T> ValidTransaction<T> {
127    /// Creates a new valid transaction with an optional sidecar.
128    pub fn new(transaction: T, sidecar: Option<BlobTransactionSidecarVariant>) -> Self {
129        if let Some(sidecar) = sidecar {
130            Self::ValidWithSidecar { transaction, sidecar }
131        } else {
132            Self::Valid(transaction)
133        }
134    }
135}
136
137impl<T: PoolTransaction> ValidTransaction<T> {
138    /// Returns the transaction.
139    #[inline]
140    pub const fn transaction(&self) -> &T {
141        match self {
142            Self::Valid(transaction) | Self::ValidWithSidecar { transaction, .. } => transaction,
143        }
144    }
145
146    /// Consumes the wrapper and returns the transaction.
147    pub fn into_transaction(self) -> T {
148        match self {
149            Self::Valid(transaction) | Self::ValidWithSidecar { transaction, .. } => transaction,
150        }
151    }
152
153    /// Returns the address of that transaction.
154    #[inline]
155    pub(crate) fn sender(&self) -> Address {
156        self.transaction().sender()
157    }
158
159    /// Returns the hash of the transaction.
160    #[inline]
161    pub fn hash(&self) -> &B256 {
162        self.transaction().hash()
163    }
164
165    /// Returns the nonce of the transaction.
166    #[inline]
167    pub fn nonce(&self) -> u64 {
168        self.transaction().nonce()
169    }
170}
171
172/// Provides support for validating transaction at any given state of the chain
173pub trait TransactionValidator: Debug + Send + Sync {
174    /// The transaction type to validate.
175    type Transaction: PoolTransaction;
176
177    /// Validates the transaction and returns a [`TransactionValidationOutcome`] describing the
178    /// validity of the given transaction.
179    ///
180    /// This will be used by the transaction-pool to check whether the transaction should be
181    /// inserted into the pool or discarded right away.
182    ///
183    /// Implementers of this trait must ensure that the transaction is well-formed, i.e. that it
184    /// complies at least all static constraints, which includes checking for:
185    ///
186    ///    * chain id
187    ///    * gas limit
188    ///    * max cost
189    ///    * nonce >= next nonce of the sender
190    ///    * ...
191    ///
192    /// See [`InvalidTransactionError`](reth_primitives_traits::transaction::error::InvalidTransactionError) for common
193    /// errors variants.
194    ///
195    /// The transaction pool makes no additional assumptions about the validity of the transaction
196    /// at the time of this call before it inserts it into the pool. However, the validity of
197    /// this transaction is still subject to future (dynamic) changes enforced by the pool, for
198    /// example nonce or balance changes. Hence, any validation checks must be applied in this
199    /// function.
200    ///
201    /// See [`TransactionValidationTaskExecutor`] for a reference implementation.
202    fn validate_transaction(
203        &self,
204        origin: TransactionOrigin,
205        transaction: Self::Transaction,
206    ) -> impl Future<Output = TransactionValidationOutcome<Self::Transaction>> + Send;
207
208    /// Validates a batch of transactions.
209    ///
210    /// Must return all outcomes for the given transactions in the same order.
211    ///
212    /// See also [`Self::validate_transaction`].
213    fn validate_transactions(
214        &self,
215        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
216    ) -> impl Future<Output = Vec<TransactionValidationOutcome<Self::Transaction>>> + Send {
217        futures_util::future::join_all(
218            transactions.into_iter().map(|(origin, tx)| self.validate_transaction(origin, tx)),
219        )
220    }
221
222    /// Validates a batch of transactions with that given origin.
223    ///
224    /// Must return all outcomes for the given transactions in the same order.
225    ///
226    /// See also [`Self::validate_transaction`].
227    fn validate_transactions_with_origin(
228        &self,
229        origin: TransactionOrigin,
230        transactions: impl IntoIterator<Item = Self::Transaction> + Send,
231    ) -> impl Future<Output = Vec<TransactionValidationOutcome<Self::Transaction>>> + Send {
232        let futures = transactions.into_iter().map(|tx| self.validate_transaction(origin, tx));
233        futures_util::future::join_all(futures)
234    }
235
236    /// Invoked when the head block changes.
237    ///
238    /// This can be used to update fork specific values (timestamp).
239    fn on_new_head_block<B>(&self, _new_tip_block: &SealedBlock<B>)
240    where
241        B: Block,
242    {
243    }
244}
245
246impl<A, B> TransactionValidator for Either<A, B>
247where
248    A: TransactionValidator,
249    B: TransactionValidator<Transaction = A::Transaction>,
250{
251    type Transaction = A::Transaction;
252
253    async fn validate_transaction(
254        &self,
255        origin: TransactionOrigin,
256        transaction: Self::Transaction,
257    ) -> TransactionValidationOutcome<Self::Transaction> {
258        match self {
259            Self::Left(v) => v.validate_transaction(origin, transaction).await,
260            Self::Right(v) => v.validate_transaction(origin, transaction).await,
261        }
262    }
263
264    async fn validate_transactions(
265        &self,
266        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
267    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
268        match self {
269            Self::Left(v) => v.validate_transactions(transactions).await,
270            Self::Right(v) => v.validate_transactions(transactions).await,
271        }
272    }
273
274    async fn validate_transactions_with_origin(
275        &self,
276        origin: TransactionOrigin,
277        transactions: impl IntoIterator<Item = Self::Transaction> + Send,
278    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
279        match self {
280            Self::Left(v) => v.validate_transactions_with_origin(origin, transactions).await,
281            Self::Right(v) => v.validate_transactions_with_origin(origin, transactions).await,
282        }
283    }
284
285    fn on_new_head_block<Bl>(&self, new_tip_block: &SealedBlock<Bl>)
286    where
287        Bl: Block,
288    {
289        match self {
290            Self::Left(v) => v.on_new_head_block(new_tip_block),
291            Self::Right(v) => v.on_new_head_block(new_tip_block),
292        }
293    }
294}
295
296/// A valid transaction in the pool.
297///
298/// This is used as the internal representation of a transaction inside the pool.
299///
300/// For EIP-4844 blob transactions this will _not_ contain the blob sidecar which is stored
301/// separately in the [`BlobStore`](crate::blobstore::BlobStore).
302pub struct ValidPoolTransaction<T: PoolTransaction> {
303    /// The transaction
304    pub transaction: T,
305    /// The identifier for this transaction.
306    pub transaction_id: TransactionId,
307    /// Whether it is allowed to propagate the transaction.
308    pub propagate: bool,
309    /// Timestamp when this was added to the pool.
310    pub timestamp: Instant,
311    /// Where this transaction originated from.
312    pub origin: TransactionOrigin,
313    /// The sender ids of the 7702 transaction authorities.
314    pub authority_ids: Option<Vec<SenderId>>,
315}
316
317// === impl ValidPoolTransaction ===
318
319impl<T: PoolTransaction> ValidPoolTransaction<T> {
320    /// Returns the hash of the transaction.
321    pub fn hash(&self) -> &TxHash {
322        self.transaction.hash()
323    }
324
325    /// Returns the type identifier of the transaction
326    pub fn tx_type(&self) -> u8 {
327        self.transaction.ty()
328    }
329
330    /// Returns the address of the sender
331    pub fn sender(&self) -> Address {
332        self.transaction.sender()
333    }
334
335    /// Returns a reference to the address of the sender
336    pub fn sender_ref(&self) -> &Address {
337        self.transaction.sender_ref()
338    }
339
340    /// Returns the recipient of the transaction if it is not a CREATE transaction.
341    pub fn to(&self) -> Option<Address> {
342        self.transaction.to()
343    }
344
345    /// Returns the internal identifier for the sender of this transaction
346    pub const fn sender_id(&self) -> SenderId {
347        self.transaction_id.sender
348    }
349
350    /// Returns the internal identifier for this transaction.
351    pub const fn id(&self) -> &TransactionId {
352        &self.transaction_id
353    }
354
355    /// Returns the length of the rlp encoded transaction
356    #[inline]
357    pub fn encoded_length(&self) -> usize {
358        self.transaction.encoded_length()
359    }
360
361    /// Returns the nonce set for this transaction.
362    pub fn nonce(&self) -> u64 {
363        self.transaction.nonce()
364    }
365
366    /// Returns the cost that this transaction is allowed to consume:
367    ///
368    /// For EIP-1559 transactions: `max_fee_per_gas * gas_limit + tx_value`.
369    /// For legacy transactions: `gas_price * gas_limit + tx_value`.
370    pub fn cost(&self) -> &U256 {
371        self.transaction.cost()
372    }
373
374    /// Returns the EIP-4844 max blob fee the caller is willing to pay.
375    ///
376    /// For non-EIP-4844 transactions, this returns [None].
377    pub fn max_fee_per_blob_gas(&self) -> Option<u128> {
378        self.transaction.max_fee_per_blob_gas()
379    }
380
381    /// Returns the EIP-1559 Max base fee the caller is willing to pay.
382    ///
383    /// For legacy transactions this is `gas_price`.
384    pub fn max_fee_per_gas(&self) -> u128 {
385        self.transaction.max_fee_per_gas()
386    }
387
388    /// Returns the effective tip for this transaction.
389    ///
390    /// For EIP-1559 transactions: `min(max_fee_per_gas - base_fee, max_priority_fee_per_gas)`.
391    /// For legacy transactions: `gas_price - base_fee`.
392    pub fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
393        self.transaction.effective_tip_per_gas(base_fee)
394    }
395
396    /// Returns the max priority fee per gas if the transaction is an EIP-1559 transaction, and
397    /// otherwise returns the gas price.
398    pub fn priority_fee_or_price(&self) -> u128 {
399        self.transaction.priority_fee_or_price()
400    }
401
402    /// Maximum amount of gas that the transaction is allowed to consume.
403    pub fn gas_limit(&self) -> u64 {
404        self.transaction.gas_limit()
405    }
406
407    /// Whether the transaction originated locally.
408    pub const fn is_local(&self) -> bool {
409        self.origin.is_local()
410    }
411
412    /// Whether the transaction is an EIP-4844 blob transaction.
413    #[inline]
414    pub fn is_eip4844(&self) -> bool {
415        self.transaction.is_eip4844()
416    }
417
418    /// The heap allocated size of this transaction.
419    pub(crate) fn size(&self) -> usize {
420        self.transaction.size()
421    }
422
423    /// Returns the [`SignedAuthorization`] list of the transaction.
424    ///
425    /// Returns `None` if this transaction is not EIP-7702.
426    pub fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
427        self.transaction.authorization_list()
428    }
429
430    /// Returns the number of blobs of [`SignedAuthorization`] in this transactions
431    ///
432    /// This is convenience function for `len(authorization_list)`.
433    ///
434    /// Returns `None` for non-eip7702 transactions.
435    pub fn authorization_count(&self) -> Option<u64> {
436        self.transaction.authorization_count()
437    }
438
439    /// EIP-4844 blob transactions and normal transactions are treated as mutually exclusive per
440    /// account.
441    ///
442    /// Returns true if the transaction is an EIP-4844 blob transaction and the other is not, or
443    /// vice versa.
444    #[inline]
445    pub(crate) fn tx_type_conflicts_with(&self, other: &Self) -> bool {
446        self.is_eip4844() != other.is_eip4844()
447    }
448
449    /// Converts to this type into the consensus transaction of the pooled transaction.
450    ///
451    /// Note: this takes `&self` since indented usage is via `Arc<Self>`.
452    pub fn to_consensus(&self) -> Recovered<T::Consensus> {
453        self.transaction.clone_into_consensus()
454    }
455
456    /// Determines whether a candidate transaction (`maybe_replacement`) is underpriced compared to
457    /// an existing transaction in the pool.
458    ///
459    /// A transaction is considered underpriced if it doesn't meet the required fee bump threshold.
460    /// This applies to both standard gas fees and, for blob-carrying transactions (EIP-4844),
461    /// the blob-specific fees.
462    #[inline]
463    pub fn is_underpriced(&self, maybe_replacement: &Self, price_bumps: &PriceBumpConfig) -> bool {
464        // Retrieve the required price bump percentage for this type of transaction.
465        //
466        // The bump is different for EIP-4844 and other transactions. See `PriceBumpConfig`.
467        let price_bump = price_bumps.price_bump(self.tx_type());
468
469        // Check if the max fee per gas is underpriced.
470        if maybe_replacement.max_fee_per_gas() < self.max_fee_per_gas() * (100 + price_bump) / 100 {
471            return true
472        }
473
474        let existing_max_priority_fee_per_gas =
475            self.transaction.max_priority_fee_per_gas().unwrap_or_default();
476        let replacement_max_priority_fee_per_gas =
477            maybe_replacement.transaction.max_priority_fee_per_gas().unwrap_or_default();
478
479        // Check max priority fee per gas (relevant for EIP-1559 transactions only)
480        if existing_max_priority_fee_per_gas != 0 &&
481            replacement_max_priority_fee_per_gas != 0 &&
482            replacement_max_priority_fee_per_gas <
483                existing_max_priority_fee_per_gas * (100 + price_bump) / 100
484        {
485            return true
486        }
487
488        // Check max blob fee per gas
489        if let Some(existing_max_blob_fee_per_gas) = self.transaction.max_fee_per_blob_gas() {
490            // This enforces that blob txs can only be replaced by blob txs
491            let replacement_max_blob_fee_per_gas =
492                maybe_replacement.transaction.max_fee_per_blob_gas().unwrap_or_default();
493            if replacement_max_blob_fee_per_gas <
494                existing_max_blob_fee_per_gas * (100 + price_bump) / 100
495            {
496                return true
497            }
498        }
499
500        false
501    }
502}
503
504#[cfg(test)]
505impl<T: PoolTransaction> Clone for ValidPoolTransaction<T> {
506    fn clone(&self) -> Self {
507        Self {
508            transaction: self.transaction.clone(),
509            transaction_id: self.transaction_id,
510            propagate: self.propagate,
511            timestamp: self.timestamp,
512            origin: self.origin,
513            authority_ids: self.authority_ids.clone(),
514        }
515    }
516}
517
518impl<T: PoolTransaction> fmt::Debug for ValidPoolTransaction<T> {
519    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520        f.debug_struct("ValidPoolTransaction")
521            .field("id", &self.transaction_id)
522            .field("propagate", &self.propagate)
523            .field("origin", &self.origin)
524            .field("hash", self.transaction.hash())
525            .field("tx", &self.transaction)
526            .finish()
527    }
528}
529
530/// Validation Errors that can occur during transaction validation.
531#[derive(thiserror::Error, Debug)]
532pub enum TransactionValidatorError {
533    /// Failed to communicate with the validation service.
534    #[error("validation service unreachable")]
535    ValidationServiceUnreachable,
536}