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