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}