Skip to content

refactor(spec-specs): add opcode gas constants#2396

Open
Carsons-Eels wants to merge 45 commits intoethereum:forks/amsterdamfrom
Carsons-Eels:intermediate_opcodes
Open

refactor(spec-specs): add opcode gas constants#2396
Carsons-Eels wants to merge 45 commits intoethereum:forks/amsterdamfrom
Carsons-Eels:intermediate_opcodes

Conversation

@Carsons-Eels
Copy link
Copy Markdown
Contributor

@Carsons-Eels Carsons-Eels commented Mar 3, 2026

🗒️ Description

Currently, multiple opcodes share the same gas tier constant (e.g. DIV, SDIV, MOD, SMOD, MUL all reference GAS_LOW = Uint(5)). This makes it impossible to reprice individual opcodes because changing GAS_LOW would affect all of them at once. EIP-7904 - General Repricing requires per-opcode repricing (e.g. DIV=15, SDIV=20, MOD=12 while MUL stays at 5).

This PR adds per-opcode gas constants (GAS_OPCODE_*) to all 24 fork gas.py modules and updates instruction files to use them instead of shared tie constants (GAS_VERY_LOW, GAS_LOW, GAS_MID, GAS_HIGH), and likewise makes the same changes to the test code by adding counterpart gas constants to the GasCosts dataclass.

This PR introduces intermediate variables like GAS_OPCODE_DIV = GAS_LOW in each fork's gas.py and updates instruction files to charge against the opcode-specific constant. The existing apply_spec_repricing() mechanism (from #2331) can then target GAS_OPCODE_DIV directly in the JSON config, enabling per-opcode repricing without changing spec code.

Additionally, GAS_BLOBHASH_OPCODE is renamed to GAS_BLOBHASH across 9 forks (cancun+) to align with the existing gas constant naming convention (GAS_EXPONENTIATION, GAS_KECCAK256, etc.) and maintain a clean separation from the new GAS_OPCODE_* repricing variables.

What changed:

Spec side:

  • 24 gas.py files: added 30–38 GAS_OPCODE_* variable definitions (varying by fork based on available opcodes)
  • 168 instruction files (7 per fork): updated imports and charge_gas() calls to use opcode-specific constants
  • 18 files (9 forks × 2): renamed GAS_BLOBHASH_OPCODEGAS_BLOBHASH

Testing side:

  • gas_costs.py: added 39 GAS_OPCODE_* fields to GasCosts dataclass
  • forks.py: updated Frontier.gas_costs() initialization and opcode_gas_map() in Frontier + 4 fork overrides (Byzantium, Constantinople, Cancun, Osaka) to reference per-opcode fields

194 files total, no behavioural change — all variables default to their original tier constant values

Fork groups by opcode availability:

Group Forks Variables
A frontier → spurious_dragon 30 (baseline)
B byzantium +RETURNDATACOPY
C constantinople → shanghai +SHL, SHR, SAR
D cancun, prague +MCOPY
E osaka → amsterdam +CLZ

🔗 Related Issues or PRs

✅ 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).
  • All: PR title adheres to the repo standard
    • it will be used as the squash commit message and should start
      type(scope):.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 3, 2026

Codecov Report

❌ Patch coverage is 99.82609% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.43%. Comparing base (f5b6d10) to head (99f5203).
⚠️ Report is 2 commits behind head on forks/amsterdam.

Files with missing lines Patch % Lines
...c/ethereum/forks/berlin/vm/instructions/storage.py 80.00% 2 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           forks/amsterdam    #2396      +/-   ##
===================================================
+ Coverage            86.24%   86.43%   +0.19%     
===================================================
  Files                  599      599              
  Lines                36984    37515     +531     
  Branches              3795     3795              
===================================================
+ Hits                 31895    32426     +531     
  Misses                4525     4525              
  Partials               564      564              
Flag Coverage Δ
unittests 86.43% <99.82%> (+0.19%) ⬆️

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.

@marioevz
Copy link
Copy Markdown
Member

marioevz commented Mar 3, 2026

Awesome!

cc @raxhvl, since this was directly mentioned in the gas-lighters call today.

@marioevz marioevz requested a review from SamWilsn March 3, 2026 18:52
@Carsons-Eels Carsons-Eels added A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) E-easy Experience: easy, good for newcomers P-high C-refactor Category: refactor A-test-tests Area: tests for packages/testing labels Mar 3, 2026
@Carsons-Eels Carsons-Eels marked this pull request as ready for review March 3, 2026 21:25
@marioevz
Copy link
Copy Markdown
Member

marioevz commented Mar 4, 2026

All failing tests are due to #2109, and unrelated to the changes in this PR:

