diff --git a/Cargo.lock b/Cargo.lock index 1d9f74a207d5f..75cb59078527f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7368,6 +7368,8 @@ dependencies = [ "parking_lot 0.12.3", "reth-chainspec", "reth-consensus", + "reth-consensus-common", + "reth-ethereum-forks", "reth-execution-errors", "reth-execution-types", "reth-metrics", diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 6081eae420cc0..6c16973b28b3b 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -14,6 +14,7 @@ workspace = true # reth reth-chainspec.workspace = true reth-consensus.workspace = true +reth-consensus-common.workspace = true reth-execution-errors.workspace = true reth-execution-types.workspace = true reth-metrics = { workspace = true, optional = true } @@ -37,6 +38,7 @@ parking_lot = { workspace = true, optional = true } [dev-dependencies] parking_lot.workspace = true +reth-ethereum-forks.workspace = true [features] default = ["std"] diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 66026a07c94e6..b75feea83a15b 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -31,6 +31,7 @@ pub mod execute; pub mod metrics; pub mod noop; pub mod provider; +pub mod state_change; pub mod system_calls; #[cfg(any(test, feature = "test-utils"))] diff --git a/crates/evm/src/state_change.rs b/crates/evm/src/state_change.rs new file mode 100644 index 0000000000000..afe92561bcd30 --- /dev/null +++ b/crates/evm/src/state_change.rs @@ -0,0 +1,315 @@ +use alloy_primitives::{map::HashMap, Address, U256}; +use reth_chainspec::EthereumHardforks; +use reth_consensus_common::calc; +use reth_primitives::{Block, Withdrawal, Withdrawals}; + +/// Collect all balance changes at the end of the block. +/// +/// Balance changes might include the block reward, uncle rewards, withdrawals, or irregular +/// state changes (DAO fork). +#[inline] +pub fn post_block_balance_increments( + chain_spec: &ChainSpec, + block: &Block, + total_difficulty: U256, +) -> HashMap { + let mut balance_increments = HashMap::default(); + + // Add block rewards if they are enabled. + if let Some(base_block_reward) = + calc::base_block_reward(chain_spec, block.number, block.difficulty, total_difficulty) + { + // Ommer rewards + for ommer in &block.body.ommers { + *balance_increments.entry(ommer.beneficiary).or_default() += + calc::ommer_reward(base_block_reward, block.number, ommer.number); + } + + // Full block reward + *balance_increments.entry(block.beneficiary).or_default() += + calc::block_reward(base_block_reward, block.body.ommers.len()); + } + + // process withdrawals + insert_post_block_withdrawals_balance_increments( + chain_spec, + block.timestamp, + block.body.withdrawals.as_ref().map(Withdrawals::as_ref), + &mut balance_increments, + ); + + balance_increments +} + +/// Returns a map of addresses to their balance increments if the Shanghai hardfork is active at the +/// given timestamp. +/// +/// Zero-valued withdrawals are filtered out. +#[inline] +pub fn post_block_withdrawals_balance_increments( + chain_spec: &ChainSpec, + block_timestamp: u64, + withdrawals: &[Withdrawal], +) -> HashMap { + let mut balance_increments = + HashMap::with_capacity_and_hasher(withdrawals.len(), Default::default()); + insert_post_block_withdrawals_balance_increments( + chain_spec, + block_timestamp, + Some(withdrawals), + &mut balance_increments, + ); + balance_increments +} + +/// Applies all withdrawal balance increments if shanghai is active at the given timestamp to the +/// given `balance_increments` map. +/// +/// Zero-valued withdrawals are filtered out. +#[inline] +pub fn insert_post_block_withdrawals_balance_increments( + chain_spec: &ChainSpec, + block_timestamp: u64, + withdrawals: Option<&[Withdrawal]>, + balance_increments: &mut HashMap, +) { + // Process withdrawals + if chain_spec.is_shanghai_active_at_timestamp(block_timestamp) { + if let Some(withdrawals) = withdrawals { + for withdrawal in withdrawals { + if withdrawal.amount > 0 { + *balance_increments.entry(withdrawal.address).or_default() += + withdrawal.amount_wei().to::(); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_chainspec::ChainSpec; + use reth_ethereum_forks::{ChainHardforks, EthereumHardfork, ForkCondition}; + use reth_primitives::constants::GWEI_TO_WEI; + + /// Tests that the function correctly inserts balance increments when the Shanghai hardfork is + /// active and there are withdrawals. + #[test] + fn test_insert_post_block_withdrawals_balance_increments_shanghai_active_with_withdrawals() { + // Arrange + // Create a ChainSpec with the Shanghai hardfork active at timestamp 100 + let chain_spec = ChainSpec { + hardforks: ChainHardforks::new(vec![( + Box::new(EthereumHardfork::Shanghai), + ForkCondition::Timestamp(100), + )]), + ..Default::default() + }; + + // Define the block timestamp and withdrawals + let block_timestamp = 1000; + let withdrawals = vec![ + Withdrawal { + address: Address::from([1; 20]), + amount: 1000, + index: 45, + validator_index: 12, + }, + Withdrawal { + address: Address::from([2; 20]), + amount: 500, + index: 412, + validator_index: 123, + }, + ]; + + // Create an empty HashMap to hold the balance increments + let mut balance_increments = HashMap::default(); + + // Act + // Call the function with the prepared inputs + insert_post_block_withdrawals_balance_increments( + &chain_spec, + block_timestamp, + Some(&withdrawals), + &mut balance_increments, + ); + + // Assert + // Verify that the balance increments map has the correct number of entries + assert_eq!(balance_increments.len(), 2); + // Verify that the balance increments map contains the correct values for each address + assert_eq!( + *balance_increments.get(&Address::from([1; 20])).unwrap(), + (1000 * GWEI_TO_WEI).into() + ); + assert_eq!( + *balance_increments.get(&Address::from([2; 20])).unwrap(), + (500 * GWEI_TO_WEI).into() + ); + } + + /// Tests that the function correctly handles the case when Shanghai is active but there are no + /// withdrawals. + #[test] + fn test_insert_post_block_withdrawals_balance_increments_shanghai_active_no_withdrawals() { + // Arrange + // Create a ChainSpec with the Shanghai hardfork active + let chain_spec = ChainSpec { + hardforks: ChainHardforks::new(vec![( + Box::new(EthereumHardfork::Shanghai), + ForkCondition::Timestamp(100), + )]), + ..Default::default() + }; + + // Define the block timestamp and an empty list of withdrawals + let block_timestamp = 1000; + let withdrawals = Vec::::new(); + + // Create an empty HashMap to hold the balance increments + let mut balance_increments = HashMap::default(); + + // Act + // Call the function with the prepared inputs + insert_post_block_withdrawals_balance_increments( + &chain_spec, + block_timestamp, + Some(&withdrawals), + &mut balance_increments, + ); + + // Assert + // Verify that the balance increments map is empty + assert!(balance_increments.is_empty()); + } + + /// Tests that the function correctly handles the case when Shanghai is not active even if there + /// are withdrawals. + #[test] + fn test_insert_post_block_withdrawals_balance_increments_shanghai_not_active_with_withdrawals() + { + // Arrange + // Create a ChainSpec without the Shanghai hardfork active + let chain_spec = ChainSpec::default(); // Mock chain spec with Shanghai not active + + // Define the block timestamp and withdrawals + let block_timestamp = 1000; + let withdrawals = vec![ + Withdrawal { + address: Address::from([1; 20]), + amount: 1000, + index: 45, + validator_index: 12, + }, + Withdrawal { + address: Address::from([2; 20]), + amount: 500, + index: 412, + validator_index: 123, + }, + ]; + + // Create an empty HashMap to hold the balance increments + let mut balance_increments = HashMap::default(); + + // Act + // Call the function with the prepared inputs + insert_post_block_withdrawals_balance_increments( + &chain_spec, + block_timestamp, + Some(&withdrawals), + &mut balance_increments, + ); + + // Assert + // Verify that the balance increments map is empty + assert!(balance_increments.is_empty()); + } + + /// Tests that the function correctly handles the case when Shanghai is active but all + /// withdrawals have zero amounts. + #[test] + fn test_insert_post_block_withdrawals_balance_increments_shanghai_active_with_zero_withdrawals() + { + // Arrange + // Create a ChainSpec with the Shanghai hardfork active + let chain_spec = ChainSpec { + hardforks: ChainHardforks::new(vec![( + Box::new(EthereumHardfork::Shanghai), + ForkCondition::Timestamp(100), + )]), + ..Default::default() + }; + + // Define the block timestamp and withdrawals with zero amounts + let block_timestamp = 1000; + let withdrawals = vec![ + Withdrawal { + address: Address::from([1; 20]), + amount: 0, // Zero withdrawal amount + index: 45, + validator_index: 12, + }, + Withdrawal { + address: Address::from([2; 20]), + amount: 0, // Zero withdrawal amount + index: 412, + validator_index: 123, + }, + ]; + + // Create an empty HashMap to hold the balance increments + let mut balance_increments = HashMap::default(); + + // Act + // Call the function with the prepared inputs + insert_post_block_withdrawals_balance_increments( + &chain_spec, + block_timestamp, + Some(&withdrawals), + &mut balance_increments, + ); + + // Assert + // Verify that the balance increments map is empty + assert!(balance_increments.is_empty()); + } + + /// Tests that the function correctly handles the case when Shanghai is active but there are no + /// withdrawals provided. + #[test] + fn test_insert_post_block_withdrawals_balance_increments_shanghai_active_with_empty_withdrawals( + ) { + // Arrange + // Create a ChainSpec with the Shanghai hardfork active + let chain_spec = ChainSpec { + hardforks: ChainHardforks::new(vec![( + Box::new(EthereumHardfork::Shanghai), + ForkCondition::Timestamp(100), + )]), + ..Default::default() + }; + + // Define the block timestamp and no withdrawals + let block_timestamp = 1000; + let withdrawals = None; // No withdrawals provided + + // Create an empty HashMap to hold the balance increments + let mut balance_increments = HashMap::default(); + + // Act + // Call the function with the prepared inputs + insert_post_block_withdrawals_balance_increments( + &chain_spec, + block_timestamp, + withdrawals, + &mut balance_increments, + ); + + // Assert + // Verify that the balance increments map is empty + assert!(balance_increments.is_empty()); + } +}