From a80b72041b63d52b6f201316dc90f2feab94c872 Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:16:48 -0400 Subject: [PATCH] feat: add pre-block EIP-4788 beacon root contract call (#4457) --- crates/consensus/auto-seal/src/lib.rs | 2 + crates/interfaces/src/executor.rs | 4 + crates/payload/basic/src/lib.rs | 79 +++- crates/primitives/src/constants/mod.rs | 9 +- crates/revm/Cargo.toml | 4 +- crates/revm/revm-primitives/src/config.rs | 4 +- crates/revm/revm-primitives/src/env.rs | 50 ++- crates/revm/src/processor.rs | 458 +++++++++++++++++++++- crates/revm/src/state_change.rs | 67 +++- 9 files changed, 660 insertions(+), 17 deletions(-) diff --git a/crates/consensus/auto-seal/src/lib.rs b/crates/consensus/auto-seal/src/lib.rs index ea6b87f5e..282cc34d4 100644 --- a/crates/consensus/auto-seal/src/lib.rs +++ b/crates/consensus/auto-seal/src/lib.rs @@ -305,6 +305,8 @@ impl StorageInner { senders: Vec
, ) -> Result<(BundleStateWithReceipts, u64), BlockExecutionError> { trace!(target: "consensus::auto", transactions=?&block.body, "executing transactions"); + // TODO: there isn't really a parent beacon block root here, so not sure whether or not to + // call the 4788 beacon contract let (receipts, gas_used) = executor.execute_transactions(block, U256::ZERO, Some(senders))?; diff --git a/crates/interfaces/src/executor.rs b/crates/interfaces/src/executor.rs index cbddf4017..fbde59bf0 100644 --- a/crates/interfaces/src/executor.rs +++ b/crates/interfaces/src/executor.rs @@ -26,6 +26,10 @@ pub enum BlockValidationError { BlockPreMerge { hash: H256 }, #[error("Missing total difficulty")] MissingTotalDifficulty { hash: H256 }, + #[error("EIP-4788 Parent beacon block root missing for active Cancun block")] + MissingParentBeaconBlockRoot, + #[error("The parent beacon block root is not zero for Cancun genesis block")] + CancunGenesisParentBeaconBlockRootNotZero, } /// BlockExecutor Errors diff --git a/crates/payload/basic/src/lib.rs b/crates/payload/basic/src/lib.rs index c209e9858..4dd68d849 100644 --- a/crates/payload/basic/src/lib.rs +++ b/crates/payload/basic/src/lib.rs @@ -33,8 +33,10 @@ use reth_primitives::{ }; use reth_provider::{BlockReaderIdExt, BlockSource, BundleStateWithReceipts, StateProviderFactory}; use reth_revm::{ - database::StateProviderDatabase, env::tx_env_with_recovered, into_reth_log, - state_change::post_block_withdrawals_balance_increments, + database::StateProviderDatabase, + env::tx_env_with_recovered, + into_reth_log, + state_change::{apply_beacon_root_contract_call, post_block_withdrawals_balance_increments}, }; use reth_rlp::Encodable; use reth_tasks::TaskSpawner; @@ -45,6 +47,7 @@ use revm::{ Database, DatabaseCommit, State, }; use std::{ + fmt::Debug, future::Future, pin::Pin, sync::{atomic::AtomicBool, Arc}, @@ -664,6 +667,16 @@ where let block_number = initialized_block_env.number.to::(); + // apply eip-4788 pre block contract call + pre_block_beacon_root_contract_call( + &mut db, + &chain_spec, + block_number, + &initialized_cfg, + &initialized_block_env, + &attributes, + )?; + let mut receipts = Vec::new(); while let Some(pool_tx) = best_txs.next() { // ensure we still have capacity for this transaction @@ -771,7 +784,8 @@ where let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, attributes.withdrawals)?; - // merge all transitions into bundle state. + // merge all transitions into bundle state, this would apply the withdrawal balance changes and + // 4788 contract call db.merge_transitions(BundleRetention::PlainState); let bundle = BundleStateWithReceipts::new(db.take_bundle(), vec![receipts], block_number); @@ -861,7 +875,7 @@ where extra_data, attributes, chain_spec, - .. + initialized_cfg, } = config; debug!(parent_hash=?parent_block.hash, parent_number=parent_block.number, "building empty payload"); @@ -876,10 +890,21 @@ where let block_number = initialized_block_env.number.to::(); let block_gas_limit: u64 = initialized_block_env.gas_limit.try_into().unwrap_or(u64::MAX); + // apply eip-4788 pre block contract call + pre_block_beacon_root_contract_call( + &mut db, + &chain_spec, + block_number, + &initialized_cfg, + &initialized_block_env, + &attributes, + )?; + let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals(&mut db, &chain_spec, attributes.timestamp, attributes.withdrawals)?; - // merge transition, this will apply the withdrawal balance changes. + // merge all transitions into bundle state, this would apply the withdrawal balance changes and + // 4788 contract call db.merge_transitions(BundleRetention::PlainState); // calculate the state root @@ -967,6 +992,50 @@ fn commit_withdrawals>( }) } +/// Apply the [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) pre block contract call. +/// +/// This constructs a new [EVM](revm::EVM) with the given DB, and environment ([CfgEnv] and +/// [BlockEnv]) to execute the pre block contract call. +/// +/// The parent beacon block root used for the call is gathered from the given +/// [PayloadBuilderAttributes]. +/// +/// This uses [apply_beacon_root_contract_call] to ultimately apply the beacon root contract state +/// change. +fn pre_block_beacon_root_contract_call( + db: &mut DB, + chain_spec: &ChainSpec, + block_number: u64, + initialized_cfg: &CfgEnv, + initialized_block_env: &BlockEnv, + attributes: &PayloadBuilderAttributes, +) -> Result<(), PayloadBuilderError> +where + DB: Database + DatabaseCommit, + ::Error: Debug, +{ + // Configure the environment for the block. + let env = Env { + cfg: initialized_cfg.clone(), + block: initialized_block_env.clone(), + ..Default::default() + }; + + // apply pre-block EIP-4788 contract call + let mut evm_pre_block = revm::EVM::with_env(env); + evm_pre_block.database(db); + + // initialize a block from the env, because the pre block call needs the block itself + apply_beacon_root_contract_call( + chain_spec, + attributes.timestamp, + block_number, + attributes.parent_beacon_block_root, + &mut evm_pre_block, + ) + .map_err(|err| PayloadBuilderError::Internal(err.into())) +} + /// Checks if the new payload is better than the current best. /// /// This compares the total fees of the blocks, higher is better. diff --git a/crates/primitives/src/constants/mod.rs b/crates/primitives/src/constants/mod.rs index 7955a6e47..2fcdf09b3 100644 --- a/crates/primitives/src/constants/mod.rs +++ b/crates/primitives/src/constants/mod.rs @@ -1,6 +1,6 @@ //! Ethereum protocol-related constants -use crate::{H256, U256}; +use crate::{H160, H256, U256}; use hex_literal::hex; use std::time::Duration; @@ -132,6 +132,13 @@ pub const BEACON_CONSENSUS_REORG_UNWIND_DEPTH: u64 = 3; /// pub const ALLOWED_FUTURE_BLOCK_TIME_SECONDS: u64 = 15; +/// The address for the beacon roots contract defined in EIP-4788. +pub const BEACON_ROOTS_ADDRESS: H160 = H160(hex!("bEac00dDB15f3B6d645C48263dC93862413A222D")); + +/// The caller to be used when calling the EIP-4788 beacon roots contract at the beginning of the +/// block. +pub const SYSTEM_ADDRESS: H160 = H160(hex!("fffffffffffffffffffffffffffffffffffffffe")); + #[cfg(test)] mod tests { use super::*; diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index c6032f436..faf692c7c 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true description = "reth specific revm utilities" [dependencies] -# reth +# reth reth-primitives.workspace = true reth-interfaces.workspace = true reth-provider.workspace = true @@ -21,4 +21,4 @@ reth-consensus-common = { path = "../consensus/common" } revm.workspace = true # common -tracing.workspace = true \ No newline at end of file +tracing.workspace = true diff --git a/crates/revm/revm-primitives/src/config.rs b/crates/revm/revm-primitives/src/config.rs index ec7188007..22d5a15cf 100644 --- a/crates/revm/revm-primitives/src/config.rs +++ b/crates/revm/revm-primitives/src/config.rs @@ -19,7 +19,9 @@ pub fn revm_spec_by_timestamp_after_merge( /// return revm_spec from spec configuration. pub fn revm_spec(chain_spec: &ChainSpec, block: Head) -> revm::primitives::SpecId { - if chain_spec.fork(Hardfork::Shanghai).active_at_head(&block) { + if chain_spec.fork(Hardfork::Cancun).active_at_head(&block) { + revm::primitives::CANCUN + } else if chain_spec.fork(Hardfork::Shanghai).active_at_head(&block) { revm::primitives::SHANGHAI } else if chain_spec.fork(Hardfork::Paris).active_at_head(&block) { revm::primitives::MERGE diff --git a/crates/revm/revm-primitives/src/env.rs b/crates/revm/revm-primitives/src/env.rs index 3efa2b19c..721a46e2d 100644 --- a/crates/revm/revm-primitives/src/env.rs +++ b/crates/revm/revm-primitives/src/env.rs @@ -1,9 +1,10 @@ use crate::config::revm_spec; use reth_primitives::{ + constants::{BEACON_ROOTS_ADDRESS, SYSTEM_ADDRESS}, recover_signer, Address, Bytes, Chain, ChainSpec, Head, Header, Transaction, TransactionKind, - TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxEip4844, TxLegacy, U256, + TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxEip4844, TxLegacy, H256, U256, }; -use revm::primitives::{AnalysisKind, BlockEnv, CfgEnv, SpecId, TransactTo, TxEnv}; +use revm::primitives::{AnalysisKind, BlockEnv, CfgEnv, Env, SpecId, TransactTo, TxEnv}; /// Convenience function to call both [fill_cfg_env] and [fill_block_env] pub fn fill_cfg_and_block_env( @@ -106,6 +107,51 @@ pub fn tx_env_with_recovered(transaction: &TransactionSignedEcRecovered) -> TxEn tx_env } +/// Fill transaction environment with the EIP-4788 system contract message data. +/// +/// This requirements for the beacon root contract call defined by +/// [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788) are: +/// +/// At the start of processing any execution block where `block.timestamp >= FORK_TIMESTAMP` (i.e. +/// before processing any transactions), call `BEACON_ROOTS_ADDRESS` as `SYSTEM_ADDRESS` with the +/// 32-byte input of `header.parent_beacon_block_root`, a gas limit of `30_000_000`, and `0` value. +/// This will trigger the `set()` routine of the beacon roots contract. This is a system operation +/// and therefore: +/// * the call must execute to completion +/// * the call does not count against the block’s gas limit +/// * the call does not follow the EIP-1559 burn semantics - no value should be transferred as +/// part of the call +/// * if no code exists at `BEACON_ROOTS_ADDRESS`, the call must fail silently +pub fn fill_tx_env_with_beacon_root_contract_call(env: &mut Env, parent_beacon_block_root: H256) { + env.tx = TxEnv { + caller: SYSTEM_ADDRESS, + transact_to: TransactTo::Call(BEACON_ROOTS_ADDRESS), + // Explicitly set nonce to None so revm does not do any nonce checks + nonce: None, + gas_limit: 30_000_000, + value: U256::ZERO, + data: parent_beacon_block_root.to_fixed_bytes().to_vec().into(), + // Setting the gas price to zero enforces that no value is transferred as part of the call, + // and that the call will not count against the block's gas limit + gas_price: U256::ZERO, + // The chain ID check is not relevant here and is disabled if set to None + chain_id: None, + // Setting the gas priority fee to None ensures the effective gas price is derived from the + // `gas_price` field, which we need to be zero + gas_priority_fee: None, + access_list: Vec::new(), + // blob fields can be None for this tx + blob_hashes: Vec::new(), + max_fee_per_blob_gas: None, + }; + + // ensure the block gas limit is >= the tx + env.block.gas_limit = U256::from(env.tx.gas_limit); + + // disable the base fee check for this call by setting the base fee to zero + env.block.basefee = U256::ZERO; +} + /// Fill transaction environment from [TransactionSignedEcRecovered]. pub fn fill_tx_env_with_recovered(tx_env: &mut TxEnv, transaction: &TransactionSignedEcRecovered) { fill_tx_env(tx_env, transaction.as_ref(), transaction.signer()) diff --git a/crates/revm/src/processor.rs b/crates/revm/src/processor.rs index 9ded17dd1..183e603d6 100644 --- a/crates/revm/src/processor.rs +++ b/crates/revm/src/processor.rs @@ -4,7 +4,7 @@ use crate::{ eth_dao_fork::{DAO_HARDFORK_BENEFICIARY, DAO_HARDKFORK_ACCOUNTS}, into_reth_log, stack::{InspectorStack, InspectorStackConfig}, - state_change::post_block_balance_increments, + state_change::{apply_beacon_root_contract_call, post_block_balance_increments}, }; use reth_interfaces::{ executor::{BlockExecutionError, BlockValidationError}, @@ -53,7 +53,7 @@ pub struct EVMProcessor<'a> { /// 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. + /// If receipt is None it means it is pruned. receipts: Vec>>, /// First block will be initialized to `None` /// and be set to the block number of first block executed. @@ -172,6 +172,24 @@ impl<'a> EVMProcessor<'a> { ); } + /// 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( @@ -256,6 +274,8 @@ impl<'a> EVMProcessor<'a> { total_difficulty: U256, senders: Option>, ) -> 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)) @@ -263,8 +283,6 @@ impl<'a> EVMProcessor<'a> { let senders = self.recover_senders(&block.body, senders)?; - self.init_env(&block.header, total_difficulty); - let mut cumulative_gas_used = 0; let mut receipts = Vec::with_capacity(block.body.len()); for (transaction, sender) in block.body.iter().zip(senders) { @@ -318,6 +336,8 @@ impl<'a> EVMProcessor<'a> { total_difficulty: U256, senders: Option>, ) -> 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, senders)?; @@ -529,3 +549,433 @@ pub fn verify_receipt<'a>( Ok(()) } + +#[cfg(test)] +mod tests { + use reth_primitives::{ + constants::{BEACON_ROOTS_ADDRESS, SYSTEM_ADDRESS}, + keccak256, Account, Bytecode, Bytes, ChainSpecBuilder, ForkCondition, StorageKey, MAINNET, + }; + use reth_provider::{AccountReader, BlockHashReader, StateRootProvider}; + use reth_revm_primitives::TransitionState; + use revm::Database; + use std::{collections::HashMap, str::FromStr}; + + use super::*; + + /// Returns the beacon root contract code + fn beacon_root_contract_code() -> Bytes { + Bytes::from_str("0x3373fffffffffffffffffffffffffffffffffffffffe14604457602036146024575f5ffd5b620180005f350680545f35146037575f5ffd5b6201800001545f5260205ff35b6201800042064281555f359062018000015500").unwrap() + } + + #[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.into())); + } + self.accounts.insert(address, (storage, account)); + } + } + + impl AccountReader for StateProviderTest { + fn basic_account(&self, address: Address) -> reth_interfaces::Result> { + let ret = Ok(self.accounts.get(&address).map(|(_, acc)| *acc)); + ret + } + } + + impl BlockHashReader for StateProviderTest { + fn block_hash(&self, number: u64) -> reth_interfaces::Result> { + Ok(self.block_hash.get(&number).cloned()) + } + + fn canonical_hashes_range( + &self, + start: BlockNumber, + end: BlockNumber, + ) -> reth_interfaces::Result> { + 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, + ) -> reth_interfaces::Result { + todo!() + } + } + + impl StateProvider for StateProviderTest { + fn storage( + &self, + account: Address, + storage_key: reth_primitives::StorageKey, + ) -> reth_interfaces::Result> { + Ok(self + .accounts + .get(&account) + .and_then(|(storage, _)| storage.get(&storage_key).cloned())) + } + + fn bytecode_by_hash(&self, code_hash: H256) -> reth_interfaces::Result> { + Ok(self.contracts.get(&code_hash).cloned()) + } + + fn proof( + &self, + _address: Address, + _keys: &[H256], + ) -> reth_interfaces::Result<(Vec, H256, Vec>)> { + todo!() + } + } + + #[test] + fn eip_4788_non_genesis_call() { + let mut header = Header { timestamp: 1, number: 1, ..Header::default() }; + + let mut db = StateProviderTest::default(); + + let beacon_root_contract_code = beacon_root_contract_code(); + + 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), + 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)); + + // attempt to execute a block without parent beacon block root, expect err + let err = executor + .execute_and_verify_receipt( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .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(H256::from_low_u64_be(0x1337)); + + // Now execute a block with the fixed header, ensure that it does not fail + executor + .execute( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .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 = 98304u64; + 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(0x1337)); + } + + #[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(H256::from_low_u64_be(0x1337)), + ..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)); + 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( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .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_code = beacon_root_contract_code(); + + 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), + 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)); + + // construct the header for block one + let header = Header { + timestamp: 1, + number: 1, + parent_beacon_block_root: Some(H256::from_low_u64_be(0x1337)), + ..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( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .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_code = beacon_root_contract_code(); + + 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), + 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)); + 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(H256::from_low_u64_be(0x1337)); + let _err = executor + .execute_and_verify_receipt( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .expect_err( + "Executing genesis cancun block with non-zero parent beacon block root field should fail", + ); + + // fix header + header.parent_beacon_block_root = Some(H256::zero()); + + // now try to process the genesis block again, this time ensuring that a system contract + // call does not occur + executor + .execute( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .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(H256::from_low_u64_be(0x1337)), + base_fee_per_gas: Some(u64::MAX), + ..Header::default() + }; + + let mut db = StateProviderTest::default(); + + let beacon_root_contract_code = beacon_root_contract_code(); + + 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), + 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)); + 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( + &Block { header: header.clone(), body: vec![], ommers: vec![], withdrawals: None }, + U256::ZERO, + None, + ) + .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 = 98304u64; + 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(0x1337)); + } +} diff --git a/crates/revm/src/state_change.rs b/crates/revm/src/state_change.rs index 6b4cbeff5..d641ad7c8 100644 --- a/crates/revm/src/state_change.rs +++ b/crates/revm/src/state_change.rs @@ -1,6 +1,11 @@ use reth_consensus_common::calc; -use reth_primitives::{Address, ChainSpec, Hardfork, Header, Withdrawal, U256}; -use std::collections::HashMap; +use reth_interfaces::executor::{BlockExecutionError, BlockValidationError}; +use reth_primitives::{ + constants::SYSTEM_ADDRESS, Address, ChainSpec, Hardfork, Header, Withdrawal, H256, U256, +}; +use reth_revm_primitives::{env::fill_tx_env_with_beacon_root_contract_call, Database}; +use revm::{primitives::ResultAndState, DatabaseCommit, EVM}; +use std::{collections::HashMap, fmt::Debug}; /// Collect all balance changes at the end of the block. /// @@ -46,6 +51,64 @@ pub fn post_block_balance_increments( balance_increments } +/// Applies the pre-block call to the EIP-4788 beacon block root contract, using the given block, +/// [ChainSpec], EVM. +/// +/// If cancun is not activated or the block is the genesis block, then this is a no-op, and no +/// state changes are made. +#[inline] +pub fn apply_beacon_root_contract_call( + chain_spec: &ChainSpec, + block_timestamp: u64, + block_number: u64, + block_parent_beacon_block_root: Option, + evm: &mut EVM, +) -> Result<(), BlockExecutionError> +where + ::Error: Debug, +{ + if chain_spec.is_cancun_activated_at_timestamp(block_timestamp) { + // if the block number is zero (genesis block) then the parent beacon block root must + // be 0x0 and no system transaction may occur as per EIP-4788 + if block_number == 0 { + if block_parent_beacon_block_root != Some(H256::zero()) { + return Err(BlockValidationError::CancunGenesisParentBeaconBlockRootNotZero.into()) + } + } else { + let parent_beacon_block_root = block_parent_beacon_block_root.ok_or( + BlockExecutionError::from(BlockValidationError::MissingParentBeaconBlockRoot), + )?; + + // get previous env + let previous_env = evm.env.clone(); + + // modify env for pre block call + fill_tx_env_with_beacon_root_contract_call(&mut evm.env, parent_beacon_block_root); + + let ResultAndState { mut state, .. } = match evm.transact() { + Ok(res) => res, + Err(e) => { + evm.env = previous_env; + return Err(BlockExecutionError::from(BlockValidationError::EVM { + hash: Default::default(), + message: format!("{e:?}"), + })) + } + }; + + state.remove(&SYSTEM_ADDRESS); + state.remove(&evm.env.block.coinbase); + + let db = evm.db().expect("db to not be moved"); + db.commit(state); + + // re-set the previous env + evm.env = previous_env; + } + } + Ok(()) +} + /// Returns a map of addresses to their balance increments if shanghai is active at the given /// timestamp. #[inline]