Skip to main content

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