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_consensus::transaction::TxSeismicElements;
10use alloy_eips::eip4844::BlobTransactionSidecar;
11use alloy_primitives::{Address, TxHash, B256, U256};
12use futures_util::future::Either;
13use reth_primitives::{RecoveredTx, SealedBlock};
14use std::{fmt, future::Future, time::Instant};
15
16mod constants;
17mod eth;
18mod task;
19
20/// A `TransactionValidator` implementation that validates ethereum transaction.
21pub use eth::*;
22
23/// A spawnable task that performs transaction validation.
24pub use task::{TransactionValidationTaskExecutor, ValidationTask};
25
26/// Validation constants.
27pub use constants::{
28    DEFAULT_MAX_TX_INPUT_BYTES, MAX_CODE_BYTE_SIZE, MAX_INIT_CODE_BYTE_SIZE, TX_SLOT_BYTE_SIZE,
29};
30
31/// A Result type returned after checking a transaction's validity.
32#[derive(Debug)]
33pub enum TransactionValidationOutcome<T: PoolTransaction> {
34    /// The transaction is considered _currently_ valid and can be inserted into the pool.
35    Valid {
36        /// Balance of the sender at the current point.
37        balance: U256,
38        /// Current nonce of the sender.
39        state_nonce: u64,
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    },
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 true if the transaction is valid.
68    pub const fn is_valid(&self) -> bool {
69        matches!(self, Self::Valid { .. })
70    }
71
72    /// Returns true if the transaction is invalid.
73    pub const fn is_invalid(&self) -> bool {
74        matches!(self, Self::Invalid(_, _))
75    }
76
77    /// Returns true if validation resulted in an error.
78    pub const fn is_error(&self) -> bool {
79        matches!(self, Self::Error(_, _))
80    }
81}
82
83/// A wrapper type for a transaction that is valid and has an optional extracted EIP-4844 blob
84/// transaction sidecar.
85///
86/// If this is provided, then the sidecar will be temporarily stored in the blob store until the
87/// transaction is finalized.
88///
89/// Note: Since blob transactions can be re-injected without their sidecar (after reorg), the
90/// validator can omit the sidecar if it is still in the blob store and return a
91/// [`ValidTransaction::Valid`] instead.
92#[derive(Debug)]
93pub enum ValidTransaction<T> {
94    /// A valid transaction without a sidecar.
95    Valid(T),
96    /// A valid transaction for which a sidecar should be stored.
97    ///
98    /// Caution: The [`TransactionValidator`] must ensure that this is only returned for EIP-4844
99    /// transactions.
100    ValidWithSidecar {
101        /// The valid EIP-4844 transaction.
102        transaction: T,
103        /// The extracted sidecar of that transaction
104        sidecar: BlobTransactionSidecar,
105    },
106}
107
108impl<T> ValidTransaction<T> {
109    /// Creates a new valid transaction with an optional sidecar.
110    pub fn new(transaction: T, sidecar: Option<BlobTransactionSidecar>) -> Self {
111        if let Some(sidecar) = sidecar {
112            Self::ValidWithSidecar { transaction, sidecar }
113        } else {
114            Self::Valid(transaction)
115        }
116    }
117}
118
119impl<T: PoolTransaction> ValidTransaction<T> {
120    /// Returns the transaction.
121    #[inline]
122    pub const fn transaction(&self) -> &T {
123        match self {
124            Self::Valid(transaction) | Self::ValidWithSidecar { transaction, .. } => transaction,
125        }
126    }
127
128    /// Consumes the wrapper and returns the transaction.
129    pub fn into_transaction(self) -> T {
130        match self {
131            Self::Valid(transaction) | Self::ValidWithSidecar { transaction, .. } => transaction,
132        }
133    }
134
135    /// Returns the address of that transaction.
136    #[inline]
137    pub(crate) fn sender(&self) -> Address {
138        self.transaction().sender()
139    }
140
141    /// Returns the hash of the transaction.
142    #[inline]
143    pub fn hash(&self) -> &B256 {
144        self.transaction().hash()
145    }
146
147    /// Returns the nonce of the transaction.
148    #[inline]
149    pub fn nonce(&self) -> u64 {
150        self.transaction().nonce()
151    }
152
153    /// Returns the encryption pubkey of the transaction (Seismic)
154    pub fn seismic_elements(&self) -> Option<&TxSeismicElements> {
155        self.transaction().seismic_elements()
156    }
157}
158
159/// Provides support for validating transaction at any given state of the chain
160pub trait TransactionValidator: Send + Sync {
161    /// The transaction type to validate.
162    type Transaction: PoolTransaction;
163
164    /// Validates the transaction and returns a [`TransactionValidationOutcome`] describing the
165    /// validity of the given transaction.
166    ///
167    /// This will be used by the transaction-pool to check whether the transaction should be
168    /// inserted into the pool or discarded right away.
169    ///
170    /// Implementers of this trait must ensure that the transaction is well-formed, i.e. that it
171    /// complies at least all static constraints, which includes checking for:
172    ///
173    ///    * chain id
174    ///    * gas limit
175    ///    * max cost
176    ///    * nonce >= next nonce of the sender
177    ///    * ...
178    ///
179    /// See [`InvalidTransactionError`](reth_primitives::InvalidTransactionError) for common errors
180    /// variants.
181    ///
182    /// The transaction pool makes no additional assumptions about the validity of the transaction
183    /// at the time of this call before it inserts it into the pool. However, the validity of
184    /// this transaction is still subject to future (dynamic) changes enforced by the pool, for
185    /// example nonce or balance changes. Hence, any validation checks must be applied in this
186    /// function.
187    ///
188    /// See [`TransactionValidationTaskExecutor`] for a reference implementation.
189    fn validate_transaction(
190        &self,
191        origin: TransactionOrigin,
192        transaction: Self::Transaction,
193    ) -> impl Future<Output = TransactionValidationOutcome<Self::Transaction>> + Send;
194
195    /// Validates a batch of transactions.
196    ///
197    /// Must return all outcomes for the given transactions in the same order.
198    ///
199    /// See also [`Self::validate_transaction`].
200    fn validate_transactions(
201        &self,
202        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
203    ) -> impl Future<Output = Vec<TransactionValidationOutcome<Self::Transaction>>> + Send {
204        async {
205            futures_util::future::join_all(
206                transactions.into_iter().map(|(origin, tx)| self.validate_transaction(origin, tx)),
207            )
208            .await
209        }
210    }
211
212    /// Invoked when the head block changes.
213    ///
214    /// This can be used to update fork specific values (timestamp).
215    fn on_new_head_block(&self, _new_tip_block: &SealedBlock) {}
216}
217
218impl<A, B> TransactionValidator for Either<A, B>
219where
220    A: TransactionValidator,
221    B: TransactionValidator<Transaction = A::Transaction>,
222{
223    type Transaction = A::Transaction;
224
225    async fn validate_transaction(
226        &self,
227        origin: TransactionOrigin,
228        transaction: Self::Transaction,
229    ) -> TransactionValidationOutcome<Self::Transaction> {
230        match self {
231            Self::Left(v) => v.validate_transaction(origin, transaction).await,
232            Self::Right(v) => v.validate_transaction(origin, transaction).await,
233        }
234    }
235
236    async fn validate_transactions(
237        &self,
238        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
239    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
240        match self {
241            Self::Left(v) => v.validate_transactions(transactions).await,
242            Self::Right(v) => v.validate_transactions(transactions).await,
243        }
244    }
245
246    fn on_new_head_block(&self, new_tip_block: &SealedBlock) {
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.tx_type()
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) -> RecoveredTx<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    /// Returns the seismic elements of the transaction (Seismic)
448    pub fn seismic_elements(&self) -> Option<&TxSeismicElements> {
449        self.transaction.seismic_elements()
450    }
451}
452
453#[cfg(test)]
454impl<T: PoolTransaction> Clone for ValidPoolTransaction<T> {
455    fn clone(&self) -> Self {
456        Self {
457            transaction: self.transaction.clone(),
458            transaction_id: self.transaction_id,
459            propagate: self.propagate,
460            timestamp: self.timestamp,
461            origin: self.origin,
462        }
463    }
464}
465
466impl<T: PoolTransaction> fmt::Debug for ValidPoolTransaction<T> {
467    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
468        f.debug_struct("ValidPoolTransaction")
469            .field("id", &self.transaction_id)
470            .field("pragate", &self.propagate)
471            .field("origin", &self.origin)
472            .field("hash", self.transaction.hash())
473            .field("tx", &self.transaction)
474            .finish()
475    }
476}
477
478/// Validation Errors that can occur during transaction validation.
479#[derive(thiserror::Error, Debug)]
480pub enum TransactionValidatorError {
481    /// Failed to communicate with the validation service.
482    #[error("validation service unreachable")]
483    ValidationServiceUnreachable,
484}