Skip to content

Conversation

@rakita
Copy link
Member

@rakita rakita commented Oct 29, 2025

A way to configure all gas-related parameters in EVM so we can easily change dynamic part of gas of opcode.

@rakita rakita marked this pull request as draft October 29, 2025 18:37
@rakita rakita changed the title feat: Gas params feat(draft): Gas params Oct 29, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Oct 29, 2025

CodSpeed Performance Report

Merging #3132 will not alter performance

Comparing rakita/gas-params (e6d9b74) with main (d0bb48e)

Summary

✅ 173 untouched

@rakita rakita changed the title feat(draft): Gas params feat: Gas params Nov 13, 2025
@rakita rakita marked this pull request as ready for review November 13, 2025 20:31
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MCOPY has a static VERY_LOG gas

gas_or_fail!(context.interpreter, gas::copy_cost_verylow(len));

VERYLOW is 3gas.

pub const fn copy_cost_verylow(len: usize) -> Option<u64> {
copy_cost(VERYLOW, len)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pub const VERYLOW: u64 = 3;
/// Gas cost for DATALOADN instruction.

} else {
20
};
gas!(context.interpreter, gas);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to static cost in instructions.rs. Left comments there.

Having this at the start does not matter for this opcode

} else {
400
};
gas!(context.interpreter, gas);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to static cost in instructions.rs. Left comments there.

Moving this to the start does not matter for this opcode

} else {
20
};
gas!(context.interpreter, gas);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to static cost in instructions.rs. Left comments there.

Moving this to the start does not matter for this opcode

} else {
20
};
gas!(context.interpreter, gas);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to static cost in instructions.rs. Left comments there.

Moving this to the start does not matter for this opcode

@@ -1,70 +1,10 @@
use super::constants::*;
use crate::{num_words, tri, SStoreResult, SelfDestructResult, StateLoad};
Copy link
Member Author

@rakita rakita Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this logic is moved to GasParams. Will add comment there

Initial gas logic is still intact, and will need a follow-up to integrate it with GasParams

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;
Copy link
Member Author

@rakita rakita Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used in GasParams::selfdestruct_cost function

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();
}

Comment on lines -290 to -299
// 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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part can now be found in:

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
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Selfdestruct logic is slightly different from other EIP-2929 changes but in general, it is correct

Comment on lines -284 to -288
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
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is moved to selfdestruct instruction:

// 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
};

Comment on lines -325 to -333
// 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
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to static gas

// Transfer value cost
if has_transfer {
gas += CALLVALUE;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5000
} else {
0
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of static gas

Comment on lines -258 to -264
const fn frontier_sstore_cost(vals: &SStoreResult) -> u64 {
if vals.is_present_zero() && !vals.is_new_zero() {
SSTORE_SET
} else {
SSTORE_RESET
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontier part as it has special logic is moved here:

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();
}
}

This is without load cost that is now part of static instruction gas

Comment on lines -225 to -229
let mut gas_cost = istanbul_sstore_cost::<WARM_STORAGE_READ_COST, WARM_SSTORE_RESET>(vals);

if is_cold {
gas_cost += COLD_SLOAD_COST;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is cold is now always added but cold cost is zero before berlin here:

// this will be zero before berlin fork.
if is_cold {
gas += self.cold_storage_cost();
}

Comment on lines -245 to -253
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
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is slighly changes as SLOAD_GAS is not part of the static instruction gas. So we have fewer branches

It is easy to follow commnets

// 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

Comment on lines -204 to -211
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
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is moved to a variable inside GasParams.

table[GasId::sstore_static().as_usize()] = gas::WARM_STORAGE_READ_COST;

table[GasId::sstore_static().as_usize()] = gas::ISTANBUL_SLOAD_GAS;

table[GasId::sstore_static().as_usize()] = 5000;

Comment on lines -166 to -182
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
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has become part of static instruction gas.

And additional gas is handled here:

pub fn sload<WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext<'_, H, WIRE>) {
popn_top!([], index, context.interpreter);
let spec_id = context.interpreter.runtime_flag.spec_id();
let target = context.interpreter.input.target_address();
if spec_id.is_enabled_in(BERLIN) {
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, additional_cold_cost);
}

Comment on lines -92 to -106
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 link
Member Author

@rakita rakita Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic part is in GasParams here:

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)
}

if spec.is_enabled_in(SpecId::SPURIOUS_DRAGON) {
table[GasId::exp_byte_gas().as_usize()] = 50;
}

table[GasId::exp_byte_gas().as_usize()] = 10;
table[GasId::logdata().as_usize()] = gas::LOGDATA;

And static part (EXP) is now part of static instruction gas

Comment on lines -62 to -64
pub const fn create2_cost(len: usize) -> Option<u64> {
CREATE.checked_add(tri!(cost_per_word(len, KECCAK256WORD)))
}
Copy link
Member Author

@rakita rakita Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fully moved to:

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),
)
}

table[GasId::create().as_usize()] = gas::CREATE;

table[GasId::keccak256_per_word().as_usize()] = gas::KECCAK256WORD;

Comment on lines -52 to -57
if !vals.is_present_zero() && vals.is_new_zero() {
REFUND_SSTORE_CLEARS
} else {
0
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before instanbul is moved here:

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;
}

Comment on lines -19 to -21
if vals.is_new_eq_present() {
0
} else {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part where we return zero is now here

// If current value equals new value (this is a no-op)
if vals.is_new_eq_present() {
return 0;
}

Comment on lines -22 to -24
if vals.is_original_eq_present() && vals.is_new_zero() {
sstore_clears_schedule
} else {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is not refactored here

// 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;
}

Comment on lines -27 to -48
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
Copy link
Member Author

@rakita rakita Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And last part is now here. Added additional comments from EIP

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
}

@rakita rakita requested a review from Copilot November 19, 2025 00:34
Copilot finished reviewing on behalf of rakita November 19, 2025 00:36
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a configurable gas parameters system (GasParams) for the EVM interpreter, enabling easy modification of dynamic gas costs for opcodes. The implementation replaces hardcoded gas calculations with a flexible, spec-aware table-based approach.

Key Changes:

  • Introduced GasParams struct with gas cost table that adapts based on EVM spec version
  • Refactored all instruction implementations to use GasParams instead of direct gas constant references
  • Updated memory resize operations to accept and use GasParams
  • Added gas parameter configuration in handler initialization
  • Enhanced test script with --keep-going flag support

Reviewed Changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/run-tests.sh Added --keep-going flag support for continuing tests after failures
crates/interpreter/src/gas/params.rs New core gas parameters implementation with configurable gas table
crates/interpreter/src/interpreter.rs Integrated GasParams into interpreter struct and initialization
crates/interpreter/src/interpreter/shared_memory.rs Updated memory resize to use GasParams and return Result
crates/interpreter/src/instructions/*.rs Refactored all instruction gas calculations to use GasParams
crates/interpreter/src/gas/*.rs Removed deprecated gas calculation functions, kept core utilities
crates/handler/src/*.rs Updated handler to configure gas parameters based on spec
crates/context/src/journal/inner.rs Minor type alias cleanup
crates/context/interface/src/context.rs Added helper methods to SStoreResult

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants