diff --git a/Cargo.toml b/Cargo.toml index b0b071a4f..8491517e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ rlp = { version = "0.5", default-features = false } primitive-types = { version = "0.10", default-features = false, features = ["rlp"] } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } codec = { package = "parity-scale-codec", version = "2.0", default-features = false, features = ["derive"], optional = true } -ethereum = { version = ">= 0.8, <= 0.9", default-features = false } +ethereum = { version = "~0.8", default-features = false } environmental = { version = "1.1.2", default-features = false, optional = true } [dev-dependencies] diff --git a/benches/loop.rs b/benches/loop.rs index d2bd767a2..abdadf776 100644 --- a/benches/loop.rs +++ b/benches/loop.rs @@ -53,6 +53,7 @@ fn run_loop_contract() { .unwrap(), // hex::decode("0f14a4060000000000000000000000000000000000000000000000000000000000002ee0").unwrap(), u64::MAX, + Vec::new(), ); } diff --git a/core/src/memory.rs b/core/src/memory.rs index 5f435cdf2..7bb09629b 100644 --- a/core/src/memory.rs +++ b/core/src/memory.rs @@ -141,6 +141,15 @@ impl Memory { len: U256, data: &[u8], ) -> Result<(), ExitFatal> { + // Needed to pass ethereum test defined in + // https://github.com/ethereum/tests/commit/17f7e7a6c64bb878c1b6af9dc8371b46c133e46d + // (regardless of other inputs, a zero-length copy is defined to be a no-op). + // TODO: refactor `set` and `copy_large` (see + // https://github.com/rust-blockchain/evm/pull/40#discussion_r677180794) + if len.is_zero() { + return Ok(()); + } + let memory_offset = if memory_offset > U256::from(usize::MAX) { return Err(ExitFatal::NotSupported); } else { diff --git a/gasometer/src/costs.rs b/gasometer/src/costs.rs index e5691c67f..9ac1702a4 100644 --- a/gasometer/src/costs.rs +++ b/gasometer/src/costs.rs @@ -39,10 +39,18 @@ pub fn sstore_refund(original: H256, current: H256, new: H256, config: &Config) } if original == new { + let (gas_sstore_reset, gas_sload) = if config.increase_state_access_gas { + ( + config.gas_sstore_reset - config.gas_sload_cold, + config.gas_storage_read_warm, + ) + } else { + (config.gas_sstore_reset, config.gas_sload) + }; if original == H256::default() { - refund += (config.gas_sstore_set - config.gas_sload) as i64; + refund += (config.gas_sstore_set - gas_sload) as i64; } else { - refund += (config.gas_sstore_reset - config.gas_sload) as i64; + refund += (gas_sstore_reset - gas_sload) as i64; } } @@ -122,11 +130,10 @@ pub fn verylowcopy_cost(len: U256) -> Result { Ok(gas.as_u64()) } -pub fn extcodecopy_cost(len: U256, config: &Config) -> Result { +pub fn extcodecopy_cost(len: U256, is_cold: bool, config: &Config) -> Result { let wordd = len / U256::from(32); let wordr = len % U256::from(32); - - let gas = U256::from(config.gas_ext_code) + let gas = U256::from(address_access_cost(is_cold, config.gas_ext_code, config)) .checked_add( U256::from(G_COPY) .checked_mul(if wordr == U256::zero() { @@ -186,39 +193,71 @@ pub fn sha3_cost(len: U256) -> Result { Ok(gas.as_u64()) } +pub fn sload_cost(is_cold: bool, config: &Config) -> u64 { + if config.increase_state_access_gas { + if is_cold { + config.gas_sload_cold + } else { + config.gas_storage_read_warm + } + } else { + config.gas_sload + } +} + +#[allow(clippy::collapsible_else_if)] pub fn sstore_cost( original: H256, current: H256, new: H256, gas: u64, + is_cold: bool, config: &Config, ) -> Result { - if config.sstore_gas_metering { - if config.sstore_revert_under_stipend && gas < config.call_stipend { + let (gas_sload, gas_sstore_reset) = if config.increase_state_access_gas { + ( + config.gas_storage_read_warm, + config.gas_sstore_reset - config.gas_sload_cold, + ) + } else { + (config.gas_sload, config.gas_sstore_reset) + }; + let gas_cost = if config.sstore_gas_metering { + if config.sstore_revert_under_stipend && gas <= config.call_stipend { return Err(ExitError::OutOfGas); } - Ok(if new == current { - config.gas_sload - } else if original == current { - if original == H256::zero() { - config.gas_sstore_set + if new == current { + gas_sload + } else { + if original == current { + if original == H256::zero() { + config.gas_sstore_set + } else { + gas_sstore_reset + } } else { - config.gas_sstore_reset + gas_sload } - } else { - config.gas_sload - }) + } } else { - Ok(if current == H256::zero() && new != H256::zero() { + if current == H256::zero() && new != H256::zero() { config.gas_sstore_set } else { - config.gas_sstore_reset - }) - } + gas_sstore_reset + } + }; + Ok( + // In EIP-2929 we charge extra if the slot has not been used yet in this transaction + if is_cold { + gas_cost + config.gas_sload_cold + } else { + gas_cost + }, + ) } -pub fn suicide_cost(value: U256, target_exists: bool, config: &Config) -> u64 { +pub fn suicide_cost(value: U256, is_cold: bool, target_exists: bool, config: &Config) -> u64 { let eip161 = !config.empty_considered_exists; let should_charge_topup = if eip161 { value != U256::zero() && !target_exists @@ -232,22 +271,39 @@ pub fn suicide_cost(value: U256, target_exists: bool, config: &Config) -> u64 { 0 }; - config.gas_suicide + suicide_gas_topup + let mut gas = config.gas_suicide + suicide_gas_topup; + if config.increase_state_access_gas && is_cold { + gas += config.gas_account_access_cold + } + gas } pub fn call_cost( value: U256, + is_cold: bool, is_call_or_callcode: bool, is_call_or_staticcall: bool, new_account: bool, config: &Config, ) -> u64 { let transfers_value = value != U256::default(); - config.gas_call + address_access_cost(is_cold, config.gas_call, config) + xfer_cost(is_call_or_callcode, transfers_value) + new_cost(is_call_or_staticcall, new_account, transfers_value, config) } +pub fn address_access_cost(is_cold: bool, regular_value: u64, config: &Config) -> u64 { + if config.increase_state_access_gas { + if is_cold { + config.gas_account_access_cold + } else { + config.gas_storage_read_warm + } + } else { + regular_value + } +} + fn xfer_cost(is_call_or_callcode: bool, transfers_value: bool) -> u64 { if is_call_or_callcode && transfers_value { G_CALLVALUE diff --git a/gasometer/src/lib.rs b/gasometer/src/lib.rs index 36ddc662a..e22511090 100644 --- a/gasometer/src/lib.rs +++ b/gasometer/src/lib.rs @@ -4,6 +4,8 @@ #![forbid(unsafe_code, unused_variables)] #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + #[cfg(feature = "tracing")] pub mod tracing; @@ -25,6 +27,7 @@ mod costs; mod memory; mod utils; +use alloc::vec::Vec; use core::cmp::max; use evm_core::{ExitError, Opcode, Stack}; use evm_runtime::{Config, Handler}; @@ -220,18 +223,26 @@ impl<'config> Gasometer<'config> { TransactionCost::Call { zero_data_len, non_zero_data_len, + access_list_address_len, + access_list_storage_len, } => { self.config.gas_transaction_call + zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + + access_list_address_len as u64 * self.config.gas_access_list_address + + access_list_storage_len as u64 * self.config.gas_access_list_storage_key } TransactionCost::Create { zero_data_len, non_zero_data_len, + access_list_address_len, + access_list_storage_len, } => { self.config.gas_transaction_create + zero_data_len as u64 * self.config.gas_transaction_zero_data + non_zero_data_len as u64 * self.config.gas_transaction_non_zero_data + + access_list_address_len as u64 * self.config.gas_access_list_address + + access_list_storage_len as u64 * self.config.gas_access_list_storage_key } }; @@ -261,27 +272,41 @@ impl<'config> Gasometer<'config> { } /// Calculate the call transaction cost. -pub fn call_transaction_cost(data: &[u8]) -> TransactionCost { +pub fn call_transaction_cost(data: &[u8], access_list: &[(H160, Vec)]) -> TransactionCost { let zero_data_len = data.iter().filter(|v| **v == 0).count(); let non_zero_data_len = data.len() - zero_data_len; + let (access_list_address_len, access_list_storage_len) = count_access_list(access_list); TransactionCost::Call { zero_data_len, non_zero_data_len, + access_list_address_len, + access_list_storage_len, } } /// Calculate the create transaction cost. -pub fn create_transaction_cost(data: &[u8]) -> TransactionCost { +pub fn create_transaction_cost(data: &[u8], access_list: &[(H160, Vec)]) -> TransactionCost { let zero_data_len = data.iter().filter(|v| **v == 0).count(); let non_zero_data_len = data.len() - zero_data_len; + let (access_list_address_len, access_list_storage_len) = count_access_list(access_list); TransactionCost::Create { zero_data_len, non_zero_data_len, + access_list_address_len, + access_list_storage_len, } } +/// Counts the number of addresses and storage keys in the access list +fn count_access_list(access_list: &[(H160, Vec)]) -> (usize, usize) { + let access_list_address_len = access_list.len(); + let access_list_storage_len = access_list.iter().map(|(_, keys)| keys.len()).sum(); + + (access_list_address_len, access_list_storage_len) +} + #[inline] pub fn static_opcode_cost(opcode: Opcode) -> Option { static TABLE: [Option; 256] = { @@ -414,7 +439,8 @@ pub fn dynamic_opcode_cost( is_static: bool, config: &Config, handler: &H, -) -> Result<(GasCost, Option), ExitError> { +) -> Result<(GasCost, StorageTarget, Option), ExitError> { + let mut storage_target = StorageTarget::None; let gas_cost = match opcode { Opcode::RETURN => GasCost::Zero, @@ -432,40 +458,84 @@ pub fn dynamic_opcode_cost( Opcode::SELFBALANCE if config.has_self_balance => GasCost::Low, Opcode::SELFBALANCE => GasCost::Invalid, - Opcode::EXTCODESIZE => GasCost::ExtCodeSize, - Opcode::BALANCE => GasCost::Balance, + Opcode::EXTCODESIZE => { + let target = stack.peek(0)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::ExtCodeSize { + target_is_cold: handler.is_cold(target, None), + } + } + Opcode::BALANCE => { + let target = stack.peek(0)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::Balance { + target_is_cold: handler.is_cold(target, None), + } + } Opcode::BLOCKHASH => GasCost::BlockHash, - Opcode::EXTCODEHASH if config.has_ext_code_hash => GasCost::ExtCodeHash, + Opcode::EXTCODEHASH if config.has_ext_code_hash => { + let target = stack.peek(0)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::ExtCodeHash { + target_is_cold: handler.is_cold(target, None), + } + } Opcode::EXTCODEHASH => GasCost::Invalid, - Opcode::CALLCODE => GasCost::CallCode { - value: U256::from_big_endian(&stack.peek(2)?[..]), - gas: U256::from_big_endian(&stack.peek(0)?[..]), - target_exists: handler.exists(stack.peek(1)?.into()), - }, - Opcode::STATICCALL => GasCost::StaticCall { - gas: U256::from_big_endian(&stack.peek(0)?[..]), - target_exists: handler.exists(stack.peek(1)?.into()), - }, + Opcode::CALLCODE => { + let target = stack.peek(1)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::CallCode { + value: U256::from_big_endian(&stack.peek(2)?[..]), + gas: U256::from_big_endian(&stack.peek(0)?[..]), + target_is_cold: handler.is_cold(target, None), + target_exists: handler.exists(target), + } + } + Opcode::STATICCALL => { + let target = stack.peek(1)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::StaticCall { + gas: U256::from_big_endian(&stack.peek(0)?[..]), + target_is_cold: handler.is_cold(target, None), + target_exists: handler.exists(target), + } + } Opcode::SHA3 => GasCost::Sha3 { len: U256::from_big_endian(&stack.peek(1)?[..]), }, - Opcode::EXTCODECOPY => GasCost::ExtCodeCopy { - len: U256::from_big_endian(&stack.peek(3)?[..]), - }, + Opcode::EXTCODECOPY => { + let target = stack.peek(0)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::ExtCodeCopy { + target_is_cold: handler.is_cold(target, None), + len: U256::from_big_endian(&stack.peek(3)?[..]), + } + } Opcode::CALLDATACOPY | Opcode::CODECOPY => GasCost::VeryLowCopy { len: U256::from_big_endian(&stack.peek(2)?[..]), }, Opcode::EXP => GasCost::Exp { power: U256::from_big_endian(&stack.peek(1)?[..]), }, - Opcode::SLOAD => GasCost::SLoad, + Opcode::SLOAD => { + let index = stack.peek(0)?; + storage_target = StorageTarget::Slot(address, index); + GasCost::SLoad { + target_is_cold: handler.is_cold(address, Some(index)), + } + } - Opcode::DELEGATECALL if config.has_delegate_call => GasCost::DelegateCall { - gas: U256::from_big_endian(&stack.peek(0)?[..]), - target_exists: handler.exists(stack.peek(1)?.into()), - }, + Opcode::DELEGATECALL if config.has_delegate_call => { + let target = stack.peek(1)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::DelegateCall { + gas: U256::from_big_endian(&stack.peek(0)?[..]), + target_is_cold: handler.is_cold(target, None), + target_exists: handler.exists(target), + } + } Opcode::DELEGATECALL => GasCost::Invalid, Opcode::RETURNDATASIZE if config.has_return_data => GasCost::Base, @@ -477,11 +547,13 @@ pub fn dynamic_opcode_cost( Opcode::SSTORE if !is_static => { let index = stack.peek(0)?; let value = stack.peek(1)?; + storage_target = StorageTarget::Slot(address, index); GasCost::SStore { original: handler.original_storage(address, index), current: handler.storage(address, index), new: value, + target_is_cold: handler.is_cold(address, Some(index)), } } Opcode::LOG0 if !is_static => GasCost::Log { @@ -508,19 +580,27 @@ pub fn dynamic_opcode_cost( Opcode::CREATE2 if !is_static && config.has_create2 => GasCost::Create2 { len: U256::from_big_endian(&stack.peek(2)?[..]), }, - Opcode::SUICIDE if !is_static => GasCost::Suicide { - value: handler.balance(address), - target_exists: handler.exists(stack.peek(0)?.into()), - already_removed: handler.deleted(address), - }, + Opcode::SUICIDE if !is_static => { + let target = stack.peek(0)?.into(); + storage_target = StorageTarget::Address(target); + GasCost::Suicide { + value: handler.balance(address), + target_is_cold: handler.is_cold(target, None), + target_exists: handler.exists(target), + already_removed: handler.deleted(address), + } + } Opcode::CALL if !is_static || (is_static && U256::from_big_endian(&stack.peek(2)?[..]) == U256::zero()) => { + let target = stack.peek(1)?.into(); + storage_target = StorageTarget::Address(target); GasCost::Call { value: U256::from_big_endian(&stack.peek(2)?[..]), gas: U256::from_big_endian(&stack.peek(0)?[..]), - target_exists: handler.exists(stack.peek(1)?.into()), + target_is_cold: handler.is_cold(target, None), + target_exists: handler.exists(target), } } @@ -590,7 +670,7 @@ pub fn dynamic_opcode_cost( _ => None, }; - Ok((gas_cost, memory_cost)) + Ok((gas_cost, storage_target, memory_cost)) } /// Holds the gas consumption for a Gasometer instance. @@ -641,40 +721,76 @@ impl<'config> Inner<'config> { Ok(match cost { GasCost::Call { value, + target_is_cold, target_exists, .. - } => costs::call_cost(value, true, true, !target_exists, self.config), + } => costs::call_cost( + value, + target_is_cold, + true, + true, + !target_exists, + self.config, + ), GasCost::CallCode { value, + target_is_cold, target_exists, .. - } => costs::call_cost(value, true, false, !target_exists, self.config), - GasCost::DelegateCall { target_exists, .. } => { - costs::call_cost(U256::zero(), false, false, !target_exists, self.config) - } - GasCost::StaticCall { target_exists, .. } => { - costs::call_cost(U256::zero(), false, true, !target_exists, self.config) - } + } => costs::call_cost( + value, + target_is_cold, + true, + false, + !target_exists, + self.config, + ), + GasCost::DelegateCall { + target_is_cold, + target_exists, + .. + } => costs::call_cost( + U256::zero(), + target_is_cold, + false, + false, + !target_exists, + self.config, + ), + GasCost::StaticCall { + target_is_cold, + target_exists, + .. + } => costs::call_cost( + U256::zero(), + target_is_cold, + false, + true, + !target_exists, + self.config, + ), + GasCost::Suicide { value, + target_is_cold, target_exists, .. - } => costs::suicide_cost(value, target_exists, self.config), + } => costs::suicide_cost(value, target_is_cold, target_exists, self.config), GasCost::SStore { .. } if self.config.estimate => self.config.gas_sstore_set, GasCost::SStore { original, current, new, - } => costs::sstore_cost(original, current, new, gas, self.config)?, + target_is_cold, + } => costs::sstore_cost(original, current, new, gas, target_is_cold, self.config)?, GasCost::Sha3 { len } => costs::sha3_cost(len)?, GasCost::Log { n, len } => costs::log_cost(n, len)?, - GasCost::ExtCodeCopy { len } => costs::extcodecopy_cost(len, self.config)?, GasCost::VeryLowCopy { len } => costs::verylowcopy_cost(len)?, GasCost::Exp { power } => costs::exp_cost(power, self.config)?, GasCost::Create => consts::G_CREATE, GasCost::Create2 { len } => costs::create2_cost(len)?, - GasCost::SLoad => self.config.gas_sload, + GasCost::SLoad { target_is_cold } => costs::sload_cost(target_is_cold, self.config), GasCost::Zero => consts::G_ZERO, GasCost::Base => consts::G_BASE, @@ -682,10 +798,22 @@ impl<'config> Inner<'config> { GasCost::Low => consts::G_LOW, GasCost::Invalid => return Err(ExitError::OutOfGas), - GasCost::ExtCodeSize => self.config.gas_ext_code, - GasCost::Balance => self.config.gas_balance, + GasCost::ExtCodeSize { target_is_cold } => { + costs::address_access_cost(target_is_cold, self.config.gas_ext_code, self.config) + } + GasCost::ExtCodeCopy { + target_is_cold, + len, + } => costs::extcodecopy_cost(len, target_is_cold, self.config)?, + GasCost::Balance { target_is_cold } => { + costs::address_access_cost(target_is_cold, self.config.gas_balance, self.config) + } GasCost::BlockHash => consts::G_BLOCKHASH, - GasCost::ExtCodeHash => self.config.gas_ext_code_hash, + GasCost::ExtCodeHash { target_is_cold } => costs::address_access_cost( + target_is_cold, + self.config.gas_ext_code_hash, + self.config, + ), }) } @@ -697,6 +825,7 @@ impl<'config> Inner<'config> { original, current, new, + .. } => costs::sstore_refund(original, current, new, self.config), GasCost::Suicide { already_removed, .. @@ -721,13 +850,22 @@ pub enum GasCost { Invalid, /// Gas cost for `EXTCODESIZE`. - ExtCodeSize, + ExtCodeSize { + /// True if address has not been previously accessed in this transaction + target_is_cold: bool, + }, /// Gas cost for `BALANCE`. - Balance, + Balance { + /// True if address has not been previously accessed in this transaction + target_is_cold: bool, + }, /// Gas cost for `BLOCKHASH`. BlockHash, /// Gas cost for `EXTBLOCKHASH`. - ExtCodeHash, + ExtCodeHash { + /// True if address has not been previously accessed in this transaction + target_is_cold: bool, + }, /// Gas cost for `CALL`. Call { @@ -735,6 +873,8 @@ pub enum GasCost { value: U256, /// Call gas. gas: U256, + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, /// Whether the target exists. target_exists: bool, }, @@ -744,6 +884,8 @@ pub enum GasCost { value: U256, /// Call gas. gas: U256, + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, /// Whether the target exists. target_exists: bool, }, @@ -751,6 +893,8 @@ pub enum GasCost { DelegateCall { /// Call gas. gas: U256, + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, /// Whether the target exists. target_exists: bool, }, @@ -758,6 +902,8 @@ pub enum GasCost { StaticCall { /// Call gas. gas: U256, + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, /// Whether the target exists. target_exists: bool, }, @@ -765,6 +911,8 @@ pub enum GasCost { Suicide { /// Value. value: U256, + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, /// Whether the target exists. target_exists: bool, /// Whether the target has already been removed. @@ -778,6 +926,8 @@ pub enum GasCost { current: H256, /// New value. new: H256, + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, }, /// Gas cost for `SHA3`. Sha3 { @@ -793,6 +943,8 @@ pub enum GasCost { }, /// Gas cost for `EXTCODECOPY`. ExtCodeCopy { + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, /// Length. len: U256, }, @@ -814,7 +966,21 @@ pub enum GasCost { len: U256, }, /// Gas cost for `SLOAD`. - SLoad, + SLoad { + /// True if target has not been previously accessed in this transaction + target_is_cold: bool, + }, +} + +/// Storage opcode will access. Used for tracking accessed storage (EIP-2929). +#[derive(Debug, Clone, Copy)] +pub enum StorageTarget { + /// No storage access + None, + /// Accessing address + Address(H160), + /// Accessing storage slot within an address + Slot(H160, H256), } /// Memory cost. @@ -835,6 +1001,10 @@ pub enum TransactionCost { zero_data_len: usize, /// Length of non-zeros in transaction data. non_zero_data_len: usize, + /// Number of addresses in transaction access list (see EIP-2930) + access_list_address_len: usize, + /// Total number of storage keys in transaction access list (see EIP-2930) + access_list_storage_len: usize, }, /// Create transaction cost. Create { @@ -842,6 +1012,10 @@ pub enum TransactionCost { zero_data_len: usize, /// Length of non-zeros in transaction data. non_zero_data_len: usize, + /// Number of addresses in transaction access list (see EIP-2930) + access_list_address_len: usize, + /// Total number of storage keys in transaction access list (see EIP-2930) + access_list_storage_len: usize, }, } diff --git a/runtime/src/handler.rs b/runtime/src/handler.rs index 803ef9abd..e7cec46c7 100644 --- a/runtime/src/handler.rs +++ b/runtime/src/handler.rs @@ -62,6 +62,13 @@ pub trait Handler { fn exists(&self, address: H160) -> bool; /// Check whether an address has already been deleted. fn deleted(&self, address: H160) -> bool; + /// Checks if the address or (address, index) pair has been previously accessed + /// (or set in `accessed_addresses` / `accessed_storage_keys` via an access list + /// transaction). + /// References: + /// * https://eips.ethereum.org/EIPS/eip-2929 + /// * https://eips.ethereum.org/EIPS/eip-2930 + fn is_cold(&self, address: H160, index: Option) -> bool; /// Set storage value of address at index. fn set_storage(&mut self, address: H160, index: H256, value: H256) -> Result<(), ExitError>; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7defd5845..753711888 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -177,6 +177,8 @@ pub struct Config { pub gas_balance: u64, /// Gas paid for SLOAD opcode. pub gas_sload: u64, + /// Gas paid for cold SLOAD opcode. + pub gas_sload_cold: u64, /// Gas paid for SUICIDE opcode. pub gas_suicide: u64, /// Gas paid for SUICIDE opcode when it hits a new account. @@ -193,10 +195,20 @@ pub struct Config { pub gas_transaction_zero_data: u64, /// Gas paid for non-zero data in a transaction. pub gas_transaction_non_zero_data: u64, + /// Gas paid per address in transaction access list (see EIP-2930). + pub gas_access_list_address: u64, + /// Gas paid per storage key in transaction access list (see EIP-2930). + pub gas_access_list_storage_key: u64, + /// Gas paid for accessing cold account. + pub gas_account_access_cold: u64, + /// Gas paid for accessing ready storage. + pub gas_storage_read_warm: u64, /// EIP-1283. pub sstore_gas_metering: bool, /// EIP-1706. pub sstore_revert_under_stipend: bool, + /// EIP-2929 + pub increase_state_access_gas: bool, /// Whether to throw out of gas error when /// CALL/CALLCODE/DELEGATECALL requires more than maximum amount /// of gas. @@ -245,6 +257,7 @@ impl Config { gas_ext_code_hash: 20, gas_balance: 20, gas_sload: 50, + gas_sload_cold: 0, gas_sstore_set: 20000, gas_sstore_reset: 5000, refund_sstore_clears: 15000, @@ -256,8 +269,13 @@ impl Config { gas_transaction_call: 21000, gas_transaction_zero_data: 4, gas_transaction_non_zero_data: 68, + gas_access_list_address: 0, + gas_access_list_storage_key: 0, + gas_account_access_cold: 0, + gas_storage_read_warm: 0, sstore_gas_metering: false, sstore_revert_under_stipend: false, + increase_state_access_gas: false, err_on_call_with_more_gas: true, empty_considered_exists: true, create_increase_nonce: false, @@ -286,6 +304,7 @@ impl Config { gas_ext_code_hash: 700, gas_balance: 700, gas_sload: 800, + gas_sload_cold: 0, gas_sstore_set: 20000, gas_sstore_reset: 5000, refund_sstore_clears: 15000, @@ -297,8 +316,60 @@ impl Config { gas_transaction_call: 21000, gas_transaction_zero_data: 4, gas_transaction_non_zero_data: 16, + gas_access_list_address: 0, + gas_access_list_storage_key: 0, + gas_account_access_cold: 0, + gas_storage_read_warm: 0, sstore_gas_metering: true, sstore_revert_under_stipend: true, + increase_state_access_gas: false, + err_on_call_with_more_gas: false, + empty_considered_exists: false, + create_increase_nonce: true, + call_l64_after_gas: true, + stack_limit: 1024, + memory_limit: usize::MAX, + call_stack_limit: 1024, + create_contract_limit: Some(0x6000), + call_stipend: 2300, + has_delegate_call: true, + has_create2: true, + has_revert: true, + has_return_data: true, + has_bitwise_shifting: true, + has_chain_id: true, + has_self_balance: true, + has_ext_code_hash: true, + estimate: false, + } + } + + /// Berlin hard fork configuration. + pub const fn berlin() -> Config { + Config { + gas_ext_code: 0, + gas_ext_code_hash: 0, + gas_balance: 0, + gas_sload: 0, + gas_sload_cold: 2100, + gas_sstore_set: 20000, + gas_sstore_reset: 5000, + refund_sstore_clears: 15000, + gas_suicide: 5000, + gas_suicide_new_account: 25000, + gas_call: 0, + gas_expbyte: 50, + gas_transaction_create: 53000, + gas_transaction_call: 21000, + gas_transaction_zero_data: 4, + gas_transaction_non_zero_data: 16, + gas_access_list_address: 2400, + gas_access_list_storage_key: 1900, + gas_account_access_cold: 2600, + gas_storage_read_warm: 100, + sstore_gas_metering: true, + sstore_revert_under_stipend: true, + increase_state_access_gas: true, err_on_call_with_more_gas: false, empty_considered_exists: false, create_increase_nonce: true, diff --git a/src/executor/mod.rs b/src/executor/mod.rs index fc9b1afde..08a8c8d67 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -6,6 +6,6 @@ mod stack; pub use self::stack::{ - MemoryStackState, PrecompileFn, PrecompileOutput, StackExecutor, StackExitKind, StackState, + MemoryStackState, Precompile, PrecompileOutput, StackExecutor, StackExitKind, StackState, StackSubstateMetadata, }; diff --git a/src/executor/stack/mod.rs b/src/executor/stack/mod.rs index c461efc8c..12f275a3f 100644 --- a/src/executor/stack/mod.rs +++ b/src/executor/stack/mod.rs @@ -2,12 +2,16 @@ mod state; pub use self::state::{MemoryStackState, MemoryStackSubstate, StackState}; -use crate::gasometer::{self, Gasometer}; +use crate::gasometer::{self, Gasometer, StorageTarget}; use crate::{ Capture, Config, Context, CreateScheme, ExitError, ExitReason, ExitSucceed, Handler, Opcode, Runtime, Stack, Transfer, }; -use alloc::{rc::Rc, vec::Vec}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + rc::Rc, + vec::Vec, +}; use core::{cmp::min, convert::Infallible}; use ethereum::Log; use primitive_types::{H160, H256, U256}; @@ -19,18 +23,55 @@ pub enum StackExitKind { Failed, } +#[derive(Default)] +struct Accessed { + accessed_addresses: BTreeSet, + accessed_storage: BTreeSet<(H160, H256)>, +} + +impl Accessed { + fn access_address(&mut self, address: H160) { + self.accessed_addresses.insert(address); + } + + fn access_addresses(&mut self, addresses: I) + where + I: Iterator, + { + for address in addresses { + self.accessed_addresses.insert(address); + } + } + + fn access_storages(&mut self, storages: I) + where + I: Iterator, + { + for storage in storages { + self.accessed_storage.insert((storage.0, storage.1)); + } + } +} + pub struct StackSubstateMetadata<'config> { gasometer: Gasometer<'config>, is_static: bool, depth: Option, + accessed: Option, } impl<'config> StackSubstateMetadata<'config> { pub fn new(gas_limit: u64, config: &'config Config) -> Self { + let accessed = if config.increase_state_access_gas { + Some(Accessed::default()) + } else { + None + }; Self { gasometer: Gasometer::new(gas_limit, config), is_static: false, depth: None, + accessed, } } @@ -39,6 +80,17 @@ impl<'config> StackSubstateMetadata<'config> { self.gasometer .record_refund(other.gasometer.refunded_gas())?; + if let (Some(mut other_accessed), Some(self_accessed)) = + (other.accessed, self.accessed.as_mut()) + { + self_accessed + .accessed_addresses + .append(&mut other_accessed.accessed_addresses); + self_accessed + .accessed_storage + .append(&mut other_accessed.accessed_storage); + } + Ok(()) } @@ -60,6 +112,7 @@ impl<'config> StackSubstateMetadata<'config> { None => Some(0), Some(n) => Some(n + 1), }, + accessed: self.accessed.as_ref().map(|_| Accessed::default()), } } @@ -78,6 +131,36 @@ impl<'config> StackSubstateMetadata<'config> { pub fn depth(&self) -> Option { self.depth } + + fn access_address(&mut self, address: H160) { + if let Some(accessed) = &mut self.accessed { + accessed.access_address(address) + } + } + + fn access_addresses(&mut self, addresses: I) + where + I: Iterator, + { + if let Some(accessed) = &mut self.accessed { + accessed.access_addresses(addresses); + } + } + + fn access_storage(&mut self, address: H160, key: H256) { + if let Some(accessed) = &mut self.accessed { + accessed.accessed_storage.insert((address, key)); + } + } + + fn access_storages(&mut self, storages: I) + where + I: Iterator, + { + if let Some(accessed) = &mut self.accessed { + accessed.access_storages(storages); + } + } } #[derive(Debug, Eq, PartialEq, Clone)] @@ -88,46 +171,32 @@ pub struct PrecompileOutput { pub logs: Vec, } +/// A precompile result. +pub type PrecompileResult = Result; + /// Precompiles function signature. Expected input arguments are: -/// * Address /// * Input /// * Context -pub type PrecompileFn = - fn(H160, &[u8], Option, &Context) -> Option>; +pub type PrecompileFn = fn(&[u8], Option, &Context) -> PrecompileResult; + +/// A map of address keys to precompile function values. +pub type Precompile = BTreeMap; /// Stack-based executor. pub struct StackExecutor<'config, S> { config: &'config Config, - precompile: PrecompileFn, + precompile: Precompile, state: S, } -fn no_precompile( - _address: H160, - _input: &[u8], - _target_gas: Option, - _context: &Context, -) -> Option> { - None -} - impl<'config, S: StackState<'config>> StackExecutor<'config, S> { - /// Create a new stack-based executor. - pub fn new(state: S, config: &'config Config) -> Self { - Self::new_with_precompile(state, config, no_precompile) - } - /// Return a reference of the Config. pub fn config(&self) -> &'config Config { self.config } /// Create a new stack-based executor with given precompiles. - pub fn new_with_precompile( - state: S, - config: &'config Config, - precompile: PrecompileFn, - ) -> Self { + pub fn new_with_precompile(state: S, config: &'config Config, precompile: Precompile) -> Self { Self { config, precompile, @@ -181,18 +250,17 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { value: U256, init_code: Vec, gas_limit: u64, + access_list: Vec<(H160, Vec)>, // See EIP-2930 ) -> ExitReason { - let transaction_cost = gasometer::create_transaction_cost(&init_code); - match self - .state - .metadata_mut() - .gasometer - .record_transaction(transaction_cost) - { + let transaction_cost = gasometer::create_transaction_cost(&init_code, &access_list); + let gasometer = &mut self.state.metadata_mut().gasometer; + match gasometer.record_transaction(transaction_cost) { Ok(()) => (), Err(e) => return e.into(), } + self.initialize_with_access_list(access_list); + match self.create_inner( caller, CreateScheme::Legacy { caller }, @@ -214,19 +282,18 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { init_code: Vec, salt: H256, gas_limit: u64, + access_list: Vec<(H160, Vec)>, // See EIP-2930 ) -> ExitReason { - let transaction_cost = gasometer::create_transaction_cost(&init_code); - match self - .state - .metadata_mut() - .gasometer - .record_transaction(transaction_cost) - { + let transaction_cost = gasometer::create_transaction_cost(&init_code, &access_list); + let gasometer = &mut self.state.metadata_mut().gasometer; + match gasometer.record_transaction(transaction_cost) { Ok(()) => (), Err(e) => return e.into(), } let code_hash = H256::from_slice(Keccak256::digest(&init_code).as_slice()); + self.initialize_with_access_list(access_list); + match self.create_inner( caller, CreateScheme::Create2 { @@ -244,7 +311,12 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { } } - /// Execute a `CALL` transaction. + /// Execute a `CALL` transaction with a given caller, address, value and + /// gas limit and data. + /// + /// Takes in an additional `access_list` parameter for EIP-2930 which was + /// introduced in the Ethereum Berlin hard fork. If you do not wish to use + /// this functionality, just pass in an empty vector. pub fn transact_call( &mut self, caller: H160, @@ -252,18 +324,30 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { value: U256, data: Vec, gas_limit: u64, + access_list: Vec<(H160, Vec)>, ) -> (ExitReason, Vec) { - let transaction_cost = gasometer::call_transaction_cost(&data); - match self - .state - .metadata_mut() - .gasometer - .record_transaction(transaction_cost) - { + let transaction_cost = gasometer::call_transaction_cost(&data, &access_list); + + let gasometer = &mut self.state.metadata_mut().gasometer; + match gasometer.record_transaction(transaction_cost) { Ok(()) => (), Err(e) => return (e.into(), Vec::new()), } + // Initialize initial addresses for EIP-2929 + if self.config.increase_state_access_gas { + let addresses = self + .precompile + .clone() + .into_keys() + .into_iter() + .chain(core::iter::once(caller)) + .chain(core::iter::once(address)); + self.state.metadata_mut().access_addresses(addresses); + + self.initialize_with_access_list(access_list); + } + self.state.inc_nonce(caller); let context = Context { @@ -337,6 +421,16 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { } } + fn initialize_with_access_list(&mut self, access_list: Vec<(H160, Vec)>) { + let addresses = access_list.iter().map(|a| a.0); + self.state.metadata_mut().access_addresses(addresses); + + let storage_keys = access_list + .into_iter() + .flat_map(|(address, keys)| keys.into_iter().map(move |key| (address, key))); + self.state.metadata_mut().access_storages(storage_keys); + } + fn create_inner( &mut self, caller: H160, @@ -361,6 +455,14 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { let address = self.create_address(scheme); + self.state.metadata_mut().access_address(caller); + self.state.metadata_mut().access_address(address); + + let addresses: Vec = self.precompile.clone().into_keys().collect(); + self.state + .metadata_mut() + .access_addresses(addresses.iter().copied()); + event!(Create { caller, address, @@ -583,8 +685,8 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { } } - if let Some(ret) = (self.precompile)(code_address, &input, Some(gas_limit), &context) { - match ret { + if let Some(precompile) = self.precompile.get(&code_address) { + return match (*precompile)(&input, Some(gas_limit), &context) { Ok(PrecompileOutput { exit_status, output, @@ -607,13 +709,13 @@ impl<'config, S: StackState<'config>> StackExecutor<'config, S> { let _ = self.state.metadata_mut().gasometer.record_cost(cost); let _ = self.exit_substate(StackExitKind::Succeeded); - return Capture::Exit((ExitReason::Succeed(exit_status), output)); + Capture::Exit((ExitReason::Succeed(exit_status), output)) } Err(e) => { let _ = self.exit_substate(StackExitKind::Failed); - return Capture::Exit((ExitReason::Error(e), Vec::new())); + Capture::Exit((ExitReason::Error(e), Vec::new())) } - } + }; } let mut runtime = Runtime::new(Rc::new(code), Rc::new(input), context, self.config); @@ -687,6 +789,13 @@ impl<'config, S: StackState<'config>> Handler for StackExecutor<'config, S> { } } + fn is_cold(&self, address: H160, maybe_index: Option) -> bool { + match maybe_index { + None => self.state.is_cold(address), + Some(index) => self.state.is_storage_cold(address, index), + } + } + fn gas_left(&self) -> U256 { U256::from(self.state.metadata().gasometer.gas()) } @@ -798,7 +907,7 @@ impl<'config, S: StackState<'config>> Handler for StackExecutor<'config, S> { self.state.metadata_mut().gasometer.record_cost(cost)?; } else { let is_static = self.state.metadata().is_static; - let (gas_cost, memory_cost) = gasometer::dynamic_opcode_cost( + let (gas_cost, target, memory_cost) = gasometer::dynamic_opcode_cost( context.address, opcode, stack, @@ -810,6 +919,15 @@ impl<'config, S: StackState<'config>> Handler for StackExecutor<'config, S> { let gasometer = &mut self.state.metadata_mut().gasometer; gasometer.record_dynamic_cost(gas_cost, memory_cost)?; + match target { + StorageTarget::Address(address) => { + self.state.metadata_mut().access_address(address) + } + StorageTarget::Slot(address, key) => { + self.state.metadata_mut().access_storage(address, key) + } + StorageTarget::None => (), + } } Ok(()) diff --git a/src/executor/stack/state.rs b/src/executor/stack/state.rs index 4c77d5f5d..96ea7a95e 100644 --- a/src/executor/stack/state.rs +++ b/src/executor/stack/state.rs @@ -1,5 +1,5 @@ use crate::backend::{Apply, Backend, Basic, Log}; -use crate::executor::stack::StackSubstateMetadata; +use crate::executor::stack::{Accessed, StackSubstateMetadata}; use crate::{ExitError, Transfer}; use alloc::{ boxed::Box, @@ -237,6 +237,26 @@ impl<'config> MemoryStackSubstate<'config> { None } + pub fn is_cold(&self, address: H160) -> bool { + self.recursive_is_cold(&|a| a.accessed_addresses.contains(&address)) + } + + pub fn is_storage_cold(&self, address: H160, key: H256) -> bool { + self.recursive_is_cold(&|a: &Accessed| a.accessed_storage.contains(&(address, key))) + } + + fn recursive_is_cold bool>(&self, f: &F) -> bool { + let local_is_accessed = self.metadata.accessed.as_ref().map(f).unwrap_or(false); + if local_is_accessed { + false + } else { + self.parent + .as_ref() + .map(|p| p.recursive_is_cold(f)) + .unwrap_or(true) + } + } + pub fn deleted(&self, address: H160) -> bool { if self.deletes.contains(&address) { return true; @@ -375,6 +395,8 @@ pub trait StackState<'config>: Backend { fn is_empty(&self, address: H160) -> bool; fn deleted(&self, address: H160) -> bool; + fn is_cold(&self, address: H160) -> bool; + fn is_storage_cold(&self, address: H160, key: H256) -> bool; fn inc_nonce(&mut self, address: H160); fn set_storage(&mut self, address: H160, key: H256, value: H256); @@ -491,6 +513,14 @@ impl<'backend, 'config, B: Backend> StackState<'config> for MemoryStackState<'ba self.substate.deleted(address) } + fn is_cold(&self, address: H160) -> bool { + self.substate.is_cold(address) + } + + fn is_storage_cold(&self, address: H160, key: H256) -> bool { + self.substate.is_storage_cold(address, key) + } + fn inc_nonce(&mut self, address: H160) { self.substate.inc_nonce(address, self.backend); }