diff --git a/Cargo.lock b/Cargo.lock index eb0bb2a3d..4d23c5b42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8105,6 +8105,7 @@ dependencies = [ "alloy-primitives", "op-alloy-consensus", "reth-chainspec", + "reth-consensus", "reth-ethereum-forks", "reth-evm", "reth-execution-errors", diff --git a/crates/evm/src/execute.rs b/crates/evm/src/execute.rs index 859dacd8a..704b6a23a 100644 --- a/crates/evm/src/execute.rs +++ b/crates/evm/src/execute.rs @@ -375,7 +375,7 @@ where /// Batch execution strategy. pub(crate) strategy: S, /// Keeps track of batch execution receipts and requests. - batch_record: BlockBatchRecord, + pub(crate) batch_record: BlockBatchRecord, _phantom: PhantomData, } diff --git a/crates/evm/src/test_utils.rs b/crates/evm/src/test_utils.rs index a033c8023..5ae7ed45b 100644 --- a/crates/evm/src/test_utils.rs +++ b/crates/evm/src/test_utils.rs @@ -11,7 +11,7 @@ use alloy_primitives::BlockNumber; use parking_lot::Mutex; use reth_execution_errors::BlockExecutionError; use reth_execution_types::ExecutionOutcome; -use reth_primitives::{BlockWithSenders, Receipt}; +use reth_primitives::{BlockWithSenders, Receipt, Receipts}; use reth_prune_types::PruneModes; use reth_storage_errors::provider::ProviderError; use revm::State; @@ -152,4 +152,9 @@ where { f(self.strategy.state_mut()) } + + /// Accessor for batch executor receipts. + pub const fn receipts(&self) -> &Receipts { + self.batch_record.receipts() + } } diff --git a/crates/optimism/evm/Cargo.toml b/crates/optimism/evm/Cargo.toml index 72231716f..afaca32b6 100644 --- a/crates/optimism/evm/Cargo.toml +++ b/crates/optimism/evm/Cargo.toml @@ -20,6 +20,7 @@ reth-revm.workspace = true reth-execution-errors.workspace = true reth-execution-types.workspace = true reth-prune-types.workspace = true +reth-consensus.workspace = true # ethereum alloy-primitives.workspace = true @@ -41,6 +42,7 @@ tracing.workspace = true [dev-dependencies] alloy-eips.workspace = true +reth-evm = { workspace = true, features = ["test-utils"] } reth-revm = { workspace = true, features = ["test-utils"] } reth-primitives = { workspace = true, features = ["test-utils"] } reth-optimism-chainspec.workspace = true diff --git a/crates/optimism/evm/src/lib.rs b/crates/optimism/evm/src/lib.rs index f3de053f7..4d0f9d89f 100644 --- a/crates/optimism/evm/src/lib.rs +++ b/crates/optimism/evm/src/lib.rs @@ -33,6 +33,8 @@ use revm_primitives::{ BlobExcessGasAndPrice, BlockEnv, Bytes, CfgEnv, Env, HandlerCfg, OptimismFields, SpecId, TxKind, }; +pub mod strategy; + /// Optimism-related EVM configuration. #[derive(Debug, Clone)] pub struct OptimismEvmConfig { diff --git a/crates/optimism/evm/src/strategy.rs b/crates/optimism/evm/src/strategy.rs new file mode 100644 index 000000000..4fc06c396 --- /dev/null +++ b/crates/optimism/evm/src/strategy.rs @@ -0,0 +1,491 @@ +//! Optimism block execution strategy, + +use crate::{l1::ensure_create2_deployer, OptimismBlockExecutionError, OptimismEvmConfig}; +use reth_chainspec::EthereumHardforks; +use reth_consensus::ConsensusError; +use reth_evm::{ + execute::{ + BlockExecutionError, BlockExecutionStrategy, BlockExecutionStrategyFactory, + BlockValidationError, ProviderError, + }, + system_calls::{OnStateHook, SystemCaller}, + ConfigureEvm, ConfigureEvmEnv, +}; +use reth_optimism_chainspec::OpChainSpec; +use reth_optimism_consensus::validate_block_post_execution; +use reth_optimism_forks::OptimismHardfork; +use reth_primitives::{BlockWithSenders, Header, Receipt, Request, TxType}; +use reth_revm::{ + db::{states::bundle_state::BundleRetention, BundleState}, + state_change::post_block_balance_increments, + Database, State, +}; +use revm_primitives::{ + db::DatabaseCommit, BlockEnv, CfgEnvWithHandlerCfg, EnvWithHandlerCfg, ResultAndState, U256, +}; +use std::{fmt::Display, sync::Arc}; +use tracing::trace; + +/// Factory for [`OpExecutionStrategy`]. +#[derive(Debug, Clone)] +pub struct OpExecutionStrategyFactory { + /// The chainspec + chain_spec: Arc, + /// How to create an EVM. + evm_config: EvmConfig, +} + +impl OpExecutionStrategyFactory { + /// Creates a new default optimism executor strategy factory. + pub fn optimism(chain_spec: Arc) -> Self { + Self::new(chain_spec.clone(), OptimismEvmConfig::new(chain_spec)) + } +} + +impl OpExecutionStrategyFactory { + /// Creates a new executor strategy factory. + pub const fn new(chain_spec: Arc, evm_config: EvmConfig) -> Self { + Self { chain_spec, evm_config } + } +} + +impl BlockExecutionStrategyFactory for OpExecutionStrategyFactory { + type Strategy + Display>> = OpExecutionStrategy; + + fn create_strategy(&self, db: DB) -> Self::Strategy + where + DB: Database + Display>, + { + let state = + State::builder().with_database(db).with_bundle_update().without_state_clear().build(); + OpExecutionStrategy::new(state, self.chain_spec.clone(), self.evm_config.clone()) + } +} + +/// Block execution strategy for Optimism. +#[allow(missing_debug_implementations)] +pub struct OpExecutionStrategy { + /// The chainspec + chain_spec: Arc, + /// How to create an EVM. + evm_config: EvmConfig, + /// Current state for block execution. + state: State, + /// Utility to call system smart contracts. + system_caller: SystemCaller, +} + +impl OpExecutionStrategy { + /// Creates a new [`OpExecutionStrategy`] + pub fn new( + state: State, + chain_spec: Arc, + evm_config: OptimismEvmConfig, + ) -> Self { + let system_caller = SystemCaller::new(evm_config.clone(), (*chain_spec).clone()); + Self { state, chain_spec, evm_config, system_caller } + } +} + +impl OpExecutionStrategy { + /// Configures a new evm configuration and block environment for the given block. + /// + /// Caution: this does not initialize the tx environment. + fn evm_env_for_block(&self, header: &Header, total_difficulty: U256) -> EnvWithHandlerCfg { + let mut cfg = CfgEnvWithHandlerCfg::new(Default::default(), Default::default()); + let mut block_env = BlockEnv::default(); + self.evm_config.fill_cfg_and_block_env(&mut cfg, &mut block_env, header, total_difficulty); + + EnvWithHandlerCfg::new_with_cfg_env(cfg, block_env, Default::default()) + } +} + +impl BlockExecutionStrategy for OpExecutionStrategy +where + DB: Database + Display>, +{ + type Error = BlockExecutionError; + + fn apply_pre_execution_changes( + &mut self, + block: &BlockWithSenders, + total_difficulty: U256, + ) -> Result<(), Self::Error> { + // Set state clear flag if the block is after the Spurious Dragon hardfork. + let state_clear_flag = + (*self.chain_spec).is_spurious_dragon_active_at_block(block.header.number); + self.state.set_state_clear_flag(state_clear_flag); + + let env = self.evm_env_for_block(&block.header, total_difficulty); + let mut evm = self.evm_config.evm_with_env(&mut self.state, env); + + self.system_caller.apply_beacon_root_contract_call( + block.timestamp, + block.number, + block.parent_beacon_block_root, + &mut evm, + )?; + + // Ensure that the create2deployer is force-deployed at the canyon transition. Optimism + // blocks will always have at least a single transaction in them (the L1 info transaction), + // so we can safely assume that this will always be triggered upon the transition and that + // the above check for empty blocks will never be hit on OP chains. + ensure_create2_deployer(self.chain_spec.clone(), block.timestamp, evm.db_mut()) + .map_err(|_| OptimismBlockExecutionError::ForceCreate2DeployerFail)?; + + Ok(()) + } + + fn execute_transactions( + &mut self, + block: &BlockWithSenders, + total_difficulty: U256, + ) -> Result<(Vec, u64), Self::Error> { + let env = self.evm_env_for_block(&block.header, total_difficulty); + let mut evm = self.evm_config.evm_with_env(&mut self.state, env); + + let is_regolith = + self.chain_spec.fork(OptimismHardfork::Regolith).active_at_timestamp(block.timestamp); + + let mut cumulative_gas_used = 0; + let mut receipts = Vec::with_capacity(block.body.transactions.len()); + for (sender, transaction) in block.transactions_with_sender() { + // The sum of the transaction’s gas limit, Tg, and the gas utilized in this block prior, + // must be no greater than the block’s gasLimit. + let block_available_gas = block.header.gas_limit - cumulative_gas_used; + if transaction.gas_limit() > block_available_gas && + (is_regolith || !transaction.is_system_transaction()) + { + return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { + transaction_gas_limit: transaction.gas_limit(), + block_available_gas, + } + .into()) + } + + // An optimism block should never contain blob transactions. + if matches!(transaction.tx_type(), TxType::Eip4844) { + return Err(OptimismBlockExecutionError::BlobTransactionRejected.into()) + } + + // Cache the depositor account prior to the state transition for the deposit nonce. + // + // Note that this *only* needs to be done post-regolith hardfork, as deposit nonces + // were not introduced in Bedrock. In addition, regular transactions don't have deposit + // nonces, so we don't need to touch the DB for those. + let depositor = (is_regolith && transaction.is_deposit()) + .then(|| { + evm.db_mut() + .load_cache_account(*sender) + .map(|acc| acc.account_info().unwrap_or_default()) + }) + .transpose() + .map_err(|_| OptimismBlockExecutionError::AccountLoadFailed(*sender))?; + + self.evm_config.fill_tx_env(evm.tx_mut(), transaction, *sender); + + // Execute transaction. + let result_and_state = evm.transact().map_err(move |err| { + let new_err = err.map_db_err(|e| e.into()); + // Ensure hash is calculated for error log, if not already done + BlockValidationError::EVM { + hash: transaction.recalculate_hash(), + error: Box::new(new_err), + } + })?; + + trace!( + target: "evm", + ?transaction, + "Executed transaction" + ); + self.system_caller.on_state(&result_and_state); + let ResultAndState { result, state } = result_and_state; + evm.db_mut().commit(state); + + // append gas used + cumulative_gas_used += result.gas_used(); + + // Push transaction changeset and calculate header bloom filter for receipt. + receipts.push(Receipt { + tx_type: transaction.tx_type(), + // Success flag was added in `EIP-658: Embedding transaction status code in + // receipts`. + success: result.is_success(), + cumulative_gas_used, + logs: result.into_logs(), + deposit_nonce: depositor.map(|account| account.nonce), + // The deposit receipt version was introduced in Canyon to indicate an update to how + // receipt hashes should be computed when set. The state transition process ensures + // this is only set for post-Canyon deposit transactions. + deposit_receipt_version: (transaction.is_deposit() && + self.chain_spec + .is_fork_active_at_timestamp(OptimismHardfork::Canyon, block.timestamp)) + .then_some(1), + }); + } + + Ok((receipts, cumulative_gas_used)) + } + + fn apply_post_execution_changes( + &mut self, + block: &BlockWithSenders, + total_difficulty: U256, + _receipts: &[Receipt], + ) -> Result, Self::Error> { + let balance_increments = + post_block_balance_increments(&self.chain_spec.clone(), block, total_difficulty); + // increment balances + self.state + .increment_balances(balance_increments) + .map_err(|_| BlockValidationError::IncrementBalanceFailed)?; + + Ok(vec![]) + } + + fn state_ref(&self) -> &State { + &self.state + } + + fn state_mut(&mut self) -> &mut State { + &mut self.state + } + + fn with_state_hook(&mut self, hook: Option>) { + self.system_caller.with_state_hook(hook); + } + + fn finish(&mut self) -> BundleState { + self.state.merge_transitions(BundleRetention::Reverts); + self.state.take_bundle() + } + + fn validate_block_post_execution( + &self, + block: &BlockWithSenders, + receipts: &[Receipt], + _requests: &[Request], + ) -> Result<(), ConsensusError> { + validate_block_post_execution(block, &self.chain_spec.clone(), receipts) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::OpChainSpec; + use alloy_consensus::TxEip1559; + use alloy_primitives::{b256, Address, StorageKey, StorageValue}; + use reth_chainspec::MIN_TRANSACTION_GAS; + use reth_evm::execute::{BatchExecutor, BlockExecutorProvider, GenericBlockExecutorProvider}; + use reth_optimism_chainspec::{optimism_deposit_tx_signature, OpChainSpecBuilder}; + use reth_primitives::{Account, Block, BlockBody, Signature, Transaction, TransactionSigned}; + use reth_revm::{ + database::StateProviderDatabase, test_utils::StateProviderTest, L1_BLOCK_CONTRACT, + }; + use std::{collections::HashMap, str::FromStr}; + + fn create_op_state_provider() -> StateProviderTest { + let mut db = StateProviderTest::default(); + + let l1_block_contract_account = + Account { balance: U256::ZERO, bytecode_hash: None, nonce: 1 }; + + let mut l1_block_storage = HashMap::default(); + // base fee + l1_block_storage.insert(StorageKey::with_last_byte(1), StorageValue::from(1000000000)); + // l1 fee overhead + l1_block_storage.insert(StorageKey::with_last_byte(5), StorageValue::from(188)); + // l1 fee scalar + l1_block_storage.insert(StorageKey::with_last_byte(6), StorageValue::from(684000)); + // l1 free scalars post ecotone + l1_block_storage.insert( + StorageKey::with_last_byte(3), + StorageValue::from_str( + "0x0000000000000000000000000000000000001db0000d27300000000000000005", + ) + .unwrap(), + ); + + db.insert_account(L1_BLOCK_CONTRACT, l1_block_contract_account, None, l1_block_storage); + + db + } + + fn executor_provider( + chain_spec: Arc, + ) -> GenericBlockExecutorProvider { + let strategy_factory = + OpExecutionStrategyFactory::new(chain_spec.clone(), OptimismEvmConfig::new(chain_spec)); + + GenericBlockExecutorProvider::new(strategy_factory) + } + + #[test] + fn op_deposit_fields_pre_canyon() { + let header = Header { + timestamp: 1, + number: 1, + gas_limit: 1_000_000, + gas_used: 42_000, + receipts_root: b256!( + "83465d1e7d01578c0d609be33570f91242f013e9e295b0879905346abbd63731" + ), + ..Default::default() + }; + + let mut db = create_op_state_provider(); + + let addr = Address::ZERO; + let account = Account { balance: U256::MAX, ..Account::default() }; + db.insert_account(addr, account, None, HashMap::default()); + + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().regolith_activated().build()); + + let tx = TransactionSigned::from_transaction_and_signature( + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce: 0, + gas_limit: MIN_TRANSACTION_GAS, + to: addr.into(), + ..Default::default() + }), + Signature::test_signature(), + ); + + let tx_deposit = TransactionSigned::from_transaction_and_signature( + Transaction::Deposit(op_alloy_consensus::TxDeposit { + from: addr, + to: addr.into(), + gas_limit: MIN_TRANSACTION_GAS, + ..Default::default() + }), + Signature::test_signature(), + ); + + let provider = executor_provider(chain_spec); + let mut executor = provider.batch_executor(StateProviderDatabase::new(&db)); + + // make sure the L1 block contract state is preloaded. + executor.with_state_mut(|state| { + state.load_cache_account(L1_BLOCK_CONTRACT).unwrap(); + }); + + // Attempt to execute a block with one deposit and one non-deposit transaction + executor + .execute_and_verify_one( + ( + &BlockWithSenders { + block: Block { + header, + body: BlockBody { + transactions: vec![tx, tx_deposit], + ..Default::default() + }, + }, + senders: vec![addr, addr], + }, + U256::ZERO, + ) + .into(), + ) + .unwrap(); + + let receipts = executor.receipts(); + let tx_receipt = receipts[0][0].as_ref().unwrap(); + let deposit_receipt = receipts[0][1].as_ref().unwrap(); + + // deposit_receipt_version is not present in pre canyon transactions + assert!(deposit_receipt.deposit_receipt_version.is_none()); + assert!(tx_receipt.deposit_receipt_version.is_none()); + + // deposit_nonce is present only in deposit transactions + assert!(deposit_receipt.deposit_nonce.is_some()); + assert!(tx_receipt.deposit_nonce.is_none()); + } + + #[test] + fn op_deposit_fields_post_canyon() { + // ensure_create2_deployer will fail if timestamp is set to less then 2 + let header = Header { + timestamp: 2, + number: 1, + gas_limit: 1_000_000, + gas_used: 42_000, + receipts_root: b256!( + "fffc85c4004fd03c7bfbe5491fae98a7473126c099ac11e8286fd0013f15f908" + ), + ..Default::default() + }; + + let mut db = create_op_state_provider(); + let addr = Address::ZERO; + let account = Account { balance: U256::MAX, ..Account::default() }; + + db.insert_account(addr, account, None, HashMap::default()); + + let chain_spec = Arc::new(OpChainSpecBuilder::base_mainnet().canyon_activated().build()); + + let tx = TransactionSigned::from_transaction_and_signature( + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce: 0, + gas_limit: MIN_TRANSACTION_GAS, + to: addr.into(), + ..Default::default() + }), + Signature::test_signature(), + ); + + let tx_deposit = TransactionSigned::from_transaction_and_signature( + Transaction::Deposit(op_alloy_consensus::TxDeposit { + from: addr, + to: addr.into(), + gas_limit: MIN_TRANSACTION_GAS, + ..Default::default() + }), + optimism_deposit_tx_signature(), + ); + + let provider = executor_provider(chain_spec); + let mut executor = provider.batch_executor(StateProviderDatabase::new(&db)); + + // make sure the L1 block contract state is preloaded. + executor.with_state_mut(|state| { + state.load_cache_account(L1_BLOCK_CONTRACT).unwrap(); + }); + + // attempt to execute an empty block with parent beacon block root, this should not fail + executor + .execute_and_verify_one( + ( + &BlockWithSenders { + block: Block { + header, + body: BlockBody { + transactions: vec![tx, tx_deposit], + ..Default::default() + }, + }, + senders: vec![addr, addr], + }, + U256::ZERO, + ) + .into(), + ) + .expect("Executing a block while canyon is active should not fail"); + + let receipts = executor.receipts(); + let tx_receipt = receipts[0][0].as_ref().unwrap(); + let deposit_receipt = receipts[0][1].as_ref().unwrap(); + + // deposit_receipt_version is set to 1 for post canyon deposit transactions + assert_eq!(deposit_receipt.deposit_receipt_version, Some(1)); + assert!(tx_receipt.deposit_receipt_version.is_none()); + + // deposit_nonce is present only in deposit transactions + assert!(deposit_receipt.deposit_nonce.is_some()); + assert!(tx_receipt.deposit_nonce.is_none()); + } +}