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::{Block, 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};
27
28/// A Result type returned after checking a transaction's validity.
29#[derive(Debug)]
30pub enum TransactionValidationOutcome<T: PoolTransaction> {
31 /// The transaction is considered _currently_ valid and can be inserted into the pool.
32 Valid {
33 /// Balance of the sender at the current point.
34 balance: U256,
35 /// Current nonce of the sender.
36 state_nonce: u64,
37 /// Code hash of the sender.
38 bytecode_hash: Option<B256>,
39 /// The validated transaction.
40 ///
41 /// See also [`ValidTransaction`].
42 ///
43 /// If this is a _new_ EIP-4844 blob transaction, then this must contain the extracted
44 /// sidecar.
45 transaction: ValidTransaction<T>,
46 /// Whether to propagate the transaction to the network.
47 propagate: bool,
48 /// The authorities of EIP-7702 transaction.
49 authorities: Option<Vec<Address>>,
50 },
51 /// The transaction is considered invalid indefinitely: It violates constraints that prevent
52 /// this transaction from ever becoming valid.
53 Invalid(T, InvalidPoolTransactionError),
54 /// An error occurred while trying to validate the transaction
55 Error(TxHash, Box<dyn core::error::Error + Send + Sync>),
56}
57
58impl<T: PoolTransaction> TransactionValidationOutcome<T> {
59 /// Returns the hash of the transactions
60 pub fn tx_hash(&self) -> TxHash {
61 match self {
62 Self::Valid { transaction, .. } => *transaction.hash(),
63 Self::Invalid(transaction, ..) => *transaction.hash(),
64 Self::Error(hash, ..) => *hash,
65 }
66 }
67
68 /// Returns the [`InvalidPoolTransactionError`] if this is an invalid variant.
69 pub const fn as_invalid(&self) -> Option<&InvalidPoolTransactionError> {
70 match self {
71 Self::Invalid(_, err) => Some(err),
72 _ => None,
73 }
74 }
75
76 /// Returns the [`ValidTransaction`] if this is a [`TransactionValidationOutcome::Valid`].
77 pub const fn as_valid_transaction(&self) -> Option<&ValidTransaction<T>> {
78 match self {
79 Self::Valid { transaction, .. } => Some(transaction),
80 _ => None,
81 }
82 }
83
84 /// Returns true if the transaction is valid.
85 pub const fn is_valid(&self) -> bool {
86 matches!(self, Self::Valid { .. })
87 }
88
89 /// Returns true if the transaction is invalid.
90 pub const fn is_invalid(&self) -> bool {
91 matches!(self, Self::Invalid(_, _))
92 }
93
94 /// Returns true if validation resulted in an error.
95 pub const fn is_error(&self) -> bool {
96 matches!(self, Self::Error(_, _))
97 }
98}
99
100/// A wrapper type for a transaction that is valid and has an optional extracted EIP-4844 blob
101/// transaction sidecar.
102///
103/// If this is provided, then the sidecar will be temporarily stored in the blob store until the
104/// transaction is finalized.
105///
106/// Note: Since blob transactions can be re-injected without their sidecar (after reorg), the
107/// validator can omit the sidecar if it is still in the blob store and return a
108/// [`ValidTransaction::Valid`] instead.
109#[derive(Debug)]
110pub enum ValidTransaction<T> {
111 /// A valid transaction without a sidecar.
112 Valid(T),
113 /// A valid transaction for which a sidecar should be stored.
114 ///
115 /// Caution: The [`TransactionValidator`] must ensure that this is only returned for EIP-4844
116 /// transactions.
117 ValidWithSidecar {
118 /// The valid EIP-4844 transaction.
119 transaction: T,
120 /// The extracted sidecar of that transaction
121 sidecar: BlobTransactionSidecarVariant,
122 },
123}
124
125impl<T> ValidTransaction<T> {
126 /// Creates a new valid transaction with an optional sidecar.
127 pub fn new(transaction: T, sidecar: Option<BlobTransactionSidecarVariant>) -> Self {
128 if let Some(sidecar) = sidecar {
129 Self::ValidWithSidecar { transaction, sidecar }
130 } else {
131 Self::Valid(transaction)
132 }
133 }
134}
135
136impl<T: PoolTransaction> ValidTransaction<T> {
137 /// Returns the transaction.
138 #[inline]
139 pub const fn transaction(&self) -> &T {
140 match self {
141 Self::Valid(transaction) | Self::ValidWithSidecar { transaction, .. } => transaction,
142 }
143 }
144
145 /// Consumes the wrapper and returns the transaction.
146 pub fn into_transaction(self) -> T {
147 match self {
148 Self::Valid(transaction) | Self::ValidWithSidecar { transaction, .. } => transaction,
149 }
150 }
151
152 /// Returns the address of that transaction.
153 #[inline]
154 pub(crate) fn sender(&self) -> Address {
155 self.transaction().sender()
156 }
157
158 /// Returns the hash of the transaction.
159 #[inline]
160 pub fn hash(&self) -> &B256 {
161 self.transaction().hash()
162 }
163
164 /// Returns the nonce of the transaction.
165 #[inline]
166 pub fn nonce(&self) -> u64 {
167 self.transaction().nonce()
168 }
169}
170
171/// Provides support for validating transaction at any given state of the chain
172pub trait TransactionValidator: Debug + Send + Sync {
173 /// The transaction type to validate.
174 type Transaction: PoolTransaction;
175
176 /// The block type used for new head block notifications.
177 type Block: Block;
178
179 /// Validates the transaction and returns a [`TransactionValidationOutcome`] describing the
180 /// validity of the given transaction.
181 ///
182 /// This will be used by the transaction-pool to check whether the transaction should be
183 /// inserted into the pool or discarded right away.
184 ///
185 /// Implementers of this trait must ensure that the transaction is well-formed, i.e. that it
186 /// complies at least all static constraints, which includes checking for:
187 ///
188 /// * chain id
189 /// * gas limit
190 /// * max cost
191 /// * nonce >= next nonce of the sender
192 /// * ...
193 ///
194 /// See [`InvalidTransactionError`](reth_primitives_traits::transaction::error::InvalidTransactionError) for common
195 /// errors variants.
196 ///
197 /// The transaction pool makes no additional assumptions about the validity of the transaction
198 /// at the time of this call before it inserts it into the pool. However, the validity of
199 /// this transaction is still subject to future (dynamic) changes enforced by the pool, for
200 /// example nonce or balance changes. Hence, any validation checks must be applied in this
201 /// function.
202 ///
203 /// See [`TransactionValidationTaskExecutor`] for a reference implementation.
204 fn validate_transaction(
205 &self,
206 origin: TransactionOrigin,
207 transaction: Self::Transaction,
208 ) -> impl Future<Output = TransactionValidationOutcome<Self::Transaction>> + Send;
209
210 /// Validates a batch of transactions.
211 ///
212 /// Must return all outcomes for the given transactions in the same order.
213 ///
214 /// See also [`Self::validate_transaction`].
215 fn validate_transactions(
216 &self,
217 transactions: Vec<(TransactionOrigin, Self::Transaction)>,
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> + Send,
233 ) -> impl Future<Output = Vec<TransactionValidationOutcome<Self::Transaction>>> + Send {
234 let futures = transactions.into_iter().map(|tx| self.validate_transaction(origin, tx));
235 futures_util::future::join_all(futures)
236 }
237
238 /// Invoked when the head block changes.
239 ///
240 /// This can be used to update fork specific values (timestamp).
241 fn on_new_head_block(&self, _new_tip_block: &SealedBlock<Self::Block>) {}
242}
243
244impl<A, B> TransactionValidator for Either<A, B>
245where
246 A: TransactionValidator,
247 B: TransactionValidator<Transaction = A::Transaction, Block = A::Block>,
248{
249 type Transaction = A::Transaction;
250 type Block = A::Block;
251
252 async fn validate_transaction(
253 &self,
254 origin: TransactionOrigin,
255 transaction: Self::Transaction,
256 ) -> TransactionValidationOutcome<Self::Transaction> {
257 match self {
258 Self::Left(v) => v.validate_transaction(origin, transaction).await,
259 Self::Right(v) => v.validate_transaction(origin, transaction).await,
260 }
261 }
262
263 async fn validate_transactions(
264 &self,
265 transactions: Vec<(TransactionOrigin, Self::Transaction)>,
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> + 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 effective tip for this transaction.
385 ///
386 /// For EIP-1559 transactions: `min(max_fee_per_gas - base_fee, max_priority_fee_per_gas)`.
387 /// For legacy transactions: `gas_price - base_fee`.
388 pub fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
389 self.transaction.effective_tip_per_gas(base_fee)
390 }
391
392 /// Returns the max priority fee per gas if the transaction is an EIP-1559 transaction, and
393 /// otherwise returns the gas price.
394 pub fn priority_fee_or_price(&self) -> u128 {
395 self.transaction.priority_fee_or_price()
396 }
397
398 /// Maximum amount of gas that the transaction is allowed to consume.
399 pub fn gas_limit(&self) -> u64 {
400 self.transaction.gas_limit()
401 }
402
403 /// Whether the transaction originated locally.
404 pub const fn is_local(&self) -> bool {
405 self.origin.is_local()
406 }
407
408 /// Whether the transaction is an EIP-4844 blob transaction.
409 #[inline]
410 pub fn is_eip4844(&self) -> bool {
411 self.transaction.is_eip4844()
412 }
413
414 /// The heap allocated size of this transaction.
415 pub(crate) fn size(&self) -> usize {
416 self.transaction.size()
417 }
418
419 /// Returns the [`SignedAuthorization`] list of the transaction.
420 ///
421 /// Returns `None` if this transaction is not EIP-7702.
422 pub fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
423 self.transaction.authorization_list()
424 }
425
426 /// Returns the number of blobs of [`SignedAuthorization`] in this transactions
427 ///
428 /// This is convenience function for `len(authorization_list)`.
429 ///
430 /// Returns `None` for non-eip7702 transactions.
431 pub fn authorization_count(&self) -> Option<u64> {
432 self.transaction.authorization_count()
433 }
434
435 /// EIP-4844 blob transactions and normal transactions are treated as mutually exclusive per
436 /// account.
437 ///
438 /// Returns true if the transaction is an EIP-4844 blob transaction and the other is not, or
439 /// vice versa.
440 #[inline]
441 pub(crate) fn tx_type_conflicts_with(&self, other: &Self) -> bool {
442 self.is_eip4844() != other.is_eip4844()
443 }
444
445 /// Converts to this type into the consensus transaction of the pooled transaction.
446 ///
447 /// Note: this takes `&self` since indented usage is via `Arc<Self>`.
448 pub fn to_consensus(&self) -> Recovered<T::Consensus> {
449 self.transaction.clone_into_consensus()
450 }
451
452 /// Determines whether a candidate transaction (`maybe_replacement`) is underpriced compared to
453 /// an existing transaction in the pool.
454 ///
455 /// A transaction is considered underpriced if it doesn't meet the required fee bump threshold.
456 /// This applies to both standard gas fees and, for blob-carrying transactions (EIP-4844),
457 /// the blob-specific fees.
458 #[inline]
459 pub fn is_underpriced(&self, maybe_replacement: &Self, price_bumps: &PriceBumpConfig) -> 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("propagate", &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}