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