Skip to main content

reth_transaction_pool/validate/
mod.rs

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