2026-03-03T22:40:39.1715358Z FAILED tests/json_infra/fixtures/amsterdam_tests/fixtures/blockchain_tests/frontier/validation/test_gas_limit_below_minimum.json::tests/frontier/validation/test_header.py::test_gas_limit_below_minimum[fork_Amsterdam-blockchain_test-gas_limit_5000]
2026-03-03T22:40:39.1719512Z FAILED tests/json_infra/fixtures/amsterdam_tests/fixtures/blockchain_tests/static/state_tests/stStaticCall/static_InternalCallHittingGasLimit.json::tests/static/state_tests/stStaticCall/static_InternalCallHittingGasLimitFiller.json::static_InternalCallHittingGasLimit[fork_Amsterdam-blockchain_test_from_state_test-]
2026-03-03T22:40:39.1723591Z FAILED tests/json_infra/fixtures/amsterdam_tests/fixtures/blockchain_tests/static/state_tests/stTransactionTest/InternalCallHittingGasLimit.json::tests/static/state_tests/stTransactionTest/InternalCallHittingGasLimitFiller.json::InternalCallHittingGasLimit[fork_Amsterdam-blockchain_test_from_state_test-]

It should be ok for us to proceed to review -> merge.

Copy link
Copy Markdown
Member

@marioevz marioevz left a comment

Choose a reason for hiding this comment

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

Looks good to me, considering we are not touching opcodes that use GAS_BASE, which I'm guessing is because the amount of changes would grow even more, and it's not necessary for the gas repricings at the moment, we could leave it for a future PR if it becomes necessary IMO.

I just left a couple of comments and leave the approval to Sam when he returns.

Copy link
Copy Markdown
Contributor

@SamWilsn SamWilsn left a comment

Choose a reason for hiding this comment

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

Looks good to me! Is the intent to make these constants modifiable at runtime? If so, there's some additional nonsense you need to do because Python.

@Carsons-Eels Carsons-Eels force-pushed the intermediate_opcodes branch from bcf06ad to 4fb062e Compare March 10, 2026 02:56
Copy link
Copy Markdown
Contributor Author

@Carsons-Eels Carsons-Eels left a comment

Choose a reason for hiding this comment

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

Regarding Louis' comment about constants in Frontier; I pushed a change that moves the 8 GAS_OPCODE_* fields that don't exist until after Frontier (SHL, SHR, SAR, RETURNDATACOPY, BLOBHASH, MCOPY, CLZ) to dataclass defaults and then initialize them via replace() where the fork introduces new terms. Frontier no longer references them.

I think we could do the same thing for the other 23 non-opcode constants that are zero-initialised in Frontier but aren't used until later forks:

Byzantium: GAS_PRECOMPILE_ECADD, GAS_PRECOMPILE_ECMUL, GAS_PRECOMPILE_ECPAIRING_BASE, GAS_PRECOMPILE_ECPAIRING_PER_POINT
Istanbul: GAS_PRECOMPILE_BLAKE2F_BASE, GAS_PRECOMPILE_BLAKE2F_PER_ROUND
Berlin: GAS_TX_ACCESS_LIST_ADDRESS, GAS_TX_ACCESS_LIST_STORAGE_KEY
Cancun: GAS_PRECOMPILE_POINT_EVALUATION
Prague: GAS_TX_DATA_TOKEN_STANDARD, GAS_TX_DATA_TOKEN_FLOOR, GAS_AUTH_PER_EMPTY_ACCOUNT, REFUND_AUTH_PER_EXISTING_ACCOUNT, GAS_PRECOMPILE_BLS_G1ADD, GAS_PRECOMPILE_BLS_G1MUL, GAS_PRECOMPILE_BLS_G1MAP, GAS_PRECOMPILE_BLS_G2ADD, GAS_PRECOMPILE_BLS_G2MUL, GAS_PRECOMPILE_BLS_G2MAP, GAS_PRECOMPILE_BLS_PAIRING_BASE, GAS_PRECOMPILE_BLS_PAIRING_PER_PAIR
Osaka: GAS_PRECOMPILE_P256VERIFY
Amsterdam: GAS_BLOCK_ACCESS_LIST_ITEM

None of these are referenced in Frontier, and they're already set by replace() by the fork that introduces them; they'd just also get = 0 defaults on the dataclass. Frontier doesn't need to mention them at all. This would follow along the same lines as the adjustment I made for Louis' comment, but that predates this PR, so I'd rather get feedback first. We're kind of already here though, so might as well if it feels right.

Thoughts? @LouisTsai-Csie @marioevz

@Carsons-Eels
Copy link
Copy Markdown
Contributor Author

Carsons-Eels commented Mar 10, 2026

Looks good to me! Is the intent to make these constants modifiable at runtime? If so, there's some additional nonsense you need to do because Python.

I'll address that in #2331 scratch that, already modifying those files here. Might as well make the changes here and keep the diff of #2331 smaller.

@raxhvl
Copy link
Copy Markdown
Member

raxhvl commented Mar 17, 2026

opitonal nit: rename GAS_OPCODE_PUSH_N to GAS_OPCODE_PUSH

To ensure consistency with GAS_OPCODE_DUP/SWAP which are now named after their opcodes

@Carsons-Eels Carsons-Eels force-pushed the intermediate_opcodes branch 3 times, most recently from 30525d8 to e9f2c37 Compare March 25, 2026 16:12
@Carsons-Eels
Copy link
Copy Markdown
Contributor Author

I updated GAS_OPCODE_PUSH_N to GAS_OPCODE_PUSH and also added GAS_OPCODE_COINBASE too. I'm validating that no more have been missed for any of the forks, and will add those opcodes as well.

