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