diff --git a/bins/revm-test/Cargo.toml b/bins/revm-test/Cargo.toml index 92577a3812..661ce1a654 100644 --- a/bins/revm-test/Cargo.toml +++ b/bins/revm-test/Cargo.toml @@ -15,3 +15,6 @@ name = "analysis" [[bin]] name = "snailtracer" + +[[bin]] +name = "transfer" \ No newline at end of file diff --git a/bins/revm-test/src/bin/analysis.rs b/bins/revm-test/src/bin/analysis.rs index 568ef39da4..3dc05de196 100644 --- a/bins/revm-test/src/bin/analysis.rs +++ b/bins/revm-test/src/bin/analysis.rs @@ -25,7 +25,6 @@ fn main() { ); //evm.env.tx.data = Bytes::from(hex::decode("30627b7c").unwrap()); evm.env.tx.data = Bytes::from(hex::decode("8035F0CE").unwrap()); - evm.env.cfg.perf_all_precompiles_have_balance = true; let bytecode_raw = Bytecode::new_raw(contract_data.clone()); let bytecode_checked = Bytecode::new_raw(contract_data.clone()).to_checked(); diff --git a/bins/revm-test/src/bin/transfer.rs b/bins/revm-test/src/bin/transfer.rs new file mode 100644 index 0000000000..ff74d442c4 --- /dev/null +++ b/bins/revm-test/src/bin/transfer.rs @@ -0,0 +1,39 @@ +use revm::{ + db::BenchmarkDB, + primitives::{Bytecode, TransactTo, U256}, +}; +use std::time::{Duration, Instant}; +extern crate alloc; + +fn main() { + // BenchmarkDB is dummy state that implements Database trait. + let mut evm = revm::new(); + + // execution globals block hash/gas_limit/coinbase/timestamp.. + evm.env.tx.caller = "0x0000000000000000000000000000000000000001" + .parse() + .unwrap(); + evm.env.tx.value = U256::from(10); + evm.env.tx.transact_to = TransactTo::Call( + "0x0000000000000000000000000000000000000000" + .parse() + .unwrap(), + ); + //evm.env.tx.data = Bytes::from(hex::decode("30627b7c").unwrap()); + + evm.database(BenchmarkDB::new_bytecode(Bytecode::new())); + + // Microbenchmark + let bench_options = microbench::Options::default().time(Duration::from_secs(1)); + + microbench::bench(&bench_options, "Simple value transfer", || { + let _ = evm.transact().unwrap(); + }); + + let time = Instant::now(); + for _ in 0..10000 { + let _ = evm.transact().unwrap(); + } + let elapsed = time.elapsed(); + println!("10k runs in {:?}", elapsed.as_nanos() / 10_000); +} diff --git a/crates/interpreter/src/gas/calc.rs b/crates/interpreter/src/gas/calc.rs index c63ce251a8..d255cee967 100644 --- a/crates/interpreter/src/gas/calc.rs +++ b/crates/interpreter/src/gas/calc.rs @@ -1,9 +1,11 @@ use super::constants::*; +use crate::alloc::vec::Vec; use crate::{ inner_models::SelfDestructResult, primitives::Spec, primitives::{SpecId::*, U256}, }; +use revm_primitives::{Bytes, B160}; #[allow(clippy::collapsible_else_if)] pub fn sstore_refund(original: U256, current: U256, new: U256) -> i64 { @@ -325,3 +327,49 @@ pub fn memory_gas(a: usize) -> u64 { .saturating_mul(a) .saturating_add(a.saturating_mul(a) / 512) } + +/// Initial gas that is deducted for transaction to be included. +/// Initial gas contains initial stipend gas, gas for access list and input data. +pub fn initial_tx_gas( + input: &Bytes, + is_create: bool, + access_list: &[(B160, Vec)], +) -> u64 { + let mut initial_gas = 0; + let zero_data_len = input.iter().filter(|v| **v == 0).count() as u64; + let non_zero_data_len = input.len() as u64 - zero_data_len; + + // initdate stipend + initial_gas += zero_data_len * TRANSACTION_ZERO_DATA; + // EIP-2028: Transaction data gas cost reduction + initial_gas += non_zero_data_len * if SPEC::enabled(ISTANBUL) { 16 } else { 68 }; + + // get number of access list account and storages. + if SPEC::enabled(BERLIN) { + let accessed_slots = access_list + .iter() + .fold(0, |slot_count, (_, slots)| slot_count + slots.len() as u64); + initial_gas += access_list.len() as u64 * ACCESS_LIST_ADDRESS; + initial_gas += accessed_slots * ACCESS_LIST_STORAGE_KEY; + } + + // base stipend + initial_gas += if is_create { + if SPEC::enabled(HOMESTEAD) { + // EIP-2: Homestead Hard-fork Changes + 53000 + } else { + 21000 + } + } else { + 21000 + }; + + // EIP-3860: Limit and meter initcode + // Initcode stipend for bytecode analysis + if SPEC::enabled(SHANGHAI) && is_create { + initial_gas += initcode_cost(input.len() as u64) + } + + initial_gas +} diff --git a/crates/primitives/src/constants.rs b/crates/primitives/src/constants.rs new file mode 100644 index 0000000000..606b77c078 --- /dev/null +++ b/crates/primitives/src/constants.rs @@ -0,0 +1,13 @@ +/// Interpreter stack limit +pub const STACK_LIMIT: u64 = 1024; +/// EVM call stack limit +pub const CALL_STACK_LIMIT: u64 = 1024; + +/// EIP-170: Contract code size limit +/// By default limit is 0x6000 (~25kb) +pub const MAX_CODE_SIZE: usize = 0x6000; + +/// EIP-3860: Limit and meter initcode +/// +/// Limit of maximum initcode size is 2 * MAX_CODE_SIZE +pub const MAX_INITCODE_SIZE: usize = 2 * MAX_CODE_SIZE; diff --git a/crates/primitives/src/env.rs b/crates/primitives/src/env.rs index 08046d95c6..5d62a1dab1 100644 --- a/crates/primitives/src/env.rs +++ b/crates/primitives/src/env.rs @@ -1,6 +1,9 @@ -use crate::{alloc::vec::Vec, SpecId, B160, B256, U256}; +use crate::{ + alloc::vec::Vec, Account, EVMError, InvalidTransaction, Spec, SpecId, B160, B256, KECCAK_EMPTY, + MAX_INITCODE_SIZE, U256, +}; use bytes::Bytes; -use core::cmp::min; +use core::cmp::{min, Ordering}; #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -78,10 +81,6 @@ pub enum CreateScheme { pub struct CfgEnv { pub chain_id: U256, pub spec_id: SpecId, - /// If all precompiles have some balance we can skip initially fetching them from the database. - /// This is is not really needed on mainnet, and defaults to false, but in most cases it is - /// safe to be set to `true`, depending on the chain. - pub perf_all_precompiles_have_balance: bool, /// Bytecode that is created with CREATE/CREATE2 is by default analysed and jumptable is created. /// This is very benefitial for testing and speeds up execution of that bytecode if called multiple times. /// @@ -121,6 +120,58 @@ pub struct CfgEnv { pub disable_base_fee: bool, } +impl CfgEnv { + #[cfg(feature = "optional_eip3607")] + pub fn is_eip3607_disabled(&self) -> bool { + self.disable_eip3607 + } + + #[cfg(not(feature = "optional_eip3607"))] + pub fn is_eip3607_disabled(&self) -> bool { + false + } + + #[cfg(feature = "optional_balance_check")] + pub fn is_balance_check_disabled(&self) -> bool { + self.disable_balance_check + } + + #[cfg(not(feature = "optional_balance_check"))] + pub fn is_balance_check_disabled(&self) -> bool { + false + } + + #[cfg(feature = "optional_gas_refund")] + pub fn is_gas_refund_disabled(&self) -> bool { + self.disable_gas_refund + } + + #[cfg(not(feature = "optional_gas_refund"))] + pub fn is_gas_refund_disabled(&self) -> bool { + false + } + + #[cfg(feature = "optional_no_base_fee")] + pub fn is_base_fee_check_disabled(&self) -> bool { + self.disable_base_fee + } + + #[cfg(not(feature = "optional_no_base_fee"))] + pub fn is_base_fee_check_disabled(&self) -> bool { + false + } + + #[cfg(feature = "optional_block_gas_limit")] + pub fn is_block_gas_limit_disabled(&self) -> bool { + self.disable_block_gas_limit + } + + #[cfg(not(feature = "optional_block_gas_limit"))] + pub fn is_block_gas_limit_disabled(&self) -> bool { + false + } +} + #[derive(Clone, Default, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum AnalysisKind { @@ -135,7 +186,6 @@ impl Default for CfgEnv { CfgEnv { chain_id: U256::from(1), spec_id: SpecId::LATEST, - perf_all_precompiles_have_balance: false, perf_analyse_created_bytecodes: Default::default(), limit_contract_code_size: None, #[cfg(feature = "memory_limit")] @@ -196,4 +246,107 @@ impl Env { ) } } + + /// Validate ENV data of the block. + /// + /// It can be skip if you are sure that PREVRANDAO is set. + #[inline] + pub fn validate_block_env(&self) -> Result<(), EVMError> { + // Prevrandao is required for merge + if SPEC::enabled(SpecId::MERGE) && self.block.prevrandao.is_none() { + return Err(EVMError::PrevrandaoNotSet); + } + Ok(()) + } + + /// Validate transaction data that is set inside ENV and return error if something is wrong. + /// + /// Return inital spend gas (Gas needed to execute transaction). + #[inline] + pub fn validate_tx(&self) -> Result<(), InvalidTransaction> { + let gas_limit = self.tx.gas_limit; + let effective_gas_price = self.effective_gas_price(); + let is_create = self.tx.transact_to.is_create(); + + // BASEFEE tx check + if SPEC::enabled(SpecId::LONDON) { + if let Some(priority_fee) = self.tx.gas_priority_fee { + if priority_fee > self.tx.gas_price { + // or gas_max_fee for eip1559 + return Err(InvalidTransaction::GasMaxFeeGreaterThanPriorityFee); + } + } + let basefee = self.block.basefee; + + // check minimal cost against basefee + if !self.cfg.is_base_fee_check_disabled() && effective_gas_price < basefee { + return Err(InvalidTransaction::GasPriceLessThanBasefee); + } + } + + // Check if gas_limit is more than block_gas_limit + if !self.cfg.is_block_gas_limit_disabled() && U256::from(gas_limit) > self.block.gas_limit { + return Err(InvalidTransaction::CallerGasLimitMoreThanBlock); + } + + // EIP-3860: Limit and meter initcode + if SPEC::enabled(SpecId::SHANGHAI) && is_create && self.tx.data.len() > MAX_INITCODE_SIZE { + return Err(InvalidTransaction::CreateInitcodeSizeLimit); + } + + // Check if the transaction's chain id is correct + if let Some(tx_chain_id) = self.tx.chain_id { + if U256::from(tx_chain_id) != self.cfg.chain_id { + return Err(InvalidTransaction::InvalidChainId); + } + } + + // Check if the transaction's chain id is correct + if !SPEC::enabled(SpecId::BERLIN) && !self.tx.access_list.is_empty() { + return Err(InvalidTransaction::AccessListNotSupported); + } + + Ok(()) + } + + /// Validate transaction agains state. + #[inline] + pub fn validate_tx_agains_state(&self, account: &Account) -> Result<(), InvalidTransaction> { + // EIP-3607: Reject transactions from senders with deployed code + // This EIP is introduced after london but there was no collision in past + // so we can leave it enabled always + if !self.cfg.is_eip3607_disabled() && account.info.code_hash != KECCAK_EMPTY { + return Err(InvalidTransaction::RejectCallerWithCode); + } + + // Check that the transaction's nonce is correct + if let Some(tx) = self.tx.nonce { + let state = account.info.nonce; + match tx.cmp(&state) { + Ordering::Greater => { + return Err(InvalidTransaction::NonceTooHigh { tx, state }); + } + Ordering::Less => { + return Err(InvalidTransaction::NonceTooLow { tx, state }); + } + _ => {} + } + } + + let balance_check = U256::from(self.tx.gas_limit) + .checked_mul(self.tx.gas_price) + .and_then(|gas_cost| gas_cost.checked_add(self.tx.value)) + .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?; + + // Check if account has enough balance for gas_limit*gas_price and value transfer. + // Transfer will be done inside `*_inner` functions. + if !self.cfg.is_balance_check_disabled() && balance_check > account.info.balance { + return Err(InvalidTransaction::LackOfFundForMaxFee { + fee: self.tx.gas_limit, + balance: account.info.balance, + }); + } + + Ok(()) + } } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index f7a1234371..19c66067ae 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -2,6 +2,7 @@ pub mod bits; pub mod bytecode; +pub mod constants; pub mod db; pub mod env; pub mod log; @@ -26,8 +27,9 @@ pub type Hash = B256; pub use bitvec; pub use bytecode::*; +pub use constants::*; pub use env::*; -pub use hashbrown::{hash_map, HashMap}; +pub use hashbrown::{hash_map, hash_set, HashMap, HashSet}; pub use log::Log; pub use precompile::*; pub use result::*; diff --git a/crates/primitives/src/result.rs b/crates/primitives/src/result.rs index abbcc4f2bf..951d7c96bf 100644 --- a/crates/primitives/src/result.rs +++ b/crates/primitives/src/result.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; use bytes::Bytes; use ruint::aliases::U256; -pub type EVMResult = core::result::Result>; +pub type EVMResult = core::result::Result>; #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -91,14 +91,14 @@ impl Output { #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum EVMError { +pub enum EVMError { Transaction(InvalidTransaction), /// REVM specific and related to environment. PrevrandaoNotSet, - Database(DB), + Database(DBError), } -impl From for EVMError { +impl From for EVMError { fn from(invalid: InvalidTransaction) -> Self { EVMError::Transaction(invalid) } @@ -114,8 +114,8 @@ pub enum InvalidTransaction { /// EIP-3607 Reject transactions from senders with deployed code RejectCallerWithCode, /// Transaction account does not have enough amount of ether to cover transferred value and gas_limit*gas_price. - LackOfFundForGasLimit { - gas_limit: U256, + LackOfFundForMaxFee { + fee: u64, balance: U256, }, /// Overflow payment in transaction. @@ -133,6 +133,9 @@ pub enum InvalidTransaction { /// EIP-3860: Limit and meter initcode CreateInitcodeSizeLimit, InvalidChainId, + /// Access list is not supported is not supported + /// for blocks before Berlin hardfork. + AccessListNotSupported, } /// When transaction return successfully without halts. diff --git a/crates/primitives/src/utilities.rs b/crates/primitives/src/utilities.rs index cf8a327ff6..ff5d364c80 100644 --- a/crates/primitives/src/utilities.rs +++ b/crates/primitives/src/utilities.rs @@ -8,7 +8,7 @@ pub const KECCAK_EMPTY: B256 = B256(hex!( #[inline(always)] pub fn keccak256(input: &[u8]) -> B256 { - B256::from_slice(Keccak256::digest(input).as_slice()) + B256(Keccak256::digest(input)[..].try_into().unwrap()) } /// Returns the address for the legacy `CREATE` scheme: [`CreateScheme::Create`] diff --git a/crates/revm/src/db/in_memory_db.rs b/crates/revm/src/db/in_memory_db.rs index dda2d30b64..120877e6d3 100644 --- a/crates/revm/src/db/in_memory_db.rs +++ b/crates/revm/src/db/in_memory_db.rs @@ -122,6 +122,9 @@ impl CacheDB { impl DatabaseCommit for CacheDB { fn commit(&mut self, changes: HashMap) { for (address, mut account) in changes { + if !account.is_touched() { + continue; + } if account.is_selfdestructed() { let db_account = self.accounts.entry(address).or_default(); db_account.storage.clear(); @@ -400,6 +403,14 @@ impl Database for BenchmarkDB { code_hash: self.1, })); } + if address == B160::from(1) { + return Ok(Some(AccountInfo { + nonce: 0, + balance: U256::from(10000000), + code: None, + code_hash: KECCAK_EMPTY, + })); + } Ok(None) } diff --git a/crates/revm/src/evm_impl.rs b/crates/revm/src/evm_impl.rs index 8f0e0b91da..6e9d30e1f7 100644 --- a/crates/revm/src/evm_impl.rs +++ b/crates/revm/src/evm_impl.rs @@ -3,18 +3,19 @@ use crate::interpreter::{ CallContext, CallInputs, CallScheme, Contract, CreateInputs, CreateScheme, Gas, Host, InstructionResult, Interpreter, SelfDestructResult, Transfer, CALL_STACK_LIMIT, }; +use crate::journaled_state::is_precompile; use crate::primitives::{ create2_address, create_address, keccak256, Account, AnalysisKind, Bytecode, Bytes, EVMError, EVMResult, Env, ExecutionResult, HashMap, InvalidTransaction, Log, Output, ResultAndState, Spec, SpecId::{self, *}, - TransactTo, B160, B256, KECCAK_EMPTY, U256, + TransactTo, B160, B256, U256, }; use crate::{db::Database, journaled_state::JournaledState, precompile, Inspector}; use alloc::vec::Vec; -use core::cmp::Ordering; use core::{cmp::min, marker::PhantomData}; -use revm_interpreter::{MAX_CODE_SIZE, MAX_INITCODE_SIZE}; +use revm_interpreter::gas::initial_tx_gas; +use revm_interpreter::MAX_CODE_SIZE; use revm_precompile::{Precompile, Precompiles}; pub struct EVMData<'a, DB: Database> { @@ -33,219 +34,125 @@ pub struct EVMImpl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> { pub trait Transact { /// Do transaction. - /// InstructionResult InstructionResult, Output for call or Address if we are creating contract, gas spend, gas refunded, State that needs to be applied. + /// InstructionResult InstructionResult, Output for call or Address if we are creating + /// contract, gas spend, gas refunded, State that needs to be applied. fn transact(&mut self) -> EVMResult; } +impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, INSPECT> { + /// Load access list for berlin hardfork. + /// + /// Loading of accounts/storages is needed to make them hot. + #[inline] + fn load_access_list(&mut self) -> Result<(), EVMError> { + for (address, slots) in self.data.env.tx.access_list.iter() { + self.data + .journaled_state + .initial_account_load(*address, slots, self.data.db) + .map_err(EVMError::Database)?; + } + Ok(()) + } +} + impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact for EVMImpl<'a, GSPEC, DB, INSPECT> { fn transact(&mut self) -> EVMResult { - let caller = self.data.env.tx.caller; - let value = self.data.env.tx.value; - let data = self.data.env.tx.data.clone(); - let gas_limit = self.data.env.tx.gas_limit; - let effective_gas_price = self.data.env.effective_gas_price(); - - if GSPEC::enabled(MERGE) && self.data.env.block.prevrandao.is_none() { - return Err(EVMError::PrevrandaoNotSet); - } - - if GSPEC::enabled(LONDON) { - if let Some(priority_fee) = self.data.env.tx.gas_priority_fee { - if priority_fee > self.data.env.tx.gas_price { - // or gas_max_fee for eip1559 - return Err(InvalidTransaction::GasMaxFeeGreaterThanPriorityFee.into()); - } - } - let basefee = self.data.env.block.basefee; - - #[cfg(feature = "optional_no_base_fee")] - let disable_base_fee = self.env().cfg.disable_base_fee; - #[cfg(not(feature = "optional_no_base_fee"))] - let disable_base_fee = false; - - // check minimal cost against basefee - // TODO maybe do this checks when creating evm. We already have all data there - // or should be move effective_gas_price inside transact fn - if !disable_base_fee && effective_gas_price < basefee { - return Err(InvalidTransaction::GasPriceLessThanBasefee.into()); - } - // check if priority fee is lower than max fee + self.env().validate_block_env::()?; + self.env().validate_tx::()?; + + let env = &self.data.env; + let tx_caller = env.tx.caller; + let tx_value = env.tx.value; + let tx_data = env.tx.data.clone(); + let tx_gas_limit = env.tx.gas_limit; + let tx_is_create = env.tx.transact_to.is_create(); + let effective_gas_price = env.effective_gas_price(); + + let initial_gas_spend = + initial_tx_gas::(&tx_data, tx_is_create, &env.tx.access_list); + + // Additonal check to see if limit is big enought to cover initial gas. + if env.tx.gas_limit < initial_gas_spend { + return Err(InvalidTransaction::CallGasCostMoreThanGasLimit.into()); } - #[cfg(feature = "optional_block_gas_limit")] - let disable_block_gas_limit = self.env().cfg.disable_block_gas_limit; - #[cfg(not(feature = "optional_block_gas_limit"))] - let disable_block_gas_limit = false; - - // unusual to be found here, but check if gas_limit is more than block_gas_limit - if !disable_block_gas_limit && U256::from(gas_limit) > self.data.env.block.gas_limit { - return Err(InvalidTransaction::CallerGasLimitMoreThanBlock.into()); + // load coinbase + // EIP-3651: Warm COINBASE. Starts the `COINBASE` address warm + if GSPEC::enabled(SHANGHAI) { + self.data + .journaled_state + .initial_account_load(self.data.env.block.coinbase, &[], self.data.db) + .map_err(EVMError::Database)?; } + self.load_access_list()?; // load acc - self.data - .journaled_state - .load_account(caller, self.data.db) + let journal = &mut self.data.journaled_state; + let (caller_account, _) = journal + .load_account(tx_caller, self.data.db) .map_err(EVMError::Database)?; - #[cfg(feature = "optional_eip3607")] - let disable_eip3607 = self.env().cfg.disable_eip3607; - #[cfg(not(feature = "optional_eip3607"))] - let disable_eip3607 = false; - - // EIP-3607: Reject transactions from senders with deployed code - // This EIP is introduced after london but there was no collision in past - // so we can leave it enabled always - if !disable_eip3607 - && self.data.journaled_state.account(caller).info.code_hash != KECCAK_EMPTY - { - return Err(InvalidTransaction::RejectCallerWithCode.into()); - } - - // Check if the transaction's chain id is correct - if let Some(tx_chain_id) = self.data.env.tx.chain_id { - if U256::from(tx_chain_id) != self.data.env.cfg.chain_id { - return Err(InvalidTransaction::InvalidChainId.into()); - } - } - - // Check that the transaction's nonce is correct - if self.data.env.tx.nonce.is_some() { - let state_nonce = self - .data - .journaled_state - .state - .get(&caller) - .unwrap() - .info - .nonce; - let tx_nonce = self.data.env.tx.nonce.unwrap(); - match tx_nonce.cmp(&state_nonce) { - Ordering::Greater => { - return Err(InvalidTransaction::NonceTooHigh { - tx: tx_nonce, - state: state_nonce, - } - .into()); - } - Ordering::Less => { - return Err(InvalidTransaction::NonceTooLow { - tx: tx_nonce, - state: state_nonce, - } - .into()); - } - _ => {} - } - } - - #[cfg(feature = "optional_balance_check")] - let disable_balance_check = self.env().cfg.disable_balance_check; - #[cfg(not(feature = "optional_balance_check"))] - let disable_balance_check = false; - - let caller_balance = &mut self - .data - .journaled_state - .state - .get_mut(&caller) - .unwrap() - .info - .balance; - - let balance_check = U256::from(gas_limit) - .checked_mul(self.data.env.tx.gas_price) - .and_then(|gas_cost| gas_cost.checked_add(value)) - .ok_or(EVMError::Transaction( - InvalidTransaction::OverflowPaymentInTransaction, - ))?; - - // Check if account has enough balance for gas_limit*gas_price and value transfer. - // Transfer will be done inside `*_inner` functions. - if balance_check > *caller_balance && !disable_balance_check { - return Err(InvalidTransaction::LackOfFundForGasLimit { - gas_limit: balance_check, - balance: *caller_balance, - } - .into()); - } + self.data.env.validate_tx_agains_state(caller_account)?; // Reduce gas_limit*gas_price amount of caller account. // unwrap_or can only occur if disable_balance_check is enabled - *caller_balance = caller_balance - .checked_sub(U256::from(gas_limit) * effective_gas_price) + caller_account.info.balance = caller_account + .info + .balance + .checked_sub(U256::from(tx_gas_limit).saturating_mul(effective_gas_price)) .unwrap_or(U256::ZERO); - let mut gas = Gas::new(gas_limit); - // record initial gas cost. if not using gas metering init will return. - if !gas.record_cost(self.initialization::()?) { - return Err(InvalidTransaction::CallGasCostMoreThanGasLimit.into()); - } - - // record all as cost. Gas limit here is reduced by init cost of bytes and access lists. - let gas_limit = gas.remaining(); - if crate::USE_GAS { - gas.record_cost(gas_limit); - } + // touch account so we know it is changed. + caller_account.mark_touch(); - // load coinbase - // EIP-3651: Warm COINBASE. Starts the `COINBASE` address warm - if GSPEC::enabled(SHANGHAI) { - self.data - .journaled_state - .load_account(self.data.env.block.coinbase, self.data.db) - .map_err(EVMError::Database)?; - } + let transact_gas_limit = tx_gas_limit - initial_gas_spend; // call inner handling of call/create - // TODO can probably be refactored to look nicer. let (exit_reason, ret_gas, output) = match self.data.env.tx.transact_to { TransactTo::Call(address) => { - if self.data.journaled_state.inc_nonce(caller).is_some() { - let context = CallContext { - caller, + // Nonce is already checked + caller_account.info.nonce = + caller_account.info.nonce.checked_add(1).unwrap_or(u64::MAX); + + let (exit, gas, bytes) = self.call(&mut CallInputs { + contract: address, + transfer: Transfer { + source: tx_caller, + target: address, + value: tx_value, + }, + input: tx_data, + gas_limit: transact_gas_limit, + context: CallContext { + caller: tx_caller, address, code_address: address, - apparent_value: value, + apparent_value: tx_value, scheme: CallScheme::Call, - }; - let mut call_input = CallInputs { - contract: address, - transfer: Transfer { - source: caller, - target: address, - value, - }, - input: data, - gas_limit, - context, - is_static: false, - }; - let (exit, gas, bytes) = self.call(&mut call_input); - (exit, gas, Output::Call(bytes)) - } else { - ( - InstructionResult::NonceOverflow, - gas, - Output::Call(Bytes::new()), - ) - } + }, + is_static: false, + }); + (exit, gas, Output::Call(bytes)) } TransactTo::Create(scheme) => { - let mut create_input = CreateInputs { - caller, + let (exit, address, ret_gas, bytes) = self.create(&mut CreateInputs { + caller: tx_caller, scheme, - value, - init_code: data, - gas_limit, - }; - let (exit, address, ret_gas, bytes) = self.create(&mut create_input); + value: tx_value, + init_code: tx_data, + gas_limit: transact_gas_limit, + }); (exit, ret_gas, Output::Create(bytes, address)) } }; + // set gas with gas limit and spend it all. Gas is going to be reimbursed when + // transaction is returned successfully. + let mut gas = Gas::new(tx_gas_limit); + gas.record_cost(tx_gas_limit); + if crate::USE_GAS { match exit_reason { return_ok!() => { @@ -259,7 +166,7 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> Transact } } - let (state, logs, gas_used, gas_refunded) = self.finalize::(caller, &gas); + let (state, logs, gas_used, gas_refunded) = self.finalize::(&gas); let result = match exit_reason.into() { SuccessOrHalt::Success(reason) => ExecutionResult::Success { @@ -314,61 +221,49 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, } } - fn finalize( - &mut self, - caller: B160, - gas: &Gas, - ) -> (HashMap, Vec, u64, u64) { + fn finalize(&mut self, gas: &Gas) -> (HashMap, Vec, u64, u64) { + let caller = self.data.env.tx.caller; let coinbase = self.data.env.block.coinbase; let (gas_used, gas_refunded) = if crate::USE_GAS { let effective_gas_price = self.data.env.effective_gas_price(); let basefee = self.data.env.block.basefee; - #[cfg(feature = "optional_gas_refund")] - let disable_gas_refund = self.env().cfg.disable_gas_refund; - #[cfg(not(feature = "optional_gas_refund"))] - let disable_gas_refund = false; - - let gas_refunded = if disable_gas_refund { + let gas_refunded = if self.env().cfg.is_gas_refund_disabled() { 0 } else { // EIP-3529: Reduction in refunds let max_refund_quotient = if SPEC::enabled(LONDON) { 5 } else { 2 }; min(gas.refunded() as u64, gas.spend() / max_refund_quotient) }; - let acc_caller = self.data.journaled_state.state().get_mut(&caller).unwrap(); - acc_caller.info.balance = acc_caller + + // return balance of not spend gas. + let caller_account = self.data.journaled_state.state().get_mut(&caller).unwrap(); + caller_account.info.balance = caller_account .info .balance .saturating_add(effective_gas_price * U256::from(gas.remaining() + gas_refunded)); - // EIP-1559 + // EIP-1559 discard basefee for coinbase transfer. Basefee amount of gas is discarded. let coinbase_gas_price = if SPEC::enabled(LONDON) { effective_gas_price.saturating_sub(basefee) } else { effective_gas_price }; - // TODO - let _ = self + // transfer fee to coinbase/beneficiary. + let Ok((coinbase_account,_)) = self .data .journaled_state - .load_account(coinbase, self.data.db); - self.data.journaled_state.touch(&coinbase); - let acc_coinbase = self - .data - .journaled_state - .state() - .get_mut(&coinbase) - .unwrap(); - acc_coinbase.info.balance = acc_coinbase + .load_account(coinbase, self.data.db) else { panic!("coinbase account not found");}; + coinbase_account.mark_touch(); + coinbase_account.info.balance = coinbase_account .info .balance .saturating_add(coinbase_gas_price * U256::from(gas.spend() - gas_refunded)); + (gas.spend() - gas_refunded, gas_refunded) } else { // touch coinbase - // TODO return let _ = self .data .journaled_state @@ -376,101 +271,11 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, self.data.journaled_state.touch(&coinbase); (0, 0) }; - let (mut new_state, logs) = self.data.journaled_state.finalize(); - // precompiles are special case. If there is precompiles in finalized Map that means some balance is - // added to it, we need now to load precompile address from db and add this amount to it so that we - // will have sum. - if self.data.env.cfg.perf_all_precompiles_have_balance { - for address in self.precompiles.addresses() { - let address = B160(*address); - if let Some(precompile) = new_state.get_mut(&address) { - // we found it. - precompile.info.balance += self - .data - .db - .basic(address) - .ok() - .flatten() - .map(|acc| acc.balance) - .unwrap_or_default(); - } - } - } - + let (new_state, logs) = self.data.journaled_state.finalize(); (new_state, logs, gas_used, gas_refunded) } - fn initialization(&mut self) -> Result> { - let is_create = matches!(self.data.env.tx.transact_to, TransactTo::Create(_)); - let input = &self.data.env.tx.data; - - // EIP-3860: Limit and meter initcode - let initcode_cost = if SPEC::enabled(SHANGHAI) && self.data.env.tx.transact_to.is_create() { - let initcode_len = self.data.env.tx.data.len(); - if initcode_len > MAX_INITCODE_SIZE { - return Err(InvalidTransaction::CreateInitcodeSizeLimit.into()); - } - if crate::USE_GAS { - gas::initcode_cost(initcode_len as u64) - } else { - 0 - } - } else { - 0 - }; - - if crate::USE_GAS { - let zero_data_len = input.iter().filter(|v| **v == 0).count() as u64; - let non_zero_data_len = input.len() as u64 - zero_data_len; - let (accessed_accounts, accessed_slots) = { - if SPEC::enabled(BERLIN) { - let mut accessed_slots = 0_u64; - - for (address, slots) in self.data.env.tx.access_list.iter() { - self.data - .journaled_state - .load_account(*address, self.data.db) - .map_err(EVMError::Database)?; - accessed_slots += slots.len() as u64; - - for slot in slots { - self.data - .journaled_state - .sload(*address, *slot, self.data.db) - .map_err(EVMError::Database)?; - } - } - (self.data.env.tx.access_list.len() as u64, accessed_slots) - } else { - (0, 0) - } - }; - - let transact = if is_create { - if SPEC::enabled(HOMESTEAD) { - // EIP-2: Homestead Hard-fork Changes - 53000 - } else { - 21000 - } - } else { - 21000 - }; - - // EIP-2028: Transaction data gas cost reduction - let gas_transaction_non_zero_data = if SPEC::enabled(ISTANBUL) { 16 } else { 68 }; - - Ok(transact - + initcode_cost - + zero_data_len * gas::TRANSACTION_ZERO_DATA - + non_zero_data_len * gas_transaction_non_zero_data - + accessed_accounts * gas::ACCESS_LIST_ADDRESS - + accessed_slots * gas::ACCESS_LIST_STORAGE_KEY) - } else { - Ok(0) - } - } - + /// EVM create opcode for both initial crate and CREATE and CREATE2 opcodes. fn create_inner( &mut self, inputs: &CreateInputs, @@ -656,12 +461,45 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, (exit_reason, interpreter) } + /// Call precompile contract + fn call_precompile( + &mut self, + mut gas: Gas, + contract: B160, + input_data: Bytes, + ) -> (InstructionResult, Gas, Bytes) { + let precompile = self + .precompiles + .get(&contract) + .expect("Check for precompile should be already done"); + let out = match precompile { + Precompile::Standard(fun) => fun(&input_data, gas.limit()), + Precompile::Custom(fun) => fun(&input_data, gas.limit()), + }; + match out { + Ok((gas_used, data)) => { + if !crate::USE_GAS || gas.record_cost(gas_used) { + (InstructionResult::Return, gas, Bytes::from(data)) + } else { + (InstructionResult::PrecompileOOG, gas, Bytes::new()) + } + } + Err(e) => { + let ret = if precompile::Error::OutOfGas == e { + InstructionResult::PrecompileOOG + } else { + InstructionResult::PrecompileError + }; + (ret, gas, Bytes::new()) + } + } + } + + /// Main contract call of the EVM. fn call_inner(&mut self, inputs: &mut CallInputs) -> (InstructionResult, Gas, Bytes) { - let mut gas = Gas::new(inputs.gas_limit); + let gas = Gas::new(inputs.gas_limit); // Load account and get code. Account is now hot. - let bytecode = if let Some((bytecode, _)) = self.code(inputs.contract) { - bytecode - } else { + let Some((bytecode,_)) = self.code(inputs.contract) else { return (InstructionResult::FatalExternalError, gas, Bytes::new()); }; @@ -690,48 +528,28 @@ impl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> EVMImpl<'a, GSPEC, DB, return (e, gas, Bytes::new()); } - // Call precompiles - if let Some(precompile) = self.precompiles.get(&inputs.contract) { - let out = match precompile { - Precompile::Standard(fun) => fun(inputs.input.as_ref(), inputs.gas_limit), - Precompile::Custom(fun) => fun(inputs.input.as_ref(), inputs.gas_limit), - }; - match out { - Ok((gas_used, data)) => { - if !crate::USE_GAS || gas.record_cost(gas_used) { - self.data.journaled_state.checkpoint_commit(); - (InstructionResult::Return, gas, Bytes::from(data)) - } else { - self.data.journaled_state.checkpoint_revert(checkpoint); - (InstructionResult::PrecompileOOG, gas, Bytes::new()) - } - } - Err(e) => { - let ret = if let precompile::Error::OutOfGas = e { - InstructionResult::PrecompileOOG - } else { - InstructionResult::PrecompileError - }; - self.data.journaled_state.checkpoint_revert(checkpoint); - (ret, gas, Bytes::new()) - } - } - } else { + let ret = if is_precompile(inputs.contract, self.precompiles.len()) { + self.call_precompile(gas, inputs.contract, inputs.input.clone()) + } else if !bytecode.is_empty() { // Create interpreter and execute subcall let (exit_reason, interpreter) = self.run_interpreter( Contract::new_with_context(inputs.input.clone(), bytecode, &inputs.context), gas.limit(), inputs.is_static, ); - - if matches!(exit_reason, return_ok!()) { - self.data.journaled_state.checkpoint_commit(); - } else { - self.data.journaled_state.checkpoint_revert(checkpoint); - } - (exit_reason, interpreter.gas, interpreter.return_value()) + } else { + (InstructionResult::Stop, gas, Bytes::new()) + }; + + // revert changes or not. + if matches!(ret.0, return_ok!()) { + self.data.journaled_state.checkpoint_commit(); + } else { + self.data.journaled_state.checkpoint_revert(checkpoint); } + + ret } } @@ -799,13 +617,8 @@ impl<'a, GSPEC: Spec, DB: Database + 'a, const INSPECT: bool> Host .load_code(address, db) .map_err(|e| *error = Some(e)) .ok()?; - //asume that all precompiles have some balance - let is_precompile = self.precompiles.contains(&address); - if is_precompile && self.data.env.cfg.perf_all_precompiles_have_balance { - return Some((KECCAK_EMPTY, is_cold)); - } + if acc.is_empty() { - // TODO check this for pre tangerine fork return Some((B256::zero(), is_cold)); } diff --git a/crates/revm/src/journaled_state.rs b/crates/revm/src/journaled_state.rs index 648a39c4c9..a76f8b9adc 100644 --- a/crates/revm/src/journaled_state.rs +++ b/crates/revm/src/journaled_state.rs @@ -133,11 +133,6 @@ impl JournaledState { pub fn finalize(&mut self) -> (State, Vec) { let state = mem::take(&mut self.state); - let state = state - .into_iter() - .filter(|(_, account)| account.is_touched()) - .collect(); - let logs = mem::take(&mut self.logs); self.journal = vec![vec![]]; self.depth = 0; @@ -479,6 +474,53 @@ impl JournaledState { }) } + pub fn initial_account_and_code_load( + &mut self, + address: B160, + db: &mut DB, + ) -> Result<&mut Account, DB::Error> { + let account = self.initial_account_load(address, &[], db)?; + if account.info.code.is_none() { + if account.info.code_hash == KECCAK_EMPTY { + account.info.code = Some(Bytecode::new()); + } else { + // load code if requested + account.info.code = Some(db.code_by_hash(account.info.code_hash)?); + } + } + + Ok(account) + } + + /// Initial load of account. This load will not be tracked inside journal + pub fn initial_account_load( + &mut self, + address: B160, + slots: &[U256], + db: &mut DB, + ) -> Result<&mut Account, DB::Error> { + match self.state.entry(address) { + Entry::Occupied(entry) => { + let account = entry.into_mut(); + + Ok(account) + } + Entry::Vacant(vac) => { + let mut account = db + .basic(address)? + .map(|i| i.into()) + .unwrap_or(Account::new_not_existing()); + + for slot in slots { + let storage = db.storage(address, *slot)?; + account.storage.insert(*slot, StorageSlot::new(storage)); + } + + Ok(vac.insert(account)) + } + } + } + /// load account into memory. return if it is cold or hot accessed pub fn load_account( &mut self, @@ -624,7 +666,7 @@ impl JournaledState { /// Check if address is precompile by having assumption /// that precompiles are in range of 1 to N. #[inline(always)] -fn is_precompile(address: B160, num_of_precompiles: usize) -> bool { +pub fn is_precompile(address: B160, num_of_precompiles: usize) -> bool { if !address[..18].iter().all(|i| *i == 0) { return false; }