diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4201561f..7e457f65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,9 @@ name: CI on: push: - branches: [main] + branches: ["**"] pull_request: + branches: ["**"] env: CARGO_TERM_COLOR: always diff --git a/Cargo.toml b/Cargo.toml index 1808686f..aa42087e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,18 +41,19 @@ alloy-op-evm = { version = "0.21.2", path = "crates/op-evm", default-features = # alloy alloy-eip2124 = { version = "0.2", default-features = false } alloy-chains = { version = "0.2.0", default-features = false } -alloy-eips = { version = "1.0.34", default-features = false } -alloy-consensus = { version = "1.0.27", default-features = false } +alloy-eips = { version = "1.0.37", default-features = false } +alloy-consensus = { version = "1.0.37", default-features = false } alloy-primitives = { version = "1.0.0", default-features = false } alloy-sol-types = { version = "1.0.0", default-features = false } -alloy-hardforks = { version = "0.3" } -alloy-rpc-types-eth = { version = "1.0.27", default-features = false } -alloy-rpc-types-engine = { version = "1.0.27", default-features = false } +alloy-hardforks = { version = "0.3.1" } +alloy-rpc-types-eth = { version = "1.0.37", default-features = false } +alloy-rlp = { version = "0.3.12", default-features = false } +alloy-rpc-types-engine = { version = "1.0.37", default-features = false } # op-alloy -alloy-op-hardforks = { version = "0.3" } -op-alloy-consensus = { version = "0.20", default-features = false } -op-alloy-rpc-types-engine = { version = "0.20", default-features = false } +alloy-op-hardforks = { version = "0.3.1" } +op-alloy-consensus = { version = "0.20.0", default-features = false } +op-alloy-rpc-types-engine = { version = "0.20.0", default-features = false } # revm revm = { version = "29.0.0", default-features = false } @@ -64,8 +65,14 @@ derive_more = { version = "2", default-features = false, features = ["full"] } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2.0.0", default-features = false } serde_json = "1" +tracing = { version = "0.1.41", default-features = false } test-case = "3" [patch.crates-io] -# revm = { git = "https://github.com/bluealloy/revm", rev = "11b16259" } -# op-revm = { git = "https://github.com/bluealloy/revm", rev = "11b16259" } +revm = { git = "https://github.com/Soubhik-10/revm", branch = "bal" } +op-revm = { git = "https://github.com/Soubhik-10/revm", branch = "bal" } +alloy-eips = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" } +alloy-consensus = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" } +# alloy-hardforks = { git = "https://github.com/Rimeeeeee/hardforks", branch = "amsterdam" } +alloy-rpc-types-eth = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" } +alloy-rpc-types-engine = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" } diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 4c06e390..5839b486 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -21,17 +21,18 @@ alloy-eips.workspace = true alloy-hardforks.workspace = true alloy-op-hardforks = { workspace = true, optional = true } alloy-rpc-types-eth = { workspace = true, optional = true } +alloy-rlp.workspace = true alloy-rpc-types-engine = { workspace = true, optional = true } op-alloy-rpc-types-engine = { workspace = true, optional = true } -revm.workspace = true +revm = { workspace = true, features = ["glamsterdam"] } op-revm = { workspace = true, optional = true } op-alloy-consensus = { workspace = true, optional = true } auto_impl.workspace = true derive_more.workspace = true thiserror.workspace = true - +tracing.workspace = true [dev-dependencies] alloy-primitives = { workspace = true, features = ["serde"] } serde_json.workspace = true @@ -39,21 +40,20 @@ test-case.workspace = true [features] default = ["std"] -secp256k1 = [ - "std", - "alloy-consensus/secp256k1", -] +secp256k1 = ["std", "alloy-consensus/secp256k1"] std = [ - "alloy-primitives/std", - "revm/std", - "alloy-consensus/std", - "alloy-eips/std", - "alloy-sol-types/std", - "derive_more/std", - "op-revm?/std", - "thiserror/std", - "op-alloy-consensus?/std", - "alloy-rpc-types-eth?/std", + "alloy-primitives/std", + "revm/std", + "alloy-consensus/std", + "alloy-eips/std", + "alloy-sol-types/std", + "derive_more/std", + "op-revm?/std", + "thiserror/std", + "op-alloy-consensus?/std", + "alloy-rpc-types-eth?/std", + "alloy-rlp/std", + "tracing/std", "alloy-rpc-types-engine?/std", "op-alloy-rpc-types-engine?/std", ] diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index c0571e59..caa31c5e 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -2,7 +2,7 @@ use crate::{Database, Evm, EvmFactory, FromRecoveredTx, FromTxWithEncoded, RecoveredTx, ToTxEnv}; use alloc::{boxed::Box, vec::Vec}; -use alloy_eips::eip7685::Requests; +use alloy_eips::{eip7685::Requests, eip7928::BlockAccessList}; use revm::{ context::result::{ExecutionResult, ResultAndState}, database::State, @@ -32,6 +32,8 @@ pub struct BlockExecutionResult { pub requests: Requests, /// The total gas used by the block. pub gas_used: u64, + /// block-level access list + pub block_access_list: Option, } /// Helper trait to encapsulate requirements for a type to be used as input for [`BlockExecutor`]. diff --git a/crates/evm/src/block/state_changes.rs b/crates/evm/src/block/state_changes.rs index 9fa67dfa..73f57cb1 100644 --- a/crates/evm/src/block/state_changes.rs +++ b/crates/evm/src/block/state_changes.rs @@ -38,6 +38,7 @@ where // Ommer rewards for ommer in ommers { *balance_increments.entry(ommer.beneficiary()).or_default() += calc::ommer_reward( + //bal base_block_reward, block_env.number.saturating_to(), ommer.number(), @@ -45,6 +46,11 @@ where } // Full block reward + tracing::debug!( + "Adding block reward of {} to beneficiary {:?}", + calc::block_reward(base_block_reward, ommers.len()), + block_env.beneficiary + ); *balance_increments.entry(block_env.beneficiary).or_default() += calc::block_reward(base_block_reward, ommers.len()); } @@ -131,6 +137,7 @@ where storage: Default::default(), status: AccountStatus::Touched, transaction_id: 0, + ..Default::default() }, )) }; diff --git a/crates/evm/src/block/system_calls/mod.rs b/crates/evm/src/block/system_calls/mod.rs index 0f63628d..d134d39a 100644 --- a/crates/evm/src/block/system_calls/mod.rs +++ b/crates/evm/src/block/system_calls/mod.rs @@ -4,13 +4,17 @@ use crate::{ block::{BlockExecutionError, OnStateHook}, Evm, }; -use alloc::{borrow::Cow, boxed::Box}; +use alloc::{borrow::Cow, boxed::Box, vec::Vec}; use alloy_consensus::BlockHeader; use alloy_eips::{ - eip7002::WITHDRAWAL_REQUEST_TYPE, eip7251::CONSOLIDATION_REQUEST_TYPE, eip7685::Requests, + eip2935::{HISTORY_SERVE_WINDOW, HISTORY_STORAGE_ADDRESS}, + eip7002::WITHDRAWAL_REQUEST_TYPE, + eip7251::CONSOLIDATION_REQUEST_TYPE, + eip7685::Requests, + eip7928::{AccountChanges, SlotChanges, StorageChange}, }; use alloy_hardforks::EthereumHardforks; -use alloy_primitives::{Bytes, B256}; +use alloy_primitives::{Bytes, B256, U256}; use revm::{state::EvmState, DatabaseCommit}; use super::{StateChangePostBlockSource, StateChangePreBlockSource, StateChangeSource}; @@ -88,7 +92,7 @@ where &mut self, parent_block_hash: B256, evm: &mut impl Evm, - ) -> Result<(), BlockExecutionError> { + ) -> Result, BlockExecutionError> { let result_and_state = eip2935::transact_blockhashes_contract_call(&self.spec, parent_block_hash, evm)?; @@ -100,9 +104,23 @@ where ); } evm.db_mut().commit(res.state); + + if self.spec.is_amsterdam_active_at_timestamp(evm.block().timestamp.saturating_to()) { + let block_num: u64 = evm.block().number.saturating_to(); + let slot_change = SlotChanges::default() + .with_change(StorageChange { + block_access_index: 0, + new_value: parent_block_hash, + }) + .with_slot(U256::from((block_num - 1) % HISTORY_SERVE_WINDOW as u64).into()); + let acc_changes = AccountChanges::default() + .with_storage_change(slot_change) + .with_address(HISTORY_STORAGE_ADDRESS); + return Ok(Some(acc_changes)); + } } - Ok(()) + Ok(None) } /// Applies the pre-block call to the EIP-4788 beacon root contract. @@ -110,7 +128,7 @@ where &mut self, parent_beacon_block_root: Option, evm: &mut impl Evm, - ) -> Result<(), BlockExecutionError> { + ) -> Result>, BlockExecutionError> { let result_and_state = eip4788::transact_beacon_root_contract_call(&self.spec, parent_beacon_block_root, evm)?; @@ -122,9 +140,33 @@ where ); } evm.db_mut().commit(res.state); + if self.spec.is_amsterdam_active_at_timestamp(evm.block().timestamp.saturating_to()) { + let timestamp: u64 = evm.block().timestamp.saturating_to(); + let slot_changes: Vec = alloc::vec![ + SlotChanges::default() + .with_change(StorageChange { + block_access_index: 0, + new_value: U256::from(timestamp).into(), + }) + .with_slot(U256::from(timestamp % HISTORY_SERVE_WINDOW as u64).into()), + SlotChanges::default() + .with_change(StorageChange { + block_access_index: 0, + new_value: parent_beacon_block_root.unwrap(), + }) + .with_slot( + U256::from( + (timestamp % HISTORY_SERVE_WINDOW as u64) + + HISTORY_SERVE_WINDOW as u64, + ) + .into(), + ), + ]; + + return Ok(Some(slot_changes)); + } } - - Ok(()) + Ok(None) } /// Applies the post-block call to the EIP-7002 withdrawal request contract. diff --git a/crates/evm/src/eth/block.rs b/crates/evm/src/eth/block.rs index 4a846bd5..2309f232 100644 --- a/crates/evm/src/eth/block.rs +++ b/crates/evm/src/eth/block.rs @@ -1,5 +1,4 @@ //! Ethereum block executor. - use super::{ dao_fork, eip6110, receipt_builder::{AlloyReceiptBuilder, ReceiptBuilder, ReceiptBuilderCtx}, @@ -17,10 +16,25 @@ use crate::{ }; use alloc::{borrow::Cow, boxed::Box, vec::Vec}; use alloy_consensus::{Header, Transaction, TxReceipt}; -use alloy_eips::{eip4895::Withdrawals, eip7685::Requests, Encodable2718}; +use alloy_eips::{ + eip2935::HISTORY_SERVE_WINDOW, + eip4788::BEACON_ROOTS_ADDRESS, + eip4895::Withdrawals, + eip7002::WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + eip7251::CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + eip7685::Requests, + eip7928::{ + balance_change::BalanceChange, AccountChanges, BlockAccessIndex, BlockAccessList, + SlotChanges, StorageChange, + }, + Encodable2718, +}; use alloy_hardforks::EthereumHardfork; -use alloy_primitives::{Log, B256}; -use revm::{context_interface::result::ResultAndState, database::State, DatabaseCommit, Inspector}; +use alloy_primitives::{Address, Log, B256, U256}; +use revm::{ + context::result::HaltReason, context_interface::result::ResultAndState, database::State, + primitives::StorageKey, state::AccountStatus, DatabaseCommit, Inspector, +}; /// Context for Ethereum block execution. #[derive(Debug, Clone)] @@ -54,6 +68,10 @@ pub struct EthBlockExecutor<'a, Evm, Spec, R: ReceiptBuilder> { receipts: Vec, /// Total gas used by transactions in this block. gas_used: u64, + /// Optional store for building bal + pub block_access_list: Option, + /// All touched accounts in the block. + pub touched_addresses: Vec
, } impl<'a, Evm, Spec, R> EthBlockExecutor<'a, Evm, Spec, R> @@ -71,6 +89,8 @@ where system_caller: SystemCaller::new(spec.clone()), spec, receipt_builder, + block_access_list: Some(BlockAccessList::default()), + touched_addresses: Vec::new(), } } } @@ -95,10 +115,113 @@ where self.spec.is_spurious_dragon_active_at_block(self.evm.block().number.saturating_to()); self.evm.db_mut().set_state_clear_flag(state_clear_flag); - self.system_caller.apply_blockhashes_contract_call(self.ctx.parent_hash, &mut self.evm)?; - self.system_caller - .apply_beacon_root_contract_call(self.ctx.parent_beacon_block_root, &mut self.evm)?; + let timestamp: u64 = self.evm.block().timestamp.saturating_to(); + if self.spec.is_amsterdam_active_at_timestamp(timestamp) { + let contract_acc_change = self + .system_caller + .apply_blockhashes_contract_call(self.ctx.parent_hash, &mut self.evm)?; + tracing::debug!("Applied blockhashes contract call, bal {:?}", contract_acc_change); + if contract_acc_change.is_some() { + self.block_access_list.as_mut().unwrap().push(contract_acc_change.clone().unwrap()); + tracing::debug!("Pushed blockhashes contract call, bal {:?}", contract_acc_change); + } + + let pre_beacon = self + .evm + .db_mut() + .database + .storage( + BEACON_ROOTS_ADDRESS, + StorageKey::from(timestamp % HISTORY_SERVE_WINDOW as u64), + ) + .ok(); + + let pre_beacon_root = self + .evm + .db_mut() + .database + .storage( + BEACON_ROOTS_ADDRESS, + StorageKey::from( + (timestamp % HISTORY_SERVE_WINDOW as u64) + HISTORY_SERVE_WINDOW as u64, + ), + ) + .ok(); + + let beacon_contract_acc_change = self.system_caller.apply_beacon_root_contract_call( + self.ctx.parent_beacon_block_root, + &mut self.evm, + )?; + tracing::debug!( + "Applied beacon root contract call, bal {:?}", + beacon_contract_acc_change + ); + if let Some(beacon_contract_acc_changes) = beacon_contract_acc_change { + let mut account_changes = + AccountChanges::default().with_address(BEACON_ROOTS_ADDRESS); + + // slot 0: timestamp % HISTORY_SERVE_WINDOW + if let Some(change) = beacon_contract_acc_changes.first() { + let new_val = change.changes[0].new_value.into(); + if pre_beacon == Some(new_val) { + account_changes + .storage_reads + .push(StorageKey::from(timestamp % HISTORY_SERVE_WINDOW as u64).into()); + } else { + account_changes.storage_changes.push( + SlotChanges::default() + .with_slot( + StorageKey::from(timestamp % HISTORY_SERVE_WINDOW as u64) + .into(), + ) + .with_change(StorageChange { + block_access_index: 0, + new_value: new_val.into(), + }), + ); + } + } + + // slot 1: timestamp % HISTORY_SERVE_WINDOW + HISTORY_SERVE_WINDOW + if let Some(change) = beacon_contract_acc_changes.get(1) { + let new_val = change.changes[0].new_value.into(); + if pre_beacon_root == Some(new_val) { + account_changes.storage_reads.push( + StorageKey::from( + (timestamp % HISTORY_SERVE_WINDOW as u64) + + HISTORY_SERVE_WINDOW as u64, + ) + .into(), + ); + } else { + account_changes.storage_changes.push( + SlotChanges::default() + .with_slot( + StorageKey::from( + (timestamp % HISTORY_SERVE_WINDOW as u64) + + HISTORY_SERVE_WINDOW as u64, + ) + .into(), + ) + .with_change(StorageChange { + block_access_index: 0, + new_value: new_val.into(), + }), + ); + } + } + + self.block_access_list.as_mut().unwrap().push(account_changes); + } + } else { + self.system_caller + .apply_blockhashes_contract_call(self.ctx.parent_hash, &mut self.evm)?; + self.system_caller.apply_beacon_root_contract_call( + self.ctx.parent_beacon_block_root, + &mut self.evm, + )?; + } Ok(()) } @@ -119,10 +242,11 @@ where } // Execute transaction and return the result - self.evm.transact(&tx).map_err(|err| { + let res = self.evm.transact(&tx).map_err(|err| { let hash = tx.tx().trie_hash(); BlockExecutionError::evm(err, hash) - }) + }); + res } fn commit_transaction( @@ -130,12 +254,55 @@ where output: ResultAndState<::HaltReason>, tx: impl ExecutableTx, ) -> Result { - let ResultAndState { result, state } = output; + let ResultAndState { result, mut state } = output; + tracing::debug!("Transaction executed with result: {:?}", result); self.system_caller.on_state(StateChangeSource::Transaction(self.receipts.len()), &state); let gas_used = result.gas_used(); + let mut halt_reason = None; + let mut is_oog = false; + + match &result { + revm::context::result::ExecutionResult::Halt { reason, .. } => { + halt_reason = Some(reason.clone()); + } + _ => {} + }; + + if let Some(halt_reason) = &halt_reason { + if *halt_reason + == ::HaltReason::from(HaltReason::OutOfGas( + revm::context::result::OutOfGasError::Basic, + )) + || *halt_reason + == ::HaltReason::from(HaltReason::OutOfGas( + revm::context::result::OutOfGasError::MemoryLimit, + )) + || *halt_reason + == ::HaltReason::from(HaltReason::OutOfGas( + revm::context::result::OutOfGasError::Memory, + )) + || *halt_reason + == ::HaltReason::from(HaltReason::OutOfGas( + revm::context::result::OutOfGasError::Precompile, + )) + || *halt_reason + == ::HaltReason::from(HaltReason::OutOfGas( + revm::context::result::OutOfGasError::InvalidOperand, + )) + || *halt_reason + == ::HaltReason::from(HaltReason::OutOfGas( + revm::context::result::OutOfGasError::ReentrancySentry, + )) + { + is_oog = true; + } else { + is_oog = false; + } + } + // append gas used self.gas_used += gas_used; @@ -148,15 +315,143 @@ where cumulative_gas_used: self.gas_used, })); - // Commit the state changes. - self.evm.db_mut().commit(state); + if self.spec.is_amsterdam_active_at_timestamp(self.evm.block().timestamp.saturating_to()) { + if let Some(addr) = tx.tx().to() { + let initial_balance = self + .evm + .db_mut() + .database + .basic(addr) + .ok() + .and_then(|acc| acc.map(|a| a.balance)) + .unwrap_or(U256::ZERO); + + if let Some(acc) = state.get(&addr) { + if let Some(bal) = self.block_access_list.as_mut() { + bal.push(crate::eth::utils::from_account_with_tx_index( + addr, + self.receipts.len() as u64, + acc, + initial_balance, + is_oog, + )); + tracing::debug!( + "BlockAccessList: CREATE parent contract {:#x}, tx_index={}, storage: {:#?}", + addr, + self.receipts.len(), + acc.storage_access, + ); + state.get_mut(&addr).unwrap().clear_state_changes(); + } + } + } + if let Some(acc) = state.get(tx.signer()) { + if *tx.signer() != tx.tx().to().unwrap_or_default() { + let initial_balance = self + .evm + .db_mut() + .database + .basic(*tx.signer()) + .ok() + .and_then(|acc| acc.map(|a| a.balance)) + .unwrap_or(U256::ZERO); + + if let Some(bal) = self.block_access_list.as_mut() { + bal.push(crate::eth::utils::from_account_with_tx_index( + *tx.signer(), + self.receipts.len() as u64, + acc, + initial_balance, + is_oog, + )); + tracing::debug!( + "BlockAccessList: Tx signer arm tx_index={}, storage: {:#?}", + self.receipts.len(), + acc.storage_access, + ); + state.get_mut(tx.signer()).unwrap().clear_state_changes(); + } + } + } + + for (address, account) in state.clone().iter() { + // Skip signer and tx.to() + if address == tx.signer() || Some(address) == tx.tx().to().as_ref() { + continue; + } + + let is_coinbase = *address == self.evm.block().beneficiary; + + // Get the initial balance from DB + let initial_balance = self + .evm + .db_mut() + .database + .basic(*address) + .ok() + .and_then(|acc| acc.map(|a| a.balance)) + .unwrap_or(U256::ZERO); + + // Check if address is in the access list + let in_access_list = tx + .tx() + .access_list() + .map(|al| al.flattened().iter().any(|(addr, _)| addr == address)) + .unwrap_or(false); + + // If address is in access list, require it to be touched + // If not in access list, push unconditionally + let should_push = if in_access_list { + account.is_touched() && account.status != AccountStatus::default() + } else { + true + }; + + if is_coinbase && account.balance_change == (U256::ZERO, U256::ZERO) { + continue; + } + + if should_push { + if let Some(bal) = self.block_access_list.as_mut() { + bal.push(crate::eth::utils::from_account_with_tx_index( + *address, + self.receipts.len() as u64, + account, + initial_balance, + is_oog, + )); + } + + tracing::debug!( + "BlockAccessList: {:#x}, tx_index={}, in_access_list={}, touched={}, pushed={}", + address, + self.receipts.len(), + in_access_list, + account.is_touched(), + should_push, + ); + + state.get_mut(address).unwrap().clear_state_changes(); + } + } + + tracing::debug!("######## Block : {:?} #########", self.evm.block()); + } + + // Commit the state changes. + self.evm.db_mut().commit(state.clone()); Ok(gas_used) } fn finish( mut self, ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { + let post_system_tx = self.receipts.len() + 1; + let mut post_system_acc_changes: Vec = Vec::new(); + let mut pre_withdrawal = Vec::new(); + let mut pre_consolidation = Vec::new(); + let requests = if self .spec .is_prague_active_at_timestamp(self.evm.block().timestamp.saturating_to()) @@ -170,8 +465,74 @@ where if !deposit_requests.is_empty() { requests.push_request_with_type(eip6110::DEPOSIT_REQUEST_TYPE, deposit_requests); } - + if self + .spec + .is_amsterdam_active_at_timestamp(self.evm.block().timestamp.saturating_to()) + { + for i in 0..=3 { + let value = self + .evm + .db_mut() + .database + .storage(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, StorageKey::from(i)) + .unwrap(); + pre_withdrawal.push(value); + } + + for i in 0..=3 { + let value = self + .evm + .db_mut() + .database + .storage(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, StorageKey::from(i)) + .unwrap_or_default(); + pre_consolidation.push(value); + } + } requests.extend(self.system_caller.apply_post_execution_changes(&mut self.evm)?); + if self + .spec + .is_amsterdam_active_at_timestamp(self.evm.block().timestamp.saturating_to()) + { + let mut post_withdrawal = Vec::new(); + for i in 0..=3 { + let value = self + .evm + .db_mut() + .database + .storage(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, StorageKey::from(i)) + .unwrap_or_default(); + post_withdrawal.push(value); + } + + let mut post_consolidation = Vec::new(); + for i in 0..=3 { + let value = self + .evm + .db_mut() + .database + .storage(CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, StorageKey::from(i)) + .unwrap(); + post_consolidation.push(value); + } + + post_system_acc_changes.push( + super::utils::build_post_execution_system_contract_account_change( + WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, + pre_withdrawal, + post_withdrawal, + post_system_tx as BlockAccessIndex, + ), + ); + post_system_acc_changes.push( + super::utils::build_post_execution_system_contract_account_change( + CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, + pre_consolidation, + post_consolidation, + post_system_tx as BlockAccessIndex, + ), + ); + } requests } else { Requests::default() @@ -203,6 +564,7 @@ where *balance_increments.entry(dao_fork::DAO_HARDFORK_BENEFICIARY).or_default() += drained_balance; } + // increment balances self.evm .db_mut() @@ -219,9 +581,34 @@ where }) })?; + if self.spec.is_amsterdam_active_at_timestamp(self.evm.block().timestamp.saturating_to()) { + // All post tx balance increments + for address in balance_increments.keys() { + let bal = self.evm.db_mut().database.basic(*address).unwrap().unwrap().balance; + self.block_access_list.as_mut().unwrap().push( + AccountChanges::default().with_address(*address).with_balance_change( + BalanceChange { + block_access_index: post_system_tx as u64, + post_balance: U256::from(bal), + }, + ), + ); + } + + tracing::debug!("Post tx balance increments: {:#?}", balance_increments); + // Add post execution system contract account changes + self.block_access_list.as_mut().unwrap().extend(post_system_acc_changes); + } Ok(( self.evm, - BlockExecutionResult { receipts: self.receipts, requests, gas_used: self.gas_used }, + BlockExecutionResult { + receipts: self.receipts, + requests, + gas_used: self.gas_used, + block_access_list: Some(super::utils::sort_and_remove_duplicates_in_bal( + self.block_access_list.unwrap(), + )), + }, )) } @@ -304,3 +691,163 @@ where EthBlockExecutor::new(evm, ctx, &self.spec, &self.receipt_builder) } } + +#[cfg(test)] +mod tests { + use alloy_consensus::{ + transaction::Recovered, EthereumTxEnvelope, SignableTransaction, TxLegacy, + }; + use alloy_eips::{eip2718::WithEncoded, Encodable2718}; + use alloy_primitives::{address, Signature, TxKind, B256, U256}; + use revm::{ + database::{CacheDB, EmptyDB, State}, + state::AccountInfo, + }; + + use crate::{ + block::{BlockExecutor, BlockExecutorFactory}, + eth::{ + receipt_builder::AlloyReceiptBuilder, spec::EthSpec, EthBlockExecutionCtx, + EthBlockExecutorFactory, + }, + EthEvmFactory, EvmEnv, EvmFactory, + }; + + // #[test] + // fn test_address() { + // let addr = address!("0x1a7d50de1c4dc7d5b696f53b65594f21aa55a826"); + // println!("Address: {:?}", addr); + // let cr_addr = alloy_primitives::Address::create(&addr, 0); + // println!("Created Address: {:?}", cr_addr); + // } + + #[test] + fn test_bal_building() { + let ctx = EthBlockExecutionCtx { + parent_hash: B256::ZERO, + parent_beacon_block_root: Some(B256::ZERO), + ommers: &[], + withdrawals: None, + }; + let legacy1 = TxLegacy { + chain_id: None, + nonce: 0, + gas_price: 1, + gas_limit: 21_000, + to: TxKind::Call(address!("000000000000000000000000000000000000dead")), + value: U256::from(100_000_000_000_u64), + input: Default::default(), + }; + let legacy2 = TxLegacy { + chain_id: None, + nonce: 1, + gas_price: 1, + gas_limit: 21_000, + to: TxKind::Call(address!("000000000000000000000000000000000000dead")), + value: U256::from(100_000_000_000_u64), + input: Default::default(), + }; + let sender = address!("000000000000000000000000000000000000beef"); + + let factory = EthBlockExecutorFactory::new( + AlloyReceiptBuilder::default(), + EthSpec::mainnet(), + EthEvmFactory::default(), + ); + let mut db = State::builder().with_database(CacheDB::::default()).build(); + db.database.insert_account_info( + sender, + AccountInfo { + balance: U256::from(50_000_000_000_000_000_u64), + nonce: 0, + code_hash: B256::ZERO, + ..Default::default() + }, + ); + db.database.insert_account_info( + address!("000000000000000000000000000000000000dead"), + AccountInfo { + balance: U256::from(150_000_000_000_000_000_u64), + nonce: 0, + code_hash: B256::ZERO, + ..Default::default() + }, + ); + let evm = factory.evm_factory.create_evm(&mut db, EvmEnv::default()); + let executor = factory.create_executor(evm, ctx); + + let sig = Signature::new(U256::from(1), U256::from(2), true); + + // Wrap legacy1 + let tx1 = + Recovered::new_unchecked(EthereumTxEnvelope::Legacy(legacy1.into_signed(sig)), sender); + + // Wrap legacy2 + let tx2 = + Recovered::new_unchecked(EthereumTxEnvelope::Legacy(legacy2.into_signed(sig)), sender); + let tx_with_encoded1 = WithEncoded::new(tx1.encoded_2718().into(), tx1); + let tx_with_encoded2 = WithEncoded::new(tx2.encoded_2718().into(), tx2); + + let _result = executor.execute_block([&tx_with_encoded1, &tx_with_encoded2]).unwrap(); + // println!("{:#?}", _result.block_access_list); + + // [ + // AccountChanges { + // address: 0x0000000000000000000000000000000000000000, + // storage_changes: [], + // storage_reads: [], + // balance_changes: [ + // BalanceChange { + // block_access_index: 3, + // post_balance: 5000000000000000000, + // }, + // ], + // nonce_changes: [], + // code_changes: [], + // }, + // AccountChanges { + // address: 0x000000000000000000000000000000000000beef, + // storage_changes: [], + // storage_reads: [], + // balance_changes: [ + // BalanceChange { + // block_access_index: 1, + // post_balance: 49999899999979000, + // }, + // BalanceChange { + // block_access_index: 2, + // post_balance: 49999799999958000, + // }, + // ], + // nonce_changes: [ + // NonceChange { + // block_access_index: 1, + // new_nonce: 1, + // }, + // NonceChange { + // block_access_index: 2, + // new_nonce: 2, + // }, + // ], + // code_changes: [], + // }, + // AccountChanges { + // address: 0x000000000000000000000000000000000000dead, + // storage_changes: [], + // storage_reads: [], + // balance_changes: [ + // BalanceChange { + // block_access_index: 1, + // post_balance: 150000100000000000, + // }, + // BalanceChange { + // block_access_index: 2, + // post_balance: 150000200000000000, + // }, + // ], + // nonce_changes: [], + // code_changes: [], + // }, + // ] + } +} diff --git a/crates/evm/src/eth/mod.rs b/crates/evm/src/eth/mod.rs index ec788c55..f82a5aee 100644 --- a/crates/evm/src/eth/mod.rs +++ b/crates/evm/src/eth/mod.rs @@ -28,6 +28,7 @@ pub mod dao_fork; pub mod eip6110; pub mod receipt_builder; pub mod spec; +pub mod utils; mod env; pub(crate) mod spec_id; diff --git a/crates/evm/src/eth/spec_id.rs b/crates/evm/src/eth/spec_id.rs index 33c92c2d..8e4922f4 100644 --- a/crates/evm/src/eth/spec_id.rs +++ b/crates/evm/src/eth/spec_id.rs @@ -21,7 +21,9 @@ pub fn spec_by_timestamp_and_block_number( where C: EthereumHardforks, { - if chain_spec.is_osaka_active_at_timestamp(timestamp) { + if chain_spec.is_amsterdam_active_at_timestamp(timestamp) { + SpecId::AMSTERDAM + } else if chain_spec.is_osaka_active_at_timestamp(timestamp) { SpecId::OSAKA } else if chain_spec.is_prague_active_at_timestamp(timestamp) { SpecId::PRAGUE diff --git a/crates/evm/src/eth/utils.rs b/crates/evm/src/eth/utils.rs new file mode 100644 index 00000000..d6a2968d --- /dev/null +++ b/crates/evm/src/eth/utils.rs @@ -0,0 +1,263 @@ +//! Utility functions for Eip-7928 implementation in Amsterdam and later hardforks. + +use alloc::{collections::BTreeMap, vec::Vec}; +use alloy_eips::eip7928::{ + balance_change::BalanceChange, code_change::CodeChange, nonce_change::NonceChange, + AccountChanges, BlockAccessIndex, BlockAccessList, SlotChanges, StorageChange, MAX_CODE_SIZE, + MAX_TXS_PER_BLOCK, +}; +use alloy_primitives::{Address, B256, U256}; +use revm::{ + primitives::{StorageKey, StorageValue}, + state::Account, +}; + +/// An utility function for system contract storage allocation. +pub fn build_post_execution_system_contract_account_change( + address: Address, + pre: Vec, + post: Vec, + tx_index: BlockAccessIndex, +) -> AccountChanges { + let mut account_changes = AccountChanges::new(address); + + for (i, (pre_val, post_val)) in pre.into_iter().zip(post.into_iter()).enumerate() { + let slot = StorageKey::from(i as u64); + + if pre_val != post_val { + let change = StorageChange { block_access_index: tx_index, new_value: post_val.into() }; + account_changes + .storage_changes + .push(SlotChanges::default().with_slot(slot.into()).with_change(change)); + } else { + account_changes.storage_reads.push(slot.into()); + } + } + + account_changes +} + +/// An utility function to build block access list with tx_index. +pub fn from_account_with_tx_index( + address: Address, + block_access_index: u64, + account: &Account, + initial_balance: U256, + _is_oog: bool, +) -> AccountChanges { + tracing::debug!("Account {:?}", account); + let mut account_changes = AccountChanges::default(); + let final_balance = account.info.balance; + for key in &account.storage_access.reads { + tracing::debug!("Storage read at {:#x}: {:#x} ", address, key); + account_changes.storage_reads.push((*key).into()); + } + + // Group writes by slots + let mut slot_map: BTreeMap> = BTreeMap::new(); + + for (slot, (_, post)) in &account.storage_access.writes { + tracing::debug!("Storage write at {:#x}: {:#x} -> {:#x}", address, slot, post); + slot_map + .entry(*slot) + .or_default() + .push(StorageChange { block_access_index, new_value: (*post).into() }); + } + + // Convert slot_map into SlotChanges and push into account_changes + for (slot, changes) in slot_map { + account_changes.storage_changes.push(SlotChanges { slot: slot.into(), changes }); + } + + // Records if only post_balance != pre_balance + let (_, post_balance) = account.balance_change; + tracing::debug!( + "Balance change at {:#x}: initial: {}, final: {}, post: {}", + address, + initial_balance, + final_balance, + post_balance + ); + if initial_balance != post_balance && initial_balance != final_balance { + account_changes.balance_changes.push(BalanceChange { block_access_index, post_balance }); + } + + let (pre_nonce, post_nonce) = account.nonce_change; + if pre_nonce != post_nonce { + account_changes + .nonce_changes + .push(NonceChange { block_access_index, new_nonce: post_nonce }); + } + + let (code, modified) = &account.code_change; + if !code.is_empty() || *modified { + account_changes + .code_changes + .push(CodeChange { block_access_index, new_code: code.clone() }); + } + + account_changes.address = address; + tracing::debug!("Account Status: {:#x} -> {:#?}", address, account.status); + if account.is_selfdestructed() || account.is_selfdestructed_locally() { + tracing::debug!( + "Account {:#x} was self-destructed. reads: {:?}, writes: {:?}", + address, + account_changes.storage_reads, + account_changes.storage_changes + ); + account_changes.nonce_changes.clear(); + account_changes.code_changes.clear(); + + for slot in &account_changes.storage_changes { + account_changes.storage_reads.push(slot.slot); + } + account_changes.storage_changes.clear(); + } + // if is_oog { + // account_changes.storage_reads.clear(); + + // account_changes.storage_changes.clear(); + // } + account_changes +} + +/// Sort block-level access list and removes duplicates entries by merging them together. +pub fn sort_and_remove_duplicates_in_bal(mut bal: BlockAccessList) -> BlockAccessList { + tracing::debug!("Bal before sort: {:#?}", bal); + bal.sort_by_key(|ac| ac.address); + let mut merged: Vec = Vec::new(); + + for account in bal { + if let Some(last) = merged.last_mut() { + if last.address == account.address { + // Same address → extend fields + last.storage_changes.extend(account.storage_changes); + last.storage_reads.extend(account.storage_reads); + last.balance_changes.extend(account.balance_changes); + last.nonce_changes.extend(account.nonce_changes); + last.code_changes.extend(account.code_changes); + continue; + } + } + merged.push(account); + } + tracing::debug!("Bal after sort: {:#?}", merged); + merged +} + +/// Validates a Block Access List against execution constraints. +pub fn validate_block_access_list_against_execution(block_access_list: &BlockAccessList) -> bool { + // 1. Validate structural constraints + for account in block_access_list { + let changed_slots: alloy_primitives::map::HashSet<_> = + account.storage_changes.iter().map(|sc| B256::from(sc.slot)).collect(); + let read_slots: alloy_primitives::map::HashSet<_> = + account.storage_reads.iter().cloned().collect(); + + // A slot should not be in both changes and reads (per EIP-7928) + if !changed_slots.is_disjoint(&read_slots) { + return false; + } + } + + // 2. Validate ordering (addresses should be sorted lexicographically) + let addresses: Vec<_> = block_access_list.iter().map(|account| account.address).collect(); + let mut sorted_addresses = addresses.clone(); + sorted_addresses.sort(); + if addresses != sorted_addresses { + return false; + } + + // 3. Validate all data is within bounds + let max_block_access_index = MAX_TXS_PER_BLOCK + 1; // 0 for pre-exec, 1..MAX_TXS for txs, MAX_TXS+1 for post-exec + for account in block_access_list { + // Validate storage slots are sorted within each account + let storage_slots: Vec<_> = account.storage_changes.iter().map(|sc| sc.slot).collect(); + let mut sorted_storage_slots = storage_slots.clone(); + sorted_storage_slots.sort(); + if storage_slots != sorted_storage_slots { + return false; + } + + // Check storage changes + for slot_changes in &account.storage_changes { + // Check changes are sorted by block_access_index + let indices: Vec<_> = + slot_changes.changes.iter().map(|c| c.block_access_index).collect(); + let mut sorted_indices = indices.clone(); + sorted_indices.sort(); + if indices != sorted_indices { + return false; + } + + for change in &slot_changes.changes { + if change.block_access_index > max_block_access_index.try_into().unwrap() { + return false; + } + } + } + + // Check balance changes are sorted by block_access_index + let balance_indices: Vec<_> = + account.balance_changes.iter().map(|bc| bc.block_access_index).collect(); + let mut sorted_balance_indices = balance_indices.clone(); + sorted_balance_indices.sort(); + if balance_indices != sorted_balance_indices { + return false; + } + + for balance_change in &account.balance_changes { + if balance_change.block_access_index > max_block_access_index.try_into().unwrap() { + return false; + } + } + + // Check nonce changes are sorted by block_access_index + let nonce_indices: Vec<_> = + account.nonce_changes.iter().map(|nc| nc.block_access_index).collect(); + let mut sorted_nonce_indices = nonce_indices.clone(); + sorted_nonce_indices.sort(); + if nonce_indices != sorted_nonce_indices { + return false; + } + + for nonce_change in &account.nonce_changes { + if nonce_change.block_access_index > max_block_access_index.try_into().unwrap() { + return false; + } + } + + // Check code changes are sorted by block_access_index + let code_indices: Vec<_> = + account.code_changes.iter().map(|cc| cc.block_access_index).collect(); + let mut sorted_code_indices = code_indices.clone(); + sorted_code_indices.sort(); + if code_indices != sorted_code_indices { + return false; + } + + for code_change in &account.code_changes { + if code_change.block_access_index > max_block_access_index.try_into().unwrap() { + return false; + } + if code_change.new_code.len() > MAX_CODE_SIZE { + return false; + } + } + } + + true +} + +/// Validate the block access list against an expected block access list +pub fn validate_block_access_list( + block_access_list: &BlockAccessList, + expected_block_access_list: &BlockAccessList, +) -> bool { + if alloy_primitives::keccak256(alloy_rlp::encode(block_access_list)) + != alloy_primitives::keccak256(alloy_rlp::encode(expected_block_access_list)) + { + return false; + } + true +} diff --git a/crates/evm/src/overrides.rs b/crates/evm/src/overrides.rs index ce71c90b..d9d9126a 100644 --- a/crates/evm/src/overrides.rs +++ b/crates/evm/src/overrides.rs @@ -148,6 +148,7 @@ where status: AccountStatus::Touched, storage: Default::default(), transaction_id: 0, + ..Default::default() }; let storage_diff = match (account_override.state, account_override.state_diff) { diff --git a/crates/op-evm/src/block/mod.rs b/crates/op-evm/src/block/mod.rs index 88073855..4828208b 100644 --- a/crates/op-evm/src/block/mod.rs +++ b/crates/op-evm/src/block/mod.rs @@ -239,6 +239,7 @@ where receipts: self.receipts, requests: Default::default(), gas_used, + block_access_list: None, }, )) }