Copy link
Copy Markdown
Contributor

@SamWilsn SamWilsn left a comment

Choose a reason for hiding this comment

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

This is packages/** and src/ethereum/forks/amsterdam/**, and the occasional comment elsewhere.

GAS_COLD_ACCOUNT_ACCESS = Uint(2600)
GAS_WARM_ACCESS = Uint(100)

# Opcode specific vars used for repricing
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some opcodes (eg. COINBASE) don't have their own constant. Is there a rule or pattern for determining when an opcode gets a constant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not as such. I was thinking to refactor so that every opcode would have a corresponding constant, and the ones that are not opcodes themselves remain as is. Does that sound reasonable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yep, that's perfect.

GAS_BLOCK_ACCESS_LIST_ITEM: int

# Opcode specific gas constants for repricing
GAS_OPCODE_ADD: int
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This ship has likely sailed, so feel free to disregard, but if we use GasCosts. as the prefix every time, we can probably drop GAS_ from the constants.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's a good point, the prefix definitely came from before I refactored.

Copy link
Copy Markdown
Contributor

@SamWilsn SamWilsn left a comment

Choose a reason for hiding this comment

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

That's all of it, quickly read.


# GAS
charge_gas(evm, GAS_BASE)
charge_gas(evm, GasCosts.GAS_BASE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing opcode constant again perhaps?

GAS_COLD_ACCOUNT_ACCESS = Uint(2600)
GAS_WARM_ACCESS = Uint(100)

# Opcode specific vars used for repricing
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yep, that's perfect.

@Carsons-Eels Carsons-Eels force-pushed the intermediate_opcodes branch from caefd35 to c5c776e Compare March 31, 2026 17:02
Comment on lines +5 to +10
# Common Gas Cost Tiers
BASE = 2
VERY_LOW = 3
LOW = 5
MID = 8
HIGH = 10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do these names provide enough context, since they are accessed without a leading GasCosts.?

from execution_testing.forks.gas_costs import VERY_LOW

This would be the standard way to import the constant, and now VERY_LOW doesn't indicate anything about gas.

I'm happy either way, but wanted to ask.

Comment on lines +141 to +148
# Defined post-Frontier
OPCODE_SHL: int = 0
OPCODE_SHR: int = 0
OPCODE_SAR: int = 0
OPCODE_RETURNDATACOPY: int = 0
OPCODE_BLOBHASH: int = 0
OPCODE_MCOPY: int = 0
OPCODE_CLZ: int = 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using zero as a sentinel for "not present" could cause issues, because zero is also (obviously) a valid integer. I can foresee a scenario where we accidentally assume OPCODE_SHL is present where it was set to zero, mypy wouldn't be able to detect it, and we end up with a zero cost operation. Using int | None would force developers to assert at the time of use.

That said, if this is already how the code works, changing it would be out of scope for this PR and this should be addressed in a separate issue.


# GAS
charge_gas(evm, GAS_BASE)
charge_gas(evm, GasCosts.BASE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why isn't this GasCosts.OPCODE_COINBASE?


# GAS
charge_gas(evm, GAS_BASE)
charge_gas(evm, GasCosts.BASE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
charge_gas(evm, GasCosts.BASE)
charge_gas(evm, GasCosts.OPCODE_TIMESTAMP)

Similarly the rest of these.

Or did we discuss that but I've forgotten?

Comment on lines +71 to +75
if address in evm.accessed_addresses:
charge_gas(evm, GasCosts.WARM_ACCESS)
else:
evm.accessed_addresses.add(address)

charge_gas(evm, gas_cost)
charge_gas(evm, GasCosts.COLD_ACCOUNT_ACCESS)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is much nicer looking.

# GAS
words = ceil32(Uint(size)) // Uint(32)
copy_gas_cost = GAS_RETURN_DATA_COPY * words
copy_gas_cost = GasCosts.RETURN_DATA_COPY * words
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When does a constant get _PER_WORD?

Comment on lines +29 to +34
class GasCosts:
"""
Constant gas values for the EVM.

These values may be patched at runtime by a future gas repricing utility
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
class GasCosts:
"""
Constant gas values for the EVM.
These values may be patched at runtime by a future gas repricing utility
"""
# These constants are in a class so they can be patched at runtime.
class GasCosts:
"""
Gas value constants for the EVM.
"""

I'd put the note about patching into a comment, since it isn't relevant to someone reading the specification; it's more of a note to other developers.


# Utility
ZERO = Uint(0)
RETURN_DATA_COPY = Uint(3) # TODO refactor to OPCODE_RETURNDATACOPY?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Put longer lived TODOs in issues, not in code.

OPCODE_SELFDESTRUCT_BASE = Uint(5000)

# TODO
MEMORY = Uint(3)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this have a _PER_WORD suffix?

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

Labels

A-spec-specs Area: Specification—The Ethereum specification itself (eg. `src/ethereum/*`) A-test-tests Area: tests for packages/testing C-refactor Category: refactor E-easy Experience: easy, good for newcomers P-high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants