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: ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt,
92 Tx: EthPoolTransaction + OpPooledTx,
93{
94 pub fn new(inner: EthTransactionValidator<Client, Tx>) -> Self {
96 let this = Self::with_block_info(inner, OpL1BlockInfo::default());
97 if let Ok(Some(block)) =
98 this.inner.client().block_by_number_or_tag(alloy_eips::BlockNumberOrTag::Latest)
99 {
100 if block.header().number() == 0 {
103 this.block_info.timestamp.store(block.header().timestamp(), Ordering::Relaxed);
104 } else {
105 this.update_l1_block_info(block.header(), block.body().transactions().first());
106 }
107 }
108
109 this
110 }
111
112 pub fn with_block_info(
114 inner: EthTransactionValidator<Client, Tx>,
115 block_info: OpL1BlockInfo,
116 ) -> Self {
117 Self {
118 inner: Arc::new(inner),
119 block_info: Arc::new(block_info),
120 require_l1_data_gas_fee: true,
121 supervisor_client: None,
122 fork_tracker: Arc::new(OpForkTracker { interop: AtomicBool::from(false) }),
123 }
124 }
125
126 pub fn with_supervisor(mut self, supervisor_client: SupervisorClient) -> Self {
128 self.supervisor_client = Some(supervisor_client);
129 self
130 }
131
132 pub fn update_l1_block_info<H, T>(&self, header: &H, tx: Option<&T>)
136 where
137 H: BlockHeader,
138 T: Transaction,
139 {
140 self.block_info.timestamp.store(header.timestamp(), Ordering::Relaxed);
141
142 if let Some(Ok(l1_block_info)) = tx.map(reth_optimism_evm::extract_l1_info_from_tx) {
143 *self.block_info.l1_block_info.write() = l1_block_info;
144 }
145
146 if self.chain_spec().is_interop_active_at_timestamp(header.timestamp()) {
147 self.fork_tracker.interop.store(true, Ordering::Relaxed);
148 }
149 }
150
151 pub async fn validate_one(
158 &self,
159 origin: TransactionOrigin,
160 transaction: Tx,
161 ) -> TransactionValidationOutcome<Tx> {
162 self.validate_one_with_state(origin, transaction, &mut None).await
163 }
164
165 pub async fn validate_one_with_state(
177 &self,
178 origin: TransactionOrigin,
179 transaction: Tx,
180 state: &mut Option<Box<dyn AccountInfoReader>>,
181 ) -> TransactionValidationOutcome<Tx> {
182 if transaction.is_eip4844() {
183 return TransactionValidationOutcome::Invalid(
184 transaction,
185 InvalidTransactionError::TxTypeNotSupported.into(),
186 )
187 }
188
189 match self.is_valid_cross_tx(&transaction).await {
191 Some(Err(err)) => {
192 let err = match err {
193 InvalidCrossTx::CrossChainTxPreInterop => {
194 InvalidTransactionError::TxTypeNotSupported.into()
195 }
196 err => InvalidPoolTransactionError::Other(Box::new(err)),
197 };
198 return TransactionValidationOutcome::Invalid(transaction, err)
199 }
200 Some(Ok(_)) => {
201 transaction.set_interop_deadline(
203 self.block_timestamp() + TRANSACTION_VALIDITY_WINDOW_SECS,
204 );
205 }
206 _ => {}
207 }
208
209 let outcome = self.inner.validate_one_with_state(origin, transaction, state);
210
211 self.apply_op_checks(outcome)
212 }
213
214 fn apply_op_checks(
216 &self,
217 outcome: TransactionValidationOutcome<Tx>,
218 ) -> TransactionValidationOutcome<Tx> {
219 if !self.requires_l1_data_gas_fee() {
220 return outcome
222 }
223 if let TransactionValidationOutcome::Valid {
225 balance,
226 state_nonce,
227 transaction: valid_tx,
228 propagate,
229 bytecode_hash,
230 authorities,
231 } = outcome
232 {
233 let mut l1_block_info = self.block_info.l1_block_info.read().clone();
234
235 let encoded = valid_tx.transaction().encoded_2718();
236
237 let cost_addition = match l1_block_info.l1_tx_data_fee(
238 self.chain_spec(),
239 self.block_timestamp(),
240 &encoded,
241 false,
242 ) {
243 Ok(cost) => cost,
244 Err(err) => {
245 return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err))
246 }
247 };
248 let cost = valid_tx.transaction().cost().saturating_add(cost_addition);
249
250 if cost > balance {
252 return TransactionValidationOutcome::Invalid(
253 valid_tx.into_transaction(),
254 InvalidTransactionError::InsufficientFunds(
255 GotExpected { got: balance, expected: cost }.into(),
256 )
257 .into(),
258 )
259 }
260
261 return TransactionValidationOutcome::Valid {
262 balance,
263 state_nonce,
264 transaction: valid_tx,
265 propagate,
266 bytecode_hash,
267 authorities,
268 }
269 }
270 outcome
271 }
272
273 pub async fn is_valid_cross_tx(&self, tx: &Tx) -> Option<Result<(), InvalidCrossTx>> {
275 self.supervisor_client
278 .as_ref()?
279 .is_valid_cross_tx(
280 tx.access_list(),
281 tx.hash(),
282 self.block_info.timestamp.load(Ordering::Relaxed),
283 Some(TRANSACTION_VALIDITY_WINDOW_SECS),
284 self.fork_tracker.is_interop_activated(),
285 )
286 .await
287 }
288}
289
290impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
291where
292 Client: ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt,
293 Tx: EthPoolTransaction + OpPooledTx,
294{
295 type Transaction = Tx;
296
297 async fn validate_transaction(
298 &self,
299 origin: TransactionOrigin,
300 transaction: Self::Transaction,
301 ) -> TransactionValidationOutcome<Self::Transaction> {
302 self.validate_one(origin, transaction).await
303 }
304
305 async fn validate_transactions(
306 &self,
307 transactions: Vec<(TransactionOrigin, Self::Transaction)>,
308 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
309 futures_util::future::join_all(
310 transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)),
311 )
312 .await
313 }
314
315 async fn validate_transactions_with_origin(
316 &self,
317 origin: TransactionOrigin,
318 transactions: impl IntoIterator<Item = Self::Transaction> + Send,
319 ) -> Vec<TransactionValidationOutcome<Self::Transaction>> {
320 futures_util::future::join_all(
321 transactions.into_iter().map(|tx| self.validate_one(origin, tx)),
322 )
323 .await
324 }
325
326 fn on_new_head_block<B>(&self, new_tip_block: &SealedBlock<B>)
327 where
328 B: Block,
329 {
330 self.inner.on_new_head_block(new_tip_block);
331 self.update_l1_block_info(
332 new_tip_block.header(),
333 new_tip_block.body().transactions().first(),
334 );
335 }
336}
337
338#[derive(Debug)]
340pub(crate) struct OpForkTracker {
341 interop: AtomicBool,
343}
344
345impl OpForkTracker {
346 pub(crate) fn is_interop_activated(&self) -> bool {
348 self.interop.load(Ordering::Relaxed)
349 }
350}