//! OP transaction pool types use parking_lot::RwLock; use reth_chainspec::ChainSpec; use reth_evm_optimism::RethL1BlockInfo; use reth_primitives::{Block, GotExpected, InvalidTransactionError, SealedBlock}; use reth_provider::{BlockReaderIdExt, StateProviderFactory}; use reth_revm::L1BlockInfo; use reth_transaction_pool::{ CoinbaseTipOrdering, EthPoolTransaction, EthPooledTransaction, EthTransactionValidator, Pool, TransactionOrigin, TransactionValidationOutcome, TransactionValidationTaskExecutor, TransactionValidator, }; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; /// Type alias for default optimism transaction pool pub type OpTransactionPool = Pool< TransactionValidationTaskExecutor>, CoinbaseTipOrdering, S, >; /// Validator for Optimism transactions. #[derive(Debug, Clone)] pub struct OpTransactionValidator { /// The type that performs the actual validation. inner: EthTransactionValidator, /// Additional block info required for validation. block_info: Arc, } impl OpTransactionValidator { /// Returns the configured chain spec pub fn chain_spec(&self) -> Arc { self.inner.chain_spec() } /// Returns the current block timestamp. fn block_timestamp(&self) -> u64 { self.block_info.timestamp.load(Ordering::Relaxed) } } impl OpTransactionValidator where Client: StateProviderFactory + BlockReaderIdExt, Tx: EthPoolTransaction, { /// Create a new [`OpTransactionValidator`]. pub fn new(inner: EthTransactionValidator) -> Self { let this = Self::with_block_info(inner, OpL1BlockInfo::default()); if let Ok(Some(block)) = this.inner.client().block_by_number_or_tag(reth_primitives::BlockNumberOrTag::Latest) { // genesis block has no txs, so we can't extract L1 info, we set the block info to empty // so that we will accept txs into the pool before the first block if block.number == 0 { this.block_info.timestamp.store(block.timestamp, Ordering::Relaxed); } else { this.update_l1_block_info(&block); } } this } /// Create a new [`OpTransactionValidator`] with the given [`OpL1BlockInfo`]. pub fn with_block_info( inner: EthTransactionValidator, block_info: OpL1BlockInfo, ) -> Self { Self { inner, block_info: Arc::new(block_info) } } /// Update the L1 block info. fn update_l1_block_info(&self, block: &Block) { self.block_info.timestamp.store(block.timestamp, Ordering::Relaxed); if let Ok(cost_addition) = reth_evm_optimism::extract_l1_info(block) { *self.block_info.l1_block_info.write() = cost_addition; } } /// Validates a single transaction. /// /// See also [`TransactionValidator::validate_transaction`] /// /// This behaves the same as [`EthTransactionValidator::validate_one`], but in addition, ensures /// that the account has enough balance to cover the L1 gas cost. pub fn validate_one( &self, origin: TransactionOrigin, transaction: Tx, ) -> TransactionValidationOutcome { if transaction.is_eip4844() { return TransactionValidationOutcome::Invalid( transaction, InvalidTransactionError::TxTypeNotSupported.into(), ) } let outcome = self.inner.validate_one(origin, transaction); // ensure that the account has enough balance to cover the L1 gas cost if let TransactionValidationOutcome::Valid { balance, state_nonce, transaction: valid_tx, propagate, } = outcome { let l1_block_info = self.block_info.l1_block_info.read().clone(); let mut encoded = Vec::with_capacity(valid_tx.transaction().encoded_length()); valid_tx.transaction().clone().into().encode_enveloped(&mut encoded); let cost_addition = match l1_block_info.l1_tx_data_fee( &self.chain_spec(), self.block_timestamp(), &encoded, false, ) { Ok(cost) => cost, Err(err) => { return TransactionValidationOutcome::Error(*valid_tx.hash(), Box::new(err)) } }; let cost = valid_tx.transaction().cost().saturating_add(cost_addition); // Checks for max cost if cost > balance { return TransactionValidationOutcome::Invalid( valid_tx.into_transaction(), InvalidTransactionError::InsufficientFunds( GotExpected { got: balance, expected: cost }.into(), ) .into(), ) } return TransactionValidationOutcome::Valid { balance, state_nonce, transaction: valid_tx, propagate, } } outcome } /// Validates all given transactions. /// /// Returns all outcomes for the given transactions in the same order. /// /// See also [`Self::validate_one`] pub fn validate_all( &self, transactions: Vec<(TransactionOrigin, Tx)>, ) -> Vec> { transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)).collect() } } impl TransactionValidator for OpTransactionValidator where Client: StateProviderFactory + BlockReaderIdExt, Tx: EthPoolTransaction, { type Transaction = Tx; async fn validate_transaction( &self, origin: TransactionOrigin, transaction: Self::Transaction, ) -> TransactionValidationOutcome { self.validate_one(origin, transaction) } async fn validate_transactions( &self, transactions: Vec<(TransactionOrigin, Self::Transaction)>, ) -> Vec> { self.validate_all(transactions) } fn on_new_head_block(&self, new_tip_block: &SealedBlock) { self.inner.on_new_head_block(new_tip_block); self.update_l1_block_info(&new_tip_block.clone().unseal()); } } /// Tracks additional infos for the current block. #[derive(Debug, Default)] pub struct OpL1BlockInfo { /// The current L1 block info. l1_block_info: RwLock, /// Current block timestamp. timestamp: AtomicU64, } #[cfg(test)] mod tests { use crate::txpool::OpTransactionValidator; use reth_chainspec::MAINNET; use reth_primitives::{ Signature, Transaction, TransactionSigned, TransactionSignedEcRecovered, TxDeposit, TxKind, U256, }; use reth_provider::test_utils::MockEthProvider; use reth_transaction_pool::{ blobstore::InMemoryBlobStore, validate::EthTransactionValidatorBuilder, EthPooledTransaction, TransactionOrigin, TransactionValidationOutcome, }; #[test] fn validate_optimism_transaction() { let client = MockEthProvider::default(); let validator = EthTransactionValidatorBuilder::new(MAINNET.clone()) .no_shanghai() .no_cancun() .build(client, InMemoryBlobStore::default()); let validator = OpTransactionValidator::new(validator); let origin = TransactionOrigin::External; let signer = Default::default(); let deposit_tx = Transaction::Deposit(TxDeposit { source_hash: Default::default(), from: signer, to: TxKind::Create, mint: None, value: U256::ZERO, gas_limit: 0u64, is_system_transaction: false, input: Default::default(), }); let signature = Signature::default(); let signed_tx = TransactionSigned::from_transaction_and_signature(deposit_tx, signature); let signed_recovered = TransactionSignedEcRecovered::from_signed_transaction(signed_tx, signer); let len = signed_recovered.length_without_header(); let pooled_tx = EthPooledTransaction::new(signed_recovered, len); let outcome = validator.validate_one(origin, pooled_tx); let err = match outcome { TransactionValidationOutcome::Invalid(_, err) => err, _ => panic!("Expected invalid transaction"), }; assert_eq!(err.to_string(), "transaction type not supported"); } }