diff --git a/Cargo.toml b/Cargo.toml index fd5cd10f..0d758550 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,3 +61,16 @@ serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2.0.0", default-features = false } serde_json = "1" test-case = "3" +[patch.crates-io] +revm = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-bytecode = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-context = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-context-interface = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-database = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-database-interface = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-handler = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-inspector = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-interpreter = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-precompile = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } +revm-state = { git = "https://github.com/bluealloy/revm", rev = "335a35ddb370b667bff248109524a4efc589213f" } diff --git a/crates/evm/src/block/error.rs b/crates/evm/src/block/error.rs index ee4c31b7..722d6739 100644 --- a/crates/evm/src/block/error.rs +++ b/crates/evm/src/block/error.rs @@ -84,6 +84,22 @@ pub enum BlockValidationError { /// The error message. message: String, }, + /// EVM error during builder deposit requests contract call [EIP-8282] + /// + /// [EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282 + #[error("failed to apply builder deposit requests contract call: {message}")] + BuilderDepositRequestsContractCall { + /// The error message. + message: String, + }, + /// EVM error during builder exit requests contract call [EIP-8282] + /// + /// [EIP-8282]: https://eips.ethereum.org/EIPS/eip-8282 + #[error("failed to apply builder exit requests contract call: {message}")] + BuilderExitRequestsContractCall { + /// The error message. + message: String, + }, /// Error when decoding deposit requests from receipts [EIP-6110] /// /// [EIP-6110]: https://eips.ethereum.org/EIPS/eip-6110 diff --git a/crates/evm/src/block/system_calls/eip7997.rs b/crates/evm/src/block/system_calls/eip7997.rs new file mode 100644 index 00000000..0acde96b --- /dev/null +++ b/crates/evm/src/block/system_calls/eip7997.rs @@ -0,0 +1,204 @@ +//! [EIP-7997](https://eips.ethereum.org/EIPS/eip-7997) deterministic factory predeploy. +//! +//! EIP-7997 inserts a minimal `CREATE2` factory as a contract at a fixed address upon activation +//! of the Amsterdam hardfork, enabling deterministic deployments at identical addresses across +//! EVM chains. + +use crate::{block::BlockExecutionError, Evm}; +use alloy_hardforks::EthereumHardforks; +use alloy_primitives::{address, b256, bytes, Address, Bytes, B256}; +use revm::{ + context::Block, + state::{Account, AccountStatus, Bytecode, EvmState}, + Database, +}; + +/// Address of the EIP-7997 deterministic `CREATE2` factory predeploy. +pub const FACTORY_ADDRESS: Address = address!("0x0000000000000000000000000000000000000012"); + +/// Runtime bytecode deployed at [`FACTORY_ADDRESS`] on activation of EIP-7997. +/// +/// When called, it invokes `CREATE2` using the first 32 bytes of the input as the salt and the +/// remaining bytes as the init code, forwarding the call value. +pub const FACTORY_CODE: Bytes = bytes!( + "0x60203610602f5760003560203603806020600037600034f5806026573d600060003e3d6000fd5b60005260206000f35b60006000fd" +); + +/// keccak256 hash of [`FACTORY_CODE`]. +/// +/// Used to detect whether the predeploy is already present, keeping the insertion idempotent. +pub const FACTORY_CODE_HASH: B256 = + b256!("0x2b2a8b8ac51cd14e2371a09cc5fa3737e74cabd8dafad0573ebca41c7cd5b51f"); + +/// Builds the pre-block state change that inserts the EIP-7997 factory bytecode at +/// [`FACTORY_ADDRESS`]. +/// +/// Returns `None` (a no-op) when Amsterdam is not active, or when the factory code is already +/// present at the address. Comparing the existing code hash makes the insertion idempotent, so it +/// only mutates state once: on the block that activates Amsterdam. +/// +/// Only the code (and its hash) is set; any existing account fields, such as balance and nonce, +/// are preserved, as EIP-7997 does not specify them. +/// +/// Note: this does not commit the state change to the database, it only constructs it. +#[inline] +pub(crate) fn build_factory_predeploy_state( + spec: impl EthereumHardforks, + evm: &mut impl Evm, +) -> Result, BlockExecutionError> { + if !spec.is_amsterdam_active_at_timestamp(evm.block().timestamp().saturating_to()) { + return Ok(None); + } + + let mut info = evm + .db_mut() + .basic(FACTORY_ADDRESS) + .map_err(|_| BlockExecutionError::msg("could not load EIP-7997 factory account"))? + .unwrap_or_default(); + + // Idempotent: only insert the factory on the block that activates Amsterdam. On later blocks + // the code is already present, so this is a no-op. + if info.code_hash == FACTORY_CODE_HASH { + return Ok(None); + } + + info.code_hash = FACTORY_CODE_HASH; + info.code = Some(Bytecode::new_legacy(FACTORY_CODE)); + + let mut account = Account::from(info); + account.status = AccountStatus::Touched; + + Ok(Some(EvmState::from_iter([(FACTORY_ADDRESS, account)]))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EthEvmFactory, EvmEnv, EvmFactory}; + use alloy_hardforks::{EthereumHardfork, ForkCondition}; + use alloy_primitives::{keccak256, U256}; + use revm::{ + context::{BlockEnv, CfgEnv}, + database::{CacheDB, EmptyDB, State}, + primitives::hardfork::SpecId, + state::AccountInfo, + }; + + /// Minimal spec that reports Amsterdam as active from genesis when `amsterdam` is set. + struct TestSpec { + amsterdam: bool, + } + + impl EthereumHardforks for TestSpec { + fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition { + if self.amsterdam && fork == EthereumHardfork::Amsterdam { + ForkCondition::Timestamp(0) + } else { + ForkCondition::Never + } + } + } + + fn evm_with(db: CacheDB) -> impl Evm { + let mut cfg = CfgEnv::default(); + cfg.spec = SpecId::AMSTERDAM; + let env = EvmEnv { + block_env: BlockEnv { timestamp: U256::from(1), ..Default::default() }, + cfg_env: cfg, + }; + let db = State::builder().with_database(db).with_bundle_update().build(); + EthEvmFactory.create_evm(db, env) + } + + #[test] + fn factory_code_hash_matches_code() { + assert_eq!(keccak256(&FACTORY_CODE), FACTORY_CODE_HASH); + assert_eq!(Bytecode::new_legacy(FACTORY_CODE).hash_slow(), FACTORY_CODE_HASH); + } + + #[test] + fn inserts_factory_when_amsterdam_active() { + let mut evm = evm_with(CacheDB::new(EmptyDB::new())); + + let state = build_factory_predeploy_state(&TestSpec { amsterdam: true }, &mut evm) + .unwrap() + .expect("factory predeploy state"); + + let account = state.get(&FACTORY_ADDRESS).expect("factory account"); + assert_eq!(account.info.code_hash, FACTORY_CODE_HASH); + assert_eq!(account.info.code.as_ref().unwrap().original_bytes(), FACTORY_CODE); + assert!(account.is_touched()); + assert_eq!(state.len(), 1); + } + + #[test] + fn idempotent_when_already_deployed() { + let mut db = CacheDB::new(EmptyDB::new()); + // Pretend the factory is already deployed at the target address. + db.insert_account_info( + FACTORY_ADDRESS, + AccountInfo { + code_hash: FACTORY_CODE_HASH, + code: Some(Bytecode::new_legacy(FACTORY_CODE)), + ..Default::default() + }, + ); + let mut evm = evm_with(db); + + assert!(build_factory_predeploy_state(&TestSpec { amsterdam: true }, &mut evm) + .unwrap() + .is_none()); + } + + #[test] + fn commit_records_change_and_preserves_balance() { + use revm::{database::states::bundle_state::BundleRetention, DatabaseCommit}; + + // Account pre-exists with a balance but no code. + let balance = U256::from(7u64); + let mut db = CacheDB::new(EmptyDB::new()); + db.insert_account_info(FACTORY_ADDRESS, AccountInfo::from_balance(balance)); + + // Built inline (not via `evm_with`) so the concrete `State` DB type is preserved and its + // `merge_transitions`/`take_bundle` methods remain callable. + let mut cfg = CfgEnv::default(); + cfg.spec = SpecId::AMSTERDAM; + let env = EvmEnv { + block_env: BlockEnv { timestamp: U256::from(1), ..Default::default() }, + cfg_env: cfg, + }; + let inner = State::builder().with_database(db).with_bundle_update().build(); + let mut evm = EthEvmFactory.create_evm(inner, env); + + let state = build_factory_predeploy_state(&TestSpec { amsterdam: true }, &mut evm) + .unwrap() + .unwrap(); + // Balance is preserved on the pre-existing account. + assert_eq!(state.get(&FACTORY_ADDRESS).unwrap().info.balance, balance); + evm.db_mut().commit(state); + + // Post-state reads back the code and the preserved balance. + let info = evm.db_mut().basic(FACTORY_ADDRESS).unwrap().unwrap(); + assert_eq!(info.code_hash, FACTORY_CODE_HASH); + assert_eq!(info.balance, balance); + + // The bundle records the transition from (balance, no code) to (balance, factory code), + // i.e. the original is tracked correctly rather than the post-insertion state. + evm.db_mut().merge_transitions(BundleRetention::Reverts); + let bundle = evm.db_mut().take_bundle(); + let acct = bundle.account(&FACTORY_ADDRESS).expect("bundle account"); + assert_eq!(acct.info.as_ref().unwrap().code_hash, FACTORY_CODE_HASH); + let original = acct.original_info.as_ref().expect("original info"); + assert_eq!(original.balance, balance); + assert!(original.is_empty_code_hash(), "original should have no code"); + } + + #[test] + fn noop_before_amsterdam() { + let mut evm = evm_with(CacheDB::new(EmptyDB::new())); + + assert!(build_factory_predeploy_state(&TestSpec { amsterdam: false }, &mut evm) + .unwrap() + .is_none()); + } +} diff --git a/crates/evm/src/block/system_calls/eip8282.rs b/crates/evm/src/block/system_calls/eip8282.rs new file mode 100644 index 00000000..8b2b8b21 --- /dev/null +++ b/crates/evm/src/block/system_calls/eip8282.rs @@ -0,0 +1,125 @@ +//! [EIP-8282](https://eips.ethereum.org/EIPS/eip-8282) builder execution requests system calls. +//! +//! EIP-8282 introduces two new EIP-7685 request types, processed by post-block system calls to +//! two predeploys, starting at the Amsterdam hardfork: +//! +//! * **Builder deposit requests** (request type `0x03`) +//! * **Builder exit requests** (request type `0x04`) +//! +//! Each predeploy maintains a FIFO request queue and an "excess" counter in storage slot 0 (the +//! same design as EIP-7002/7251). At the end of every block from Amsterdam onward the contract is +//! invoked as `SYSTEM_ADDRESS` with empty calldata, which drains the queue (returning the encoded +//! requests) and, on first invocation, initializes the excess counter from the +//! `EXCESS_INHIBITOR` sentinel (`2^256 - 1`) to `0`. + +use crate::{ + block::{BlockExecutionError, BlockValidationError}, + Evm, +}; +use alloc::format; +use alloy_eips::eip7002::SYSTEM_ADDRESS; +use alloy_primitives::{address, Address, Bytes}; +use core::fmt::Debug; +use revm::context_interface::result::{ExecutionResult, ResultAndState}; + +/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for EIP-8282 builder +/// deposit requests. +pub const BUILDER_DEPOSIT_REQUEST_TYPE: u8 = 0x03; + +/// The [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) request type for EIP-8282 builder exit +/// requests. +pub const BUILDER_EXIT_REQUEST_TYPE: u8 = 0x04; + +/// The address of the EIP-8282 builder deposit requests predeploy. +pub const BUILDER_DEPOSIT_REQUEST_PREDEPLOY_ADDRESS: Address = + address!("0x0000BFF46984E3725691FA540A8C7589300D8282"); + +/// The address of the EIP-8282 builder exit requests predeploy. +pub const BUILDER_EXIT_REQUEST_PREDEPLOY_ADDRESS: Address = + address!("0x000064D678505AD48F8CCB093BC65613800E8282"); + +/// Applies the post-block call to the EIP-8282 builder deposit requests contract. +/// +/// Note: this does not commit the state changes to the database, it only transacts the call. +#[inline] +pub(crate) fn transact_builder_deposit_requests_contract_call( + evm: &mut impl Evm, +) -> Result, BlockExecutionError> { + // At the end of processing any execution block where Amsterdam is active, call the builder + // deposit requests contract as `SYSTEM_ADDRESS` with empty calldata. + match evm.transact_system_call( + SYSTEM_ADDRESS, + BUILDER_DEPOSIT_REQUEST_PREDEPLOY_ADDRESS, + Bytes::new(), + ) { + Ok(res) => Ok(res), + Err(e) => Err(BlockValidationError::BuilderDepositRequestsContractCall { + message: format!("execution failed: {e}"), + } + .into()), + } +} + +/// Applies the post-block call to the EIP-8282 builder exit requests contract. +/// +/// Note: this does not commit the state changes to the database, it only transacts the call. +#[inline] +pub(crate) fn transact_builder_exit_requests_contract_call( + evm: &mut impl Evm, +) -> Result, BlockExecutionError> { + match evm.transact_system_call( + SYSTEM_ADDRESS, + BUILDER_EXIT_REQUEST_PREDEPLOY_ADDRESS, + Bytes::new(), + ) { + Ok(res) => Ok(res), + Err(e) => Err(BlockValidationError::BuilderExitRequestsContractCall { + message: format!("execution failed: {e}"), + } + .into()), + } +} + +/// Extracts the builder deposit requests from the execution output. +#[inline] +pub(crate) fn deposit_post_commit( + result: ExecutionResult, +) -> Result { + match result { + ExecutionResult::Success { output, .. } => Ok(output.into_data()), + ExecutionResult::Revert { output, .. } => { + Err(BlockValidationError::BuilderDepositRequestsContractCall { + message: format!("execution reverted: {output}"), + } + .into()) + } + ExecutionResult::Halt { reason, .. } => { + Err(BlockValidationError::BuilderDepositRequestsContractCall { + message: format!("execution halted: {reason:?}"), + } + .into()) + } + } +} + +/// Extracts the builder exit requests from the execution output. +#[inline] +pub(crate) fn exit_post_commit( + result: ExecutionResult, +) -> Result { + match result { + ExecutionResult::Success { output, .. } => Ok(output.into_data()), + ExecutionResult::Revert { output, .. } => { + Err(BlockValidationError::BuilderExitRequestsContractCall { + message: format!("execution reverted: {output}"), + } + .into()) + } + ExecutionResult::Halt { reason, .. } => { + Err(BlockValidationError::BuilderExitRequestsContractCall { + message: format!("execution halted: {reason:?}"), + } + .into()) + } + } +} diff --git a/crates/evm/src/block/system_calls/mod.rs b/crates/evm/src/block/system_calls/mod.rs index 6d714642..89b9bda3 100644 --- a/crates/evm/src/block/system_calls/mod.rs +++ b/crates/evm/src/block/system_calls/mod.rs @@ -7,12 +7,20 @@ use alloy_eips::{ }; use alloy_hardforks::EthereumHardforks; use alloy_primitives::{Bytes, B256}; -use revm::DatabaseCommit; +use revm::{context::Block, DatabaseCommit}; mod eip2935; mod eip4788; mod eip7002; mod eip7251; +mod eip7997; +mod eip8282; + +pub use eip7997::{FACTORY_ADDRESS, FACTORY_CODE, FACTORY_CODE_HASH}; +pub use eip8282::{ + BUILDER_DEPOSIT_REQUEST_PREDEPLOY_ADDRESS, BUILDER_DEPOSIT_REQUEST_TYPE, + BUILDER_EXIT_REQUEST_PREDEPLOY_ADDRESS, BUILDER_EXIT_REQUEST_TYPE, +}; /// An ephemeral helper type for executing system calls. /// @@ -42,6 +50,7 @@ where ) -> Result<(), BlockExecutionError> { self.apply_blockhashes_contract_call(header.parent_hash(), evm)?; self.apply_beacon_root_contract_call(header.parent_beacon_block_root(), evm)?; + self.apply_factory_predeploy(evm)?; Ok(()) } @@ -74,6 +83,25 @@ where requests.push_request_with_type(CONSOLIDATION_REQUEST_TYPE, consolidation_requests); } + // Collect all EIP-8282 builder execution requests, introduced in Amsterdam. The system + // calls must run from the Amsterdam activation block onward (they also reset each + // predeploy's excess counter from the `EXCESS_INHIBITOR` sentinel to 0 on first call). + if self.spec.is_amsterdam_active_at_timestamp(evm.block().timestamp().saturating_to()) { + // EIP-8282 builder deposit requests + let builder_deposit_requests = + self.apply_builder_deposit_requests_contract_call(evm)?; + if !builder_deposit_requests.is_empty() { + requests + .push_request_with_type(BUILDER_DEPOSIT_REQUEST_TYPE, builder_deposit_requests); + } + + // EIP-8282 builder exit requests + let builder_exit_requests = self.apply_builder_exit_requests_contract_call(evm)?; + if !builder_exit_requests.is_empty() { + requests.push_request_with_type(BUILDER_EXIT_REQUEST_TYPE, builder_exit_requests); + } + } + Ok(()) } @@ -136,4 +164,47 @@ where eip7251::post_commit(result_and_state.result) } + + /// Applies the EIP-7997 pre-block state transition, inserting the deterministic `CREATE2` + /// factory bytecode at [`FACTORY_ADDRESS`]. + /// + /// This is a no-op unless Amsterdam is active and the factory is not already deployed, so it + /// only mutates state on the block that activates Amsterdam. + pub fn apply_factory_predeploy( + &mut self, + evm: &mut impl Evm, + ) -> Result<(), BlockExecutionError> { + let _span = tracing::debug_span!("eip7997_factory_predeploy").entered(); + if let Some(state) = eip7997::build_factory_predeploy_state(&self.spec, evm)? { + evm.db_mut().commit(state); + } + + Ok(()) + } + + /// Applies the post-block call to the EIP-8282 builder deposit requests contract. + pub fn apply_builder_deposit_requests_contract_call( + &mut self, + evm: &mut impl Evm, + ) -> Result { + let _span = tracing::debug_span!("eip8282_builder_deposit_requests").entered(); + let result_and_state = eip8282::transact_builder_deposit_requests_contract_call(evm)?; + + evm.db_mut().commit(result_and_state.state); + + eip8282::deposit_post_commit(result_and_state.result) + } + + /// Applies the post-block call to the EIP-8282 builder exit requests contract. + pub fn apply_builder_exit_requests_contract_call( + &mut self, + evm: &mut impl Evm, + ) -> Result { + let _span = tracing::debug_span!("eip8282_builder_exit_requests").entered(); + let result_and_state = eip8282::transact_builder_exit_requests_contract_call(evm)?; + + evm.db_mut().commit(result_and_state.state); + + eip8282::exit_post_commit(result_and_state.result) + } }