Skip to content

Commit

Permalink
feat: implement EIP-2935 (#7818)
Browse files Browse the repository at this point in the history
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
  • Loading branch information
2 people authored and shekhirin committed May 28, 2024
1 parent f6e1c7f commit f5d03ba
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 17 deletions.
30 changes: 29 additions & 1 deletion crates/ethereum/evm/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,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::{
Expand Down Expand Up @@ -134,6 +136,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;
Expand Down Expand Up @@ -182,6 +185,16 @@ where
}
drop(evm);

// 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())
}

Ok((receipts, cumulative_gas_used))
}
}
Expand Down Expand Up @@ -265,6 +278,21 @@ where
// 3. apply post execution changes
self.post_execution(block, total_difficulty)?;

// Before Byzantium, receipts contained state root that would mean that expensive
// operation as hashing that is required 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().is_byzantium_active_at_block(block.header.number) {
if let Err(error) = verify_receipts(
block.header.receipts_root,
block.header.logs_bloom,
receipts.iter(),
) {
debug!(target: "evm", %error, ?receipts, "receipts verification failed");
return Err(error)
};
}

Ok((receipts, gas_used))
}

Expand Down
13 changes: 12 additions & 1 deletion crates/evm/execution-errors/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,25 @@ 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
parent_beacon_block_root: Box<B256>,
/// 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
Expand Down
34 changes: 27 additions & 7 deletions crates/payload/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -107,6 +107,17 @@ where
err
})?;

// apply eip-2935 blockhashes update
apply_blockhashes_update(
&chain_spec,
initialized_block_env.timestamp.to::<u64>(),
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,
Expand Down Expand Up @@ -240,6 +251,15 @@ where
&attributes,
)?;

// apply eip-2935 blockhashes update
apply_blockhashes_update(
&chain_spec,
initialized_block_env.timestamp.to::<u64>(),
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
Expand All @@ -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
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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));
}
}
}
Expand Down Expand Up @@ -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 } =
Expand Down
136 changes: 130 additions & 6 deletions crates/revm/src/state_change.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use reth_consensus_common::calc;
use reth_execution_errors::{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.
///
Expand Down Expand Up @@ -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<DB: Database + DatabaseCommit>(
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<AccountInfo>` 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<DB: Database>(
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<EXT, DB: Database + DatabaseCommit>(
chain_spec: &ChainSpec,
Expand Down
5 changes: 5 additions & 0 deletions crates/revm/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit f5d03ba

Please sign in to comment.