Skip to content

feat(test): add fast gas repricing and gas-map CLI tool#2331

Draft
Carsons-Eels wants to merge 12 commits intoethereum:forks/amsterdamfrom
Carsons-Eels:upgrade_repricing_support
Draft

feat(test): add fast gas repricing and gas-map CLI tool#2331
Carsons-Eels wants to merge 12 commits intoethereum:forks/amsterdamfrom
Carsons-Eels:upgrade_repricing_support

Conversation

@Carsons-Eels
Copy link
Copy Markdown
Contributor

@Carsons-Eels Carsons-Eels commented Feb 26, 2026

🗒️ Description

Implements gas repricing support for both the EEST test framework and the spec-side execution code, allowing users to override gas constants via a JSON config file without modifying source code. This enables fast iteration on alternative gas schedules, "what-if" analysis of proposed EIP gas changes, and comparative test runs under different cost models.

🔗 Related Issues or PRs

Core Repricing Mechanism

  • Shared config reader ethereum.utils.gas_repricing: Loads a JSON config from the path specified by the EELS_GAS_REPRICING_CONFIG environment variable. The config maps fork names to field overrides (e.g., {"Osaka": {"GAS_VERY_LOW": 4}}). Results are cached via lru_cache. Both spec-side and testing-side consumers import this shared reader.

  • Testing-side repricing: BaseFork.gas_costs() is now concrete — it calls the new abstract _base_gas_costs() to get the fork's default costs, then passes them through apply_repricing(). Validation (field name + type checks against GasCosts dataclass) happens at apply time in the testing module. All fork subclasses have been renamed from gas_costs() to _base_gas_costs() accordingly.

  • Spec-side repricing: apply_spec_repricing() is called at the bottom of all 24 src/ethereum/forks/*/vm/gas.py modules. It mutates module globals in place, preserving Uint/U64 type wrappers via type(original)(value). Validation against the module's actual globals ensures only real constants can be overridden. With no config set, this is a complete no-op.

  • .gitignore: Added gas_repricing.json so local config files aren't accidentally committed.

Gas Constant Name Unification

Important

Covered in PR #2383 which should be merged BEFORE this PR

Standardized 17 spec-side gas constant names to match the testing-side conventions (e.g., GAS_ECRECOVERGAS_PRECOMPILE_ECRECOVER, GAS_WARM_ACCESSGAS_WARM_ACCOUNT_ACCESS). This ensures users can use the same field names in their repricing config for both spec and test overrides. ~362 files affected, all within src/ethereum/forks/.

✨ CLI Tool gas-map

Invoke with:

uv run gas-map
# or
uv run eest gas-map

The mapping between opcodes (ADD, SLOAD, etc.) and GasCosts field names (GAS_LOW, GAS_WARM_SLOAD, etc.) is not obvious without reading source code. The gas-map CLI tool bridges this gap:

  • Registered both as a standalone entry point and as an eest subcommand
  • --fork/-f option (defaults to latest fork)
  • --opcode/-o option for single-opcode detail view
  • Uses source code analysis of the opcode_gas_map() inheritance chain and helper methods to accurately reverse-map opcodes to their GasCosts fields
  • Groups static opcodes by tier (GAS_VERY_LOW, GAS_LOW, etc.)
  • Details dynamic opcodes' relevant fields (including GAS_MEMORY for memory-expanding opcodes like REVERT), and shows constants
  • Single-opcode view shows type, field values, and a ready-to-paste gas_repricing.json snippet

📝 Documentation

  • docs/gas_repricing/repricing_guide.md: What repricing is, JSON config format, activation via env var, gas-map CLI usage, and an end-to-end example workflow.
  • docs/gas_repricing/reference.md: Static convenience reference of all ~90 GasCosts fields grouped by category, with affected opcodes and typical values. uv run gas-map is the authoritative source of truth per fork.
  • docs/navigation.md: New "Gas Repricing" nav section added after "Writing Tests".

🔬 Tests

  • test_gas_repricing.py: Full test suite covering:
    • load_repricing_config() (shared reader):
      • no env var, empty env var, missing file
      • unknown fields pass through (no validation in shared reader)
      • valid config loading, warning emission
    • apply_repricing() (testing-side):
      • no config, no matching fork
      • field overrides applied correctly
      • invalid field names raise ValueError
      • non-int values raise TypeError
    • apply_spec_repricing() (spec-side):
      • no config (no-op), missing fork (no-op)
      • module globals mutated with correct Uint/U64 type wrappers
      • unknown constant names raise ValueError
      • non-existent config file raises FileNotFoundError
    • Integration with fork classes:
      • FORK.gas_costs() returns repriced values
      • Earlier forks are unaffected by later fork repricing
      • Transition forks work correctly
    • Cache clearing between tests via autouse fixture

⚠️ Notes

Note

  • ethereum-spec-lint warnings: The apply_spec_repricing() calls at the bottom of each gas.py produce 8 "The expression <class 'ast.Expr'> has been ignored." print messages from the GlacierForksHygiene lint. These are informational prints (not diagnostic errors) — the lint compares glacier forks against predecessors and doesn't have a visitor for ast.Call expressions. The warnings are harmless and do not cause lint failure. We could update the linter to ignore this, but I didn't want to go changing the linter in this PR.
  • reference.md maintenance: The static tables in reference.md are a convenience reference. uv run gas-map is the authoritative per-fork source. A CI freshness check could be added in the future.

✅ Checklist

  • All: Ran fast tox checks to avoid unnecessary CI fails, see also Code Standards and Enabling Pre-commit Checks:
    uvx tox -e static
  • All: PR title adheres to the repo standard - it will be used as the squash commit message and should start type(scope):.
  • All: Considered updating the online docs in the ./docs/ directory.
  • All: Set appropriate labels for the changes (only maintainers can apply labels).
  • Tests: Ran mkdocs build --strict locally and verified docs render correctly.

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->
Bobcat by Jean Beaufort
License: CC0 Public Domain

@Carsons-Eels Carsons-Eels added C-feat Category: an improvement or new feature P-medium A-tooling Area: Improvements or changes to auxiliary tooling such as uv, ruff, mypy, ... labels Feb 26, 2026
@LouisTsai-Csie
Copy link
Copy Markdown
Collaborator

Amazing!

Link related issue #1879 .

@LouisTsai-Csie
Copy link
Copy Markdown
Collaborator

LouisTsai-Csie commented Feb 27, 2026

There is a problem using this tool for repricing testing: eip-7904 reprices opcodes that currently share a GasCosts tier to different individual costs:

Example:

┌────────┬─────────┬────────────────┬───────────────────┐
│ Opcode │ Current │ GasCosts field │ EIP-7904 new cost │
├────────┼─────────┼────────────────┼───────────────────┤
│ DIV    │ 5       │ GAS_LOW        │ 15                │
├────────┼─────────┼────────────────┼───────────────────┤
│ SDIV   │ 5       │ GAS_LOW        │ 20                │
├────────┼─────────┼────────────────┼───────────────────┤
│ MOD    │ 5       │ GAS_LOW        │ 12                │
├────────┼─────────┼────────────────┼───────────────────┤
│ SMOD   │ 5       │ GAS_LOW        │ 5 (unchanged)     │
├────────┼─────────┼────────────────┼───────────────────┤
│ MUL    │ 5       │ GAS_LOW        │ 5 (unchanged)     │
├────────┼─────────┼────────────────┼───────────────────┤
│ ADDMOD │ 8       │ GAS_MID        │ 8 (unchanged)     │
├────────┼─────────┼────────────────┼───────────────────┤
│ MULMOD │ 8       │ GAS_MID        │ 11                │
└────────┴─────────┴────────────────┴───────────────────┘

The repricing config operates at the GasCosts field level, there's no way to express: DIV=15, SDIV=20, MOD=12, SMOD=5, changing GAS_LOW would move all five opcodes to the same value. Same issue with GAS_MID: MULMOD needs 11 but ADDMOD stays at 8.

I would suggest the gas_repricings.json follow the format defined in issue #2200

{
  "amsterdam": {
    "DUP": 1000,
    "SSTORE": 5000
  },
  "osaka": {
    "CALL": 800
  }
}

@Carsons-Eels Carsons-Eels force-pushed the upgrade_repricing_support branch from 97be373 to 73b88e3 Compare March 2, 2026 17:00
@marioevz marioevz requested a review from LouisTsai-Csie March 2, 2026 22:33
@marioevz
Copy link
Copy Markdown
Member

marioevz commented Mar 2, 2026

The repricing config operates at the GasCosts field level, there's no way to express: DIV=15, SDIV=20, MOD=12, SMOD=5, changing GAS_LOW would move all five opcodes to the same value. Same issue with GAS_MID: MULMOD needs 11 but ADDMOD stays at 8.

I would suggest the gas_repricings.json follow the format defined in issue #2200

{
  "amsterdam": {
    "DUP": 1000,
    "SSTORE": 5000
  },
  "osaka": {
    "CALL": 800
  }
}

We would need an intermediate mapping in each fork:

GAS_LOW = 5
...
GAS_OPCODE_DIV = GAS_LOW
GAS_OPCODE_SDIV = GAS_LOW
GAS_OPCODE_MOD = GAS_LOW

Only then we would be able to target specific opcode prices with this tool.

It feels necessary too, I don't think we plan on affecting other Opcodes when repricing only because they use the same constant predecessor.

Should we open a precursor PR to create these new variables?

@Carsons-Eels
Copy link
Copy Markdown
Contributor Author

I ran into this issue headfirst while implementing, and my decision at the time was to implement a simpler version that dealt only with the constants just to get the thing up and running.

The problems with doing it per-opcode then were:

  • Updating the costs for the constants took care of keeping the downstream opcode costs in line
  • Doing it per-opcode pollutes the spec code by adding indirection that is harder to read and follow
  • Easier to keep spec/test in sync with a single, simple config file

However, I think I misunderstood that we would not strictly need per-opcode repricing right from the outset, and by that time I was halfway done implementing and wanted to at least reach an island of sanity before planning a refactor. I also wasn't sure that this PR was the place to do that, and thought about kicking that to a different issue because, yeah, as you pointed out @marioevz, this requires a bunch of intermediate mapping that I wasn't sure was appropriate for this PR.

I think a precursor PR is the way to go so that this PR has the pieces it needs to implement it properly. I'm happy to do that tomorrow after I fix the 5 test failures in the CI.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 3, 2026

Codecov Report

❌ Patch coverage is 70.17544% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.32%. Comparing base (0958360) to head (dad13e5).

Files with missing lines Patch % Lines
src/ethereum/utils/gas_repricing.py 45.16% 15 Missing and 2 partials ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           forks/amsterdam    #2331      +/-   ##
===================================================
- Coverage            86.35%   86.32%   -0.03%     
===================================================
  Files                  599      600       +1     
  Lines                36904    36961      +57     
  Branches              3771     3777       +6     
===================================================
+ Hits                 31868    31908      +40     
- Misses                4485     4500      +15     
- Partials               551      553       +2     
Flag Coverage Δ
unittests 86.32% <70.17%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Carsons-Eels Carsons-Eels force-pushed the upgrade_repricing_support branch 3 times, most recently from 4d1e93b to b1cbe30 Compare March 10, 2026 03:55
@Carsons-Eels
Copy link
Copy Markdown
Contributor Author

Carsons-Eels commented Mar 10, 2026

Right now the instruction files from all 24 forks import gas constants using from ..gas import CONSTANT, which creates shallow copies at import time rather than live references to the gas module's namespace. The apply_spec_repricing() function (from #2331) mutates gas.py's module globals at load time. If a gas constant has already been copied into the module's namespace with from ..gas import, the repriced value is silently lost — the instruction module would use the original value because only the copy changed.

Right now it works because interpreter.py happens to import gas (line 46) before importing instruction modules (line 55), so gas.py executes (including apply_spec_repricing()) before any instruction module copies those constants. This makes it pretty fragile; reordering those imports or introducing a circular dependency would silently break the repricing. Though, our ruff formatting keeps this kind of predictable at least.

PR #2396 has this fixed for the new gas.GAS_OPCODE_* module references. However, the remaining ~96 instruction files that would need modification still use the from ..gas import pattern.

I'm wondering if it's the right call to do this here in this PR, or in a separate PR. Mostly because it would bloat the PR another 96 files changed to 134 files which makes the other changes a pain to review.

For now, I've made those changes in a separate branch as a PR to myself targeted at PR #2396

@Carsons-Eels Carsons-Eels force-pushed the upgrade_repricing_support branch from 2994e8f to dad13e5 Compare March 25, 2026 18:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-tooling Area: Improvements or changes to auxiliary tooling such as uv, ruff, mypy, ... C-feat Category: an improvement or new feature P-medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(all): Repricing Tool

3 participants