feat: implement EIP-2935 (#8431)

Co-authored-by: Oliver Nordbjerg <onbjerg@users.noreply.github.com>
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Alexey Shekhirin
2024-05-30 13:18:00 +01:00
committed by GitHub
parent bafec99f6b
commit 633b655fef
7 changed files with 573 additions and 29 deletions

View File

@ -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::{
@ -142,6 +144,13 @@ where
block.parent_beacon_block_root,
&mut evm,
)?;
apply_blockhashes_update(
evm.db_mut(),
&self.chain_spec,
block.timestamp,
block.number,
block.parent_hash,
)?;
// execute transactions
let mut cumulative_gas_used = 0;
@ -429,10 +438,16 @@ where
#[cfg(test)]
mod tests {
use super::*;
use alloy_eips::eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE, SYSTEM_ADDRESS};
use reth_primitives::{keccak256, Account, Block, ChainSpecBuilder, ForkCondition, B256};
use alloy_eips::{
eip2935::HISTORY_STORAGE_ADDRESS,
eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE, SYSTEM_ADDRESS},
};
use reth_primitives::{
keccak256, trie::EMPTY_ROOT_HASH, Account, Block, ChainSpecBuilder, ForkCondition, B256,
};
use reth_revm::{
database::StateProviderDatabase, test_utils::StateProviderTest, TransitionState,
database::StateProviderDatabase, state_change::HISTORY_SERVE_WINDOW,
test_utils::StateProviderTest, TransitionState,
};
use std::collections::HashMap;
@ -574,8 +589,8 @@ mod tests {
// attempt to execute an empty block with parent beacon block root, this should not fail
provider
.executor(StateProviderDatabase::new(&db))
.execute(
.batch_executor(StateProviderDatabase::new(&db), PruneModes::none())
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
@ -624,22 +639,26 @@ mod tests {
..Header::default()
};
let mut executor = provider.executor(StateProviderDatabase::new(&db));
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
// attempt to execute an empty block with parent beacon block root, this should not fail
executor
.execute_without_verification(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
senders: vec![],
},
U256::ZERO,
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while cancun is active should not fail",
@ -663,9 +682,7 @@ mod tests {
);
let mut header = chain_spec.genesis_header();
let provider = executor_provider(chain_spec);
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
@ -801,4 +818,378 @@ mod tests {
.unwrap();
assert_eq!(parent_beacon_block_root_storage, U256::from(0x69));
}
fn create_state_provider_with_block_hashes(latest_block: u64) -> StateProviderTest {
let mut db = StateProviderTest::default();
for block_number in 0..=latest_block {
db.insert_block_hash(block_number, keccak256(block_number.to_string()));
}
db
}
#[test]
fn eip_2935_pre_fork() {
let db = create_state_provider_with_block_hashes(1);
let chain_spec = Arc::new(
ChainSpecBuilder::from(&*MAINNET)
.shanghai_activated()
.with_fork(Hardfork::Prague, ForkCondition::Never)
.build(),
);
let provider = executor_provider(chain_spec);
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
// construct the header for block one
let header = Header { timestamp: 1, number: 1, ..Header::default() };
// attempt to execute an empty block, this should not fail
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// ensure that the block hash was *not* written to storage, since this is before the fork
// was activated
//
// we load the account first, which should also not exist, because revm expects it to be
// loaded
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_none());
assert!(executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::ZERO)
.unwrap()
.is_zero());
}
#[test]
fn eip_2935_fork_activation_genesis() {
let db = create_state_provider_with_block_hashes(0);
let chain_spec = Arc::new(
ChainSpecBuilder::from(&*MAINNET)
.shanghai_activated()
.with_fork(Hardfork::Prague, ForkCondition::Timestamp(0))
.build(),
);
let header = chain_spec.genesis_header();
let provider = executor_provider(chain_spec);
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
// attempt to execute genesis block, this should not fail
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// ensure that the block hash was *not* written to storage, since there are no blocks
// preceding genesis
//
// we load the account first, which should also not exist, because revm expects it to be
// loaded
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_none());
assert!(executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::ZERO)
.unwrap()
.is_zero());
}
#[test]
fn eip_2935_fork_activation_within_window_bounds() {
let fork_activation_block = HISTORY_SERVE_WINDOW - 10;
let db = create_state_provider_with_block_hashes(fork_activation_block);
let chain_spec = Arc::new(
ChainSpecBuilder::from(&*MAINNET)
.shanghai_activated()
.with_fork(Hardfork::Prague, ForkCondition::Timestamp(1))
.build(),
);
let header = Header {
parent_hash: B256::random(),
timestamp: 1,
number: fork_activation_block,
requests_root: Some(EMPTY_ROOT_HASH),
..Header::default()
};
let provider = executor_provider(chain_spec);
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
// attempt to execute the fork activation block, this should not fail
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// the hash for the ancestor of the fork activation block should be present
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_some());
assert_ne!(
executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::from(fork_activation_block - 1))
.unwrap(),
U256::ZERO
);
// the hash of the block itself should not be in storage
assert!(executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::from(fork_activation_block))
.unwrap()
.is_zero());
}
#[test]
fn eip_2935_fork_activation_outside_window_bounds() {
let fork_activation_block = HISTORY_SERVE_WINDOW + 256;
let db = create_state_provider_with_block_hashes(fork_activation_block);
let chain_spec = Arc::new(
ChainSpecBuilder::from(&*MAINNET)
.shanghai_activated()
.with_fork(Hardfork::Prague, ForkCondition::Timestamp(1))
.build(),
);
let provider = executor_provider(chain_spec);
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
let header = Header {
parent_hash: B256::random(),
timestamp: 1,
number: fork_activation_block,
requests_root: Some(EMPTY_ROOT_HASH),
..Header::default()
};
// attempt to execute the fork activation block, this should not fail
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// the hash for the ancestor of the fork activation block should be present
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_some());
assert_ne!(
executor
.state_mut()
.storage(
HISTORY_STORAGE_ADDRESS,
U256::from(fork_activation_block % HISTORY_SERVE_WINDOW - 1)
)
.unwrap(),
U256::ZERO
);
}
#[test]
fn eip_2935_state_transition_inside_fork() {
let db = create_state_provider_with_block_hashes(2);
let chain_spec = Arc::new(
ChainSpecBuilder::from(&*MAINNET)
.shanghai_activated()
.with_fork(Hardfork::Prague, ForkCondition::Timestamp(0))
.build(),
);
let mut header = chain_spec.genesis_header();
header.requests_root = Some(EMPTY_ROOT_HASH);
let header_hash = header.hash_slow();
let provider = executor_provider(chain_spec);
let mut executor =
provider.batch_executor(StateProviderDatabase::new(&db), PruneModes::none());
// attempt to execute the genesis block, this should not fail
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// nothing should be written as the genesis has no ancestors
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_none());
assert!(executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::ZERO)
.unwrap()
.is_zero());
// attempt to execute block 1, this should not fail
let header = Header {
parent_hash: header_hash,
timestamp: 1,
number: 1,
requests_root: Some(EMPTY_ROOT_HASH),
..Header::default()
};
let header_hash = header.hash_slow();
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// the block hash of genesis should now be in storage, but not block 1
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_some());
assert_ne!(
executor.state_mut().storage(HISTORY_STORAGE_ADDRESS, U256::ZERO).unwrap(),
U256::ZERO
);
assert!(executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::from(1))
.unwrap()
.is_zero());
// attempt to execute block 2, this should not fail
let header = Header {
parent_hash: header_hash,
timestamp: 1,
number: 2,
requests_root: Some(EMPTY_ROOT_HASH),
..Header::default()
};
executor
.execute_and_verify_one(
(
&BlockWithSenders {
block: Block {
header,
body: vec![],
ommers: vec![],
withdrawals: None,
requests: None,
},
senders: vec![],
},
U256::ZERO,
)
.into(),
)
.expect(
"Executing a block with no transactions while Prague is active should not fail",
);
// the block hash of genesis and block 1 should now be in storage, but not block 2
assert!(executor.state_mut().basic(HISTORY_STORAGE_ADDRESS).unwrap().is_some());
assert_ne!(
executor.state_mut().storage(HISTORY_STORAGE_ADDRESS, U256::ZERO).unwrap(),
U256::ZERO
);
assert_ne!(
executor.state_mut().storage(HISTORY_STORAGE_ADDRESS, U256::from(1)).unwrap(),
U256::ZERO
);
assert!(executor
.state_mut()
.storage(HISTORY_STORAGE_ADDRESS, U256::from(2))
.unwrap()
.is_zero());
}
}

