reth_optimism_txpool/
validator.rs

1use crate::{supervisor::SupervisorClient, InvalidCrossTx, OpPooledTx};
2use alloy_consensus::{BlockHeader, Transaction};
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::{AccountInfoReader, BlockReaderIdExt, StateProviderFactory};
12use reth_transaction_pool::{
13    error::InvalidPoolTransactionError, EthPoolTransaction, EthTransactionValidator,
14    TransactionOrigin, TransactionValidationOutcome, TransactionValidator,
15};
16use std::sync::{
17    atomic::{AtomicBool, AtomicU64, Ordering},
18    Arc,
19};
20
21/// The interval for which we check transaction against supervisor, 1 hour.
22const TRANSACTION_VALIDITY_WINDOW_SECS: u64 = 3600;
23
24/// Tracks additional infos for the current block.
25#[derive(Debug, Default)]
26pub struct OpL1BlockInfo {
27    /// The current L1 block info.
28    l1_block_info: RwLock<L1BlockInfo>,
29    /// Current block timestamp.
30    timestamp: AtomicU64,
31}
32
33impl OpL1BlockInfo {
34    /// Returns the most recent timestamp
35    pub fn timestamp(&self) -> u64 {
36        self.timestamp.load(Ordering::Relaxed)
37    }
38}
39
40/// Validator for Optimism transactions.
41#[derive(Debug, Clone)]
42pub struct OpTransactionValidator<Client, Tx> {
43    /// The type that performs the actual validation.
44    inner: Arc<EthTransactionValidator<Client, Tx>>,
45    /// Additional block info required for validation.
46    block_info: Arc<OpL1BlockInfo>,
47    /// If true, ensure that the transaction's sender has enough balance to cover the L1 gas fee
48    /// derived from the tracked L1 block info that is extracted from the first transaction in the
49    /// L2 block.
50    require_l1_data_gas_fee: bool,
51    /// Client used to check transaction validity with op-supervisor
52    supervisor_client: Option<SupervisorClient>,
53    /// tracks activated forks relevant for transaction validation
54    fork_tracker: Arc<OpForkTracker>,
55}
56
57impl<Client, Tx> OpTransactionValidator<Client, Tx> {
58    /// Returns the configured chain spec
59    pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
60    where
61        Client: ChainSpecProvider,
62    {
63        self.inner.chain_spec()
64    }
65
66    /// Returns the configured client
67    pub fn client(&self) -> &Client {
68        self.inner.client()
69    }
70
71    /// Returns the current block timestamp.
72    fn block_timestamp(&self) -> u64 {
73        self.block_info.timestamp.load(Ordering::Relaxed)
74    }
75
76    /// Whether to ensure that the transaction's sender has enough balance to also cover the L1 gas
77    /// fee.
78    pub fn require_l1_data_gas_fee(self, require_l1_data_gas_fee: bool) -> Self {
79        Self { require_l1_data_gas_fee, ..self }
80    }
81
82    /// Returns whether this validator also requires the transaction's sender to have enough balance
83    /// to cover the L1 gas fee.
84    pub const fn requires_l1_data_gas_fee(&self) -> bool {
85        self.require_l1_data_gas_fee
86    }
87}
88
89impl<Client, Tx> OpTransactionValidator<Client, Tx>
90where
91    Client:
92        ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt + Sync,
93    Tx: EthPoolTransaction + OpPooledTx,
94{
95    /// Create a new [`OpTransactionValidator`].
96    pub fn new(inner: EthTransactionValidator<Client, Tx>) -> Self {
97        let this = Self::with_block_info(inner, OpL1BlockInfo::default());
98        if let Ok(Some(block)) =
99            this.inner.client().block_by_number_or_tag(alloy_eips::BlockNumberOrTag::Latest)
100        {
101            // genesis block has no txs, so we can't extract L1 info, we set the block info to empty
102            // so that we will accept txs into the pool before the first block
103            if block.header().number() == 0 {
104                this.block_info.timestamp.store(block.header().timestamp(), Ordering::Relaxed);
105            } else {
106                this.update_l1_block_info(block.header(), block.body().transactions().first());
107            }
108        }
109
110        this
111    }
112
113    /// Create a new [`OpTransactionValidator`] with the given [`OpL1BlockInfo`].
114    pub fn with_block_info(
115        inner: EthTransactionValidator<Client, Tx>,
116        block_info: OpL1BlockInfo,
117    ) -> Self {
118        Self {
119            inner: Arc::new(inner),
120            block_info: Arc::new(block_info),
121            require_l1_data_gas_fee: true,
122            supervisor_client: None,
123            fork_tracker: Arc::new(OpForkTracker { interop: AtomicBool::from(false) }),
124        }
125    }
126
127    /// Set the supervisor client and safety level
128    pub fn with_supervisor(mut self, supervisor_client: SupervisorClient) -> Self {
129        self.supervisor_client = Some(supervisor_client);
130        self
131    }
132
133    /// Update the L1 block info for the given header and system transaction, if any.
134    ///
135    /// Note: this supports optional system transaction, in case this is used in a dev setup
136    pub fn update_l1_block_info<H, T>(&self, header: &H, tx: Option<&T>)
137    where
138        H: BlockHeader,
139        T: Transaction,
140    {
141        self.block_info.timestamp.store(header.timestamp(), Ordering::Relaxed);
142
143        if let Some(Ok(l1_block_info)) = tx.map(reth_optimism_evm::extract_l1_info_from_tx) {
144            *self.block_info.l1_block_info.write() = l1_block_info;
145        }
146
147        if self.chain_spec().is_interop_active_at_timestamp(header.timestamp()) {
148            self.fork_tracker.interop.store(true, Ordering::Relaxed);
149        }
150    }
151
152    /// Validates a single transaction.
153    ///
154    /// See also [`TransactionValidator::validate_transaction`]
155    ///
156    /// This behaves the same as [`OpTransactionValidator::validate_one_with_state`], but creates
157    /// a new state provider internally.
158    pub async fn validate_one(
159        &self,
160        origin: TransactionOrigin,
161        transaction: Tx,
162    ) -> TransactionValidationOutcome<Tx> {
163        self.validate_one_with_state(origin, transaction, &mut None).await
164    }
165
166    /// Validates a single transaction with a provided state provider.
167    ///
168    /// This allows reusing the same state provider across multiple transaction validations.
169    ///
170    /// See also [`TransactionValidator::validate_transaction`]
171    ///
172    /// This behaves the same as [`EthTransactionValidator::validate_one_with_state`], but in
173    /// addition applies OP validity checks:
174    /// - ensures tx is not eip4844
175    /// - ensures cross chain transactions are valid wrt locally configured safety level
176    /// - ensures that the account has enough balance to cover the L1 gas cost
177    pub async fn validate_one_with_state(
178        &self,
179        origin: TransactionOrigin,
180        transaction: Tx,
181        state: &mut Option<Box<dyn AccountInfoReader + Send>>,
182    ) -> TransactionValidationOutcome<Tx> {
183        if transaction.is_eip4844() {
184            return TransactionValidationOutcome::Invalid(
185                transaction,
186                InvalidTransactionError::TxTypeNotSupported.into(),
187            )
188        }
189
190        // Interop cross tx validation
191        match self.is_valid_cross_tx(&transaction).await {
192            Some(Err(err)) => {
193                let err = match err {
194                    InvalidCrossTx::CrossChainTxPreInterop => {
195                        InvalidTransactionError::TxTypeNotSupported.into()
196                    }
197                    err => InvalidPoolTransactionError::Other(Box::new(err)),
198                };
199                return TransactionValidationOutcome::Invalid(transaction, err)
200            }
201            Some(Ok(_)) => {
202                // valid interop tx
203                transaction.set_interop_deadline(
204                    self.block_timestamp() + TRANSACTION_VALIDITY_WINDOW_SECS,
205                );
206            }
207            _ => {}
208        }
209
210        let outcome = self.inner.validate_one_with_state(origin, transaction, state);
211
212        self.apply_op_checks(outcome)
213    }
214
215    /// Performs the necessary opstack specific checks based on top of the regular eth outcome.
216    fn apply_op_checks(
217        &self,
218        outcome: TransactionValidationOutcome<Tx>,
219    ) -> TransactionValidationOutcome<Tx> {
220        if !self.requires_l1_data_gas_fee() {
221            // no need to check L1 gas fee
222            return outcome
223        }
224        // ensure that the account has enough balance to cover the L1 gas cost
225        if let TransactionValidationOutcome::Valid {
226            balance,
227            state_nonce,
228            transaction: valid_tx,
229            propagate,
230            bytecode_hash,
231            authorities,
232        } = outcome
233        {
234            let mut l1_block_info = self.block_info.l1_block_info.read().clone();
235
236            let encoded = valid_tx.transaction().encoded_2718();
237
238            let cost_addition = match l1_block_info.l1_tx_data_fee(
239                self.chain_spec(),
240                self.block_timestamp(),
241                &encoded,
242                false,
243            ) {
244                Ok(cost) => cost,
245                Err(err) => {
246                    return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err))
247                }
248            };
249            let cost = valid_tx.transaction().cost().saturating_add(cost_addition);
250
251            // Checks for max cost
252            if cost > balance {
253                return TransactionValidationOutcome::Invalid(
254                    valid_tx.into_transaction(),
255                    InvalidTransactionError::InsufficientFunds(
256                        GotExpected { got: balance, expected: cost }.into(),
257                    )
258                    .into(),
259                )
260            }
261
262            return TransactionValidationOutcome::Valid {
263                balance,
264                state_nonce,
265                transaction: valid_tx,
266                propagate,
267                bytecode_hash,
268                authorities,
269            }
270        }
271        outcome
272    }
273
274    /// Wrapper for is valid cross tx
275    pub async fn is_valid_cross_tx(&self, tx: &Tx) -> Option<Result<(), InvalidCrossTx>> {
276        // We don't need to check for deposit transaction in here, because they won't come from
277        // txpool
278        self.supervisor_client
279            .as_ref()?
280            .is_valid_cross_tx(
281                tx.access_list(),
282                tx.hash(),
283                self.block_info.timestamp.load(Ordering::Relaxed),
284                Some(TRANSACTION_VALIDITY_WINDOW_SECS),
285                self.fork_tracker.is_interop_activated(),
286            )
287            .await
288    }
289}
290
291impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
292where
293    Client:
294        ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt + Sync,
295    Tx: EthPoolTransaction + OpPooledTx,
296{
297    type Transaction = Tx;
298
299    async fn validate_transaction(
300        &self,
301        origin: TransactionOrigin,
302        transaction: Self::Transaction,
303    ) -> TransactionValidationOutcome<Self::Transaction> {
304        self.validate_one(origin, transaction).await
305    }
306
307    async fn validate_transactions(
308        &self,
309        transactions: Vec<(TransactionOrigin, Self::Transaction)>,
310    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
311        futures_util::future::join_all(
312            transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)),
313        )
314        .await
315    }
316
317    async fn validate_transactions_with_origin(
318        &self,
319        origin: TransactionOrigin,
320        transactions: impl IntoIterator<Item = Self::Transaction> + Send,
321    ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
322        futures_util::future::join_all(
323            transactions.into_iter().map(|tx| self.validate_one(origin, tx)),
324        )
325        .await
326    }
327
328    fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
329    where
330        B: Block,
331    {
332        self.inner.on_new_head_block(new_tip_block);
333        self.update_l1_block_info(
334            new_tip_block.header(),
335            new_tip_block.body().transactions().first(),
336        );
337    }
338}
339
340/// Keeps track of whether certain forks are activated
341#[derive(Debug)]
342pub(crate) struct OpForkTracker {
343    /// Tracks if interop is activated at the block's timestamp.
344    interop: AtomicBool,
345}
346
347impl OpForkTracker {
348    /// Returns `true` if Interop fork is activated.
349    pub(crate) fn is_interop_activated(&self) -> bool {
350        self.interop.load(Ordering::Relaxed)
351    }
352}