reth_optimism_txpool/
validator.rs

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