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