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}