Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ee5f210
feat: implement EIP-8037 state gas (reservoir model)
rakita Mar 13, 2026
d154685
Merge remote-tracking branch 'origin/main' into state-gas
rakita Mar 13, 2026
556602b
rm statetest jsons
rakita Mar 13, 2026
1e9fe4a
Rename spent to regular_gas_spent in ResultGas and simplify reservoir…
rakita Mar 16, 2026
257d261
Simplify EIP-8037 state gas: remove redundant fields from ResultGas a…
rakita Mar 18, 2026
4534164
Move EIP-7702 state gas refund split into pre_execution
rakita Mar 18, 2026
7a826db
Add PrecompileFailure type for state gas propagation on error
rakita Mar 18, 2026
a9b3634
Merge remote-tracking branch 'origin/main' into state-gas
rakita Mar 18, 2026
03b7c8d
Convert precompile errors to PrecompileFailure via .into()
rakita Mar 18, 2026
72b376c
Fix EIP-8037 state gas accounting regression
rakita Mar 18, 2026
3a29466
Move state gas deduction into first_frame_input
rakita Mar 18, 2026
b6ccdff
Fix EIP-8037 gas accounting: floor gas, reservoir, inspector, validation
rakita Mar 21, 2026
242243f
Fix EIP-8037 reservoir accounting: tx_gas_used, child revert refunds,…
rakita Mar 22, 2026
712dac7
Fix CREATE/CREATE2 static call check ordering for EIP-8037 state gas …
rakita Mar 22, 2026
7a999fc
Simplify last_frame_result: remove initial_reservoir parameter and fi…
rakita Mar 23, 2026
5f94ddb
Revert CREATE/CREATE2 static call check to before gas charging
rakita Mar 24, 2026
1e1d64d
Fix reservoir handling: restore state gas on revert/halt
rakita Mar 24, 2026
5001497
Fix EIP-8037 state_gas_spent accounting for EIP-7702 auth list
rakita Mar 24, 2026
319fe0a
Merge remote-tracking branch 'origin/main' into rakita/state-gas
rakita Mar 24, 2026
5aef2ce
Improve precompile reservoir handling and apply formatting fixes
rakita Mar 24, 2026
033d0ef
Fix precompile_provider compilation errors against current precompile…
rakita Mar 25, 2026
81d3b17
Simplify precompile interface: remove gas_limit from PrecompileOutput…
rakita Mar 25, 2026
94e0305
Fix EIP-8037 reservoir refill tests and complete precompile interface…
rakita Mar 25, 2026
03986fc
temporary remove all ee-tests
rakita Mar 25, 2026
9d5d898
Fix state gas accounting: include reservoir in total_gas_spent and pr…
rakita Mar 25, 2026
f77d65f
Apply formatting fixes and use saturating arithmetic for state gas spent
rakita Mar 25, 2026
ef18e6a
Revert "temporary remove all ee-tests"
rakita Mar 25, 2026
be396f7
Fix total_gas_spent to exclude reservoir and use saturating arithmetic
rakita Mar 25, 2026
7e01885
Use build_result_gas helper in system call gas handling
rakita Mar 27, 2026
4b975de
Merge remote-tracking branch 'origin/main' into rakita/state-gas
rakita Mar 27, 2026
7efb2be
remove testing files
rakita Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
805 changes: 483 additions & 322 deletions Cargo.lock

Large diffs are not rendered by default.

37 changes: 22 additions & 15 deletions bins/revme/src/cmd/blockchaintest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ fn execute_blockchain_test(
| ForkSpec::CancunToPragueAtTime15k
| ForkSpec::PragueToOsakaAtTime15k
| ForkSpec::BPO1ToBPO2AtTime15k
| ForkSpec::BPO2ToAmsterdamAtTime15k
) {
eprintln!("⚠️ Skipping transition fork: {:?}", test_case.network);
return Ok(());
Expand Down Expand Up @@ -760,8 +761,11 @@ fn execute_blockchain_test(
error: format!("{e:?}"),
})?;

// Track cumulative gas used across all transactions in this block
let mut cumulative_gas_used: u64 = 0;
// Track cumulative gas used across all transactions in this block.
// EIP-8037: Split gas accounting into regular (execution) and state gas.
let mut cumulative_tx_gas_used: u64 = 0;
let mut block_regular_gas_used: u64 = 0;
let mut block_state_gas_used: u64 = 0;
let mut block_completed = true;

