diff --git a/crates/optimism/node/src/txpool.rs b/crates/optimism/node/src/txpool.rs index a0e6c6119c54..6792148e979c 100644 --- a/crates/optimism/node/src/txpool.rs +++ b/crates/optimism/node/src/txpool.rs @@ -325,26 +325,63 @@ where } } - /// 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( + fn validate_batch( &self, - origin: TransactionOrigin, - transaction: Tx, - ) -> TransactionValidationOutcome { - if transaction.is_eip4844() { - return TransactionValidationOutcome::Invalid( - transaction, - InvalidTransactionError::TxTypeNotSupported.into(), - ) + transactions: Vec<(TransactionOrigin, Tx)>, + ) -> Vec> { + let length = transactions.len(); + let mut transactions_to_validate = Vec::with_capacity(length); + let mut invalid_tx_type_outcomes = Vec::new(); + for (i, (tx_origin, tx)) in transactions.into_iter().enumerate() { + match Self::validate_transaction_type(tx) { + Ok(tx) => transactions_to_validate.push((tx_origin, tx)), + Err(outcome) => invalid_tx_type_outcomes.push((i, outcome)), + } } + let validated_outcomes = self.inner.validate_all(transactions_to_validate); + + // final vector containing all outcomes + let mut outcomes = Vec::with_capacity(length); + + // merge outcomes vectors, keeping original order + let mut invalid_type_outcomes_it = invalid_tx_type_outcomes.into_iter(); + let mut validated_outcomes_it = validated_outcomes.into_iter(); + let mut curr_invalid_type_outcome = invalid_type_outcomes_it.next(); + let mut curr_validated_outcome = validated_outcomes_it.next(); + for i in 0..length { + match curr_invalid_type_outcome { + Some((j, outcome)) if i == j => { + outcomes.push(outcome); + curr_invalid_type_outcome = invalid_type_outcomes_it.next(); + continue; + } + _ => {} + } - let outcome = self.inner.validate_one(origin, transaction); + if let Some(outcome) = curr_validated_outcome { + outcomes.push(self.validate_gas_fee(outcome)); + curr_validated_outcome = validated_outcomes_it.next(); + } + } + + outcomes + } + fn validate_transaction_type(tx: Tx) -> Result> { + if tx.is_eip4844() { + Err(TransactionValidationOutcome::Invalid( + tx, + InvalidTransactionError::TxTypeNotSupported.into(), + )) + } else { + Ok(tx) + } + } + + fn validate_gas_fee( + &self, + outcome: TransactionValidationOutcome, + ) -> TransactionValidationOutcome { if !self.requires_l1_data_gas_fee() { // no need to check L1 gas fee return outcome @@ -399,6 +436,25 @@ where outcome } + /// 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 { + let transaction = match Self::validate_transaction_type(transaction) { + Ok(tx) => tx, + Err(outcome) => return outcome, + }; + let outcome = self.inner.validate_one(origin, transaction); + self.validate_gas_fee(outcome) + } + /// Validates all given transactions. /// /// Returns all outcomes for the given transactions in the same order. @@ -408,7 +464,7 @@ where &self, transactions: Vec<(TransactionOrigin, Tx)>, ) -> Vec> { - transactions.into_iter().map(|(origin, tx)| self.validate_one(origin, tx)).collect() + self.validate_batch(transactions) } } @@ -503,4 +559,41 @@ mod tests { }; assert_eq!(err.to_string(), "transaction type not supported"); } + + #[test] + fn validate_optimism_transaction_all() { + 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 = OpTypedTransaction::Deposit(TxDeposit { + source_hash: Default::default(), + from: signer, + to: TxKind::Create, + mint: None, + value: U256::ZERO, + gas_limit: 0, + is_system_transaction: false, + input: Default::default(), + }); + let signature = Signature::test_signature(); + let signed_tx = OpTransactionSigned::new_unhashed(deposit_tx, signature); + let signed_recovered = Recovered::new_unchecked(signed_tx, signer); + let len = signed_recovered.encode_2718_len(); + let pooled_tx = OpPooledTransaction::new(signed_recovered, len); + let outcomes = validator.validate_all(vec![(origin, pooled_tx)]); + + for outcome in outcomes { + let err = match outcome { + TransactionValidationOutcome::Invalid(_, err) => err, + _ => panic!("Expected invalid transaction"), + }; + assert_eq!(err.to_string(), "transaction type not supported"); + } + } }