Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement EIP-2935 #7818

Merged
merged 18 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions crates/ethereum/evm/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -291,7 +294,7 @@ where
receipts.iter(),
) {
debug!(target: "evm", %error, ?receipts, "receipts verification failed");
return Err(error)
return Err(error);
};
}

Expand Down
13 changes: 12 additions & 1 deletion crates/interfaces/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,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_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.
///
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");
Comment on lines -54 to +62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameter Value
FORK_TIMESTAMP TBD
HISTORY_STORAGE_ADDRESS 0x25a219378dad9b3503c8268c9ca836a52427a4fb
HISTORY_SERVE_WINDOW 8192
BLOCKHASH_OLD_WINDOW 256

from the https://eips.ethereum.org/EIPS/eip-2935, seems OK?


/// 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);
Comment on lines +98 to +101
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this create a genesis hash mismatch with other clients? Or any state root conflicts bc we have diff balance vs others in the state?


// 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
32 changes: 30 additions & 2 deletions crates/rpc/rpc/src/eth/api/pending_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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: Database + DatabaseCommit>(
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::<u64>(),
block_number,
db,
)
.map_err(|err| EthApiError::Internal(err.into()))
}

/// The origin for a configured [PendingBlockEnv]
#[derive(Clone, Debug)]
pub(crate) enum PendingBlockEnvOrigin {
Expand Down
Loading