diff --git a/crates/ethereum/evm/src/execute.rs b/crates/ethereum/evm/src/execute.rs index 15702ba7508..0decdf493e4 100644 --- a/crates/ethereum/evm/src/execute.rs +++ b/crates/ethereum/evm/src/execute.rs @@ -23,7 +23,9 @@ use reth_primitives::{ use reth_revm::{ batch::{BlockBatchRecord, BlockExecutorStats}, db::states::bundle_state::BundleRetention, - state_change::{apply_beacon_root_contract_call, post_block_balance_increments}, + state_change::{ + apply_beacon_root_contract_call, apply_blockhashes_update, post_block_balance_increments, + }, Evm, State, }; use revm_primitives::{ @@ -139,6 +141,7 @@ where block.parent_beacon_block_root, &mut evm, )?; + apply_blockhashes_update(&self.chain_spec, block.timestamp, block.number, evm.db_mut())?; // execute transactions let mut cumulative_gas_used = 0; @@ -152,7 +155,7 @@ where transaction_gas_limit: transaction.gas_limit(), block_available_gas, } - .into()) + .into()); } EvmConfig::fill_tx_env(evm.tx_mut(), transaction, *sender); @@ -194,7 +197,7 @@ where gas: GotExpected { got: cumulative_gas_used, expected: block.gas_used }, gas_spent_by_tx: receipts.gas_spent_by_tx()?, } - .into()) + .into()); } Ok((receipts, cumulative_gas_used)) @@ -291,7 +294,7 @@ where receipts.iter(), ) { debug!(target: "evm", %error, ?receipts, "receipts verification failed"); - return Err(error) + return Err(error); }; } diff --git a/crates/interfaces/src/executor.rs b/crates/interfaces/src/executor.rs index 04b9832f092..bbaa547ad14 100644 --- a/crates/interfaces/src/executor.rs +++ b/crates/interfaces/src/executor.rs @@ -69,7 +69,9 @@ pub enum BlockValidationError { /// The beacon block root parent_beacon_block_root: B256, }, - /// EVM error during beacon root contract call + /// EVM error during [EIP-4788] beacon root contract call. + /// + /// [EIP-4788]: https://eips.ethereum.org/EIPS/eip-4788 #[error("failed to apply beacon root contract call at {parent_beacon_block_root}: {message}")] BeaconRootContractCall { /// The beacon block root @@ -77,6 +79,15 @@ pub enum BlockValidationError { /// The error message. message: String, }, + /// EVM error during [EIP-2935] pre-block state transition. + /// + /// [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935 + #[error("failed to apply EIP-2935 pre-block state transition: {message}")] + // todo: better variant name + Eip2935StateTransition { + /// The error message. + message: String, + }, } /// BlockExecutor Errors diff --git a/crates/payload/ethereum/src/lib.rs b/crates/payload/ethereum/src/lib.rs index e34287f7651..ac9dafb1143 100644 --- a/crates/payload/ethereum/src/lib.rs +++ b/crates/payload/ethereum/src/lib.rs @@ -26,7 +26,7 @@ use reth_primitives::{ Block, Header, IntoRecoveredTransaction, Receipt, Receipts, EMPTY_OMMER_ROOT_HASH, U256, }; use reth_provider::{BundleStateWithReceipts, StateProviderFactory}; -use reth_revm::database::StateProviderDatabase; +use reth_revm::{database::StateProviderDatabase, state_change::apply_blockhashes_update}; use reth_transaction_pool::{BestTransactionsAttributes, TransactionPool}; use revm::{ db::states::bundle_state::BundleRetention, @@ -107,6 +107,17 @@ where err })?; + // apply eip-2935 blockhashes update + apply_blockhashes_update( + &chain_spec, + initialized_block_env.timestamp.to::(), + block_number, + &mut db, + ).map_err(|err| { + warn!(target: "payload_builder", parent_hash=%parent_block.hash(), %err, "failed to update blockhashes for empty payload"); + PayloadBuilderError::Internal(err.into()) + })?; + let WithdrawalsOutcome { withdrawals_root, withdrawals } = commit_withdrawals( &mut db, &chain_spec, @@ -240,6 +251,15 @@ where &attributes, )?; + // apply eip-2935 blockhashes update + apply_blockhashes_update( + &chain_spec, + initialized_block_env.timestamp.to::(), + block_number, + &mut db, + ) + .map_err(|err| PayloadBuilderError::Internal(err.into()))?; + let mut receipts = Vec::new(); while let Some(pool_tx) = best_txs.next() { // ensure we still have capacity for this transaction @@ -248,12 +268,12 @@ where // which also removes all dependent transaction from the iterator before we can // continue best_txs.mark_invalid(&pool_tx); - continue + continue; } // check if the job was cancelled, if so we can exit early if cancel.is_cancelled() { - return Ok(BuildOutcome::Cancelled) + return Ok(BuildOutcome::Cancelled); } // convert tx to a signed transaction @@ -270,7 +290,7 @@ where // for regular transactions above. trace!(target: "payload_builder", tx=?tx.hash, ?sum_blob_gas_used, ?tx_blob_gas, "skipping blob transaction because it would exceed the max data gas per block"); best_txs.mark_invalid(&pool_tx); - continue + continue; } } @@ -299,11 +319,11 @@ where best_txs.mark_invalid(&pool_tx); } - continue + continue; } err => { // this is an error that we should treat as fatal for this attempt - return Err(PayloadBuilderError::EvmExecutionError(err)) + return Err(PayloadBuilderError::EvmExecutionError(err)); } } } @@ -352,7 +372,7 @@ where // check if we have a better block if !is_better_payload(best_payload.as_ref(), total_fees) { // can skip building the block - return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }) + return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads }); } let WithdrawalsOutcome { withdrawals_root, withdrawals } = diff --git a/crates/revm/src/state_change.rs b/crates/revm/src/state_change.rs index 27997342547..fa2b3818060 100644 --- a/crates/revm/src/state_change.rs +++ b/crates/revm/src/state_change.rs @@ -1,11 +1,15 @@ use reth_consensus_common::calc; use reth_interfaces::executor::{BlockExecutionError, BlockValidationError}; use reth_primitives::{ - revm::env::fill_tx_env_with_beacon_root_contract_call, Address, ChainSpec, Header, Withdrawal, - B256, U256, + address, revm::env::fill_tx_env_with_beacon_root_contract_call, Address, ChainSpec, Header, + Withdrawal, B256, U256, }; -use revm::{interpreter::Host, Database, DatabaseCommit, Evm}; -use std::collections::HashMap; +use revm::{ + interpreter::Host, + primitives::{Account, AccountInfo, StorageSlot}, + Database, DatabaseCommit, Evm, +}; +use std::{collections::HashMap, ops::Rem}; /// Collect all balance changes at the end of the block. /// @@ -51,11 +55,131 @@ 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, +/// todo: temporary move over of constants from revm until we've migrated to the latest version +pub const HISTORY_SERVE_WINDOW: usize = 8192; + +/// todo: temporary move over of constants from revm until we've migrated to the latest version +pub const HISTORY_STORAGE_ADDRESS: Address = address!("25a219378dad9b3503c8268c9ca836a52427a4fb"); + +/// Applies the pre-block state change outlined in [EIP-2935] to store historical blockhashes in a +/// system contract. +/// +/// If Prague is not activated, or the block is the genesis block, then this is a no-op, and no +/// state changes are made. +/// +/// If the provided block is the fork activation block, this will generate multiple state changes, +/// as it inserts multiple historical blocks, as outlined in the EIP. +/// +/// If the provided block is after Prague has been activated, this will only insert a single block +/// hash. +/// +/// [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935 +#[inline] +pub fn apply_blockhashes_update( + chain_spec: &ChainSpec, + block_timestamp: u64, + block_number: u64, + db: &mut DB, +) -> Result<(), BlockExecutionError> +where + DB::Error: std::fmt::Display, +{ + // If Prague is not activated or this is the genesis block, no hashes are added. + if !chain_spec.is_prague_active_at_timestamp(block_timestamp) || block_number == 0 { + return Ok(()) + } + assert!(block_number > 0); + + // Create an empty account using the `From` impl of `Account`. This marks the + // account internally as `Loaded`, which is required, since we want the EVM to retrieve storage + // values from the DB when `BLOCKHASH` is invoked. + let mut account = Account::from(AccountInfo::default()); + + // HACK(onbjerg): This is a temporary workaround to make sure the account does not get cleared + // by state clearing later. This balance will likely be present in the devnet 0 genesis file + // until the EIP itself is fixed. + account.info.balance = U256::from(1); + + // We load the `HISTORY_STORAGE_ADDRESS` account because REVM expects this to be loaded in order + // to access any storage, which we will do below. + db.basic(HISTORY_STORAGE_ADDRESS) + .map_err(|err| BlockValidationError::Eip2935StateTransition { message: err.to_string() })?; + + // Insert the state change for the slot + let (slot, value) = eip2935_block_hash_slot(block_number - 1, db) + .map_err(|err| BlockValidationError::Eip2935StateTransition { message: err.to_string() })?; + account.storage.insert(slot, value); + + // If the first slot in the ring is `U256::ZERO`, then we can assume the ring has not been + // filled before, and this is the activation block. + // + // Reasoning: + // - If `block_number <= HISTORY_SERVE_WINDOW`, then the ring will be filled with as many blocks + // as possible, down to slot 0. + // + // For example, if it is activated at block 100, then slots `0..100` will be filled. + // + // - If the fork is activated at genesis, then this will only run at block 1, which will fill + // the ring with the hash of block 0 at slot 0. + // + // - If the activation block is above `HISTORY_SERVE_WINDOW`, then `0..HISTORY_SERVE_WINDOW` + // will be filled. + let is_activation_block = db + .storage(HISTORY_STORAGE_ADDRESS, U256::ZERO) + .map_err(|err| BlockValidationError::Eip2935StateTransition { message: err.to_string() })? + .is_zero(); + + // If this is the activation block, then we backfill the storage of the account with up to + // `HISTORY_SERVE_WINDOW - 1` ancestors' blockhashes as well, per the EIP. + // + // Note: The -1 is because the ancestor itself was already inserted up above. + if is_activation_block { + let mut ancestor_block_number = block_number - 1; + for _ in 0..HISTORY_SERVE_WINDOW - 1 { + // Stop at genesis + if ancestor_block_number == 0 { + break + } + ancestor_block_number -= 1; + + let (slot, value) = + eip2935_block_hash_slot(ancestor_block_number, db).map_err(|err| { + BlockValidationError::Eip2935StateTransition { message: err.to_string() } + })?; + account.storage.insert(slot, value); + } + } + + // Mark the account as touched and commit the state change + account.mark_touch(); + db.commit(HashMap::from([(HISTORY_STORAGE_ADDRESS, account)])); + + Ok(()) +} + +/// Helper function to create a [`StorageSlot`] for [EIP-2935] state transitions for a given block +/// number. +/// +/// This calculates the correct storage slot in the `BLOCKHASH` history storage address, fetches the +/// blockhash and creates a [`StorageSlot`] with appropriate previous and new values. +fn eip2935_block_hash_slot( + block_number: u64, + db: &mut DB, +) -> Result<(U256, StorageSlot), DB::Error> { + let slot = U256::from(block_number).rem(U256::from(HISTORY_SERVE_WINDOW)); + let current_hash = db.storage(HISTORY_STORAGE_ADDRESS, slot)?; + let ancestor_hash = db.block_hash(U256::from(block_number))?; + + Ok((slot, StorageSlot::new_changed(current_hash, ancestor_hash.into()))) +} + +/// 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 +/// If Cancun is not activated or the block is the genesis block, then this is a no-op, and no /// state changes are made. +/// +/// [EIP-4788]: https://eips.ethereum.org/EIPS/eip-4788 #[inline] pub fn apply_beacon_root_contract_call( chain_spec: &ChainSpec, diff --git a/crates/revm/src/test_utils.rs b/crates/revm/src/test_utils.rs index 8c4d1894c5d..63da12ce889 100644 --- a/crates/revm/src/test_utils.rs +++ b/crates/revm/src/test_utils.rs @@ -32,6 +32,11 @@ impl StateProviderTest { } self.accounts.insert(address, (storage, account)); } + + /// Insert a block hash. + pub fn insert_block_hash(&mut self, block_number: u64, block_hash: B256) { + self.block_hash.insert(block_number, block_hash); + } } impl AccountReader for StateProviderTest { diff --git a/crates/rpc/rpc/src/eth/api/pending_block.rs b/crates/rpc/rpc/src/eth/api/pending_block.rs index dbb148981bf..60bba2a8d1f 100644 --- a/crates/rpc/rpc/src/eth/api/pending_block.rs +++ b/crates/rpc/rpc/src/eth/api/pending_block.rs @@ -14,7 +14,10 @@ use reth_primitives::{ use reth_provider::{BundleStateWithReceipts, ChainSpecProvider, StateProviderFactory}; use reth_revm::{ database::StateProviderDatabase, - state_change::{apply_beacon_root_contract_call, post_block_withdrawals_balance_increments}, + state_change::{ + apply_beacon_root_contract_call, apply_blockhashes_update, + post_block_withdrawals_balance_increments, + }, }; use reth_transaction_pool::{BestTransactionsAttributes, TransactionPool}; use revm::{db::states::bundle_state::BundleRetention, Database, DatabaseCommit, State}; @@ -92,6 +95,7 @@ impl PendingBlockEnv { } else { None }; + pre_block_blockhashes_update(&mut db, &block_env, chain_spec.as_ref(), block_number)?; let mut receipts = Vec::new(); @@ -272,7 +276,7 @@ impl PendingBlockEnv { /// 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 [CfgEnvWithHandlerCfg] -/// and [BlockEnv]) to execute the pre block contract call. +/// and [BlockEnv] to execute the pre block contract call. /// /// This uses [apply_beacon_root_contract_call] to ultimately apply the beacon root contract state /// change. @@ -308,6 +312,30 @@ where .map_err(|err| EthApiError::Internal(err.into())) } +/// Apply the [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) pre block state transitions. +/// +/// This constructs a new [Evm](revm::Evm) with the given DB, and environment [CfgEnvWithHandlerCfg] +/// and [BlockEnv]. +/// +/// This uses [apply_blockhashes_update]. +fn pre_block_blockhashes_update( + db: &mut DB, + initialized_block_env: &BlockEnv, + chain_spec: &ChainSpec, + block_number: u64, +) -> EthResult<()> +where + DB::Error: std::fmt::Display, +{ + apply_blockhashes_update( + chain_spec, + initialized_block_env.timestamp.to::(), + block_number, + db, + ) + .map_err(|err| EthApiError::Internal(err.into())) +} + /// The origin for a configured [PendingBlockEnv] #[derive(Clone, Debug)] pub(crate) enum PendingBlockEnvOrigin {