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 number: AtomicU64,
33}
34
35impl OpL1BlockInfo {
36 pub fn timestamp(&self) -> u64 {
38 self.timestamp.load(Ordering::Relaxed)
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct OpTransactionValidator<Client, Tx> {
45 inner: Arc<EthTransactionValidator<Client, Tx>>,
47 block_info: Arc<OpL1BlockInfo>,
49 require_l1_data_gas_fee: bool,
53 supervisor_client: Option<SupervisorClient>,
55 fork_tracker: Arc<OpForkTracker>,
57}
58
59impl<Client, Tx> OpTransactionValidator<Client, Tx> {
60 pub fn chain_spec(&self) -> Arc<Client::ChainSpec>
62 where
63 Client: ChainSpecProvider,
64 {
65 self.inner.chain_spec()
66 }
67
68 pub fn client(&self) -> &Client {
70 self.inner.client()
71 }
72
73 fn block_timestamp(&self) -> u64 {
75 self.block_info.timestamp.load(Ordering::Relaxed)
76 }
77
78 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 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 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 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 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 pub fn with_supervisor(mut self, supervisor_client: SupervisorClient) -> Self {
131 self.supervisor_client = Some(supervisor_client);
132 self
133 }
134
135 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 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 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 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 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 fn apply_op_checks(
220 &self,
221 outcome: TransactionValidationOutcome<Tx>,
222 ) -> TransactionValidationOutcome<Tx> {
223 if !self.requires_l1_data_gas_fee() {
224 return outcome
226 }
227 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 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 pub async fn is_valid_cross_tx(&self, tx: &Tx) -> Option<Result<(), InvalidCrossTx>> {
279 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#[derive(Debug)]
344pub(crate) struct OpForkTracker {
345 interop: AtomicBool,
347}
348
349impl OpForkTracker {
350 pub(crate) fn is_interop_activated(&self) -> bool {
352 self.interop.load(Ordering::Relaxed)
353 }
354}