diff --git a/crates/context/interface/src/context.rs b/crates/context/interface/src/context.rs index 114235cb6e..014dcf85e1 100644 --- a/crates/context/interface/src/context.rs +++ b/crates/context/interface/src/context.rs @@ -195,6 +195,18 @@ impl SStoreResult { self.new_value.const_eq(&self.present_value) } + /// Returns `true` if the new values changes the present value. + #[inline] + pub const fn new_values_changes_present(&self) -> bool { + !self.is_new_eq_present() + } + + /// Returns `true` if the original value is zero and the new value is not zero. + #[inline] + pub const fn have_changed_from_zero(&self) -> bool { + self.is_original_zero() && !self.is_new_zero() + } + /// Returns `true` if the original value is equal to the present value. #[inline] pub const fn is_original_eq_present(&self) -> bool { diff --git a/crates/context/src/journal/inner.rs b/crates/context/src/journal/inner.rs index 6328b4262f..34f4e7d70e 100644 --- a/crates/context/src/journal/inner.rs +++ b/crates/context/src/journal/inner.rs @@ -499,7 +499,7 @@ impl JournalInner { address: Address, target: Address, skip_cold_load: bool, - ) -> Result, JournalLoadError<::Error>> { + ) -> Result, JournalLoadError> { let spec = self.spec; let account_load = self.load_account_optional(db, target, false, skip_cold_load)?; let is_cold = account_load.is_cold; diff --git a/crates/handler/src/evm.rs b/crates/handler/src/evm.rs index 0ad3130f0c..8607f48dce 100644 --- a/crates/handler/src/evm.rs +++ b/crates/handler/src/evm.rs @@ -183,7 +183,13 @@ where let ctx = &mut self.ctx; let precompiles = &mut self.precompiles; - let res = Self::Frame::init_with_context(new_frame, ctx, precompiles, frame_input)?; + let res = Self::Frame::init_with_context( + new_frame, + ctx, + precompiles, + frame_input, + self.instruction.gas_params(), + )?; Ok(res.map_frame(|token| { if is_first_init { diff --git a/crates/handler/src/frame.rs b/crates/handler/src/frame.rs index a40b1f4535..6338ea44af 100644 --- a/crates/handler/src/frame.rs +++ b/crates/handler/src/frame.rs @@ -12,7 +12,7 @@ use context_interface::{ use core::cmp::min; use derive_where::derive_where; use interpreter::{ - gas, + gas::{self, params::GasParams}, interpreter::{EthInterpreter, ExtBytecode}, interpreter_action::FrameInit, interpreter_types::ReturnData, @@ -101,6 +101,7 @@ pub type ContextTrDbError = <::Db as Database>::Error; impl EthFrame { /// Clear and initialize a frame. #[allow(clippy::too_many_arguments)] + #[inline(always)] pub fn clear( &mut self, data: FrameData, @@ -113,6 +114,7 @@ impl EthFrame { spec_id: SpecId, gas_limit: u64, checkpoint: JournalCheckpoint, + gas_params: GasParams, ) { let Self { data: data_ref, @@ -126,7 +128,9 @@ impl EthFrame { *input_ref = input; *depth_ref = depth; *is_finished_ref = false; - interpreter.clear(memory, bytecode, inputs, is_static, spec_id, gas_limit); + interpreter.clear( + memory, bytecode, inputs, is_static, spec_id, gas_limit, gas_params, + ); *checkpoint_ref = checkpoint; } @@ -143,6 +147,7 @@ impl EthFrame { depth: usize, memory: SharedMemory, inputs: Box, + gas_params: GasParams, ) -> Result, ERROR> { let gas = Gas::new(inputs.gas_limit); let return_result = |instruction_result: InstructionResult| { @@ -242,6 +247,7 @@ impl EthFrame { ctx.cfg().spec().into(), gas_limit, checkpoint, + gas_params, ); Ok(ItemOrResult::Item(this.consume())) } @@ -257,6 +263,7 @@ impl EthFrame { depth: usize, memory: SharedMemory, inputs: Box, + gas_params: GasParams, ) -> Result, ERROR> { let spec = context.cfg().spec().into(); let return_error = |e| { @@ -343,6 +350,7 @@ impl EthFrame { spec, gas_limit, checkpoint, + gas_params, ); Ok(ItemOrResult::Item(this.consume())) } @@ -356,6 +364,7 @@ impl EthFrame { ctx: &mut CTX, precompiles: &mut PRECOMPILES, frame_init: FrameInit, + gas_params: GasParams, ) -> Result< ItemOrResult, ContextError<<::Db as Database>::Error>, @@ -369,9 +378,11 @@ impl EthFrame { match frame_input { FrameInput::Call(inputs) => { - Self::make_call_frame(this, ctx, precompiles, depth, memory, inputs) + Self::make_call_frame(this, ctx, precompiles, depth, memory, inputs, gas_params) + } + FrameInput::Create(inputs) => { + Self::make_create_frame(this, ctx, depth, memory, inputs, gas_params) } - FrameInput::Create(inputs) => Self::make_create_frame(this, ctx, depth, memory, inputs), FrameInput::Empty => unreachable!(), } } diff --git a/crates/handler/src/handler.rs b/crates/handler/src/handler.rs index 9c862d9b4b..ca905a1683 100644 --- a/crates/handler/src/handler.rs +++ b/crates/handler/src/handler.rs @@ -1,3 +1,4 @@ +use crate::instructions::InstructionProvider; use crate::{ evm::FrameTr, execution, post_execution, @@ -98,6 +99,7 @@ pub trait Handler { &mut self, evm: &mut Self::Evm, ) -> Result, Self::Error> { + self.configure(evm); // Run inner handler and catch all errors to handle cleanup. match self.run_without_catch_error(evm) { Ok(output) => Ok(output), @@ -105,6 +107,15 @@ pub trait Handler { } } + /// Configure the handler: + /// * Set Instruction gas table to the spec id. + #[inline] + fn configure(&mut self, evm: &mut Self::Evm) { + let spec_id = evm.ctx().cfg().spec().into(); + // sets static gas depending on the spec id. + evm.ctx_instructions().1.set_spec(spec_id); + } + /// Runs the system call. /// /// System call is a special transaction where caller is a [`crate::SYSTEM_ADDRESS`] @@ -126,6 +137,8 @@ pub trait Handler { ) -> Result, Self::Error> { // dummy values that are not used. let init_and_floor_gas = InitialAndFloorGas::new(0, 0); + // configure the evm for system call. + self.configure(evm); // call execution and than output. match self .execution(evm, &init_and_floor_gas) diff --git a/crates/handler/src/instructions.rs b/crates/handler/src/instructions.rs index b45582f0d4..1af53ddbc6 100644 --- a/crates/handler/src/instructions.rs +++ b/crates/handler/src/instructions.rs @@ -1,12 +1,14 @@ use auto_impl::auto_impl; use interpreter::{ - instructions::{instruction_table, InstructionTable}, + gas::params::GasParams, + instructions::{instruction_table_gas_changes_spec, InstructionTable}, Host, Instruction, InterpreterTypes, }; +use primitives::hardfork::SpecId; use std::boxed::Box; /// Stores instructions for EVM. -#[auto_impl(&, Arc, Rc)] +#[auto_impl(&mut, Box)] pub trait InstructionProvider { /// Context type. type Context; @@ -15,6 +17,12 @@ pub trait InstructionProvider { /// Returns the instruction table that is used by EvmTr to execute instructions. fn instruction_table(&self) -> &InstructionTable; + + /// Returns the gas params that is used by EvmTr to execute instructions. + fn gas_params(&self) -> GasParams; + + /// Sets the spec. Return true if the spec was changed. + fn set_spec(&mut self, spec: SpecId) -> bool; } /// Ethereum instruction contains list of mainnet instructions that is used for Interpreter execution. @@ -22,6 +30,10 @@ pub trait InstructionProvider { pub struct EthInstructions { /// Table containing instruction implementations indexed by opcode. pub instruction_table: Box>, + /// Gas params that sets gas costs for instructions. + pub gas_params: GasParams, + /// Spec that is used to set gas costs for instructions. + pub spec: SpecId, } impl Clone for EthInstructions @@ -31,6 +43,8 @@ where fn clone(&self) -> Self { Self { instruction_table: self.instruction_table.clone(), + gas_params: self.gas_params.clone(), + spec: self.spec, } } } @@ -42,14 +56,25 @@ where { /// Returns `EthInstructions` with mainnet spec. pub fn new_mainnet() -> Self { - Self::new(instruction_table::()) + let spec = SpecId::default(); + Self::new( + instruction_table_gas_changes_spec(spec), + GasParams::new_spec(spec), + spec, + ) } /// Returns a new instance of `EthInstructions` with custom instruction table. #[inline] - pub fn new(base_table: InstructionTable) -> Self { + pub fn new( + base_table: InstructionTable, + gas_params: GasParams, + spec: SpecId, + ) -> Self { Self { instruction_table: Box::new(base_table), + gas_params, + spec, } } @@ -71,6 +96,20 @@ where fn instruction_table(&self) -> &InstructionTable { &self.instruction_table } + + fn gas_params(&self) -> GasParams { + self.gas_params.clone() + } + + fn set_spec(&mut self, spec: SpecId) -> bool { + if spec == self.spec { + return false; + } + self.instruction_table = Box::new(instruction_table_gas_changes_spec(spec)); + self.gas_params = GasParams::new_spec(spec); + + true + } } impl Default for EthInstructions diff --git a/crates/inspector/src/handler.rs b/crates/inspector/src/handler.rs index 53bdf436db..fe43a71b46 100644 --- a/crates/inspector/src/handler.rs +++ b/crates/inspector/src/handler.rs @@ -42,6 +42,7 @@ where &mut self, evm: &mut Self::Evm, ) -> Result, Self::Error> { + self.configure(evm); match self.inspect_run_without_catch_error(evm) { Ok(output) => Ok(output), Err(e) => self.catch_error(evm, e), @@ -136,6 +137,8 @@ where ) -> Result, Self::Error> { // dummy values that are not used. let init_and_floor_gas = InitialAndFloorGas::new(0, 0); + // configure + self.configure(evm); // call execution with inspection and then output. match self .inspect_execution(evm, &init_and_floor_gas) diff --git a/crates/interpreter/src/gas.rs b/crates/interpreter/src/gas.rs index 37a0421835..a8ac93a37c 100644 --- a/crates/interpreter/src/gas.rs +++ b/crates/interpreter/src/gas.rs @@ -2,6 +2,7 @@ mod calc; mod constants; +pub mod params; pub use calc::*; pub use constants::*; @@ -91,11 +92,6 @@ impl Gas { self.remaining } - /// Return remaining gas after subtracting 63/64 parts. - pub const fn remaining_63_of_64_parts(&self) -> u64 { - self.remaining - self.remaining / 64 - } - /// Erases a gas cost from the totals. #[inline] pub fn erase_cost(&mut self, returned: u64) { @@ -199,15 +195,30 @@ impl MemoryGas { } } + /// Sets the number of words and the expansion cost. + /// + /// Returns the difference between the new and old expansion cost. + #[inline] + pub fn set_words_num(&mut self, words_num: usize, mut expansion_cost: u64) -> Option { + self.words_num = words_num; + core::mem::swap(&mut self.expansion_cost, &mut expansion_cost); + self.expansion_cost.checked_sub(expansion_cost) + } + /// Records a new memory length and calculates additional cost if memory is expanded. /// Returns the additional gas cost required, or None if no expansion is needed. #[inline] - pub fn record_new_len(&mut self, new_num: usize) -> Option { + pub fn record_new_len( + &mut self, + new_num: usize, + linear_cost: u64, + quadratic_cost: u64, + ) -> Option { if new_num <= self.words_num { return None; } self.words_num = new_num; - let mut cost = crate::gas::calc::memory_gas(new_num); + let mut cost = crate::gas::calc::memory_gas(new_num, linear_cost, quadratic_cost); core::mem::swap(&mut self.expansion_cost, &mut cost); // Safe to subtract because we know that new_len > length // Notice the swap above. diff --git a/crates/interpreter/src/gas/calc.rs b/crates/interpreter/src/gas/calc.rs index 8b41601c01..06d9e6550d 100644 --- a/crates/interpreter/src/gas/calc.rs +++ b/crates/interpreter/src/gas/calc.rs @@ -1,70 +1,10 @@ use super::constants::*; -use crate::{num_words, tri, SStoreResult, SelfDestructResult, StateLoad}; -use context_interface::{ - journaled_state::AccountLoad, transaction::AccessListItemTr as _, Transaction, TransactionType, -}; +use crate::num_words; +use context_interface::{transaction::AccessListItemTr as _, Transaction, TransactionType}; use primitives::{eip7702, hardfork::SpecId, U256}; -/// `SSTORE` opcode refund calculation. -#[allow(clippy::collapsible_else_if)] #[inline] -pub fn sstore_refund(spec_id: SpecId, vals: &SStoreResult) -> i64 { - if spec_id.is_enabled_in(SpecId::ISTANBUL) { - // EIP-3529: Reduction in refunds - let sstore_clears_schedule = if spec_id.is_enabled_in(SpecId::LONDON) { - (SSTORE_RESET - COLD_SLOAD_COST + ACCESS_LIST_STORAGE_KEY) as i64 - } else { - REFUND_SSTORE_CLEARS - }; - if vals.is_new_eq_present() { - 0 - } else { - if vals.is_original_eq_present() && vals.is_new_zero() { - sstore_clears_schedule - } else { - let mut refund = 0; - - if !vals.is_original_zero() { - if vals.is_present_zero() { - refund -= sstore_clears_schedule; - } else if vals.is_new_zero() { - refund += sstore_clears_schedule; - } - } - - if vals.is_original_eq_new() { - let (gas_sstore_reset, gas_sload) = if spec_id.is_enabled_in(SpecId::BERLIN) { - (SSTORE_RESET - COLD_SLOAD_COST, WARM_STORAGE_READ_COST) - } else { - (SSTORE_RESET, sload_cost(spec_id, false)) - }; - if vals.is_original_zero() { - refund += (SSTORE_SET - gas_sload) as i64; - } else { - refund += (gas_sstore_reset - gas_sload) as i64; - } - } - - refund - } - } - } else { - if !vals.is_present_zero() && vals.is_new_zero() { - REFUND_SSTORE_CLEARS - } else { - 0 - } - } -} - -/// `CREATE2` opcode cost calculation. -#[inline] -pub const fn create2_cost(len: usize) -> Option { - CREATE.checked_add(tri!(cost_per_word(len, KECCAK256WORD))) -} - -#[inline] -const fn log2floor(value: U256) -> u64 { +pub(crate) const fn log2floor(value: U256) -> u64 { let mut l: u64 = 256; let mut i = 3; loop { @@ -86,62 +26,6 @@ const fn log2floor(value: U256) -> u64 { l } -/// `EXP` opcode cost calculation. -#[inline] -pub fn exp_cost(spec_id: SpecId, power: U256) -> Option { - if power.is_zero() { - Some(EXP) - } else { - // EIP-160: EXP cost increase - let gas_byte = U256::from(if spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) { - 50 - } else { - 10 - }); - let gas = U256::from(EXP) - .checked_add(gas_byte.checked_mul(U256::from(log2floor(power) / 8 + 1))?)?; - - u64::try_from(gas).ok() - } -} - -/// `*COPY` opcodes cost calculation. -#[inline] -pub const fn copy_cost_verylow(len: usize) -> Option { - copy_cost(VERYLOW, len) -} - -/// `EXTCODECOPY` opcode cost calculation. -#[inline] -pub const fn extcodecopy_cost(spec_id: SpecId, len: usize, is_cold: bool) -> Option { - let base_gas = if spec_id.is_enabled_in(SpecId::BERLIN) { - warm_cold_cost(is_cold) - } else if spec_id.is_enabled_in(SpecId::TANGERINE) { - 700 - } else { - 20 - }; - copy_cost(base_gas, len) -} - -#[inline] -/// Calculates the gas cost for copy operations based on data length. -pub const fn copy_cost(base_cost: u64, len: usize) -> Option { - base_cost.checked_add(tri!(cost_per_word(len, COPY))) -} - -/// `LOG` opcode cost calculation. -#[inline] -pub const fn log_cost(n: u8, len: u64) -> Option { - tri!(LOG.checked_add(tri!(LOGDATA.checked_mul(len)))).checked_add(LOGTOPIC * n as u64) -} - -/// `KECCAK256` opcode cost calculation. -#[inline] -pub const fn keccak256_cost(len: usize) -> Option { - KECCAK256.checked_add(tri!(cost_per_word(len, KECCAK256WORD))) -} - /// Calculate the cost of buffer per word. #[inline] pub const fn cost_per_word(len: usize, multiple: u64) -> Option { @@ -161,214 +45,13 @@ pub const fn initcode_cost(len: usize) -> u64 { cost } -/// `SLOAD` opcode cost calculation. -#[inline] -pub const fn sload_cost(spec_id: SpecId, is_cold: bool) -> u64 { - if spec_id.is_enabled_in(SpecId::BERLIN) { - if is_cold { - COLD_SLOAD_COST - } else { - WARM_STORAGE_READ_COST - } - } else if spec_id.is_enabled_in(SpecId::ISTANBUL) { - // EIP-1884: Repricing for trie-size-dependent opcodes - ISTANBUL_SLOAD_GAS - } else if spec_id.is_enabled_in(SpecId::TANGERINE) { - // EIP-150: Gas cost changes for IO-heavy operations - 200 - } else { - 50 - } -} - -/// Static gas cost for sstore. -#[inline] -pub const fn sstore_cost_static(spec_id: SpecId) -> u64 { - if spec_id.is_enabled_in(SpecId::BERLIN) { - WARM_STORAGE_READ_COST - } else if spec_id.is_enabled_in(SpecId::ISTANBUL) { - ISTANBUL_SLOAD_GAS - } else { - SSTORE_RESET - } -} - -/// Dynamic gas cost for sstore. -#[inline] -pub const fn sstore_cost_dynamic(spec_id: SpecId, vals: &SStoreResult, is_cold: bool) -> u64 { - sstore_cost(spec_id, vals, is_cold) - sstore_cost_static(spec_id) -} - -/// Static gas cost for sstore. -#[inline] -pub const fn static_sstore_cost(spec_id: SpecId) -> u64 { - if spec_id.is_enabled_in(SpecId::BERLIN) { - WARM_STORAGE_READ_COST - } else if spec_id.is_enabled_in(SpecId::ISTANBUL) { - ISTANBUL_SLOAD_GAS - } else { - SSTORE_RESET - } -} - -/// Dynamic gas cost for sstore. -#[inline] -pub const fn dyn_sstore_cost(spec_id: SpecId, vals: &SStoreResult, is_cold: bool) -> u64 { - sstore_cost(spec_id, vals, is_cold) - static_sstore_cost(spec_id) -} - -/// `SSTORE` opcode cost calculation. -#[inline] -pub const fn sstore_cost(spec_id: SpecId, vals: &SStoreResult, is_cold: bool) -> u64 { - if spec_id.is_enabled_in(SpecId::BERLIN) { - // Berlin specification logic - let mut gas_cost = istanbul_sstore_cost::(vals); - - if is_cold { - gas_cost += COLD_SLOAD_COST; - } - gas_cost - } else if spec_id.is_enabled_in(SpecId::ISTANBUL) { - // Istanbul logic - istanbul_sstore_cost::(vals) - } else { - // Frontier logic - frontier_sstore_cost(vals) - } -} - -/// EIP-2200: Structured Definitions for Net Gas Metering -#[inline] -const fn istanbul_sstore_cost( - vals: &SStoreResult, -) -> u64 { - if vals.is_new_eq_present() { - SLOAD_GAS - } else if vals.is_original_eq_present() && vals.is_original_zero() { - SSTORE_SET - } else if vals.is_original_eq_present() { - SSTORE_RESET_GAS - } else { - SLOAD_GAS - } -} - -/// Frontier sstore cost just had two cases set and reset values. -#[inline] -const fn frontier_sstore_cost(vals: &SStoreResult) -> u64 { - if vals.is_present_zero() && !vals.is_new_zero() { - SSTORE_SET - } else { - SSTORE_RESET - } -} - -/// Static gas cost for selfdestruct. -#[inline] -pub const fn static_selfdestruct_cost(spec_id: SpecId) -> u64 { - // EIP-150: Gas cost changes for IO-heavy operations - if spec_id.is_enabled_in(SpecId::TANGERINE) { - 5000 - } else { - 0 - } -} - -/// `SELFDESTRUCT` opcode cost calculation. -#[inline] -pub const fn dyn_selfdestruct_cost(spec_id: SpecId, res: &StateLoad) -> u64 { - let is_tangerine = spec_id.is_enabled_in(SpecId::TANGERINE); - let mut gas = 0; - - // EIP-161: State trie clearing (invariant-preserving alternative) - let should_charge_topup = if spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) { - res.data.had_value && !res.data.target_exists - } else { - !res.data.target_exists - }; - - // EIP-150: Gas cost changes for IO-heavy operations - if is_tangerine && should_charge_topup { - gas += NEWACCOUNT - } - - if res.is_cold { - gas += selfdestruct_cold_beneficiary_cost(spec_id); - } - - gas -} - -/// EIP-2929: Gas cost increases for state access opcodes -#[inline] -pub const fn selfdestruct_cold_beneficiary_cost(spec_id: SpecId) -> u64 { - if spec_id.is_enabled_in(SpecId::BERLIN) { - COLD_ACCOUNT_ACCESS_COST - } else { - 0 - } -} - -/// `SELFDESTRUCT` opcode cost calculation. -#[inline] -pub const fn selfdestruct_cost(spec_id: SpecId, res: StateLoad) -> u64 { - static_selfdestruct_cost(spec_id) + dyn_selfdestruct_cost(spec_id, &res) -} - -/// Calculate static gas for the call -/// -/// Gas depends on: -/// * Spec. For berlin hardfork only warm gas [`WARM_STORAGE_READ_COST`] is calculated. -/// * If there is transfer value. additional gas of [`CALLVALUE`] is added. -#[inline] -pub fn calc_call_static_gas(spec_id: SpecId, has_transfer: bool) -> u64 { - // Account access. - let mut gas = if spec_id.is_enabled_in(SpecId::BERLIN) { - WARM_STORAGE_READ_COST - } else if spec_id.is_enabled_in(SpecId::TANGERINE) { - // EIP-150: Gas cost changes for IO-heavy operations - 700 - } else { - 40 - }; - - // Transfer value cost - if has_transfer { - gas += CALLVALUE; - } - - gas -} - -/// Berlin warm and cold storage access cost for account access. -#[inline] -pub const fn warm_cold_cost(is_cold: bool) -> u64 { - if is_cold { - COLD_ACCOUNT_ACCESS_COST - } else { - WARM_STORAGE_READ_COST - } -} - -/// Berlin warm and cold storage access cost for account access. -/// -/// If delegation is Some, add additional cost for delegation account load. -#[inline] -pub const fn warm_cold_cost_with_delegation(load: StateLoad) -> u64 { - let mut gas = warm_cold_cost(load.is_cold); - if let Some(is_cold) = load.data.is_delegate_account_cold { - gas += warm_cold_cost(is_cold); - } - gas -} - /// Memory expansion cost calculation for a given number of words. #[inline] -pub const fn memory_gas(num_words: usize) -> u64 { +pub const fn memory_gas(num_words: usize, linear_cost: u64, quadratic_cost: u64) -> u64 { let num_words = num_words as u64; - MEMORY + linear_cost .saturating_mul(num_words) - .saturating_add(num_words.saturating_mul(num_words) / 512) + .saturating_add(num_words.saturating_mul(num_words) / quadratic_cost) } /// Init and floor gas from transaction diff --git a/crates/interpreter/src/gas/constants.rs b/crates/interpreter/src/gas/constants.rs index 1739e8ca08..a4a101a985 100644 --- a/crates/interpreter/src/gas/constants.rs +++ b/crates/interpreter/src/gas/constants.rs @@ -83,6 +83,7 @@ pub const EOF_CREATE_GAS: u64 = 32000; pub const ACCESS_LIST_ADDRESS: u64 = 2400; /// Gas cost for accessing a storage key in the access list (EIP-2930). pub const ACCESS_LIST_STORAGE_KEY: u64 = 1900; + /// Gas cost for SLOAD when accessing a cold storage slot (EIP-2929). pub const COLD_SLOAD_COST: u64 = 2100; /// Gas cost for accessing a cold account (EIP-2929). @@ -90,8 +91,6 @@ pub const COLD_ACCOUNT_ACCESS_COST: u64 = 2600; /// Additional gas cost for accessing a cold account. pub const COLD_ACCOUNT_ACCESS_COST_ADDITIONAL: u64 = COLD_ACCOUNT_ACCESS_COST - WARM_STORAGE_READ_COST; -/// Additional gas cost for accessing a cold storage -pub const COLD_SLOAD_COST_ADDITIONAL: u64 = COLD_SLOAD_COST - WARM_STORAGE_READ_COST; /// Gas cost for reading from a warm storage slot (EIP-2929). pub const WARM_STORAGE_READ_COST: u64 = 100; /// Gas cost for SSTORE reset operation on a warm storage slot. @@ -102,5 +101,3 @@ pub const INITCODE_WORD_COST: u64 = 2; /// Gas stipend provided to the recipient of a CALL with value transfer. pub const CALL_STIPEND: u64 = 2300; -/// Minimum gas that must be provided to a callee. -pub const MIN_CALLEE_GAS: u64 = CALL_STIPEND; diff --git a/crates/interpreter/src/gas/params.rs b/crates/interpreter/src/gas/params.rs new file mode 100644 index 0000000000..77db84955b --- /dev/null +++ b/crates/interpreter/src/gas/params.rs @@ -0,0 +1,775 @@ +//! Gas table for dynamic gas constants. + +use crate::{ + gas::{self, log2floor, ISTANBUL_SLOAD_GAS, SSTORE_RESET, SSTORE_SET, WARM_SSTORE_RESET}, + num_words, +}; +use context_interface::context::SStoreResult; +use primitives::{ + hardfork::SpecId::{self}, + U256, +}; +use std::sync::Arc; + +/// Gas table for dynamic gas constants. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct GasParams { + /// Table of gas costs for operations + table: Arc<[u64; 256]>, + /// Pointer to the table. + ptr: *const u64, +} + +#[cfg(feature = "serde")] +mod serde { + use super::{Arc, GasParams}; + use std::vec::Vec; + + #[derive(serde::Serialize, serde::Deserialize)] + struct GasParamsSerde { + table: Vec, + } + + #[cfg(feature = "serde")] + impl serde::Serialize for GasParams { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + GasParamsSerde { + table: self.table.to_vec(), + } + .serialize(serializer) + } + } + + impl<'de> serde::Deserialize<'de> for GasParams { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let table = GasParamsSerde::deserialize(deserializer)?; + if table.table.len() != 256 { + return Err(serde::de::Error::custom("Invalid gas params length")); + } + Ok(Self::new(Arc::new(table.table.try_into().unwrap()))) + } + } +} + +impl Default for GasParams { + fn default() -> Self { + Self::new_spec(SpecId::default()) + } +} + +impl GasParams { + /// Creates a new `GasParams` with the given table. + #[inline] + pub fn new(table: Arc<[u64; 256]>) -> Self { + Self { + ptr: table.as_ptr(), + table, + } + } + + /// Overrides the gas cost for the given gas id. + /// + /// It will clone underlying table and override the values. + /// + /// Use to override default gas cost + /// + /// ```rust + /// use revm_interpreter::gas::params::{GasParams, GasId}; + /// use primitives::hardfork::SpecId; + /// + /// let mut gas_table = GasParams::new_spec(SpecId::default()); + /// gas_table.override_gas([(GasId::memory_linear_cost(), 2), (GasId::memory_quadratic_reduction(), 512)].into_iter()); + /// assert_eq!(gas_table.get(GasId::memory_linear_cost()), 2); + /// assert_eq!(gas_table.get(GasId::memory_quadratic_reduction()), 512); + /// ``` + pub fn override_gas(&mut self, values: impl IntoIterator) { + let mut table = *self.table.clone(); + for (id, value) in values.into_iter() { + table[id.as_usize()] = value; + } + *self = Self::new(Arc::new(table)); + } + + /// Returns the table. + #[inline] + pub fn table(&self) -> &[u64; 256] { + &self.table + } + + /// Creates a new `GasParams` for the given spec. + #[inline] + pub fn new_spec(spec: SpecId) -> Self { + let mut table = [0; 256]; + + table[GasId::exp_byte_gas().as_usize()] = 10; + table[GasId::logdata().as_usize()] = gas::LOGDATA; + table[GasId::logtopic().as_usize()] = gas::LOGTOPIC; + table[GasId::copy_per_word().as_usize()] = gas::COPY; + table[GasId::extcodecopy_per_word().as_usize()] = gas::COPY; + table[GasId::mcopy_per_word().as_usize()] = gas::COPY; + table[GasId::keccak256_per_word().as_usize()] = gas::KECCAK256WORD; + table[GasId::memory_linear_cost().as_usize()] = gas::MEMORY; + table[GasId::memory_quadratic_reduction().as_usize()] = 512; + table[GasId::initcode_per_word().as_usize()] = gas::INITCODE_WORD_COST; + table[GasId::create().as_usize()] = gas::CREATE; + table[GasId::call_stipend_reduction().as_usize()] = 64; + table[GasId::transfer_value_cost().as_usize()] = gas::CALLVALUE; + table[GasId::cold_account_additional_cost().as_usize()] = 0; + table[GasId::new_account_cost().as_usize()] = gas::NEWACCOUNT; + table[GasId::warm_storage_read_cost().as_usize()] = 0; + // Frontiers had fixed 5k cost. + table[GasId::sstore_static().as_usize()] = SSTORE_RESET; + // SSTORE SET + table[GasId::sstore_set_without_load_cost().as_usize()] = SSTORE_SET - SSTORE_RESET; + // SSTORE RESET Is covered in SSTORE_STATIC. + table[GasId::sstore_reset_without_cold_load_cost().as_usize()] = 0; + // SSTORE CLEARING SLOT REFUND + table[GasId::sstore_clearing_slot_refund().as_usize()] = 15000; + table[GasId::selfdestruct_refund().as_usize()] = 24000; + table[GasId::call_stipend().as_usize()] = gas::CALL_STIPEND; + table[GasId::cold_storage_additional_cost().as_usize()] = 0; + table[GasId::cold_storage_cost().as_usize()] = 0; + table[GasId::new_account_cost_for_selfdestruct().as_usize()] = 0; + + if spec.is_enabled_in(SpecId::TANGERINE) { + table[GasId::new_account_cost_for_selfdestruct().as_usize()] = gas::NEWACCOUNT; + } + + if spec.is_enabled_in(SpecId::SPURIOUS_DRAGON) { + table[GasId::exp_byte_gas().as_usize()] = 50; + } + + if spec.is_enabled_in(SpecId::ISTANBUL) { + table[GasId::sstore_static().as_usize()] = gas::ISTANBUL_SLOAD_GAS; + table[GasId::sstore_set_without_load_cost().as_usize()] = + SSTORE_SET - ISTANBUL_SLOAD_GAS; + table[GasId::sstore_reset_without_cold_load_cost().as_usize()] = + SSTORE_RESET - ISTANBUL_SLOAD_GAS; + } + + if spec.is_enabled_in(SpecId::BERLIN) { + table[GasId::sstore_static().as_usize()] = gas::WARM_STORAGE_READ_COST; + table[GasId::cold_account_additional_cost().as_usize()] = + gas::COLD_ACCOUNT_ACCESS_COST_ADDITIONAL; + table[GasId::cold_storage_additional_cost().as_usize()] = + gas::COLD_SLOAD_COST - gas::WARM_STORAGE_READ_COST; + table[GasId::cold_storage_cost().as_usize()] = gas::COLD_SLOAD_COST; + table[GasId::warm_storage_read_cost().as_usize()] = gas::WARM_STORAGE_READ_COST; + + table[GasId::sstore_reset_without_cold_load_cost().as_usize()] = + WARM_SSTORE_RESET - gas::WARM_STORAGE_READ_COST; + table[GasId::sstore_set_without_load_cost().as_usize()] = + SSTORE_SET - gas::WARM_STORAGE_READ_COST; + } + + if spec.is_enabled_in(SpecId::LONDON) { + // EIP-3529: Reduction in refunds + + // Replace SSTORE_CLEARS_SCHEDULE (as defined in EIP-2200) with + // SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST (4,800 gas as of EIP-2929 + EIP-2930) + table[GasId::sstore_clearing_slot_refund().as_usize()] = + WARM_SSTORE_RESET + gas::ACCESS_LIST_STORAGE_KEY; + + table[GasId::selfdestruct_refund().as_usize()] = 0; + } + + Self::new(Arc::new(table)) + } + + /// Gets the gas cost for the given gas id. + #[inline] + pub const fn get(&self, id: GasId) -> u64 { + unsafe { *self.ptr.add(id.as_usize()) } + } + + /// `EXP` opcode cost calculation. + #[inline] + pub fn exp_cost(&self, power: U256) -> u64 { + if power.is_zero() { + return 0; + } + // EIP-160: EXP cost increase + self.get(GasId::exp_byte_gas()) + .saturating_mul(log2floor(power) / 8 + 1) + } + + /// Selfdestruct refund. + #[inline] + pub fn selfdestruct_refund(&self) -> i64 { + self.get(GasId::selfdestruct_refund()) as i64 + } + + /// Selfdestruct cost. + #[inline] + pub fn selfdestruct_cost(&self, should_charge_topup: bool, is_cold: bool) -> u64 { + let mut gas = 0; + + // EIP-150: Gas cost changes for IO-heavy operations + if should_charge_topup { + gas += self.new_account_cost_for_selfdestruct(); + } + + if is_cold { + // Note: SELFDESTRUCT does not charge a WARM_STORAGE_READ_COST in case the recipient is already warm, + // which differs from how the other call-variants work. The reasoning behind this is to keep + // the changes small, a SELFDESTRUCT already costs 5K and is a no-op if invoked more than once. + // + // For GasParams both values are zero before BERLIN fork. + gas += self.cold_account_additional_cost() + self.warm_storage_read_cost(); + } + gas + } + + /// EXTCODECOPY gas cost + #[inline] + pub fn extcodecopy(&self, len: usize) -> u64 { + self.get(GasId::extcodecopy_per_word()) + .saturating_mul(num_words(len) as u64) + } + + /// MCOPY gas cost + #[inline] + pub fn mcopy_cost(&self, len: usize) -> u64 { + self.get(GasId::mcopy_per_word()) + .saturating_mul(num_words(len) as u64) + } + + /// Static gas cost for SSTORE opcode + #[inline] + pub fn sstore_static_gas(&self) -> u64 { + self.get(GasId::sstore_static()) + } + + /// SSTORE set cost + #[inline] + pub fn sstore_set_without_load_cost(&self) -> u64 { + self.get(GasId::sstore_set_without_load_cost()) + } + + /// SSTORE reset cost + #[inline] + pub fn sstore_reset_without_cold_load_cost(&self) -> u64 { + self.get(GasId::sstore_reset_without_cold_load_cost()) + } + + /// SSTORE clearing slot refund + #[inline] + pub fn sstore_clearing_slot_refund(&self) -> u64 { + self.get(GasId::sstore_clearing_slot_refund()) + } + + /// Dynamic gas cost for SSTORE opcode. + /// + /// Dynamic gas cost is gas that needs input from SSTORE operation to be calculated. + #[inline] + pub fn sstore_dynamic_gas(&self, is_istanbul: bool, vals: &SStoreResult, is_cold: bool) -> u64 { + // frontier logic gets charged for every SSTORE operation if original value is zero. + // this behaviour is fixed in istanbul fork. + if !is_istanbul { + if vals.is_present_zero() && !vals.is_new_zero() { + return self.sstore_set_without_load_cost(); + } else { + return self.sstore_reset_without_cold_load_cost(); + } + } + + let mut gas = 0; + + // this will be zero before berlin fork. + if is_cold { + gas += self.cold_storage_cost(); + } + + // if new values changed present value and present value is unchanged from original. + if vals.new_values_changes_present() && vals.is_original_eq_present() { + gas += if vals.is_original_zero() { + // set cost for creating storage slot (Zero slot means it is not existing). + // and previous condition says present is same as original. + self.sstore_set_without_load_cost() + } else { + // if new value is not zero, this means we are setting some value to it. + self.sstore_reset_without_cold_load_cost() + }; + } + gas + } + + /// SSTORE refund calculation. + #[inline] + pub fn sstore_refund(&self, is_istanbul: bool, vals: &SStoreResult) -> i64 { + // EIP-3529: Reduction in refunds + let sstore_clearing_slot_refund = self.sstore_clearing_slot_refund() as i64; + + if !is_istanbul { + // // before istanbul fork, refund was always awarded without checking original state. + if !vals.is_present_zero() && vals.is_new_zero() { + return sstore_clearing_slot_refund; + } + return 0; + } + + // If current value equals new value (this is a no-op) + if vals.is_new_eq_present() { + return 0; + } + + // refund for the clearing of storage slot. + // As new is not equal to present, new values zero means that original and present values are not zero + if vals.is_original_eq_present() && vals.is_new_zero() { + return sstore_clearing_slot_refund; + } + + let mut refund = 0; + // If original value is not 0 + if !vals.is_original_zero() { + // If current value is 0 (also means that new value is not 0), + if vals.is_present_zero() { + // remove SSTORE_CLEARS_SCHEDULE gas from refund counter. + refund -= sstore_clearing_slot_refund; + // If new value is 0 (also means that current value is not 0), + } else if vals.is_new_zero() { + // add SSTORE_CLEARS_SCHEDULE gas to refund counter. + refund += sstore_clearing_slot_refund; + } + } + + // If original value equals new value (this storage slot is reset) + if vals.is_original_eq_new() { + // If original value is 0 + if vals.is_original_zero() { + // add SSTORE_SET_GAS - SLOAD_GAS to refund counter. + refund += self.sstore_set_without_load_cost() as i64; + // Otherwise + } else { + // add SSTORE_RESET_GAS - SLOAD_GAS gas to refund counter. + refund += self.sstore_reset_without_cold_load_cost() as i64; + } + } + refund + } + + /// `LOG` opcode cost calculation. + #[inline] + pub const fn log_cost(&self, n: u8, len: u64) -> u64 { + self.get(GasId::logdata()) + .saturating_mul(len) + .saturating_add(self.get(GasId::logtopic()) * n as u64) + } + + /// KECCAK256 gas cost per word + #[inline] + pub fn keccak256_cost(&self, len: usize) -> u64 { + self.get(GasId::keccak256_per_word()) + .saturating_mul(num_words(len) as u64) + } + + /// Memory gas cost + #[inline] + pub fn memory_cost(&self, len: usize) -> u64 { + let len = len as u64; + self.get(GasId::memory_linear_cost()) + .saturating_mul(len) + .saturating_add( + (len.saturating_mul(len)) + .saturating_div(self.get(GasId::memory_quadratic_reduction())), + ) + } + + /// Initcode word cost + #[inline] + pub fn initcode_cost(&self, len: usize) -> u64 { + self.get(GasId::initcode_per_word()) + .saturating_mul(num_words(len) as u64) + } + + /// Create gas cost + #[inline] + pub fn create_cost(&self) -> u64 { + self.get(GasId::create()) + } + + /// Create2 gas cost. + #[inline] + pub fn create2_cost(&self, len: usize) -> u64 { + self.get(GasId::create()).saturating_add( + self.get(GasId::keccak256_per_word()) + .saturating_mul(num_words(len) as u64), + ) + } + + /// Call stipend. + #[inline] + pub fn call_stipend(&self) -> u64 { + self.get(GasId::call_stipend()) + } + + /// Call stipend reduction. Call stipend is reduced by 1/64 of the gas limit. + #[inline] + pub fn call_stipend_reduction(&self, gas_limit: u64) -> u64 { + gas_limit - gas_limit / self.get(GasId::call_stipend_reduction()) + } + + /// Transfer value cost + #[inline] + pub fn transfer_value_cost(&self) -> u64 { + self.get(GasId::transfer_value_cost()) + } + + /// Additional cold cost. Additional cold cost is added to the gas cost if the account is cold loaded. + #[inline] + pub fn cold_account_additional_cost(&self) -> u64 { + self.get(GasId::cold_account_additional_cost()) + } + + /// Cold storage additional cost. + #[inline] + pub fn cold_storage_additional_cost(&self) -> u64 { + self.get(GasId::cold_storage_additional_cost()) + } + + /// Cold storage cost. + #[inline] + pub fn cold_storage_cost(&self) -> u64 { + self.get(GasId::cold_storage_cost()) + } + + /// New account cost. New account cost is added to the gas cost if the account is empty. + #[inline] + pub fn new_account_cost(&self, is_spurious_dragon: bool, transfers_value: bool) -> u64 { + // EIP-161: State trie clearing (invariant-preserving alternative) + // Pre-Spurious Dragon: always charge for new account + // Post-Spurious Dragon: only charge if value is transferred + if !is_spurious_dragon || transfers_value { + return self.get(GasId::new_account_cost()); + } + 0 + } + + /// New account cost for selfdestruct. + #[inline] + pub fn new_account_cost_for_selfdestruct(&self) -> u64 { + self.get(GasId::new_account_cost_for_selfdestruct()) + } + + /// Warm storage read cost. Warm storage read cost is added to the gas cost if the account is warm loaded. + #[inline] + pub fn warm_storage_read_cost(&self) -> u64 { + self.get(GasId::warm_storage_read_cost()) + } + + /// Copy cost + #[inline] + pub fn copy_cost(&self, len: usize) -> u64 { + self.copy_per_word_cost(num_words(len)) + } + + /// Copy per word cost + #[inline] + pub fn copy_per_word_cost(&self, word_num: usize) -> u64 { + self.get(GasId::copy_per_word()) + .saturating_mul(word_num as u64) + } +} + +/// Gas identifier that maps onto index in gas table. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GasId(u8); + +impl GasId { + /// Creates a new `GasId` with the given id. + pub const fn new(id: u8) -> Self { + Self(id) + } + + /// Returns the id of the gas. + pub const fn as_u8(&self) -> u8 { + self.0 + } + + /// Returns the id of the gas as a usize. + pub const fn as_usize(&self) -> usize { + self.0 as usize + } + + /// Returns the name of the gas identifier as a string. + /// + /// # Examples + /// + /// ``` + /// use revm_interpreter::gas::params::GasId; + /// + /// assert_eq!(GasId::exp_byte_gas().name(), "exp_byte_gas"); + /// assert_eq!(GasId::memory_linear_cost().name(), "memory_linear_cost"); + /// assert_eq!(GasId::sstore_static().name(), "sstore_static"); + /// ``` + pub const fn name(&self) -> &'static str { + match self.0 { + x if x == Self::exp_byte_gas().as_u8() => "exp_byte_gas", + x if x == Self::extcodecopy_per_word().as_u8() => "extcodecopy_per_word", + x if x == Self::copy_per_word().as_u8() => "copy_per_word", + x if x == Self::logdata().as_u8() => "logdata", + x if x == Self::logtopic().as_u8() => "logtopic", + x if x == Self::mcopy_per_word().as_u8() => "mcopy_per_word", + x if x == Self::keccak256_per_word().as_u8() => "keccak256_per_word", + x if x == Self::memory_linear_cost().as_u8() => "memory_linear_cost", + x if x == Self::memory_quadratic_reduction().as_u8() => "memory_quadratic_reduction", + x if x == Self::initcode_per_word().as_u8() => "initcode_per_word", + x if x == Self::create().as_u8() => "create", + x if x == Self::call_stipend_reduction().as_u8() => "call_stipend_reduction", + x if x == Self::transfer_value_cost().as_u8() => "transfer_value_cost", + x if x == Self::cold_account_additional_cost().as_u8() => { + "cold_account_additional_cost" + } + x if x == Self::new_account_cost().as_u8() => "new_account_cost", + x if x == Self::warm_storage_read_cost().as_u8() => "warm_storage_read_cost", + x if x == Self::sstore_static().as_u8() => "sstore_static", + x if x == Self::sstore_set_without_load_cost().as_u8() => { + "sstore_set_without_load_cost" + } + x if x == Self::sstore_reset_without_cold_load_cost().as_u8() => { + "sstore_reset_without_cold_load_cost" + } + x if x == Self::sstore_clearing_slot_refund().as_u8() => "sstore_clearing_slot_refund", + x if x == Self::selfdestruct_refund().as_u8() => "selfdestruct_refund", + x if x == Self::call_stipend().as_u8() => "call_stipend", + x if x == Self::cold_storage_additional_cost().as_u8() => { + "cold_storage_additional_cost" + } + x if x == Self::cold_storage_cost().as_u8() => "cold_storage_cost", + x if x == Self::new_account_cost_for_selfdestruct().as_u8() => { + "new_account_cost_for_selfdestruct" + } + _ => "unknown", + } + } + + /// Converts a string to a `GasId`. + /// + /// Returns `None` if the string does not match any known gas identifier. + /// + /// # Examples + /// + /// ``` + /// use revm_interpreter::gas::params::GasId; + /// + /// assert_eq!(GasId::from_name("exp_byte_gas"), Some(GasId::exp_byte_gas())); + /// assert_eq!(GasId::from_name("memory_linear_cost"), Some(GasId::memory_linear_cost())); + /// assert_eq!(GasId::from_name("invalid_name"), None); + /// ``` + pub fn from_name(s: &str) -> Option { + match s { + "exp_byte_gas" => Some(Self::exp_byte_gas()), + "extcodecopy_per_word" => Some(Self::extcodecopy_per_word()), + "copy_per_word" => Some(Self::copy_per_word()), + "logdata" => Some(Self::logdata()), + "logtopic" => Some(Self::logtopic()), + "mcopy_per_word" => Some(Self::mcopy_per_word()), + "keccak256_per_word" => Some(Self::keccak256_per_word()), + "memory_linear_cost" => Some(Self::memory_linear_cost()), + "memory_quadratic_reduction" => Some(Self::memory_quadratic_reduction()), + "initcode_per_word" => Some(Self::initcode_per_word()), + "create" => Some(Self::create()), + "call_stipend_reduction" => Some(Self::call_stipend_reduction()), + "transfer_value_cost" => Some(Self::transfer_value_cost()), + "cold_account_additional_cost" => Some(Self::cold_account_additional_cost()), + "new_account_cost" => Some(Self::new_account_cost()), + "warm_storage_read_cost" => Some(Self::warm_storage_read_cost()), + "sstore_static" => Some(Self::sstore_static()), + "sstore_set_without_load_cost" => Some(Self::sstore_set_without_load_cost()), + "sstore_reset_without_cold_load_cost" => { + Some(Self::sstore_reset_without_cold_load_cost()) + } + "sstore_clearing_slot_refund" => Some(Self::sstore_clearing_slot_refund()), + "selfdestruct_refund" => Some(Self::selfdestruct_refund()), + "call_stipend" => Some(Self::call_stipend()), + "cold_storage_additional_cost" => Some(Self::cold_storage_additional_cost()), + "cold_storage_cost" => Some(Self::cold_storage_cost()), + "new_account_cost_for_selfdestruct" => Some(Self::new_account_cost_for_selfdestruct()), + _ => None, + } + } + + /// EXP gas cost per byte + pub const fn exp_byte_gas() -> GasId { + Self::new(1) + } + + /// EXTCODECOPY gas cost per word + pub const fn extcodecopy_per_word() -> GasId { + Self::new(2) + } + + /// Copy copy per word + pub const fn copy_per_word() -> GasId { + Self::new(3) + } + + /// Log data gas cost per byte + pub const fn logdata() -> GasId { + Self::new(4) + } + + /// Log topic gas cost per topic + pub const fn logtopic() -> GasId { + Self::new(5) + } + + /// MCOPY gas cost per word + pub const fn mcopy_per_word() -> GasId { + Self::new(6) + } + + /// KECCAK256 gas cost per word + pub const fn keccak256_per_word() -> GasId { + Self::new(7) + } + + /// Memory linear cost. Memory is additionally added as n*linear_cost. + pub const fn memory_linear_cost() -> GasId { + Self::new(8) + } + + /// Memory quadratic reduction. Memory is additionally added as n*n/quadratic_reduction. + pub const fn memory_quadratic_reduction() -> GasId { + Self::new(9) + } + + /// Initcode word cost + pub const fn initcode_per_word() -> GasId { + Self::new(10) + } + + /// Create gas cost + pub const fn create() -> GasId { + Self::new(11) + } + + /// Call stipend reduction. Call stipend is reduced by 1/64 of the gas limit. + pub const fn call_stipend_reduction() -> GasId { + Self::new(12) + } + + /// Transfer value cost + pub const fn transfer_value_cost() -> GasId { + Self::new(13) + } + + /// Additional cold cost. Additional cold cost is added to the gas cost if the account is cold loaded. + pub const fn cold_account_additional_cost() -> GasId { + Self::new(14) + } + + /// New account cost. New account cost is added to the gas cost if the account is empty. + pub const fn new_account_cost() -> GasId { + Self::new(15) + } + + /// Warm storage read cost. Warm storage read cost is added to the gas cost if the account is warm loaded. + /// + /// Used in delegated account access to specify delegated account warm gas cost. + pub const fn warm_storage_read_cost() -> GasId { + Self::new(16) + } + + /// Static gas cost for SSTORE opcode. This gas in comparison with other gas const needs + /// to be deducted after check for minimal stipend gas cost. This is a reason why it is here. + pub const fn sstore_static() -> GasId { + Self::new(17) + } + + /// SSTORE set cost additional amount after SSTORE_RESET is added. + pub const fn sstore_set_without_load_cost() -> GasId { + Self::new(18) + } + + /// SSTORE reset cost + pub const fn sstore_reset_without_cold_load_cost() -> GasId { + Self::new(19) + } + + /// SSTORE clearing slot refund + pub const fn sstore_clearing_slot_refund() -> GasId { + Self::new(20) + } + + /// Selfdestruct refund. + pub const fn selfdestruct_refund() -> GasId { + Self::new(21) + } + + /// Call stipend checked in sstore. + pub const fn call_stipend() -> GasId { + Self::new(22) + } + + /// Cold storage additional cost. + pub const fn cold_storage_additional_cost() -> GasId { + Self::new(23) + } + + /// Cold storage cost + pub const fn cold_storage_cost() -> GasId { + Self::new(24) + } + + /// New account cost for selfdestruct. + pub const fn new_account_cost_for_selfdestruct() -> GasId { + Self::new(25) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + #[test] + fn test_gas_id_name_and_from_str_coverage() { + let mut unique_names = HashSet::new(); + let mut known_gas_ids = 0; + + // Iterate over all possible GasId values (0..256) + for i in 0..=255 { + let gas_id = GasId::new(i); + let name = gas_id.name(); + + // Count unique names (excluding "unknown") + if name != "unknown" { + unique_names.insert(name); + } + } + + // Now test from_str for each unique name + for name in &unique_names { + if let Some(gas_id) = GasId::from_name(name) { + known_gas_ids += 1; + // Verify round-trip: name -> GasId -> name should be consistent + assert_eq!(gas_id.name(), *name, "Round-trip failed for {}", name); + } + } + + println!("Total unique named GasIds: {}", unique_names.len()); + println!("GasIds resolvable via from_str: {}", known_gas_ids); + + // All unique names should be resolvable via from_str + assert_eq!( + unique_names.len(), + known_gas_ids, + "Not all unique names are resolvable via from_str" + ); + + // We should have exactly 25 known GasIds (based on the indices 1-25 used) + assert_eq!( + unique_names.len(), + 25, + "Expected 25 unique GasIds, found {}", + unique_names.len() + ); + } +} diff --git a/crates/interpreter/src/instructions.rs b/crates/interpreter/src/instructions.rs index 185b11b6e1..cae338c29f 100644 --- a/crates/interpreter/src/instructions.rs +++ b/crates/interpreter/src/instructions.rs @@ -27,7 +27,9 @@ pub mod tx_info; /// Utility functions and helpers for instruction implementation. pub mod utility; -use crate::{interpreter_types::InterpreterTypes, Host, InstructionContext}; +use primitives::hardfork::SpecId; + +use crate::{gas, interpreter_types::InterpreterTypes, Host, InstructionContext}; /// EVM opcode function signature. #[derive(Debug)] @@ -81,6 +83,51 @@ pub const fn instruction_table() -> [Instructio const { instruction_table_impl::() } } +/// Create a instruction table with applied spec changes to static gas cost. +#[inline] +pub fn instruction_table_gas_changes_spec( + spec: SpecId, +) -> [Instruction; 256] { + use bytecode::opcode::*; + use SpecId::*; + let mut table = instruction_table(); + + if spec.is_enabled_in(TANGERINE) { + // EIP-150: Gas cost changes for IO-heavy operations + table[SLOAD as usize].static_gas = 200; + table[BALANCE as usize].static_gas = 400; + table[EXTCODESIZE as usize].static_gas = 700; + table[EXTCODECOPY as usize].static_gas = 700; + table[CALL as usize].static_gas = 700; + table[CALLCODE as usize].static_gas = 700; + table[DELEGATECALL as usize].static_gas = 700; + table[STATICCALL as usize].static_gas = 700; + table[SELFDESTRUCT as usize].static_gas = 5000; + } + + if spec.is_enabled_in(ISTANBUL) { + // EIP-1884: Repricing for trie-size-dependent opcodes + table[SLOAD as usize].static_gas = gas::ISTANBUL_SLOAD_GAS; + table[BALANCE as usize].static_gas = 700; + table[EXTCODEHASH as usize].static_gas = 700; + } + + if spec.is_enabled_in(BERLIN) { + // warm account cost is base gas that is spend. Additional gas depends if account is cold loaded. + table[SLOAD as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[BALANCE as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[EXTCODESIZE as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[EXTCODEHASH as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[EXTCODECOPY as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[CALL as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[CALLCODE as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[DELEGATECALL as usize].static_gas = gas::WARM_STORAGE_READ_COST; + table[STATICCALL as usize].static_gas = gas::WARM_STORAGE_READ_COST; + } + + table +} + const fn instruction_table_impl() -> [Instruction; 256] { use bytecode::opcode::*; let mut table = [Instruction::unknown(); 256]; @@ -95,7 +142,7 @@ const fn instruction_table_impl() -> [Instructi table[SMOD as usize] = Instruction::new(arithmetic::smod, 5); table[ADDMOD as usize] = Instruction::new(arithmetic::addmod, 8); table[MULMOD as usize] = Instruction::new(arithmetic::mulmod, 8); - table[EXP as usize] = Instruction::new(arithmetic::exp, 0); // dynamic + table[EXP as usize] = Instruction::new(arithmetic::exp, gas::EXP); // base table[SIGNEXTEND as usize] = Instruction::new(arithmetic::signextend, 5); table[LT as usize] = Instruction::new(bitwise::lt, 3); @@ -114,25 +161,25 @@ const fn instruction_table_impl() -> [Instructi table[SAR as usize] = Instruction::new(bitwise::sar, 3); table[CLZ as usize] = Instruction::new(bitwise::clz, 5); - table[KECCAK256 as usize] = Instruction::new(system::keccak256, 0); // dynamic + table[KECCAK256 as usize] = Instruction::new(system::keccak256, gas::KECCAK256); table[ADDRESS as usize] = Instruction::new(system::address, 2); - table[BALANCE as usize] = Instruction::new(host::balance, 0); // dynamic + table[BALANCE as usize] = Instruction::new(host::balance, 20); table[ORIGIN as usize] = Instruction::new(tx_info::origin, 2); table[CALLER as usize] = Instruction::new(system::caller, 2); table[CALLVALUE as usize] = Instruction::new(system::callvalue, 2); table[CALLDATALOAD as usize] = Instruction::new(system::calldataload, 3); table[CALLDATASIZE as usize] = Instruction::new(system::calldatasize, 2); - table[CALLDATACOPY as usize] = Instruction::new(system::calldatacopy, 0); // static 2, mostly dynamic + table[CALLDATACOPY as usize] = Instruction::new(system::calldatacopy, 3); table[CODESIZE as usize] = Instruction::new(system::codesize, 2); - table[CODECOPY as usize] = Instruction::new(system::codecopy, 0); // static 2, mostly dynamic + table[CODECOPY as usize] = Instruction::new(system::codecopy, 3); table[GASPRICE as usize] = Instruction::new(tx_info::gasprice, 2); - table[EXTCODESIZE as usize] = Instruction::new(host::extcodesize, 0); // dynamic - table[EXTCODECOPY as usize] = Instruction::new(host::extcodecopy, 0); // dynamic + table[EXTCODESIZE as usize] = Instruction::new(host::extcodesize, 20); + table[EXTCODECOPY as usize] = Instruction::new(host::extcodecopy, 20); table[RETURNDATASIZE as usize] = Instruction::new(system::returndatasize, 2); - table[RETURNDATACOPY as usize] = Instruction::new(system::returndatacopy, 0); // static 2, mostly dynamic - table[EXTCODEHASH as usize] = Instruction::new(host::extcodehash, 0); // dynamic + table[RETURNDATACOPY as usize] = Instruction::new(system::returndatacopy, 3); + table[EXTCODEHASH as usize] = Instruction::new(host::extcodehash, 400); table[BLOCKHASH as usize] = Instruction::new(host::blockhash, 20); table[COINBASE as usize] = Instruction::new(block_info::coinbase, 2); table[TIMESTAMP as usize] = Instruction::new(block_info::timestamp, 2); @@ -149,8 +196,10 @@ const fn instruction_table_impl() -> [Instructi table[MLOAD as usize] = Instruction::new(memory::mload, 3); table[MSTORE as usize] = Instruction::new(memory::mstore, 3); table[MSTORE8 as usize] = Instruction::new(memory::mstore8, 3); - table[SLOAD as usize] = Instruction::new(host::sload, 0); // dynamic - table[SSTORE as usize] = Instruction::new(host::sstore, 0); // dynamic + table[SLOAD as usize] = Instruction::new(host::sload, 50); + // SSTORE static gas can be found in GasParams as check for minimal stipend + // needs to be done before deduction of static gas. + table[SSTORE as usize] = Instruction::new(host::sstore, 0); table[JUMP as usize] = Instruction::new(control::jump, 8); table[JUMPI as usize] = Instruction::new(control::jumpi, 10); table[PC as usize] = Instruction::new(control::pc, 2); @@ -159,7 +208,7 @@ const fn instruction_table_impl() -> [Instructi table[JUMPDEST as usize] = Instruction::new(control::jumpdest, 1); table[TLOAD as usize] = Instruction::new(host::tload, 100); table[TSTORE as usize] = Instruction::new(host::tstore, 100); - table[MCOPY as usize] = Instruction::new(memory::mcopy, 0); // static 2, mostly dynamic + table[MCOPY as usize] = Instruction::new(memory::mcopy, 3); // static 2, mostly dynamic table[PUSH0 as usize] = Instruction::new(stack::push0, 2); table[PUSH1 as usize] = Instruction::new(stack::push::<1, _, _>, 3); @@ -229,23 +278,23 @@ const fn instruction_table_impl() -> [Instructi table[SWAP15 as usize] = Instruction::new(stack::swap::<15, _, _>, 3); table[SWAP16 as usize] = Instruction::new(stack::swap::<16, _, _>, 3); - table[LOG0 as usize] = Instruction::new(host::log::<0, _>, 0); // dynamic - table[LOG1 as usize] = Instruction::new(host::log::<1, _>, 0); // dynamic - table[LOG2 as usize] = Instruction::new(host::log::<2, _>, 0); // dynamic - table[LOG3 as usize] = Instruction::new(host::log::<3, _>, 0); // dynamic - table[LOG4 as usize] = Instruction::new(host::log::<4, _>, 0); // dynamic + table[LOG0 as usize] = Instruction::new(host::log::<0, _>, gas::LOG); + table[LOG1 as usize] = Instruction::new(host::log::<1, _>, gas::LOG); + table[LOG2 as usize] = Instruction::new(host::log::<2, _>, gas::LOG); + table[LOG3 as usize] = Instruction::new(host::log::<3, _>, gas::LOG); + table[LOG4 as usize] = Instruction::new(host::log::<4, _>, gas::LOG); - table[CREATE as usize] = Instruction::new(contract::create::<_, false, _>, 0); // dynamic - table[CALL as usize] = Instruction::new(contract::call, 0); // dynamic - table[CALLCODE as usize] = Instruction::new(contract::call_code, 0); // dynamic + table[CREATE as usize] = Instruction::new(contract::create::<_, false, _>, 0); + table[CALL as usize] = Instruction::new(contract::call, 40); + table[CALLCODE as usize] = Instruction::new(contract::call_code, 40); table[RETURN as usize] = Instruction::new(control::ret, 0); - table[DELEGATECALL as usize] = Instruction::new(contract::delegate_call, 0); // dynamic - table[CREATE2 as usize] = Instruction::new(contract::create::<_, true, _>, 0); // dynamic + table[DELEGATECALL as usize] = Instruction::new(contract::delegate_call, 40); + table[CREATE2 as usize] = Instruction::new(contract::create::<_, true, _>, 0); - table[STATICCALL as usize] = Instruction::new(contract::static_call, 0); // dynamic + table[STATICCALL as usize] = Instruction::new(contract::static_call, 40); table[REVERT as usize] = Instruction::new(control::revert, 0); table[INVALID as usize] = Instruction::new(control::invalid, 0); - table[SELFDESTRUCT as usize] = Instruction::new(host::selfdestruct, 0); // dynamic + table[SELFDESTRUCT as usize] = Instruction::new(host::selfdestruct, 0); table } diff --git a/crates/interpreter/src/instructions/arithmetic.rs b/crates/interpreter/src/instructions/arithmetic.rs index 68333c9385..024938ad8d 100644 --- a/crates/interpreter/src/instructions/arithmetic.rs +++ b/crates/interpreter/src/instructions/arithmetic.rs @@ -1,35 +1,30 @@ use super::i256::{i256_div, i256_mod}; use crate::{ - gas, - interpreter_types::{InterpreterTypes, RuntimeFlag, StackTr}, + interpreter_types::{InterpreterTypes, StackTr}, InstructionContext, }; use primitives::U256; /// Implements the ADD instruction - adds two values from stack. pub fn add(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); *op2 = op1.wrapping_add(*op2); } /// Implements the MUL instruction - multiplies two values from stack. pub fn mul(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::LOW); popn_top!([op1], op2, context.interpreter); *op2 = op1.wrapping_mul(*op2); } /// Implements the SUB instruction - subtracts two values from stack. pub fn sub(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); *op2 = op1.wrapping_sub(*op2); } /// Implements the DIV instruction - divides two values from stack. pub fn div(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::LOW); popn_top!([op1], op2, context.interpreter); if !op2.is_zero() { *op2 = op1.wrapping_div(*op2); @@ -40,7 +35,6 @@ pub fn div(context: InstructionContext<'_, H, /// /// Performs signed division of two values from stack. pub fn sdiv(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::LOW); popn_top!([op1], op2, context.interpreter); *op2 = i256_div(op1, *op2); } @@ -49,7 +43,6 @@ pub fn sdiv(context: InstructionContext<'_, H /// /// Pops two values from stack and pushes the remainder of their division. pub fn rem(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::LOW); popn_top!([op1], op2, context.interpreter); if !op2.is_zero() { *op2 = op1.wrapping_rem(*op2); @@ -60,7 +53,6 @@ pub fn rem(context: InstructionContext<'_, H, /// /// Performs signed modulo of two values from stack. pub fn smod(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::LOW); popn_top!([op1], op2, context.interpreter); *op2 = i256_mod(op1, *op2) } @@ -69,7 +61,6 @@ pub fn smod(context: InstructionContext<'_, H /// /// Pops three values from stack and pushes (a + b) % n. pub fn addmod(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::MID); popn_top!([op1, op2], op3, context.interpreter); *op3 = op1.add_mod(op2, *op3) } @@ -78,16 +69,17 @@ pub fn addmod(context: InstructionContext<'_, /// /// Pops three values from stack and pushes (a * b) % n. pub fn mulmod(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::MID); popn_top!([op1, op2], op3, context.interpreter); *op3 = op1.mul_mod(op2, *op3) } /// Implements the EXP instruction - exponentiates two values from stack. pub fn exp(context: InstructionContext<'_, H, WIRE>) { - let spec_id = context.interpreter.runtime_flag.spec_id(); popn_top!([op1], op2, context.interpreter); - gas_or_fail!(context.interpreter, gas::exp_cost(spec_id, *op2)); + gas!( + context.interpreter, + context.interpreter.gas_params.exp_cost(*op2) + ); *op2 = op1.pow(*op2); } @@ -121,7 +113,6 @@ pub fn exp(context: InstructionContext<'_, H, /// Similarly, if `b == 0` then the yellow paper says the output should start with all zeros, /// then end with bits from `b`; this is equal to `y & mask` where `&` is bitwise `AND`. pub fn signextend(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::LOW); popn_top!([ext], x, context.interpreter); // For 31 we also don't need to do anything. if ext < U256::from(31) { diff --git a/crates/interpreter/src/instructions/bitwise.rs b/crates/interpreter/src/instructions/bitwise.rs index d07f7638e1..170fbe56bd 100644 --- a/crates/interpreter/src/instructions/bitwise.rs +++ b/crates/interpreter/src/instructions/bitwise.rs @@ -8,25 +8,20 @@ use primitives::U256; /// Implements the LT instruction - less than comparison. pub fn lt(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); *op2 = U256::from(op1 < *op2); } /// Implements the GT instruction - greater than comparison. pub fn gt(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - *op2 = U256::from(op1 > *op2); } /// Implements the CLZ instruction - count leading zeros. pub fn clz(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, OSAKA); - //gas!(context.interpreter, gas::LOW); popn_top!([], op1, context.interpreter); - let leading_zeros = op1.leading_zeros(); *op1 = U256::from(leading_zeros); } @@ -35,9 +30,7 @@ pub fn clz(context: InstructionContext<'_, H, /// /// Signed less than comparison of two values from stack. pub fn slt(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - *op2 = U256::from(i256_cmp(&op1, op2) == Ordering::Less); } @@ -45,9 +38,7 @@ pub fn slt(context: InstructionContext<'_, H, /// /// Signed greater than comparison of two values from stack. pub fn sgt(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - *op2 = U256::from(i256_cmp(&op1, op2) == Ordering::Greater); } @@ -55,9 +46,7 @@ pub fn sgt(context: InstructionContext<'_, H, /// /// Equality comparison of two values from stack. pub fn eq(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - *op2 = U256::from(op1 == *op2); } @@ -65,7 +54,6 @@ pub fn eq(context: InstructionContext<'_, H, /// /// Checks if the top stack value is zero. pub fn iszero(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([], op1, context.interpreter); *op1 = U256::from(op1.is_zero()); } @@ -74,7 +62,6 @@ pub fn iszero(context: InstructionContext<'_, /// /// Bitwise AND of two values from stack. pub fn bitand(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); *op2 = op1 & *op2; } @@ -83,9 +70,7 @@ pub fn bitand(context: InstructionContext<'_, /// /// Bitwise OR of two values from stack. pub fn bitor(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - *op2 = op1 | *op2; } @@ -93,9 +78,7 @@ pub fn bitor(context: InstructionContext<'_, /// /// Bitwise XOR of two values from stack. pub fn bitxor(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - *op2 = op1 ^ *op2; } @@ -103,9 +86,7 @@ pub fn bitxor(context: InstructionContext<'_, /// /// Bitwise NOT (negation) of the top stack value. pub fn not(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([], op1, context.interpreter); - *op1 = !*op1; } @@ -113,9 +94,7 @@ pub fn not(context: InstructionContext<'_, H, /// /// Extracts a single byte from a word at a given index. pub fn byte(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - let o1 = as_usize_saturated!(op1); *op2 = if o1 < 32 { // `31 - o1` because `byte` returns LE, while we want BE @@ -128,9 +107,7 @@ pub fn byte(context: InstructionContext<'_, H /// EIP-145: Bitwise shifting instructions in EVM pub fn shl(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, CONSTANTINOPLE); - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - let shift = as_usize_saturated!(op1); *op2 = if shift < 256 { *op2 << shift @@ -142,9 +119,7 @@ pub fn shl(context: InstructionContext<'_, H, /// EIP-145: Bitwise shifting instructions in EVM pub fn shr(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, CONSTANTINOPLE); - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - let shift = as_usize_saturated!(op1); *op2 = if shift < 256 { *op2 >> shift @@ -156,9 +131,7 @@ pub fn shr(context: InstructionContext<'_, H, /// EIP-145: Bitwise shifting instructions in EVM pub fn sar(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, CONSTANTINOPLE); - //gas!(context.interpreter, gas::VERYLOW); popn_top!([op1], op2, context.interpreter); - let shift = as_usize_saturated!(op1); *op2 = if shift < 256 { op2.arithmetic_shr(shift) diff --git a/crates/interpreter/src/instructions/block_info.rs b/crates/interpreter/src/instructions/block_info.rs index fa2c118c7a..8956d55378 100644 --- a/crates/interpreter/src/instructions/block_info.rs +++ b/crates/interpreter/src/instructions/block_info.rs @@ -9,7 +9,6 @@ use crate::InstructionContext; /// EIP-1344: ChainID opcode pub fn chainid(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, ISTANBUL); - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.chain_id()); } @@ -19,7 +18,6 @@ pub fn chainid(context: InstructionCon pub fn coinbase( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, context.host.beneficiary().into_word().into() @@ -32,7 +30,6 @@ pub fn coinbase( pub fn timestamp( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.timestamp()); } @@ -42,7 +39,6 @@ pub fn timestamp( pub fn block_number( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.block_number()); } @@ -52,7 +48,6 @@ pub fn block_number( pub fn difficulty( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BASE); if context .interpreter .runtime_flag @@ -72,14 +67,12 @@ pub fn difficulty( pub fn gaslimit( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.gas_limit()); } /// EIP-3198: BASEFEE opcode pub fn basefee(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, LONDON); - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.basefee()); } @@ -88,6 +81,5 @@ pub fn blob_basefee( context: InstructionContext<'_, H, WIRE>, ) { check!(context.interpreter, CANCUN); - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.blob_gasprice()); } diff --git a/crates/interpreter/src/instructions/contract.rs b/crates/interpreter/src/instructions/contract.rs index 22a36f6629..51e1ab955e 100644 --- a/crates/interpreter/src/instructions/contract.rs +++ b/crates/interpreter/src/instructions/contract.rs @@ -2,7 +2,7 @@ mod call_helpers; pub use call_helpers::{ get_memory_input_and_out_ranges, load_acc_and_calc_gas, load_account_delegated, - load_account_delegated_handle_error, new_account_cost, resize_memory, + load_account_delegated_handle_error, resize_memory, }; use crate::{ @@ -51,11 +51,15 @@ pub fn create( .halt(InstructionResult::CreateInitCodeSizeLimit); return; } - gas!(context.interpreter, gas::initcode_cost(len)); + gas!( + context.interpreter, + context.interpreter.gas_params.initcode_cost(len) + ); } let code_offset = as_usize_or_fail!(context.interpreter, code_offset); resize_memory!(context.interpreter, code_offset, len); + code = Bytes::copy_from_slice( context .interpreter @@ -69,10 +73,16 @@ pub fn create( let scheme = if IS_CREATE2 { popn!([salt], context.interpreter); // SAFETY: `len` is reasonable in size as gas for it is already deducted. - gas_or_fail!(context.interpreter, gas::create2_cost(len)); + gas!( + context.interpreter, + context.interpreter.gas_params.create2_cost(len) + ); CreateScheme::Create2 { salt } } else { - gas!(context.interpreter, gas::CREATE); + gas!( + context.interpreter, + context.interpreter.gas_params.create_cost() + ); CreateScheme::Create }; @@ -86,7 +96,10 @@ pub fn create( .is_enabled_in(SpecId::TANGERINE) { // Take remaining gas and deduce l64 part of it. - gas_limit -= gas_limit / 64 + gas_limit = context + .interpreter + .gas_params + .call_stipend_reduction(gas_limit); } gas!(context.interpreter, gas_limit); @@ -123,10 +136,12 @@ pub fn call( .halt(InstructionResult::CallNotAllowedInsideStatic); return; } + let Some((input, return_memory_offset)) = get_memory_input_and_out_ranges(context.interpreter) else { return; }; + let Some((gas_limit, bytecode, bytecode_hash)) = load_acc_and_calc_gas(&mut context, to, has_transfer, true, local_gas_limit) else { diff --git a/crates/interpreter/src/instructions/contract/call_helpers.rs b/crates/interpreter/src/instructions/contract/call_helpers.rs index 88163bea09..f58ad3ae7a 100644 --- a/crates/interpreter/src/instructions/contract/call_helpers.rs +++ b/crates/interpreter/src/instructions/contract/call_helpers.rs @@ -1,8 +1,5 @@ use crate::{ - gas::{ - self, calc_call_static_gas, COLD_ACCOUNT_ACCESS_COST_ADDITIONAL, NEWACCOUNT, - WARM_STORAGE_READ_COST, - }, + gas::params::GasParams, interpreter::Interpreter, interpreter_types::{InterpreterTypes, MemoryTr, RuntimeFlag, StackTr}, InstructionContext, @@ -61,10 +58,14 @@ pub fn load_acc_and_calc_gas( create_empty_account: bool, stack_gas_limit: u64, ) -> Option<(u64, Bytecode, B256)> { - let spec = context.interpreter.runtime_flag.spec_id(); - // calculate static gas first. For berlin hardfork it will take warm gas. - let static_gas = calc_call_static_gas(spec, transfers_value); - gas!(context.interpreter, static_gas, None); + // Transfer value cost + if transfers_value { + gas!( + context.interpreter, + context.interpreter.gas_params.transfer_value_cost(), + None + ); + } // load account delegated and deduct dynamic gas. let (gas, bytecode, code_hash) = @@ -74,18 +75,23 @@ pub fn load_acc_and_calc_gas( // deduct dynamic gas. gas!(interpreter, gas, None); + let interpreter = &mut context.interpreter; + // EIP-150: Gas cost changes for IO-heavy operations let mut gas_limit = if interpreter.runtime_flag.spec_id().is_enabled_in(TANGERINE) { - // Take l64 part of gas_limit - min(interpreter.gas.remaining_63_of_64_parts(), stack_gas_limit) + // On mainnet this will take return 63/64 of gas_limit. + let reduced_gas_limit = interpreter + .gas_params + .call_stipend_reduction(interpreter.gas.remaining()); + min(reduced_gas_limit, stack_gas_limit) } else { stack_gas_limit }; - gas!(interpreter, gas_limit, None); + // Add call stipend if there is value to be transferred. if transfers_value { - gas_limit = gas_limit.saturating_add(gas::CALL_STIPEND); + gas_limit = gas_limit.saturating_add(interpreter.gas_params.call_stipend()); } Some((gas_limit, bytecode, code_hash)) @@ -101,8 +107,10 @@ pub fn load_account_delegated_handle_error( ) -> Option<(u64, Bytecode, B256)> { // move this to static gas. let remaining_gas = context.interpreter.gas.remaining(); + let gas_table = &context.interpreter.gas_params; match load_account_delegated( context.host, + gas_table, context.interpreter.runtime_flag.spec_id(), remaining_gas, to, @@ -126,6 +134,7 @@ pub fn load_account_delegated_handle_error( #[inline] pub fn load_account_delegated( host: &mut H, + gas_table: &GasParams, spec: SpecId, remaining_gas: u64, address: Address, @@ -136,35 +145,37 @@ pub fn load_account_delegated( let is_berlin = spec.is_enabled_in(SpecId::BERLIN); let is_spurious_dragon = spec.is_enabled_in(SpecId::SPURIOUS_DRAGON); - let skip_cold_load = is_berlin && remaining_gas < COLD_ACCOUNT_ACCESS_COST_ADDITIONAL; + let additional_cold_cost = gas_table.cold_account_additional_cost(); + + let skip_cold_load = is_berlin && remaining_gas < additional_cold_cost; let account = host.load_account_info_skip_cold_load(address, true, skip_cold_load)?; if is_berlin && account.is_cold { - cost += COLD_ACCOUNT_ACCESS_COST_ADDITIONAL; + cost += additional_cold_cost; } let mut bytecode = account.code.clone().unwrap_or_default(); let mut code_hash = account.code_hash(); // New account cost, as account is empty there is no delegated account and we can return early. if create_empty_account && account.is_empty { - cost += new_account_cost(is_spurious_dragon, transfers_value); + cost += gas_table.new_account_cost(is_spurious_dragon, transfers_value); return Ok((cost, bytecode, code_hash)); } // load delegate code if account is EIP-7702 if let Some(Bytecode::Eip7702(code)) = &account.code { // EIP-7702 is enabled after berlin hardfork. - cost += WARM_STORAGE_READ_COST; + cost += gas_table.warm_storage_read_cost(); if cost > remaining_gas { return Err(LoadError::ColdLoadSkipped); } let address = code.address(); // skip cold load if there is enough gas to cover the cost. - let skip_cold_load = remaining_gas < cost + COLD_ACCOUNT_ACCESS_COST_ADDITIONAL; + let skip_cold_load = remaining_gas < cost + additional_cold_cost; let delegate_account = host.load_account_info_skip_cold_load(address, true, skip_cold_load)?; if delegate_account.is_cold { - cost += COLD_ACCOUNT_ACCESS_COST_ADDITIONAL; + cost += additional_cold_cost; } bytecode = delegate_account.code.clone().unwrap_or_default(); code_hash = delegate_account.code_hash(); @@ -172,15 +183,3 @@ pub fn load_account_delegated( Ok((cost, bytecode, code_hash)) } - -/// Returns new account cost. -#[inline] -pub fn new_account_cost(is_spurious_dragon: bool, transfers_value: bool) -> u64 { - // EIP-161: State trie clearing (invariant-preserving alternative) - // Pre-Spurious Dragon: always charge for new account - // Post-Spurious Dragon: only charge if value is transferred - if !is_spurious_dragon || transfers_value { - return NEWACCOUNT; - } - 0 -} diff --git a/crates/interpreter/src/instructions/control.rs b/crates/interpreter/src/instructions/control.rs index 66224360c4..32ff53ca81 100644 --- a/crates/interpreter/src/instructions/control.rs +++ b/crates/interpreter/src/instructions/control.rs @@ -11,7 +11,6 @@ use crate::InstructionContext; /// /// Unconditional jump to a valid destination. pub fn jump(context: InstructionContext<'_, H, ITy>) { - //gas!(context.interpreter, gas::MID); popn!([target], context.interpreter); jump_inner(context.interpreter, target); } @@ -20,9 +19,7 @@ pub fn jump(context: InstructionContext<'_, H, /// /// Conditional jump to a valid destination if condition is true. pub fn jumpi(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::HIGH); popn!([target, cond], context.interpreter); - if !cond.is_zero() { jump_inner(context.interpreter, target); } @@ -45,15 +42,12 @@ fn jump_inner(interpreter: &mut Interpreter, targe /// Implements the JUMPDEST instruction. /// /// Marks a valid destination for jump operations. -pub fn jumpdest(_context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::JUMPDEST); -} +pub fn jumpdest(_context: InstructionContext<'_, H, WIRE>) {} /// Implements the PC instruction. /// /// Pushes the current program counter onto the stack. pub fn pc(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); // - 1 because we have already advanced the instruction pointer in `Interpreter::step` push!( context.interpreter, @@ -69,15 +63,15 @@ fn return_inner( interpreter: &mut Interpreter, instruction_result: InstructionResult, ) { - // Zero gas cost - // //gas!(interpreter, gas::ZERO) popn!([offset, len], interpreter); let len = as_usize_or_fail!(interpreter, len); // Important: Offset must be ignored if len is zeros let mut output = Bytes::default(); if len != 0 { let offset = as_usize_or_fail!(interpreter, offset); - resize_memory!(interpreter, offset, len); + if !interpreter.resize_memory(offset, len) { + return; + } output = interpreter.memory.slice_len(offset, len).to_vec().into() } diff --git a/crates/interpreter/src/instructions/host.rs b/crates/interpreter/src/instructions/host.rs index e9d1c3645f..7324c39802 100644 --- a/crates/interpreter/src/instructions/host.rs +++ b/crates/interpreter/src/instructions/host.rs @@ -1,16 +1,14 @@ use crate::{ - gas::{ - self, selfdestruct_cold_beneficiary_cost, CALL_STIPEND, - COLD_ACCOUNT_ACCESS_COST_ADDITIONAL, COLD_SLOAD_COST_ADDITIONAL, ISTANBUL_SLOAD_GAS, - WARM_STORAGE_READ_COST, - }, instructions::utility::{IntoAddress, IntoU256}, interpreter_types::{InputsTr, InterpreterTypes, MemoryTr, RuntimeFlag, StackTr}, Host, InstructionResult, }; use context_interface::host::LoadError; use core::cmp::min; -use primitives::{hardfork::SpecId::*, Bytes, Log, LogData, B256, BLOCK_HASH_HISTORY, U256}; +use primitives::{ + hardfork::SpecId::{self, *}, + Bytes, Log, LogData, B256, BLOCK_HASH_HISTORY, U256, +}; use crate::InstructionContext; @@ -25,15 +23,6 @@ pub fn balance(context: InstructionCon let account = berlin_load_account!(context, address, false); *top = account.balance; } else { - let gas = if spec_id.is_enabled_in(ISTANBUL) { - // EIP-1884: Repricing for trie-size-dependent opcodes - 700 - } else if spec_id.is_enabled_in(TANGERINE) { - 400 - } else { - 20 - }; - gas!(context.interpreter, gas); let Ok(account) = context .host .load_account_info_skip_cold_load(address, false, false) @@ -49,7 +38,6 @@ pub fn selfbalance( context: InstructionContext<'_, H, WIRE>, ) { check!(context.interpreter, ISTANBUL); - //gas!(context.interpreter, gas::LOW); let Some(balance) = context .host @@ -68,18 +56,13 @@ pub fn extcodesize( ) { popn_top!([], top, context.interpreter); let address = top.into_address(); + let spec_id = context.interpreter.runtime_flag.spec_id(); if spec_id.is_enabled_in(BERLIN) { let account = berlin_load_account!(context, address, true); // safe to unwrap because we are loading code *top = U256::from(account.code.as_ref().unwrap().len()); } else { - let gas = if spec_id.is_enabled_in(TANGERINE) { - 700 - } else { - 20 - }; - gas!(context.interpreter, gas); let Ok(account) = context .host .load_account_info_skip_cold_load(address, true, false) @@ -103,12 +86,6 @@ pub fn extcodehash( let account = if spec_id.is_enabled_in(BERLIN) { berlin_load_account!(context, address, true) } else { - let gas = if spec_id.is_enabled_in(ISTANBUL) { - 700 - } else { - 400 - }; - gas!(context.interpreter, gas); let Ok(account) = context .host .load_account_info_skip_cold_load(address, true, false) @@ -143,7 +120,7 @@ pub fn extcodecopy( let len = as_usize_or_fail!(context.interpreter, len_u256); gas!( context.interpreter, - gas::copy_cost(0, len).unwrap_or(u64::MAX) + context.interpreter.gas_params.extcodecopy(len) ); let mut memory_offset_usize = 0; @@ -151,6 +128,7 @@ pub fn extcodecopy( if len != 0 { // fail on casting of memory_offset only if len is not zero. memory_offset_usize = as_usize_or_fail!(context.interpreter, memory_offset); + // Resize memory to fit the code resize_memory!(context.interpreter, memory_offset_usize, len); } @@ -158,13 +136,6 @@ pub fn extcodecopy( let account = berlin_load_account!(context, address, true); account.code.as_ref().unwrap().original_bytes() } else { - let gas = if spec_id.is_enabled_in(TANGERINE) { - 700 - } else { - 20 - }; - gas!(context.interpreter, gas); - let Some(code) = context.host.load_account_code(address) else { return context.interpreter.halt_fatal(); }; @@ -187,7 +158,6 @@ pub fn extcodecopy( pub fn blockhash( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BLOCKHASH); popn_top!([], number, context.interpreter); let requested_number = *number; @@ -224,26 +194,17 @@ pub fn sload(context: InstructionConte let spec_id = context.interpreter.runtime_flag.spec_id(); let target = context.interpreter.input.target_address(); - // `SLOAD` opcode cost calculation. - let gas = if spec_id.is_enabled_in(BERLIN) { - WARM_STORAGE_READ_COST - } else if spec_id.is_enabled_in(ISTANBUL) { - // EIP-1884: Repricing for trie-size-dependent opcodes - ISTANBUL_SLOAD_GAS - } else if spec_id.is_enabled_in(TANGERINE) { - // EIP-150: Gas cost changes for IO-heavy operations - 200 - } else { - 50 - }; - gas!(context.interpreter, gas); if spec_id.is_enabled_in(BERLIN) { - let skip_cold = context.interpreter.gas.remaining() < COLD_SLOAD_COST_ADDITIONAL; + let additional_cold_cost = context + .interpreter + .gas_params + .cold_storage_additional_cost(); + let skip_cold = context.interpreter.gas.remaining() < additional_cold_cost; let res = context.host.sload_skip_cold_load(target, *index, skip_cold); match res { Ok(storage) => { if storage.is_cold { - gas!(context.interpreter, COLD_SLOAD_COST_ADDITIONAL); + gas!(context.interpreter, additional_cold_cost); } *index = storage.data; @@ -269,13 +230,10 @@ pub fn sstore(context: InstructionCont let target = context.interpreter.input.target_address(); let spec_id = context.interpreter.runtime_flag.spec_id(); - // EIP-1706 Disable SSTORE with gasleft lower than call stipend - if context - .interpreter - .runtime_flag - .spec_id() - .is_enabled_in(ISTANBUL) - && context.interpreter.gas.remaining() <= CALL_STIPEND + // EIP-2200: Structured Definitions for Net Gas Metering + // If gasleft is less than or equal to gas stipend, fail the current call frame with ‘out of gas’ exception. + if spec_id.is_enabled_in(ISTANBUL) + && context.interpreter.gas.remaining() <= context.interpreter.gas_params.call_stipend() { context .interpreter @@ -283,14 +241,17 @@ pub fn sstore(context: InstructionCont return; } - // static gas gas!( context.interpreter, - gas::static_sstore_cost(context.interpreter.runtime_flag.spec_id()) + context.interpreter.gas_params.sstore_static_gas() ); let state_load = if spec_id.is_enabled_in(BERLIN) { - let skip_cold = context.interpreter.gas.remaining() < COLD_SLOAD_COST_ADDITIONAL; + let additional_cold_cost = context + .interpreter + .gas_params + .cold_storage_additional_cost(); + let skip_cold = context.interpreter.gas.remaining() < additional_cold_cost; let res = context .host .sstore_skip_cold_load(target, index, value, skip_cold); @@ -306,21 +267,25 @@ pub fn sstore(context: InstructionCont load }; + let is_istanbul = spec_id.is_enabled_in(ISTANBUL); + // dynamic gas gas!( context.interpreter, - gas::dyn_sstore_cost( - context.interpreter.runtime_flag.spec_id(), + context.interpreter.gas_params.sstore_dynamic_gas( + is_istanbul, &state_load.data, state_load.is_cold ) ); // refund - context.interpreter.gas.record_refund(gas::sstore_refund( - context.interpreter.runtime_flag.spec_id(), - &state_load.data, - )); + context.interpreter.gas.record_refund( + context + .interpreter + .gas_params + .sstore_refund(is_istanbul, &state_load.data), + ); } /// EIP-1153: Transient storage opcodes @@ -328,8 +293,6 @@ pub fn sstore(context: InstructionCont pub fn tstore(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, CANCUN); require_non_staticcall!(context.interpreter); - //gas!(context.interpreter, gas::WARM_STORAGE_READ_COST); - popn!([index, value], context.interpreter); context @@ -341,8 +304,6 @@ pub fn tstore(context: InstructionCont /// Load value from transient storage pub fn tload(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, CANCUN); - //gas!(context.interpreter, gas::WARM_STORAGE_READ_COST); - popn_top!([], index, context.interpreter); *index = context @@ -360,11 +321,15 @@ pub fn log( popn!([offset, len], context.interpreter); let len = as_usize_or_fail!(context.interpreter, len); - gas_or_fail!(context.interpreter, gas::log_cost(N as u8, len as u64)); + gas!( + context.interpreter, + context.interpreter.gas_params.log_cost(N as u8, len as u64) + ); let data = if len == 0 { Bytes::new() } else { let offset = as_usize_or_fail!(context.interpreter, offset); + // Resize memory to fit the data resize_memory!(context.interpreter, offset, len); Bytes::copy_from_slice(context.interpreter.memory.slice_len(offset, len).as_ref()) }; @@ -393,34 +358,42 @@ pub fn selfdestruct( let target = target.into_address(); let spec = context.interpreter.runtime_flag.spec_id(); - // static gas - gas!(context.interpreter, gas::static_selfdestruct_cost(spec)); + let cold_load_gas = context + .interpreter + .gas_params + .cold_account_additional_cost(); - let skip_cold = context.interpreter.gas.remaining() < selfdestruct_cold_beneficiary_cost(spec); + let skip_cold_load = context.interpreter.gas.remaining() < cold_load_gas; let res = match context.host.selfdestruct( context.interpreter.input.target_address(), target, - skip_cold, + skip_cold_load, ) { Ok(res) => res, Err(LoadError::ColdLoadSkipped) => return context.interpreter.halt_oog(), Err(LoadError::DBError) => return context.interpreter.halt_fatal(), }; - gas!(context.interpreter, gas::dyn_selfdestruct_cost(spec, &res)); + // EIP-161: State trie clearing (invariant-preserving alternative) + let should_charge_topup = if spec.is_enabled_in(SpecId::SPURIOUS_DRAGON) { + res.had_value && !res.target_exists + } else { + !res.target_exists + }; + + gas!( + context.interpreter, + context + .interpreter + .gas_params + .selfdestruct_cost(should_charge_topup, res.is_cold) + ); - // EIP-3529: Reduction in refunds - if !context - .interpreter - .runtime_flag - .spec_id() - .is_enabled_in(LONDON) - && !res.previously_destroyed - { + if !res.previously_destroyed { context .interpreter .gas - .record_refund(gas::SELFDESTRUCT_REFUND); + .record_refund(context.interpreter.gas_params.selfdestruct_refund()); } context.interpreter.halt(InstructionResult::SelfDestruct); diff --git a/crates/interpreter/src/instructions/macros.rs b/crates/interpreter/src/instructions/macros.rs index 791400643f..30e30fd949 100644 --- a/crates/interpreter/src/instructions/macros.rs +++ b/crates/interpreter/src/instructions/macros.rs @@ -1,17 +1,5 @@ //! Utility macros to help implementing opcode instruction functions. -/// `const` Option `?`. -#[macro_export] -#[collapse_debuginfo(yes)] -macro_rules! tri { - ($e:expr) => { - match $e { - Some(v) => v, - None => return None, - } - }; -} - /// Fails the instruction if the current call is static. #[macro_export] #[collapse_debuginfo(yes)] @@ -24,23 +12,6 @@ macro_rules! require_non_staticcall { }; } -/// Macro for optional try - returns early if the expression evaluates to None. -/// Similar to the `?` operator but for use in instruction implementations. -#[macro_export] -#[collapse_debuginfo(yes)] -#[deprecated( - since = "29.0.0", - note = "Prefer `let Some(x) = expr else { return; };` for early return in instruction functions" -)] -macro_rules! otry { - ($expression: expr) => {{ - let Some(value) = $expression else { - return; - }; - value - }}; -} - /// Check if the `SPEC` is enabled, and fail the instruction if it is not. #[macro_export] #[collapse_debuginfo(yes)] @@ -80,20 +51,18 @@ macro_rules! berlin_load_account { $crate::berlin_load_account!($context, $address, $load_code, ()) }; ($context:expr, $address:expr, $load_code:expr, $ret:expr) => {{ - $crate::gas!($context.interpreter, WARM_STORAGE_READ_COST, $ret); - let skip_cold_load = - $context.interpreter.gas.remaining() < COLD_ACCOUNT_ACCESS_COST_ADDITIONAL; + let cold_load_gas = $context + .interpreter + .gas_params + .cold_account_additional_cost(); + let skip_cold_load = $context.interpreter.gas.remaining() < cold_load_gas; match $context .host .load_account_info_skip_cold_load($address, $load_code, skip_cold_load) { Ok(account) => { if account.is_cold { - $crate::gas!( - $context.interpreter, - COLD_ACCOUNT_ACCESS_COST_ADDITIONAL, - $ret - ); + $crate::gas!($context.interpreter, cold_load_gas, $ret); } account } @@ -109,24 +78,6 @@ macro_rules! berlin_load_account { }}; } -/// Same as [`gas!`], but with `gas` as an option. -#[macro_export] -#[collapse_debuginfo(yes)] -macro_rules! gas_or_fail { - ($interpreter:expr, $gas:expr) => { - $crate::gas_or_fail!($interpreter, $gas, ()) - }; - ($interpreter:expr, $gas:expr, $ret:expr) => { - match $gas { - Some(gas_used) => $crate::gas!($interpreter, gas_used, $ret), - None => { - $interpreter.halt_oog(); - return $ret; - } - } - }; -} - /// Resizes the interpreter memory if necessary. Fails the instruction if the memory or gas limit /// is exceeded. #[macro_export] @@ -136,18 +87,14 @@ macro_rules! resize_memory { $crate::resize_memory!($interpreter, $offset, $len, ()) }; ($interpreter:expr, $offset:expr, $len:expr, $ret:expr) => { - #[cfg(feature = "memory_limit")] - if $interpreter.memory.limit_reached($offset, $len) { - $interpreter.halt_memory_limit_oog(); - return $ret; - } - if !$crate::interpreter::resize_memory( + if let Err(result) = $crate::interpreter::resize_memory( &mut $interpreter.gas, &mut $interpreter.memory, + &$interpreter.gas_params, $offset, $len, ) { - $interpreter.halt_memory_oog(); + $interpreter.halt(result); return $ret; } }; diff --git a/crates/interpreter/src/instructions/memory.rs b/crates/interpreter/src/instructions/memory.rs index 9a48d88f3d..8c6b6acc1d 100644 --- a/crates/interpreter/src/instructions/memory.rs +++ b/crates/interpreter/src/instructions/memory.rs @@ -11,7 +11,6 @@ use crate::InstructionContext; /// /// Loads a 32-byte word from memory. pub fn mload(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([], top, context.interpreter); let offset = as_usize_or_fail!(context.interpreter, top); resize_memory!(context.interpreter, offset, 32); @@ -23,7 +22,6 @@ pub fn mload(context: InstructionContext<'_, /// /// Stores a 32-byte word to memory. pub fn mstore(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn!([offset, value], context.interpreter); let offset = as_usize_or_fail!(context.interpreter, offset); resize_memory!(context.interpreter, offset, 32); @@ -37,7 +35,6 @@ pub fn mstore(context: InstructionContext<'_, /// /// Stores a single byte to memory. pub fn mstore8(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn!([offset, value], context.interpreter); let offset = as_usize_or_fail!(context.interpreter, offset); resize_memory!(context.interpreter, offset, 1); @@ -48,7 +45,6 @@ pub fn mstore8(context: InstructionContext<'_ /// /// Gets the size of active memory in bytes. pub fn msize(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, U256::from(context.interpreter.memory.size()) @@ -65,7 +61,11 @@ pub fn mcopy(context: InstructionContext<'_, // Into usize or fail let len = as_usize_or_fail!(context.interpreter, len); // Deduce gas - gas_or_fail!(context.interpreter, gas::copy_cost_verylow(len)); + gas!( + context.interpreter, + context.interpreter.gas_params.mcopy_cost(len) + ); + if len == 0 { return; } diff --git a/crates/interpreter/src/instructions/stack.rs b/crates/interpreter/src/instructions/stack.rs index 822e6240d7..f2fb7f4cba 100644 --- a/crates/interpreter/src/instructions/stack.rs +++ b/crates/interpreter/src/instructions/stack.rs @@ -10,7 +10,6 @@ use crate::InstructionContext; /// /// Removes the top item from the stack. pub fn pop(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); // Can ignore return. as relative N jump is safe operation. popn!([_i], context.interpreter); } @@ -20,7 +19,6 @@ pub fn pop(context: InstructionContext<'_, H, /// Introduce a new instruction which pushes the constant value 0 onto the stack. pub fn push0(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, SHANGHAI); - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, U256::ZERO); } @@ -30,8 +28,6 @@ pub fn push0(context: InstructionContext<'_, pub fn push( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::VERYLOW); - let slice = context.interpreter.bytecode.read_slice(N); if !context.interpreter.stack.push_slice(slice) { context.interpreter.halt(InstructionResult::StackOverflow); @@ -48,7 +44,6 @@ pub fn push( pub fn dup( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::VERYLOW); if !context.interpreter.stack.dup(N) { context.interpreter.halt(InstructionResult::StackOverflow); } @@ -60,7 +55,6 @@ pub fn dup( pub fn swap( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::VERYLOW); assert!(N != 0); if !context.interpreter.stack.exchange(0, N) { context.interpreter.halt(InstructionResult::StackOverflow); diff --git a/crates/interpreter/src/instructions/system.rs b/crates/interpreter/src/instructions/system.rs index 0bea684e7f..cf6f7e2f76 100644 --- a/crates/interpreter/src/instructions/system.rs +++ b/crates/interpreter/src/instructions/system.rs @@ -17,7 +17,10 @@ use crate::InstructionContext; pub fn keccak256(context: InstructionContext<'_, H, WIRE>) { popn_top!([offset], top, context.interpreter); let len = as_usize_or_fail!(context.interpreter, top); - gas_or_fail!(context.interpreter, gas::keccak256_cost(len)); + gas!( + context.interpreter, + context.interpreter.gas_params.keccak256_cost(len) + ); let hash = if len == 0 { KECCAK_EMPTY } else { @@ -32,7 +35,6 @@ pub fn keccak256(context: InstructionContext< /// /// Pushes the current contract's address onto the stack. pub fn address(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, context @@ -48,7 +50,6 @@ pub fn address(context: InstructionContext<'_ /// /// Pushes the caller's address onto the stack. pub fn caller(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, context @@ -64,7 +65,6 @@ pub fn caller(context: InstructionContext<'_, /// /// Pushes the size of running contract's bytecode onto the stack. pub fn codesize(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, U256::from(context.interpreter.bytecode.bytecode_len()) @@ -77,7 +77,8 @@ pub fn codesize(context: InstructionContext<' pub fn codecopy(context: InstructionContext<'_, H, WIRE>) { popn!([memory_offset, code_offset, len], context.interpreter); let len = as_usize_or_fail!(context.interpreter, len); - let Some(memory_offset) = memory_resize(context.interpreter, memory_offset, len) else { + let Some(memory_offset) = copy_cost_and_memory_resize(context.interpreter, memory_offset, len) + else { return; }; let code_offset = as_usize_saturated!(code_offset); @@ -95,7 +96,6 @@ pub fn codecopy(context: InstructionContext<' /// /// Loads 32 bytes of input data from the specified offset. pub fn calldataload(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::VERYLOW); popn_top!([], offset_ptr, context.interpreter); let mut word = B256::ZERO; let offset = as_usize_saturated!(offset_ptr); @@ -133,7 +133,6 @@ pub fn calldataload(context: InstructionConte /// /// Pushes the size of input data onto the stack. pub fn calldatasize(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, U256::from(context.interpreter.input.input().len()) @@ -144,7 +143,6 @@ pub fn calldatasize(context: InstructionConte /// /// Pushes the value sent with the current call onto the stack. pub fn callvalue(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.interpreter.input.call_value()); } @@ -154,7 +152,8 @@ pub fn callvalue(context: InstructionContext< pub fn calldatacopy(context: InstructionContext<'_, H, WIRE>) { popn!([memory_offset, data_offset, len], context.interpreter); let len = as_usize_or_fail!(context.interpreter, len); - let Some(memory_offset) = memory_resize(context.interpreter, memory_offset, len) else { + let Some(memory_offset) = copy_cost_and_memory_resize(context.interpreter, memory_offset, len) + else { return; }; @@ -180,7 +179,6 @@ pub fn calldatacopy(context: InstructionConte /// EIP-211: New opcodes: RETURNDATASIZE and RETURNDATACOPY pub fn returndatasize(context: InstructionContext<'_, H, WIRE>) { check!(context.interpreter, BYZANTIUM); - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, U256::from(context.interpreter.return_data.buffer().len()) @@ -202,7 +200,8 @@ pub fn returndatacopy(context: InstructionCon return; } - let Some(memory_offset) = memory_resize(context.interpreter, memory_offset, len) else { + let Some(memory_offset) = copy_cost_and_memory_resize(context.interpreter, memory_offset, len) + else { return; }; @@ -219,7 +218,6 @@ pub fn returndatacopy(context: InstructionCon /// /// Pushes the amount of remaining gas onto the stack. pub fn gas(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, U256::from(context.interpreter.gas.remaining()) @@ -229,13 +227,13 @@ pub fn gas(context: InstructionContext<'_, H, /// Common logic for copying data from a source buffer to the EVM's memory. /// /// Handles memory expansion and gas calculation for data copy operations. -pub fn memory_resize( +pub fn copy_cost_and_memory_resize( interpreter: &mut Interpreter, memory_offset: U256, len: usize, ) -> Option { // Safe to cast usize to u64 - gas_or_fail!(interpreter, gas::copy_cost_verylow(len), None); + gas!(interpreter, interpreter.gas_params.copy_cost(len), None); if len == 0 { return None; } diff --git a/crates/interpreter/src/instructions/tx_info.rs b/crates/interpreter/src/instructions/tx_info.rs index d710b1d24f..90395ee54f 100644 --- a/crates/interpreter/src/instructions/tx_info.rs +++ b/crates/interpreter/src/instructions/tx_info.rs @@ -11,7 +11,6 @@ use crate::InstructionContext; pub fn gasprice( context: InstructionContext<'_, H, WIRE>, ) { - //gas!(context.interpreter, gas::BASE); push!(context.interpreter, context.host.effective_gas_price()); } @@ -19,7 +18,6 @@ pub fn gasprice( /// /// Gets the execution origination address. pub fn origin(context: InstructionContext<'_, H, WIRE>) { - //gas!(context.interpreter, gas::BASE); push!( context.interpreter, context.host.caller().into_word().into() @@ -33,7 +31,6 @@ pub fn blob_hash( context: InstructionContext<'_, H, WIRE>, ) { check!(context.interpreter, CANCUN); - //gas!(context.interpreter, gas::VERYLOW); popn_top!([], index, context.interpreter); let i = as_usize_saturated!(index); *index = context.host.blob_hash(i).unwrap_or_default(); diff --git a/crates/interpreter/src/interpreter.rs b/crates/interpreter/src/interpreter.rs index 3eeb6f10b0..2bfdab36a5 100644 --- a/crates/interpreter/src/interpreter.rs +++ b/crates/interpreter/src/interpreter.rs @@ -19,8 +19,8 @@ pub use stack::{Stack, STACK_LIMIT}; // imports use crate::{ - host::DummyHost, instruction_context::InstructionContext, interpreter_types::*, Gas, Host, - InstructionResult, InstructionTable, InterpreterAction, + gas::params::GasParams, host::DummyHost, instruction_context::InstructionContext, + interpreter_types::*, Gas, Host, InstructionResult, InstructionTable, InterpreterAction, }; use bytecode::Bytecode; use primitives::{hardfork::SpecId, Bytes}; @@ -29,6 +29,8 @@ use primitives::{hardfork::SpecId, Bytes}; #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Interpreter { + /// Gas table for dynamic gas constants. + pub gas_params: GasParams, /// Bytecode being executed. pub bytecode: WIRE::Bytecode, /// Gas tracking for execution costs. @@ -56,6 +58,7 @@ impl Interpreter> { is_static: bool, spec_id: SpecId, gas_limit: u64, + gas_params: GasParams, ) -> Self { Self::new_inner( Stack::new(), @@ -65,6 +68,7 @@ impl Interpreter> { is_static, spec_id, gas_limit, + gas_params, ) } @@ -87,6 +91,7 @@ impl Interpreter> { false, SpecId::default(), u64::MAX, + GasParams::default(), ) } @@ -99,10 +104,12 @@ impl Interpreter> { is_static: bool, spec_id: SpecId, gas_limit: u64, + gas_params: GasParams, ) -> Self { Self { bytecode, gas: Gas::new(gas_limit), + gas_params, stack, return_data: Default::default(), memory, @@ -114,6 +121,7 @@ impl Interpreter> { /// Clears and reinitializes the interpreter with new parameters. #[allow(clippy::too_many_arguments)] + #[inline(always)] pub fn clear( &mut self, memory: SharedMemory, @@ -122,10 +130,12 @@ impl Interpreter> { is_static: bool, spec_id: SpecId, gas_limit: u64, + gas_params: GasParams, ) { let Self { bytecode: bytecode_ref, gas, + gas_params: gas_params_ref, stack, return_data, memory: memory_ref, @@ -144,6 +154,7 @@ impl Interpreter> { *memory_ref = memory; *input_ref = input; *runtime_flag = RuntimeFlags { spec_id, is_static }; + *gas_params_ref = gas_params; *extend = EXT::default(); } @@ -155,6 +166,7 @@ impl Interpreter> { /// Sets the specid for the interpreter. pub fn set_spec_id(&mut self, spec_id: SpecId) { + self.gas_params = GasParams::new_spec(spec_id); self.runtime_flag.spec_id = spec_id; } } @@ -187,7 +199,17 @@ impl Interpreter { #[inline] #[must_use] pub fn resize_memory(&mut self, offset: usize, len: usize) -> bool { - resize_memory(&mut self.gas, &mut self.memory, offset, len) + if let Err(result) = resize_memory( + &mut self.gas, + &mut self.memory, + &self.gas_params, + offset, + len, + ) { + self.halt(result); + return false; + } + true } /// Takes the next action from the control and returns it. @@ -426,6 +448,7 @@ mod tests { false, SpecId::default(), u64::MAX, + GasParams::default(), ); let serialized = serde_json::to_string_pretty(&interpreter).unwrap(); @@ -463,6 +486,7 @@ fn test_mstore_big_offset_memory_oog() { false, SpecId::default(), 1000, + GasParams::default(), ); let table = instruction_table::(); @@ -501,6 +525,7 @@ fn test_mstore_big_offset_memory_limit_oog() { false, SpecId::default(), 100000, + GasParams::default(), ); let table = instruction_table::(); diff --git a/crates/interpreter/src/interpreter/shared_memory.rs b/crates/interpreter/src/interpreter/shared_memory.rs index f83dde5e14..abaf4ca1df 100644 --- a/crates/interpreter/src/interpreter/shared_memory.rs +++ b/crates/interpreter/src/interpreter/shared_memory.rs @@ -1,4 +1,5 @@ use super::MemoryTr; +use crate::{gas::params::GasParams, InstructionResult}; use core::{ cell::{Ref, RefCell, RefMut}, cmp::min, @@ -556,24 +557,29 @@ unsafe fn set_data(dst: &mut [u8], src: &[u8], dst_offset: usize, src_offset: us /// i.e. it rounds up the number bytes to number of words. #[inline] pub const fn num_words(len: usize) -> usize { - len.saturating_add(31) / 32 + len.div_ceil(32) } /// Performs EVM memory resize. #[inline] -#[must_use] pub fn resize_memory( gas: &mut crate::Gas, memory: &mut Memory, + gas_table: &GasParams, offset: usize, len: usize, -) -> bool { +) -> Result<(), InstructionResult> { + #[cfg(feature = "memory_limit")] + if memory.limit_reached(offset, len) { + return Err(InstructionResult::MemoryLimitOOG); + } + let new_num_words = num_words(offset.saturating_add(len)); if new_num_words > gas.memory().words_num { - resize_memory_cold(gas, memory, new_num_words) - } else { - true + return resize_memory_cold(gas, memory, gas_table, new_num_words); } + + Ok(()) } #[cold] @@ -581,18 +587,21 @@ pub fn resize_memory( fn resize_memory_cold( gas: &mut crate::Gas, memory: &mut Memory, + gas_table: &GasParams, new_num_words: usize, -) -> bool { +) -> Result<(), InstructionResult> { + let cost = gas_table.memory_cost(new_num_words); let cost = unsafe { gas.memory_mut() - .record_new_len(new_num_words) + .set_words_num(new_num_words, cost) .unwrap_unchecked() }; + if !gas.record_cost(cost) { - return false; + return Err(InstructionResult::MemoryOOG); } memory.resize(new_num_words * 32); - true + Ok(()) } #[cfg(test)] @@ -609,7 +618,9 @@ mod tests { assert_eq!(num_words(63), 2); assert_eq!(num_words(64), 2); assert_eq!(num_words(65), 3); - assert_eq!(num_words(usize::MAX), usize::MAX / 32); + assert_eq!(num_words(usize::MAX - 31), usize::MAX / 32); + assert_eq!(num_words(usize::MAX - 30), (usize::MAX / 32) + 1); + assert_eq!(num_words(usize::MAX), (usize::MAX / 32) + 1); } #[test] diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 3b53c88d57..b9aa17480f 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -31,9 +31,12 @@ LEGACY_REPO_URL="https://github.com/ethereum/legacytests.git" # Print usage information and exit usage() { - echo "Usage: $0 [clean] [runner] [profile] [target]" + echo "Usage: $0 [clean] [--keep-going] [runner] [profile] [target]" echo "" - echo "Arguments (after optional 'clean'):" + echo "Flags (can be specified before or after 'clean'):" + echo " --keep-going Continue running tests even after failures." + echo "" + echo "Arguments (after optional 'clean' and '--keep-going'):" echo " runner (Optional) Rust runner command. Must be either 'cargo' or 'cross'. Defaults to 'cargo'." echo " profile (Optional) Rust profile to use. Defaults to 'debug' if not provided." echo " target (Optional) Rust target. Only used if provided." @@ -45,14 +48,17 @@ usage() { echo " $0 release" echo " Uses runner 'cargo', profile 'release', and no target." echo "" + echo " $0 --keep-going release" + echo " Uses runner 'cargo', profile 'release', and keeps going on test failures." + echo "" echo " $0 release x86-win" echo " Uses runner 'cargo', profile 'release', with target 'x86-win'." echo "" echo " $0 clean" echo " Cleans fixtures then uses runner 'cargo', profile 'debug', and no target." echo "" - echo " $0 clean cross release x86-win" - echo " Cleans fixtures then uses runner 'cross', profile 'release', and target 'x86-win'." + echo " $0 clean --keep-going cross release x86-win" + echo " Cleans fixtures then uses runner 'cross', profile 'release', target 'x86-win', and keeps going on failures." exit 1 } @@ -145,36 +151,51 @@ build_cargo_options() { # Run tests for each set of fixtures using the chosen runner. run_tests() { echo "Running main stable statetests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest "$MAIN_STABLE_DIR/state_tests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest $KEEP_GOING_FLAG "$MAIN_STABLE_DIR/state_tests" echo "Running main develop statetests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest "$MAIN_DEVELOP_DIR/state_tests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest $KEEP_GOING_FLAG "$MAIN_DEVELOP_DIR/state_tests" echo "Running devnet statetests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest "$DEVNET_DIR/state_tests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest $KEEP_GOING_FLAG "$DEVNET_DIR/state_tests" echo "Running legacy Cancun tests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest "$LEGACY_DIR/Cancun/GeneralStateTests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest $KEEP_GOING_FLAG "$LEGACY_DIR/Cancun/GeneralStateTests" echo "Running legacy Constantinople tests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest "$LEGACY_DIR/Constantinople/GeneralStateTests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- statetest $KEEP_GOING_FLAG "$LEGACY_DIR/Constantinople/GeneralStateTests" echo "Running main develop blockchain tests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- btest "$MAIN_DEVELOP_DIR/blockchain_tests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- btest $KEEP_GOING_FLAG "$MAIN_DEVELOP_DIR/blockchain_tests" echo "Running main stable blockchain tests..." - $RUST_RUNNER run $CARGO_OPTS -p revme -- btest "$MAIN_STABLE_DIR/blockchain_tests" + $RUST_RUNNER run $CARGO_OPTS -p revme -- btest $KEEP_GOING_FLAG "$MAIN_STABLE_DIR/blockchain_tests" } ############################## # Main logic -# If the first argument is "clean", perform cleaning and download fixtures. -if [ "$1" = "clean" ]; then - clean - download_fixtures - shift -else +# Initialize flags +KEEP_GOING_FLAG="" +DID_CLEAN=false + +# Process "clean" and "--keep-going" flags +while true; do + if [ "$1" = "clean" ]; then + clean + download_fixtures + DID_CLEAN=true + shift + elif [ "$1" = "--keep-going" ]; then + KEEP_GOING_FLAG="--keep-going" + shift + else + break + fi +done + +# If no clean was specified, check for existing fixtures +if [ "$DID_CLEAN" = false ]; then if check_fixtures; then echo "Using existing test fixtures." else @@ -184,7 +205,7 @@ else fi # Argument parsing for runner, profile, target. -# Expected order (after optional clean): [runner] [profile] [target] +# Expected order (after optional clean and --keep-going): [runner] [profile] [target] # If the first argument is "cargo" or "cross", then it is the runner. # Otherwise, runner defaults to "cargo", and the arguments are profile and target. if [ "$#" -eq 0 ]; then