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
21const TRANSACTION_VALIDITY_WINDOW_SECS: u64 = 3600;
23
24#[derive(Debug, Default)]
26pub struct OpL1BlockInfo {
27 l1_block_info: RwLock<L1BlockInfo>,
29 timestamp: AtomicU64,
31}
32
33impl OpL1BlockInfo {
34 pub fn timestamp(&self) -> u64 {
36 self.timestamp.load(Ordering::Relaxed)
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct OpTransactionValidator<Client, Tx> {
43 inner: Arc<EthTransactionValidator<Client, Tx>>,
45 block_info: Arc<OpL1BlockInfo>,
47 require_l1_data_gas_fee: bool,
51 supervisor_client: Option<SupervisorClient>,
53 fork_tracker: Arc<OpForkTracker>,
55}
56
57impl<Client, Tx> OpTransactionValidator<Client, Tx> {
58 pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
60 where
61 Client: ChainSpecProvider,
62 {
63 self.inner.chain_spec()
64 }
65
66 pub fn client(&self) -> &Client {
68 self.inner.client()
69 }
70
71 fn block_timestamp(&self) -> u64 {
73 self.block_info.timestamp.load(Ordering::Relaxed)
74 }
75
76 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 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 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 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 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 pub fn with_supervisor(mut self, supervisor_client: SupervisorClient) -> Self {
129 self.supervisor_client = Some(supervisor_client);
130 self
131 }
132
133 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 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 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 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 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 fn apply_op_checks(
217 &self,
218 outcome: TransactionValidationOutcome<Tx>,
219 ) -> TransactionValidationOutcome<Tx> {
220 if !self.requires_l1_data_gas_fee() {
221 return outcome
223 }
224 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 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 pub async fn is_valid_cross_tx(&self, tx: &Tx) -> Option<Result<(), InvalidCrossTx>> {
276 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#[derive(Debug)]
342pub(crate) struct OpForkTracker {
343 interop: AtomicBool,
345}
346
347impl OpForkTracker {
348 pub(crate) fn is_interop_activated(&self) -> bool {
350 self.interop.load(Ordering::Relaxed)
351 }
352}