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