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 degrade performances by 3.96%

Comparing rakita/gas-params (53141bd) with main (9afd166)

Summary

❌ 2 regressions
✅ 171 untouched

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Benchmarks breakdown

Benchmark BASE HEAD Change
transfer_finalize 17 µs 17.7 µs -3.96%
transfer 13.9 µs 14.5 µs -3.92%

@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.

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
}

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.

if !is_spurious_dragon || transfers_value {
return NEWACCOUNT;
}
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.

Moved to:

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

@rakita rakita merged commit 2befb62 into main Dec 10, 2025
30 of 31 checks passed
@github-actions github-actions bot mentioned this pull request Dec 10, 2025
&mut self,
evm: &mut Self::Evm,
) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
self.configure(evm);
Copy link
Collaborator

Choose a reason for hiding this comment

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

will this not work properly now for any impl overriding this trait? i don't think this is acceptable and we need to either hide this deeper or make it so this change is obvious from compilation issues after the bump

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.

3 participants