// Execute each transaction in the block
Expand Down Expand Up @@ -884,7 +888,7 @@ fn execute_blockchain_test(
"block": block_idx,
"tx": tx_idx,
"expected_exception": expected_exception,
"gas_used": result.result.gas_used(),
"gas_used": result.result.gas().tx_gas_used(),
"status": "unexpected_success"
});
print_json(&output);
Expand All @@ -896,15 +900,11 @@ fn execute_blockchain_test(
block_completed = false;
break; // Skip to next block
}
// EIP-7778: Block gas accounting without refunds.
// For Amsterdam+, block gas = max(spent, floor_gas).
// For pre-Amsterdam, block gas = used() = max(spent - refunded, floor_gas).
// EIP-8037: Split gas accounting.
let gas = result.result.gas();
cumulative_gas_used += if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
gas.spent().max(gas.floor_gas())
} else {
gas.used()
};
cumulative_tx_gas_used += gas.tx_gas_used();
block_regular_gas_used += gas.block_regular_gas_used();
block_state_gas_used += gas.block_state_gas_used();
evm.commit(result.state);
}
Err(e) => {
Expand Down Expand Up @@ -962,20 +962,27 @@ fn execute_blockchain_test(
}
}

// Validate block gas used against header
// Validate block gas used against header.
// EIP-8037 (Amsterdam+): block gas_used = max(regular_gas, state_gas).
// Pre-Amsterdam: block gas_used = cumulative tx_gas_used (includes refunds).
if block_completed && !should_fail {
if let Some(block_header) = block.block_header.as_ref() {
let expected_gas_used = block_header.gas_used.to::<u64>();
if cumulative_gas_used != expected_gas_used {
let actual_block_gas_used = if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
block_regular_gas_used.max(block_state_gas_used)
} else {
cumulative_tx_gas_used
};
if actual_block_gas_used != expected_gas_used {
if print_env_on_error {
eprintln!(
"Block gas used mismatch at block {block_idx}: expected {expected_gas_used}, got {cumulative_gas_used}"
"Block gas used mismatch at block {block_idx}: expected {expected_gas_used}, got {actual_block_gas_used} (regular: {block_regular_gas_used}, state: {block_state_gas_used}, tx: {cumulative_tx_gas_used})"
);
}
return Err(TestExecutionError::BlockGasUsedMismatch {
block_idx,
expected: expected_gas_used,
actual: cumulative_gas_used,
actual: actual_block_gas_used,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion bins/revme/src/cmd/statetest/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ fn build_json_output(
"stateRoot": validation.state_root,
"logsRoot": validation.logs_root,
"output": exec_result.as_ref().ok().and_then(|r| r.output().cloned()).unwrap_or_default(),
"gasUsed": exec_result.as_ref().ok().map(|r| r.gas_used()).unwrap_or_default(),
"gasUsed": exec_result.as_ref().ok().map(|r| r.tx_gas_used()).unwrap_or_default(),
"pass": error.is_none(),
"errorMsg": error.unwrap_or_default(),
"evmResult": format_evm_result(exec_result),
Expand Down
7 changes: 7 additions & 0 deletions crates/context/interface/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ pub trait Cfg {

/// Returns the gas params for the EVM.
fn gas_params(&self) -> &GasParams;

/// Returns whether EIP-8037 (Amsterdam) state creation gas cost increase is enabled.
///
/// When enabled, storage creation gas is tracked separately from regular gas
/// via the reservoir model. EIP-8037 specifies concrete gas values based on
/// `cost_per_state_byte` and adds a hash cost for deployed bytecode.
fn is_amsterdam_eip8037_enabled(&self) -> bool;
}

/// What bytecode analysis to perform
Expand Down
190 changes: 187 additions & 3 deletions crates/context/interface/src/cfg/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,161 @@
use crate::{cfg::GasParams, transaction::AccessListItemTr as _, Transaction, TransactionType};
use primitives::hardfork::SpecId;

/// Tracker for gas during execution.
///
/// This is used to track the gas during execution.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GasTracker {
/// Gas Limit,
gas_limit: u64,
/// Regular gas remaining (`gas_left`). Reservoir is tracked separately.
remaining: u64,
/// State gas reservoir (gas exceeding TX_MAX_GAS_LIMIT). Starts as `execution_gas - min(execution_gas, regular_gas_budget)`.
/// When 0, all remaining gas is regular gas with hard cap at `TX_MAX_GAS_LIMIT`.
reservoir: u64,
/// Total state gas spent so far.
state_gas_spent: u64,
/// Refunded gas. Used to refund the gas to the caller at the end of execution.
refunded: i64,
}

impl GasTracker {
/// Creates a new `GasTracker` with the given remaining gas and reservoir.
#[inline]
pub const fn new(gas_limit: u64, remaining: u64, reservoir: u64) -> Self {
Self {
gas_limit,
remaining,
reservoir,
state_gas_spent: 0,
refunded: 0,
}
}

/// Creates a new `GasTracker` with the given used gas and reservoir.
#[inline]
pub const fn new_used_gas(gas_limit: u64, used_gas: u64, reservoir: u64) -> Self {
Self::new(gas_limit, gas_limit - used_gas, reservoir)
}

/// Returns the gas limit.
#[inline]
pub const fn limit(&self) -> u64 {
self.gas_limit
}

/// Sets the gas limit.
#[inline]
pub fn set_limit(&mut self, val: u64) {
self.gas_limit = val;
}

/// Returns the remaining gas.
#[inline]
pub const fn remaining(&self) -> u64 {
self.remaining
}

/// Sets the remaining gas.
#[inline]
pub fn set_remaining(&mut self, val: u64) {
self.remaining = val;
}

/// Returns the reservoir gas.
#[inline]
pub const fn reservoir(&self) -> u64 {
self.reservoir
}

/// Sets the reservoir gas.
#[inline]
pub fn set_reservoir(&mut self, val: u64) {
self.reservoir = val;
}

/// Returns the state gas spent.
#[inline]
pub const fn state_gas_spent(&self) -> u64 {
self.state_gas_spent
}

/// Sets the state gas spent.
#[inline]
pub fn set_state_gas_spent(&mut self, val: u64) {
self.state_gas_spent = val;
}

/// Returns the refunded gas.
#[inline]
pub const fn refunded(&self) -> i64 {
self.refunded
}

/// Sets the refunded gas.
#[inline]
pub fn set_refunded(&mut self, val: i64) {
self.refunded = val;
}

/// Records a regular gas cost.
///
/// Deducts from `remaining`. Returns `false` if insufficient gas.
#[inline]
#[must_use]
pub fn record_regular_cost(&mut self, cost: u64) -> bool {
if let Some(new_remaining) = self.remaining.checked_sub(cost) {
self.remaining = new_remaining;
return true;
}
false
}

/// Records a state gas cost (EIP-8037 reservoir model).
///
/// State gas charges deduct from the reservoir first. If the reservoir is exhausted,
/// remaining charges spill into `remaining` (requiring `remaining >= cost`).
/// Tracks state gas spent.
///
/// Returns `false` if total remaining gas is insufficient.
#[inline]
pub fn record_state_cost(&mut self, cost: u64) -> bool {
if self.reservoir >= cost {
self.state_gas_spent = self.state_gas_spent.saturating_add(cost);
self.reservoir -= cost;
return true;
}

let spill = cost - self.reservoir;

let success = self.record_regular_cost(spill);
if success {
self.state_gas_spent = self.state_gas_spent.saturating_add(cost);
self.reservoir = 0;
}
success
}

/// Records a refund value.
#[inline]
pub fn record_refund(&mut self, refund: i64) {
self.refunded += refund;
}

/// Erases a gas cost from remaining (returns gas from child frame).
#[inline]
pub fn erase_cost(&mut self, returned: u64) {
self.remaining += returned;
}

/// Spends all remaining gas excluding the reservoir.
#[inline]
pub fn spend_all(&mut self) {
self.remaining = 0;
}
}

/// Gas cost for operations that consume zero gas.
pub const ZERO: u64 = 0;
/// Base gas cost for basic operations.
Expand Down Expand Up @@ -112,19 +267,48 @@ pub const CALL_STIPEND: u64 = 2300;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InitialAndFloorGas {
/// Initial gas for transaction.
pub initial_gas: u64,
pub initial_total_gas: u64,
/// State gas component of initial_gas (subset of initial_total_gas).
/// Under EIP-8037, this includes:
/// - EIP-7702 auth list state gas (per-auth account creation + metadata costs)
/// - For CREATE transactions: `create_state_gas` (account creation + contract metadata)
/// - For CALL transactions: 0 (state gas is unpredictable at validation time)
pub initial_state_gas: u64,
/// If transaction is a Call and Prague is enabled
/// floor_gas is at least amount of gas that is going to be spent.
pub floor_gas: u64,
/// EIP-7702 state gas refund for existing authorities.
/// Added to the reservoir after initial_state_gas is deducted.
/// In the Python spec, set_delegation adds this back to state_gas_reservoir
/// rather than reducing initial_state_gas, so the refunded gas stays as
/// reservoir gas (not regular gas).
pub eip7702_reservoir_refund: u64,
}

impl InitialAndFloorGas {
/// Create a new InitialAndFloorGas instance.
#[inline]
pub const fn new(initial_gas: u64, floor_gas: u64) -> Self {
pub const fn new(initial_total_gas: u64, floor_gas: u64) -> Self {
Self {
initial_total_gas,
initial_state_gas: 0,
floor_gas,
eip7702_reservoir_refund: 0,
}
}

/// Create a new InitialAndFloorGas instance with state gas tracking.
#[inline]
pub const fn new_with_state_gas(
initial_total_gas: u64,
initial_state_gas: u64,
floor_gas: u64,
) -> Self {
Self {
initial_gas,
initial_total_gas,
initial_state_gas,
floor_gas,
eip7702_reservoir_refund: 0,
}
}
}
Expand Down
Loading
Loading