use crate::{ database::StateProviderDatabase, eth_dao_fork::{DAO_HARDFORK_BENEFICIARY, DAO_HARDKFORK_ACCOUNTS}, stack::{InspectorStack, InspectorStackConfig}, state_change::{apply_beacon_root_contract_call, post_block_balance_increments}, }; use reth_interfaces::executor::{BlockExecutionError, BlockValidationError}; use reth_node_api::EvmEnvConfig; use reth_primitives::{ Address, Block, BlockNumber, BlockWithSenders, Bloom, ChainSpec, GotExpected, Hardfork, Header, PruneMode, PruneModes, PruneSegmentError, Receipt, ReceiptWithBloom, Receipts, TransactionSigned, B256, MINIMUM_PRUNING_DISTANCE, U256, }; use reth_provider::{ BlockExecutor, BlockExecutorStats, ProviderError, PrunableBlockExecutor, StateProvider, }; use revm::{ db::{states::bundle_state::BundleRetention, StateDBBox}, primitives::ResultAndState, State, EVM, }; use std::{sync::Arc, time::Instant}; #[cfg(feature = "optimism")] use reth_primitives::revm::env::fill_op_tx_env; #[cfg(not(feature = "optimism"))] use reth_primitives::revm::env::fill_tx_env; #[cfg(not(feature = "optimism"))] use reth_primitives::revm::compat::into_reth_log; #[cfg(not(feature = "optimism"))] use reth_provider::BundleStateWithReceipts; #[cfg(not(feature = "optimism"))] use revm::DatabaseCommit; #[cfg(not(feature = "optimism"))] use tracing::{debug, trace}; /// EVMProcessor is a block executor that uses revm to execute blocks or multiple blocks. /// /// Output is obtained by calling `take_output_state` function. /// /// It is capable of pruning the data that will be written to the database /// and implemented [PrunableBlockExecutor] traits. /// /// It implemented the [BlockExecutor] that give it the ability to take block /// apply pre state (Cancun system contract call), execute transaction and apply /// state change and then apply post execution changes (block reward, withdrawals, irregular DAO /// hardfork state change). And if `execute_and_verify_receipt` is called it will verify the /// receipt. /// /// InspectorStack are used for optional inspecting execution. And it contains /// various duration of parts of execution. // TODO: https://github.com/bluealloy/revm/pull/745 // #[derive(Debug)] #[allow(missing_debug_implementations)] pub struct EVMProcessor<'a, EvmConfig> { /// The configured chain-spec pub(crate) chain_spec: Arc, /// revm instance that contains database and env environment. pub(crate) evm: EVM>, /// Hook and inspector stack that we want to invoke on that hook. stack: InspectorStack, /// The collection of receipts. /// Outer vector stores receipts for each block sequentially. /// The inner vector stores receipts ordered by transaction number. /// /// If receipt is None it means it is pruned. pub(crate) receipts: Receipts, /// First block will be initialized to `None` /// and be set to the block number of first block executed. pub(crate) first_block: Option, /// The maximum known block. tip: Option, /// Pruning configuration. prune_modes: PruneModes, /// Memoized address pruning filter. /// Empty implies that there is going to be addresses to include in the filter in a future /// block. None means there isn't any kind of configuration. pruning_address_filter: Option<(u64, Vec
)>, /// Execution stats pub(crate) stats: BlockExecutorStats, /// The type that is able to configure the EVM environment. _evm_config: EvmConfig, } impl<'a, EvmConfig> EVMProcessor<'a, EvmConfig> where EvmConfig: EvmEnvConfig, { /// Return chain spec. pub fn chain_spec(&self) -> &Arc { &self.chain_spec } /// Create a new pocessor with the given chain spec. pub fn new(chain_spec: Arc, evm_config: EvmConfig) -> Self { let evm = EVM::new(); EVMProcessor { chain_spec, evm, stack: InspectorStack::new(InspectorStackConfig::default()), receipts: Receipts::new(), first_block: None, tip: None, prune_modes: PruneModes::none(), pruning_address_filter: None, stats: BlockExecutorStats::default(), _evm_config: evm_config, } } /// Creates a new executor from the given chain spec and database. pub fn new_with_db( chain_spec: Arc, db: StateProviderDatabase, evm_config: EvmConfig, ) -> Self { let state = State::builder() .with_database_boxed(Box::new(db)) .with_bundle_update() .without_state_clear() .build(); EVMProcessor::new_with_state(chain_spec, state, evm_config) } /// Create a new EVM processor with the given revm state. pub fn new_with_state( chain_spec: Arc, revm_state: StateDBBox<'a, ProviderError>, evm_config: EvmConfig, ) -> Self { let mut evm = EVM::new(); evm.database(revm_state); EVMProcessor { chain_spec, evm, stack: InspectorStack::new(InspectorStackConfig::default()), receipts: Receipts::new(), first_block: None, tip: None, prune_modes: PruneModes::none(), pruning_address_filter: None, stats: BlockExecutorStats::default(), _evm_config: evm_config, } } /// Configures the executor with the given inspectors. pub fn set_stack(&mut self, stack: InspectorStack) { self.stack = stack; } /// Configure the executor with the given block. pub fn set_first_block(&mut self, num: BlockNumber) { self.first_block = Some(num); } /// Returns a reference to the database pub fn db_mut(&mut self) -> &mut StateDBBox<'a, ProviderError> { // Option will be removed from EVM in the future. // as it is always some. // https://github.com/bluealloy/revm/issues/697 self.evm.db().expect("Database inside EVM is always set") } /// Initializes the config and block env. pub(crate) fn init_env(&mut self, header: &Header, total_difficulty: U256) { // Set state clear flag. let state_clear_flag = self.chain_spec.fork(Hardfork::SpuriousDragon).active_at_block(header.number); self.db_mut().set_state_clear_flag(state_clear_flag); EvmConfig::fill_cfg_and_block_env( &mut self.evm.env.cfg, &mut self.evm.env.block, &self.chain_spec, header, total_difficulty, ); } /// Applies the pre-block call to the EIP-4788 beacon block root contract. /// /// If cancun is not activated or the block is the genesis block, then this is a no-op, and no /// state changes are made. pub fn apply_beacon_root_contract_call( &mut self, block: &Block, ) -> Result<(), BlockExecutionError> { apply_beacon_root_contract_call( &self.chain_spec, block.timestamp, block.number, block.parent_beacon_block_root, &mut self.evm, )?; Ok(()) } /// Apply post execution state changes, including block rewards, withdrawals, and irregular DAO /// hardfork state change. pub fn apply_post_execution_state_change( &mut self, block: &Block, total_difficulty: U256, ) -> Result<(), BlockExecutionError> { let mut balance_increments = post_block_balance_increments( &self.chain_spec, block.number, block.difficulty, block.beneficiary, block.timestamp, total_difficulty, &block.ommers, block.withdrawals.as_ref(), ); // Irregular state change at Ethereum DAO hardfork if self.chain_spec.fork(Hardfork::Dao).transitions_at_block(block.number) { // drain balances from hardcoded addresses. let drained_balance: u128 = self .db_mut() .drain_balances(DAO_HARDKFORK_ACCOUNTS) .map_err(|_| BlockValidationError::IncrementBalanceFailed)? .into_iter() .sum(); // return balance to DAO beneficiary. *balance_increments.entry(DAO_HARDFORK_BENEFICIARY).or_default() += drained_balance; } // increment balances self.db_mut() .increment_balances(balance_increments) .map_err(|_| BlockValidationError::IncrementBalanceFailed)?; Ok(()) } /// Runs a single transaction in the configured environment and proceeds /// to return the result and state diff (without applying it). /// /// Assumes the rest of the block environment has been filled via `init_block_env`. pub fn transact( &mut self, transaction: &TransactionSigned, sender: Address, ) -> Result { // Fill revm structure. #[cfg(not(feature = "optimism"))] fill_tx_env(&mut self.evm.env.tx, transaction, sender); #[cfg(feature = "optimism")] { let mut envelope_buf = Vec::with_capacity(transaction.length_without_header()); transaction.encode_enveloped(&mut envelope_buf); fill_op_tx_env(&mut self.evm.env.tx, transaction, sender, envelope_buf.into()); } let hash = transaction.hash(); let out = if self.stack.should_inspect(&self.evm.env, hash) { // execution with inspector. let output = self.evm.inspect(&mut self.stack); tracing::trace!( target: "evm", ?hash, ?output, ?transaction, env = ?self.evm.env, "Executed transaction" ); output } else { // main execution. self.evm.transact() }; out.map_err(|e| BlockValidationError::EVM { hash, error: e.into() }.into()) } /// Execute the block, verify gas usage and apply post-block state changes. pub(crate) fn execute_inner( &mut self, block: &BlockWithSenders, total_difficulty: U256, ) -> Result, BlockExecutionError> { self.init_env(&block.header, total_difficulty); self.apply_beacon_root_contract_call(block)?; let (receipts, cumulative_gas_used) = self.execute_transactions(block, total_difficulty)?; // Check if gas used matches the value set in header. if block.gas_used != cumulative_gas_used { let receipts = Receipts::from_block_receipt(receipts); return Err(BlockValidationError::BlockGasUsed { gas: GotExpected { got: cumulative_gas_used, expected: block.gas_used }, gas_spent_by_tx: receipts.gas_spent_by_tx()?, } .into()) } let time = Instant::now(); self.apply_post_execution_state_change(block, total_difficulty)?; self.stats.apply_post_execution_state_changes_duration += time.elapsed(); let time = Instant::now(); let retention = if self.tip.map_or(true, |tip| { !self .prune_modes .account_history .map_or(false, |mode| mode.should_prune(block.number, tip)) && !self .prune_modes .storage_history .map_or(false, |mode| mode.should_prune(block.number, tip)) }) { BundleRetention::Reverts } else { BundleRetention::PlainState }; self.db_mut().merge_transitions(retention); self.stats.merge_transitions_duration += time.elapsed(); if self.first_block.is_none() { self.first_block = Some(block.number); } Ok(receipts) } /// Save receipts to the executor. pub fn save_receipts(&mut self, receipts: Vec) -> Result<(), BlockExecutionError> { let mut receipts = receipts.into_iter().map(Option::Some).collect(); // Prune receipts if necessary. self.prune_receipts(&mut receipts)?; // Save receipts. self.receipts.push(receipts); Ok(()) } /// Prune receipts according to the pruning configuration. fn prune_receipts( &mut self, receipts: &mut Vec>, ) -> Result<(), PruneSegmentError> { let (first_block, tip) = match self.first_block.zip(self.tip) { Some((block, tip)) => (block, tip), _ => return Ok(()), }; let block_number = first_block + self.receipts.len() as u64; // Block receipts should not be retained if self.prune_modes.receipts == Some(PruneMode::Full) || // [`PruneSegment::Receipts`] takes priority over [`PruneSegment::ContractLogs`] self.prune_modes.receipts.map_or(false, |mode| mode.should_prune(block_number, tip)) { receipts.clear(); return Ok(()) } // All receipts from the last 128 blocks are required for blockchain tree, even with // [`PruneSegment::ContractLogs`]. let prunable_receipts = PruneMode::Distance(MINIMUM_PRUNING_DISTANCE).should_prune(block_number, tip); if !prunable_receipts { return Ok(()) } let contract_log_pruner = self.prune_modes.receipts_log_filter.group_by_block(tip, None)?; if !contract_log_pruner.is_empty() { let (prev_block, filter) = self.pruning_address_filter.get_or_insert((0, Vec::new())); for (_, addresses) in contract_log_pruner.range(*prev_block..=block_number) { filter.extend(addresses.iter().copied()); } } for receipt in receipts.iter_mut() { let inner_receipt = receipt.as_ref().expect("receipts have not been pruned"); // If there is an address_filter, and it does not contain any of the // contract addresses, then remove this receipts if let Some((_, filter)) = &self.pruning_address_filter { if !inner_receipt.logs.iter().any(|log| filter.contains(&log.address)) { receipt.take(); } } } Ok(()) } } /// Default Ethereum implementation of the [BlockExecutor] trait for the [EVMProcessor]. #[cfg(not(feature = "optimism"))] impl<'a, EvmConfig> BlockExecutor for EVMProcessor<'a, EvmConfig> where EvmConfig: EvmEnvConfig, { fn execute( &mut self, block: &BlockWithSenders, total_difficulty: U256, ) -> Result<(), BlockExecutionError> { let receipts = self.execute_inner(block, total_difficulty)?; self.save_receipts(receipts) } fn execute_and_verify_receipt( &mut self, block: &BlockWithSenders, total_difficulty: U256, ) -> Result<(), BlockExecutionError> { // execute block let receipts = self.execute_inner(block, total_difficulty)?; // TODO Before Byzantium, receipts contained state root that would mean that expensive // operation as hashing that is needed for state root got calculated in every // transaction This was replaced with is_success flag. // See more about EIP here: https://eips.ethereum.org/EIPS/eip-658 if self.chain_spec.fork(Hardfork::Byzantium).active_at_block(block.header.number) { let time = Instant::now(); if let Err(error) = verify_receipt(block.header.receipts_root, block.header.logs_bloom, receipts.iter()) { debug!(target: "evm", ?error, ?receipts, "receipts verification failed"); return Err(error) }; self.stats.receipt_root_duration += time.elapsed(); } self.save_receipts(receipts) } fn execute_transactions( &mut self, block: &BlockWithSenders, total_difficulty: U256, ) -> Result<(Vec, u64), BlockExecutionError> { self.init_env(&block.header, total_difficulty); // perf: do not execute empty blocks if block.body.is_empty() { return Ok((Vec::new(), 0)) } let mut cumulative_gas_used = 0; let mut receipts = Vec::with_capacity(block.body.len()); for (sender, transaction) in block.transactions_with_sender() { let time = Instant::now(); // 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 { return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { transaction_gas_limit: transaction.gas_limit(), block_available_gas, } .into()) } // Execute transaction. let ResultAndState { result, state } = self.transact(transaction, *sender)?; trace!( target: "evm", ?transaction, ?result, ?state, "Executed transaction" ); self.stats.execution_duration += time.elapsed(); let time = Instant::now(); self.db_mut().commit(state); self.stats.apply_state_duration += time.elapsed(); // 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, // convert to reth log logs: result.into_logs().into_iter().map(into_reth_log).collect(), }); } Ok((receipts, cumulative_gas_used)) } fn take_output_state(&mut self) -> BundleStateWithReceipts { let receipts = std::mem::take(&mut self.receipts); BundleStateWithReceipts::new( self.evm.db().unwrap().take_bundle(), receipts, self.first_block.unwrap_or_default(), ) } fn stats(&self) -> BlockExecutorStats { self.stats.clone() } fn size_hint(&self) -> Option { self.evm.db.as_ref().map(|db| db.bundle_size_hint()) } } impl<'a, EvmConfig> PrunableBlockExecutor for EVMProcessor<'a, EvmConfig> where EvmConfig: EvmEnvConfig, { fn set_tip(&mut self, tip: BlockNumber) { self.tip = Some(tip); } fn set_prune_modes(&mut self, prune_modes: PruneModes) { self.prune_modes = prune_modes; } } /// Verify receipts pub fn verify_receipt<'a>( expected_receipts_root: B256, expected_logs_bloom: Bloom, receipts: impl Iterator + Clone, #[cfg(feature = "optimism")] chain_spec: &ChainSpec, #[cfg(feature = "optimism")] timestamp: u64, ) -> Result<(), BlockExecutionError> { // Check receipts root. let receipts_with_bloom = receipts.map(|r| r.clone().into()).collect::>(); let receipts_root = reth_primitives::proofs::calculate_receipt_root( &receipts_with_bloom, #[cfg(feature = "optimism")] chain_spec, #[cfg(feature = "optimism")] timestamp, ); if receipts_root != expected_receipts_root { return Err(BlockValidationError::ReceiptRootDiff( GotExpected { got: receipts_root, expected: expected_receipts_root }.into(), ) .into()) } // Create header log bloom. let logs_bloom = receipts_with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom); if logs_bloom != expected_logs_bloom { return Err(BlockValidationError::BloomLogDiff( GotExpected { got: logs_bloom, expected: expected_logs_bloom }.into(), ) .into()) } Ok(()) } #[cfg(test)] mod tests { use super::*; use reth_interfaces::provider::ProviderResult; use reth_node_ethereum::EthEvmConfig; use reth_primitives::{ bytes, constants::{BEACON_ROOTS_ADDRESS, SYSTEM_ADDRESS}, keccak256, trie::AccountProof, Account, Bytecode, Bytes, ChainSpecBuilder, ForkCondition, StorageKey, MAINNET, }; use reth_provider::{ AccountReader, BlockHashReader, BundleStateWithReceipts, StateRootProvider, }; use reth_trie::updates::TrieUpdates; use revm::{Database, TransitionState}; use std::collections::HashMap; static BEACON_ROOT_CONTRACT_CODE: Bytes = bytes!("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500"); #[derive(Debug, Default, Clone, Eq, PartialEq)] struct StateProviderTest { accounts: HashMap, Account)>, contracts: HashMap, block_hash: HashMap, } impl StateProviderTest { /// Insert account. fn insert_account( &mut self, address: Address, mut account: Account, bytecode: Option, storage: HashMap, ) { if let Some(bytecode) = bytecode { let hash = keccak256(&bytecode); account.bytecode_hash = Some(hash); self.contracts.insert(hash, Bytecode::new_raw(bytecode)); } self.accounts.insert(address, (storage, account)); } } impl AccountReader for StateProviderTest { fn basic_account(&self, address: Address) -> ProviderResult> { Ok(self.accounts.get(&address).map(|(_, acc)| *acc)) } } impl BlockHashReader for StateProviderTest { fn block_hash(&self, number: u64) -> ProviderResult> { Ok(self.block_hash.get(&number).cloned()) } fn canonical_hashes_range( &self, start: BlockNumber, end: BlockNumber, ) -> ProviderResult> { let range = start..end; Ok(self .block_hash .iter() .filter_map(|(block, hash)| range.contains(block).then_some(*hash)) .collect()) } } impl StateRootProvider for StateProviderTest { fn state_root(&self, _bundle_state: &BundleStateWithReceipts) -> ProviderResult { unimplemented!("state root computation is not supported") } fn state_root_with_updates( &self, _bundle_state: &BundleStateWithReceipts, ) -> ProviderResult<(B256, TrieUpdates)> { unimplemented!("state root computation is not supported") } } impl StateProvider for StateProviderTest { fn storage( &self, account: Address, storage_key: StorageKey, ) -> ProviderResult> { Ok(self .accounts .get(&account) .and_then(|(storage, _)| storage.get(&storage_key).cloned())) } fn bytecode_by_hash(&self, code_hash: B256) -> ProviderResult> { Ok(self.contracts.get(&code_hash).cloned()) } fn proof(&self, _address: Address, _keys: &[B256]) -> ProviderResult { unimplemented!("proof generation is not supported") } } #[test] fn eip_4788_non_genesis_call() { let mut header = Header { timestamp: 1, number: 1, excess_blob_gas: Some(0), ..Header::default() }; let mut db = StateProviderTest::default(); let beacon_root_contract_account = Account { balance: U256::ZERO, bytecode_hash: Some(keccak256(BEACON_ROOT_CONTRACT_CODE.clone())), nonce: 1, }; db.insert_account( BEACON_ROOTS_ADDRESS, beacon_root_contract_account, Some(BEACON_ROOT_CONTRACT_CODE.clone()), HashMap::new(), ); let chain_spec = Arc::new( ChainSpecBuilder::from(&*MAINNET) .shanghai_activated() .with_fork(Hardfork::Cancun, ForkCondition::Timestamp(1)) .build(), ); // execute invalid header (no parent beacon block root) let mut executor = EVMProcessor::new_with_db( chain_spec, StateProviderDatabase::new(db), EthEvmConfig::default(), ); // attempt to execute a block without parent beacon block root, expect err let err = executor .execute_and_verify_receipt( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .expect_err( "Executing cancun block without parent beacon block root field should fail", ); assert_eq!( err, BlockExecutionError::Validation(BlockValidationError::MissingParentBeaconBlockRoot) ); // fix header, set a gas limit header.parent_beacon_block_root = Some(B256::with_last_byte(0x69)); // Now execute a block with the fixed header, ensure that it does not fail executor .execute( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .unwrap(); // check the actual storage of the contract - it should be: // * The storage value at header.timestamp % HISTORY_BUFFER_LENGTH should be // header.timestamp // * The storage value at header.timestamp % HISTORY_BUFFER_LENGTH + HISTORY_BUFFER_LENGTH // should be parent_beacon_block_root let history_buffer_length = 8191u64; let timestamp_index = header.timestamp % history_buffer_length; let parent_beacon_block_root_index = timestamp_index % history_buffer_length + history_buffer_length; // get timestamp storage and compare let timestamp_storage = executor.db_mut().storage(BEACON_ROOTS_ADDRESS, U256::from(timestamp_index)).unwrap(); assert_eq!(timestamp_storage, U256::from(header.timestamp)); // get parent beacon block root storage and compare let parent_beacon_block_root_storage = executor .db_mut() .storage(BEACON_ROOTS_ADDRESS, U256::from(parent_beacon_block_root_index)) .expect("storage value should exist"); assert_eq!(parent_beacon_block_root_storage, U256::from(0x69)); } #[test] fn eip_4788_no_code_cancun() { // This test ensures that we "silently fail" when cancun is active and there is no code at // BEACON_ROOTS_ADDRESS let header = Header { timestamp: 1, number: 1, parent_beacon_block_root: Some(B256::with_last_byte(0x69)), excess_blob_gas: Some(0), ..Header::default() }; let db = StateProviderTest::default(); // DON'T deploy the contract at genesis let chain_spec = Arc::new( ChainSpecBuilder::from(&*MAINNET) .shanghai_activated() .with_fork(Hardfork::Cancun, ForkCondition::Timestamp(1)) .build(), ); let mut executor = EVMProcessor::new_with_db( chain_spec, StateProviderDatabase::new(db), EthEvmConfig::default(), ); executor.init_env(&header, U256::ZERO); // get the env let previous_env = executor.evm.env.clone(); // attempt to execute an empty block with parent beacon block root, this should not fail executor .execute_and_verify_receipt( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .expect( "Executing a block with no transactions while cancun is active should not fail", ); // ensure that the env has not changed assert_eq!(executor.evm.env, previous_env); } #[test] fn eip_4788_empty_account_call() { // This test ensures that we do not increment the nonce of an empty SYSTEM_ADDRESS account // during the pre-block call let mut db = StateProviderTest::default(); let beacon_root_contract_account = Account { balance: U256::ZERO, bytecode_hash: Some(keccak256(BEACON_ROOT_CONTRACT_CODE.clone())), nonce: 1, }; db.insert_account( BEACON_ROOTS_ADDRESS, beacon_root_contract_account, Some(BEACON_ROOT_CONTRACT_CODE.clone()), HashMap::new(), ); // insert an empty SYSTEM_ADDRESS db.insert_account(SYSTEM_ADDRESS, Account::default(), None, HashMap::new()); let chain_spec = Arc::new( ChainSpecBuilder::from(&*MAINNET) .shanghai_activated() .with_fork(Hardfork::Cancun, ForkCondition::Timestamp(1)) .build(), ); let mut executor = EVMProcessor::new_with_db( chain_spec, StateProviderDatabase::new(db), EthEvmConfig::default(), ); // construct the header for block one let header = Header { timestamp: 1, number: 1, parent_beacon_block_root: Some(B256::with_last_byte(0x69)), excess_blob_gas: Some(0), ..Header::default() }; executor.init_env(&header, U256::ZERO); // attempt to execute an empty block with parent beacon block root, this should not fail executor .execute_and_verify_receipt( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .expect( "Executing a block with no transactions while cancun is active should not fail", ); // ensure that the nonce of the system address account has not changed let nonce = executor.db_mut().basic(SYSTEM_ADDRESS).unwrap().unwrap().nonce; assert_eq!(nonce, 0); } #[test] fn eip_4788_genesis_call() { let mut db = StateProviderTest::default(); let beacon_root_contract_account = Account { balance: U256::ZERO, bytecode_hash: Some(keccak256(BEACON_ROOT_CONTRACT_CODE.clone())), nonce: 1, }; db.insert_account( BEACON_ROOTS_ADDRESS, beacon_root_contract_account, Some(BEACON_ROOT_CONTRACT_CODE.clone()), HashMap::new(), ); // activate cancun at genesis let chain_spec = Arc::new( ChainSpecBuilder::from(&*MAINNET) .shanghai_activated() .with_fork(Hardfork::Cancun, ForkCondition::Timestamp(0)) .build(), ); let mut header = chain_spec.genesis_header(); let mut executor = EVMProcessor::new_with_db( chain_spec, StateProviderDatabase::new(db), EthEvmConfig::default(), ); executor.init_env(&header, U256::ZERO); // attempt to execute the genesis block with non-zero parent beacon block root, expect err header.parent_beacon_block_root = Some(B256::with_last_byte(0x69)); let _err = executor .execute_and_verify_receipt( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .expect_err( "Executing genesis cancun block with non-zero parent beacon block root field should fail", ); // fix header header.parent_beacon_block_root = Some(B256::ZERO); // now try to process the genesis block again, this time ensuring that a system contract // call does not occur executor .execute( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .unwrap(); // there is no system contract call so there should be NO STORAGE CHANGES // this means we'll check the transition state let state = executor.evm.db().unwrap(); let transition_state = state .transition_state .clone() .expect("the evm should be initialized with bundle updates"); // assert that it is the default (empty) transition state assert_eq!(transition_state, TransitionState::default()); } #[test] fn eip_4788_high_base_fee() { // This test ensures that if we have a base fee, then we don't return an error when the // system contract is called, due to the gas price being less than the base fee. let header = Header { timestamp: 1, number: 1, parent_beacon_block_root: Some(B256::with_last_byte(0x69)), base_fee_per_gas: Some(u64::MAX), excess_blob_gas: Some(0), ..Header::default() }; let mut db = StateProviderTest::default(); let beacon_root_contract_account = Account { balance: U256::ZERO, bytecode_hash: Some(keccak256(BEACON_ROOT_CONTRACT_CODE.clone())), nonce: 1, }; db.insert_account( BEACON_ROOTS_ADDRESS, beacon_root_contract_account, Some(BEACON_ROOT_CONTRACT_CODE.clone()), HashMap::new(), ); let chain_spec = Arc::new( ChainSpecBuilder::from(&*MAINNET) .shanghai_activated() .with_fork(Hardfork::Cancun, ForkCondition::Timestamp(1)) .build(), ); // execute header let mut executor = EVMProcessor::new_with_db( chain_spec, StateProviderDatabase::new(db), EthEvmConfig::default(), ); executor.init_env(&header, U256::ZERO); // ensure that the env is configured with a base fee assert_eq!(executor.evm.env.block.basefee, U256::from(u64::MAX)); // Now execute a block with the fixed header, ensure that it does not fail executor .execute( &BlockWithSenders { block: Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None, }, senders: vec![], }, U256::ZERO, ) .unwrap(); // check the actual storage of the contract - it should be: // * The storage value at header.timestamp % HISTORY_BUFFER_LENGTH should be // header.timestamp // * The storage value at header.timestamp % HISTORY_BUFFER_LENGTH + HISTORY_BUFFER_LENGTH // should be parent_beacon_block_root let history_buffer_length = 8191u64; let timestamp_index = header.timestamp % history_buffer_length; let parent_beacon_block_root_index = timestamp_index % history_buffer_length + history_buffer_length; // get timestamp storage and compare let timestamp_storage = executor.db_mut().storage(BEACON_ROOTS_ADDRESS, U256::from(timestamp_index)).unwrap(); assert_eq!(timestamp_storage, U256::from(header.timestamp)); // get parent beacon block root storage and compare let parent_beacon_block_root_storage = executor .db_mut() .storage(BEACON_ROOTS_ADDRESS, U256::from(parent_beacon_block_root_index)) .unwrap(); assert_eq!(parent_beacon_block_root_storage, U256::from(0x69)); } }