From 9dd9f9eaab64c9630b2e898ce7719ab799931421 Mon Sep 17 00:00:00 2001 From: rakita Date: Mon, 6 Jan 2025 10:29:45 +0100 Subject: [PATCH] chore: optimize tx l1 fetches (#1967) * chore: optimize tx l1 fetches * add std box --- crates/revm/src/optimism.rs | 2 +- crates/revm/src/optimism/handler_register.rs | 137 ++++++++++++++++--- crates/revm/src/optimism/l1block.rs | 38 +++-- 3 files changed, 152 insertions(+), 25 deletions(-) diff --git a/crates/revm/src/optimism.rs b/crates/revm/src/optimism.rs index 64bff5295e..2f910729c6 100644 --- a/crates/revm/src/optimism.rs +++ b/crates/revm/src/optimism.rs @@ -7,7 +7,7 @@ mod l1block; mod precompile; pub use handler_register::{ - deduct_caller, end, last_frame_return, load_accounts, load_precompiles, + clear, deduct_caller, end, last_frame_return, load_accounts, load_precompiles, optimism_handle_register, output, refund, reimburse_caller, reward_beneficiary, validate_env, validate_tx_against_state, }; diff --git a/crates/revm/src/optimism/handler_register.rs b/crates/revm/src/optimism/handler_register.rs index 7185dbc287..e004f6444f 100644 --- a/crates/revm/src/optimism/handler_register.rs +++ b/crates/revm/src/optimism/handler_register.rs @@ -14,10 +14,9 @@ use crate::{ }, Context, ContextPrecompiles, FrameResult, }; -use core::ops::Mul; +use core::{cmp::Ordering, ops::Mul}; use revm_precompile::PrecompileSpecId; -use std::string::ToString; -use std::sync::Arc; +use std::{boxed::Box, string::ToString, sync::Arc}; use super::l1block::OPERATOR_FEE_RECIPIENT; @@ -41,6 +40,7 @@ pub fn optimism_handle_register(handler: &mut EvmHandler<'_, // In case of halt of deposit transaction return Error. handler.post_execution.output = Arc::new(output::); handler.post_execution.end = Arc::new(end::); + handler.post_execution.clear = Arc::new(clear::); }); } @@ -70,10 +70,113 @@ pub fn validate_env(env: &Env) -> Result<(), EVMError< pub fn validate_tx_against_state( context: &mut Context, ) -> Result<(), EVMError> { - if context.evm.inner.env.tx.optimism.source_hash.is_some() { + let env @ Env { cfg, tx, .. } = context.evm.inner.env.as_ref(); + + // No validation is needed for deposit transactions, as they are pre-verified on L1. + if tx.optimism.source_hash.is_some() { return Ok(()); } - mainnet::validate_tx_against_state::(context) + + // load acc + let tx_caller = tx.caller; + let account = context + .evm + .inner + .journaled_state + .load_code(tx_caller, &mut context.evm.inner.db)? + .data; + + // EIP-3607: Reject transactions from senders with deployed code + // This EIP is introduced after london but there was no collision in past + // so we can leave it enabled always + if !cfg.is_eip3607_disabled() { + let bytecode = &account.info.code.as_ref().unwrap(); + // allow EOAs whose code is a valid delegation designation, + // i.e. 0xef0100 || address, to continue to originate transactions. + if !bytecode.is_empty() && !bytecode.is_eip7702() { + return Err(EVMError::Transaction( + InvalidTransaction::RejectCallerWithCode, + )); + } + } + + // Check that the transaction's nonce is correct + if let Some(tx) = tx.nonce { + let state = account.info.nonce; + match tx.cmp(&state) { + Ordering::Greater => { + return Err(EVMError::Transaction(InvalidTransaction::NonceTooHigh { + tx, + state, + })); + } + Ordering::Less => { + return Err(EVMError::Transaction(InvalidTransaction::NonceTooLow { + tx, + state, + })); + } + _ => {} + } + } + + // get envelope + let Some(enveloped_tx) = &tx.optimism.enveloped_tx else { + return Err(EVMError::Custom( + "[OPTIMISM] Failed to load enveloped transaction.".to_string(), + )); + }; + + // compute L1 cost + let tx_l1_cost = context + .evm + .inner + .l1_block_info + .as_mut() + .expect("L1BlockInfo should be loaded") + .calculate_tx_l1_cost(enveloped_tx, SPEC::SPEC_ID); + + let gas_limit = U256::from(tx.gas_limit); + let operator_fee_charge = context + .evm + .inner + .l1_block_info + .as_ref() + .expect("L1BlockInfo should be loaded") + .operator_fee_charge(gas_limit, SPEC::SPEC_ID); + + let mut balance_check = gas_limit + .checked_mul(tx.gas_price) + .and_then(|gas_cost| gas_cost.checked_add(tx.value)) + .and_then(|total_cost| total_cost.checked_add(tx_l1_cost)) + .and_then(|total_cost| total_cost.checked_add(operator_fee_charge)) + .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?; + + if SPEC::enabled(SpecId::CANCUN) { + // if the tx is not a blob tx, this will be None, so we add zero + let data_fee = env.calc_max_data_fee().unwrap_or_default(); + balance_check = balance_check + .checked_add(U256::from(data_fee)) + .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?; + } + + // Check if account has enough balance for gas_limit*gas_price and value transfer. + // Transfer will be done inside `*_inner` functions. + if balance_check > account.info.balance { + if cfg.is_balance_check_disabled() { + // Add transaction cost to balance to ensure execution doesn't fail. + account.info.balance = balance_check; + } else { + return Err(EVMError::Transaction( + InvalidTransaction::LackOfFundForMaxFee { + fee: Box::new(balance_check), + balance: Box::new(account.info.balance), + }, + )); + } + } + + Ok(()) } /// Handle output of the transaction @@ -266,17 +369,9 @@ pub fn deduct_caller( .evm .inner .l1_block_info - .as_ref() + .as_mut() .expect("L1BlockInfo should be loaded") .calculate_tx_l1_cost(enveloped_tx, SPEC::SPEC_ID); - if tx_l1_cost.gt(&caller_account.info.balance) { - return Err(EVMError::Transaction( - InvalidTransaction::LackOfFundForMaxFee { - fee: tx_l1_cost.into(), - balance: caller_account.info.balance.into(), - }, - )); - } caller_account.info.balance = caller_account.info.balance.saturating_sub(tx_l1_cost); // Deduct the operator fee from the caller's account. @@ -314,7 +409,7 @@ pub fn reward_beneficiary( if !is_deposit { // If the transaction is not a deposit transaction, fees are paid out // to both the Base Fee Vault as well as the L1 Fee Vault. - let Some(l1_block_info) = &context.evm.inner.l1_block_info else { + let Some(l1_block_info) = &mut context.evm.inner.l1_block_info else { return Err(EVMError::Custom( "[OPTIMISM] Failed to load L1 block information.".to_string(), )); @@ -459,6 +554,16 @@ pub fn end( }) } +/// Clears cache OP l1 value. +#[inline] +pub fn clear(context: &mut Context) { + // clear error and journaled state. + mainnet::clear(context); + if let Some(l1_block) = &mut context.evm.inner.l1_block_info { + l1_block.clear_tx_l1_cost(); + } +} + #[cfg(test)] mod tests { use revm_interpreter::{CallOutcome, InterpreterResult}; @@ -714,7 +819,7 @@ mod tests { context.evm.inner.env.tx.optimism.enveloped_tx = Some(bytes!("FACADE")); assert_eq!( - deduct_caller::(&mut context), + validate_tx_against_state::(&mut context), Err(EVMError::Transaction( InvalidTransaction::LackOfFundForMaxFee { fee: Box::new(U256::from(1048)), diff --git a/crates/revm/src/optimism/l1block.rs b/crates/revm/src/optimism/l1block.rs index 780b5726da..8b94acaabb 100644 --- a/crates/revm/src/optimism/l1block.rs +++ b/crates/revm/src/optimism/l1block.rs @@ -94,6 +94,8 @@ pub struct L1BlockInfo { pub operator_fee_constant: Option, /// True if Ecotone is activated, but the L1 fee scalars have not yet been set. pub(crate) empty_ecotone_scalars: bool, + /// Last calculated l1 fee cost. Uses as a cache between validation and pre execution stages. + pub tx_l1_cost: Option, } impl L1BlockInfo { @@ -169,6 +171,7 @@ impl L1BlockInfo { l1_fee_overhead, operator_fee_scalar: Some(operator_fee_scalar), operator_fee_constant: Some(operator_fee_constant), + tx_l1_cost: None, }) } else { // Pre-isthmus L1 block info @@ -269,20 +272,30 @@ impl L1BlockInfo { ) } + /// Clears the cached L1 cost of the transaction. + pub fn clear_tx_l1_cost(&mut self) { + self.tx_l1_cost = None; + } + /// Calculate the gas cost of a transaction based on L1 block data posted on L2, depending on the [SpecId] passed. - pub fn calculate_tx_l1_cost(&self, input: &[u8], spec_id: SpecId) -> U256 { + /// And cache the result for future use. + pub fn calculate_tx_l1_cost(&mut self, input: &[u8], spec_id: SpecId) -> U256 { + if let Some(tx_l1_cost) = self.tx_l1_cost { + return tx_l1_cost; + } // If the input is a deposit transaction or empty, the default value is zero. - if input.is_empty() || input.first() == Some(&0x7F) { + let tx_l1_cost = if input.is_empty() || input.first() == Some(&0x7F) { return U256::ZERO; - } - - if spec_id.is_enabled_in(SpecId::FJORD) { + } else if spec_id.is_enabled_in(SpecId::FJORD) { self.calculate_tx_l1_cost_fjord(input) } else if spec_id.is_enabled_in(SpecId::ECOTONE) { self.calculate_tx_l1_cost_ecotone(input, spec_id) } else { self.calculate_tx_l1_cost_bedrock(input, spec_id) - } + }; + + self.tx_l1_cost = Some(tx_l1_cost); + tx_l1_cost } /// Calculate the gas cost of a transaction based on L1 block data posted on L2, pre-Ecotone. @@ -416,7 +429,7 @@ mod tests { #[test] fn test_calculate_tx_l1_cost() { - let l1_block_info = L1BlockInfo { + let mut l1_block_info = L1BlockInfo { l1_base_fee: U256::from(1_000), l1_fee_overhead: Some(U256::from(1_000)), l1_base_fee_scalar: U256::from(1_000), @@ -426,16 +439,19 @@ mod tests { let input = bytes!("FACADE"); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::REGOLITH); assert_eq!(gas_cost, U256::from(1048)); + l1_block_info.clear_tx_l1_cost(); // Zero rollup data gas cost should result in zero let input = bytes!(""); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::REGOLITH); assert_eq!(gas_cost, U256::ZERO); + l1_block_info.clear_tx_l1_cost(); // Deposit transactions with the EIP-2718 type of 0x7F should result in zero let input = bytes!("7FFACADE"); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::REGOLITH); assert_eq!(gas_cost, U256::ZERO); + l1_block_info.clear_tx_l1_cost(); } #[test] @@ -455,16 +471,19 @@ mod tests { let input = bytes!("FACADE"); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); assert_eq!(gas_cost, U256::from(51)); + l1_block_info.clear_tx_l1_cost(); // Zero rollup data gas cost should result in zero let input = bytes!(""); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); assert_eq!(gas_cost, U256::ZERO); + l1_block_info.clear_tx_l1_cost(); // Deposit transactions with the EIP-2718 type of 0x7F should result in zero let input = bytes!("7FFACADE"); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::ECOTONE); assert_eq!(gas_cost, U256::ZERO); + l1_block_info.clear_tx_l1_cost(); // If the scalars are empty, the bedrock cost function should be used. l1_block_info.empty_ecotone_scalars = true; @@ -478,7 +497,7 @@ mod tests { // l1FeeScaled = baseFeeScalar*l1BaseFee*16 + blobFeeScalar*l1BlobBaseFee // = 1000 * 1000 * 16 + 1000 * 1000 // = 17e6 - let l1_block_info = L1BlockInfo { + let mut l1_block_info = L1BlockInfo { l1_base_fee: U256::from(1_000), l1_base_fee_scalar: U256::from(1_000), l1_blob_base_fee: Some(U256::from(1_000)), @@ -496,6 +515,7 @@ mod tests { // = 1700 let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::FJORD); assert_eq!(gas_cost, U256::from(1700)); + l1_block_info.clear_tx_l1_cost(); // fastLzSize = 202 // estimatedSize = max(minTransactionSize, intercept + fastlzCoef*fastlzSize) @@ -507,11 +527,13 @@ mod tests { // = 2148 let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::FJORD); assert_eq!(gas_cost, U256::from(2148)); + l1_block_info.clear_tx_l1_cost(); // Zero rollup data gas cost should result in zero let input = bytes!(""); let gas_cost = l1_block_info.calculate_tx_l1_cost(&input, SpecId::FJORD); assert_eq!(gas_cost, U256::ZERO); + l1_block_info.clear_tx_l1_cost(); // Deposit transactions with the EIP-2718 type of 0x7F should result in zero let input = bytes!("7FFACADE");