View File

@ -67,7 +67,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
@ -75,6 +77,9 @@ pub enum BlockValidationError {
/// The error message.
message: String,
},
/// Provider error during the [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) block hash account loading.
#[error(transparent)]
BlockHashAccountLoadingFailed(#[from] ProviderError),
}
/// BlockExecutor Errors

View File

@ -30,7 +30,7 @@ use reth_primitives::{
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,
@ -127,6 +127,18 @@ where
err
})?;
// apply eip-2935 blockhashes update
apply_blockhashes_update(
&mut db,
&chain_spec,
initialized_block_env.timestamp.to::<u64>(),
block_number,
parent_block.hash(),
).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,
@ -271,6 +283,16 @@ where
&attributes,
)?;
// apply eip-2935 blockhashes update
apply_blockhashes_update(
&mut db,
&chain_spec,
initialized_block_env.timestamp.to::<u64>(),
block_number,
parent_block.hash(),
)
.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

View File

@ -21,7 +21,9 @@ pub fn revm_spec_by_timestamp_after_merge(
}
}
if chain_spec.is_cancun_active_at_timestamp(timestamp) {
if chain_spec.is_prague_active_at_timestamp(timestamp) {
revm_primitives::PRAGUE
} else if chain_spec.is_cancun_active_at_timestamp(timestamp) {
revm_primitives::CANCUN
} else if chain_spec.is_shanghai_active_at_timestamp(timestamp) {
revm_primitives::SHANGHAI
@ -45,7 +47,9 @@ pub fn revm_spec(chain_spec: &ChainSpec, block: Head) -> revm_primitives::SpecId
}
}
if chain_spec.fork(Hardfork::Cancun).active_at_head(&block) {
if chain_spec.fork(Hardfork::Prague).active_at_head(&block) {
revm_primitives::PRAGUE
} else 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

View File

@ -1,10 +1,16 @@
use alloy_eips::eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE};
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,
};
use revm::{interpreter::Host, Database, DatabaseCommit, Evm};
use reth_storage_errors::provider::ProviderError;
use revm::{
interpreter::Host,
primitives::{Account, AccountInfo, Bytecode, EvmStorageSlot},
Database, DatabaseCommit, Evm,
};
use std::collections::HashMap;
/// Collect all balance changes at the end of the block.
@ -51,11 +57,85 @@ 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: u64 = 8192;
/// 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 after Prague has been activated, the parent hash will be inserted.
///
/// [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935
#[inline]
pub fn apply_blockhashes_update<DB: Database<Error = ProviderError> + DatabaseCommit>(
db: &mut DB,
chain_spec: &ChainSpec,
block_timestamp: u64,
block_number: u64,
parent_block_hash: B256,
) -> 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);
// Account is expected to exist either in genesis (for tests) or deployed on mainnet or
// testnets.
// If the account for any reason does not exist, we create it with the EIP-2935 bytecode and a
// nonce of 1, so it does not get deleted.
let mut account: Account = db
.basic(HISTORY_STORAGE_ADDRESS)
.map_err(BlockValidationError::BlockHashAccountLoadingFailed)?
.unwrap_or_else(|| AccountInfo {
nonce: 1,
code: Some(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
..Default::default()
})
.into();
// Insert the state change for the slot
let (slot, value) = eip2935_block_hash_slot(db, block_number - 1, parent_block_hash)?;
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 [`EvmStorageSlot`] 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 [`EvmStorageSlot`] with appropriate previous and new values.
fn eip2935_block_hash_slot<DB: Database<Error = ProviderError>>(
db: &mut DB,
block_number: u64,
block_hash: B256,
) -> Result<(U256, EvmStorageSlot), BlockValidationError> {
let slot = U256::from(block_number % HISTORY_SERVE_WINDOW);
let current_hash = db
.storage(HISTORY_STORAGE_ADDRESS, slot)
.map_err(BlockValidationError::BlockHashAccountLoadingFailed)?;
Ok((slot, EvmStorageSlot::new_changed(current_hash, block_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,

View File

@ -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 {

View File

@ -1,6 +1,7 @@
//! Support for building a pending block via local txpool.
use crate::eth::error::{EthApiError, EthResult};
use reth_errors::ProviderError;
use reth_primitives::{
constants::{eip4844::MAX_DATA_GAS_PER_BLOCK, BEACON_NONCE},
proofs,
@ -15,7 +16,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};
@ -93,6 +97,13 @@ impl PendingBlockEnv {
} else {
None
};
pre_block_blockhashes_update(
&mut db,
chain_spec.as_ref(),
&block_env,
block_number,
parent_hash,
)?;
let mut receipts = Vec::new();
@ -283,7 +294,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.
@ -319,6 +330,32 @@ 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<Error = ProviderError> + DatabaseCommit>(
db: &mut DB,
chain_spec: &ChainSpec,
initialized_block_env: &BlockEnv,
block_number: u64,
parent_block_hash: B256,
) -> EthResult<()>
where
DB::Error: std::fmt::Display,
{
apply_blockhashes_update(
db,
chain_spec,
initialized_block_env.timestamp.to::<u64>(),
block_number,
parent_block_hash,
)
.map_err(|err| EthApiError::Internal(err.into()))
}
/// The origin for a configured [PendingBlockEnv]
#[derive(Clone, Debug)]
pub(crate) enum PendingBlockEnvOrigin {