reth_optimism_txpool/
validator.rs

1use alloy_consensus::{BlockHeader, Transaction};
2use alloy_eips::Encodable2718;
3use op_revm::L1BlockInfo;
4use parking_lot::RwLock;
5use reth_chainspec::ChainSpecProvider;
6use reth_optimism_evm::RethL1BlockInfo;
7use reth_optimism_forks::OpHardforks;
8use reth_primitives_traits::{
9    transaction::error::InvalidTransactionError, Block, BlockBody, GotExpected, SealedBlock,
10};
11use reth_storage_api::{BlockReaderIdExt, StateProviderFactory};
12use reth_transaction_pool::{
13    EthPoolTransaction, EthTransactionValidator, TransactionOrigin, TransactionValidationOutcome,
14    TransactionValidator,
15};
16use std::sync::{
17    atomic::{AtomicU64, Ordering},
18    Arc,
19};
20
21/// Tracks additional infos for the current block.
22#[derive(Debug, Default)]
23pub struct OpL1BlockInfo {
24    /// The current L1 block info.
25    l1_block_info: RwLock<L1BlockInfo>,
26    /// Current block timestamp.
27    timestamp: AtomicU64,
28    /// Current block number.
29    number: AtomicU64,
30}
31
32/// Validator for Optimism transactions.
33#[derive(Debug, Clone)]
34pub struct OpTransactionValidator<Client, Tx> {
35    /// The type that performs the actual validation.
36    inner: EthTransactionValidator<Client, Tx>,
37    /// Additional block info required for validation.
38    block_info: Arc<OpL1BlockInfo>,
39    /// If true, ensure that the transaction's sender has enough balance to cover the L1 gas fee
40    /// derived from the tracked L1 block info that is extracted from the first transaction in the
41    /// L2 block.
42    require_l1_data_gas_fee: bool,
43}
44
45impl<Client, Tx> OpTransactionValidator<Client, Tx> {
46    /// Returns the configured chain spec
47    pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
48    where
49        Client: ChainSpecProvider,
50    {
51        self.inner.chain_spec()
52    }
53
54    /// Returns the configured client
55    pub fn client(&self) -> &Client {
56        self.inner.client()
57    }
58
59    /// Returns the current block timestamp.
60    fn block_timestamp(&self) -> u64 {
61        self.block_info.timestamp.load(Ordering::Relaxed)
62    }
63
64    /// Returns the current block number.
65    fn block_number(&self) -> u64 {
66        self.block_info.number.load(Ordering::Relaxed)
67    }
68
69    /// Whether to ensure that the transaction's sender has enough balance to also cover the L1 gas
70    /// fee.
71    pub fn require_l1_data_gas_fee(self, require_l1_data_gas_fee: bool) -> Self {
72        Self { require_l1_data_gas_fee, ..self }
73    }
74
75    /// Returns whether this validator also requires the transaction's sender to have enough balance
76    /// to cover the L1 gas fee.
77    pub const fn requires_l1_data_gas_fee(&self) -> bool {
78        self.require_l1_data_gas_fee
79    }
80}
81
82impl<Client, Tx> OpTransactionValidator<Client, Tx>
83where
84    Client: ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt,
85    Tx: EthPoolTransaction,
86{
87    /// Create a new [`OpTransactionValidator`].
88    pub fn new(inner: EthTransactionValidator<Client, Tx>) -> Self {
89        let this = Self::with_block_info(inner, OpL1BlockInfo::default());
90        if let Ok(Some(block)) =
91            this.inner.client().block_by_number_or_tag(alloy_eips::BlockNumberOrTag::Latest)
92        {
93            // genesis block has no txs, so we can't extract L1 info, we set the block info to empty
94            // so that we will accept txs into the pool before the first block
95            if block.header().number() == 0 {
96                this.block_info.timestamp.store(block.header().timestamp(), Ordering::Relaxed);
97                this.block_info.number.store(block.header().number(), Ordering::Relaxed);
98            } else {
99                this.update_l1_block_info(block.header(), block.body().transactions().first());
100            }
101        }
102
103        this
104    }
105
106    /// Create a new [`OpTransactionValidator`] with the given [`OpL1BlockInfo`].
107    pub fn with_block_info(
108        inner: EthTransactionValidator<Client, Tx>,
109        block_info: OpL1BlockInfo,
110    ) -> Self {
111        Self { inner, block_info: Arc::new(block_info), require_l1_data_gas_fee: true }
112    }
113
114    /// Update the L1 block info for the given header and system transaction, if any.
115    ///
116    /// Note: this supports optional system transaction, in case this is used in a dev setuo
117    pub fn update_l1_block_info<H, T>(&self, header: &H, tx: Option<&T>)
118    where
119        H: BlockHeader,
120        T: Transaction,
121    {
122        self.block_info.timestamp.store(header.timestamp(), Ordering::Relaxed);
123        self.block_info.number.store(header.number(), Ordering::Relaxed);
124
125        if let Some(Ok(cost_addition)) = tx.map(reth_optimism_evm::extract_l1_info_from_tx) {
126            *self.block_info.l1_block_info.write() = cost_addition;
127        }
128    }
129
130    /// Validates a single transaction.
131    ///
132    /// See also [`TransactionValidator::validate_transaction`]
133    ///
134    /// This behaves the same as [`EthTransactionValidator::validate_one`], but in addition, ensures
135    /// that the account has enough balance to cover the L1 gas cost.
136    pub fn validate_one(
137        &self,
138        origin: TransactionOrigin,
139        transaction: Tx,
140    ) -> TransactionValidationOutcome<Tx> {
141        if transaction.is_eip4844() {
142            return TransactionValidationOutcome::Invalid(
143                transaction,
144                InvalidTransactionError::TxTypeNotSupported.into(),
145            )
146        }
147
148        let outcome = self.inner.validate_one(origin, transaction);
149
150        if !self.requires_l1_data_gas_fee() {
151            // no need to check L1 gas fee
152            return outcome
153        }
154
155        // ensure that the account has enough balance to cover the L1 gas cost
156        if let TransactionValidationOutcome::Valid {
157            balance,
158            state_nonce,
159            transaction: valid_tx,
160            propagate,
161        } = outcome
162        {
163            let mut l1_block_info = self.block_info.l1_block_info.read().clone();
164
165            let mut encoded = Vec::with_capacity(valid_tx.transaction().encoded_length());
166            let tx = valid_tx.transaction().clone_into_consensus();
167            tx.encode_2718(&mut encoded);
168
169            let cost_addition = match l1_block_info.l1_tx_data_fee(
170                self.chain_spec(),
171                self.block_timestamp(),
172                self.block_number(),
173                &encoded,
174                false,
175            ) {
176                Ok(cost) => cost,
177                Err(err) => {
178                    return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err))
179                }
180            };
181            let cost = valid_tx.transaction().cost().saturating_add(cost_addition);
182
183            // Checks for max cost
184            if cost > balance {
185                return TransactionValidationOutcome::Invalid(
186                    valid_tx.into_transaction(),
187                    InvalidTransactionError::InsufficientFunds(
188                        GotExpected { got: balance, expected: cost }.into(),
189                    )
190                    .into(),
191                )
192            }
193
194            return TransactionValidationOutcome::Valid {
195                balance,
196                state_nonce,
197                transaction: valid_tx,
198                propagate,
199            }
200        }
201
202        outcome
203    }
204
205    /// Validates all given transactions.
206    ///
207    /// Returns all outcomes for the given transactions in the same order.
208    ///
209    /// See also [`Self::validate_one`]
210    pub fn validate_all(
211        &self,
212        transactions: Vec<(TransactionOrigin, Tx)>,
213    ) -> Vec<TransactionValidationOutcome<Tx>> {
214        transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)).collect()
215    }
216}
217
218impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
219where
220    Client: ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt,
221    Tx: EthPoolTransaction,
222{
223    type Transaction = Tx;
224
225    async fn validate_transaction(
226        &self,
227        origin: TransactionOrigin,
228        transaction: Self::Transaction,
229    ) -> TransactionValidationOutcome<Self::Transaction> {
230        self.validate_one(origin, transaction)
231    }
232
233    async fn validate_transactions(
234        &self,
235        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
236    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
237        self.validate_all(transactions)
238    }
239
240    fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
241    where
242        B: Block,
243    {
244        self.inner.on_new_head_block(new_tip_block);
245        self.update_l1_block_info(
246            new_tip_block.header(),
247            new_tip_block.body().transactions().first(),
248        );
249    }
250}