From 91b83291b5993a50b2e8c890ce291994ece7e22e Mon Sep 17 00:00:00 2001 From: carsons-eels Date: Wed, 25 Feb 2026 16:32:32 -0500 Subject: [PATCH 1/4] feat(spec,test): fast repricing for test code - add new gas-map tool and documentation to help with repricing - wire gas-map into the eest commands for discoverability --- .gitignore | 3 + docs/gas_repricing/reference.md | 161 +++++++++ docs/gas_repricing/repricing_guide.md | 89 +++++ docs/navigation.md | 3 + packages/testing/pyproject.toml | 1 + .../src/execution_testing/cli/eest/cli.py | 2 + .../cli/eest/commands/gas_map.py | 300 +++++++++++++++++ .../src/execution_testing/forks/base_fork.py | 21 +- .../forks/forks/eips/amsterdam/eip_7928.py | 5 +- .../forks/forks/eips/byzantium/eip_196.py | 5 +- .../forks/forks/eips/byzantium/eip_197.py | 5 +- .../forks/forks/eips/cancun/eip_4844.py | 5 +- .../forks/forks/eips/istanbul/eip_1108.py | 5 +- .../forks/forks/eips/istanbul/eip_152.py | 5 +- .../forks/forks/eips/istanbul/eip_2028.py | 5 +- .../forks/forks/eips/osaka/eip_7951.py | 5 +- .../forks/forks/eips/prague/eip_2537.py | 5 +- .../forks/forks/eips/prague/eip_7623.py | 5 +- .../forks/forks/eips/prague/eip_7702.py | 5 +- .../execution_testing/forks/forks/forks.py | 5 +- .../execution_testing/forks/gas_repricing.py | 43 +++ .../forks/tests/test_gas_repricing.py | 305 ++++++++++++++++++ .../forks/transition_base_fork.py | 13 +- src/ethereum/forks/amsterdam/vm/gas.py | 4 + src/ethereum/forks/arrow_glacier/vm/gas.py | 4 + src/ethereum/forks/berlin/vm/gas.py | 4 + src/ethereum/forks/bpo1/vm/gas.py | 4 + src/ethereum/forks/bpo2/vm/gas.py | 4 + src/ethereum/forks/bpo3/vm/gas.py | 4 + src/ethereum/forks/bpo4/vm/gas.py | 4 + src/ethereum/forks/bpo5/vm/gas.py | 4 + src/ethereum/forks/byzantium/vm/gas.py | 4 + src/ethereum/forks/cancun/vm/gas.py | 4 + src/ethereum/forks/constantinople/vm/gas.py | 4 + src/ethereum/forks/dao_fork/vm/gas.py | 4 + src/ethereum/forks/frontier/vm/gas.py | 4 + src/ethereum/forks/gray_glacier/vm/gas.py | 4 + src/ethereum/forks/homestead/vm/gas.py | 4 + src/ethereum/forks/istanbul/vm/gas.py | 4 + src/ethereum/forks/london/vm/gas.py | 4 + src/ethereum/forks/muir_glacier/vm/gas.py | 4 + src/ethereum/forks/osaka/vm/gas.py | 4 + src/ethereum/forks/paris/vm/gas.py | 4 + src/ethereum/forks/prague/vm/gas.py | 4 + src/ethereum/forks/shanghai/vm/gas.py | 1 + src/ethereum/forks/spurious_dragon/vm/gas.py | 4 + .../forks/tangerine_whistle/vm/gas.py | 4 + src/ethereum/utils/gas_repricing.py | 66 ++++ vulture_whitelist.py | 10 + 49 files changed, 1155 insertions(+), 15 deletions(-) create mode 100644 docs/gas_repricing/reference.md create mode 100644 docs/gas_repricing/repricing_guide.md create mode 100644 packages/testing/src/execution_testing/cli/eest/commands/gas_map.py create mode 100644 packages/testing/src/execution_testing/forks/gas_repricing.py create mode 100644 packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py create mode 100644 src/ethereum/utils/gas_repricing.py diff --git a/.gitignore b/.gitignore index b736a90b110..7388d5159c3 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,6 @@ site/ # Temporary data; used for generated checklists tmp/ + +# Gas Repricing +gas_repricing.json diff --git a/docs/gas_repricing/reference.md b/docs/gas_repricing/reference.md new file mode 100644 index 00000000000..6f9e0ab69ef --- /dev/null +++ b/docs/gas_repricing/reference.md @@ -0,0 +1,161 @@ +# GasCosts Reference + +This page lists the `GasCosts` fields and the opcodes they affect. Use this as +a reference when creating or editing a `gas_repricing.json` config file to help +you know which field to override. + +For an up-to-date, fork-specific mapping, run: + +```bash +uv run gas-map --fork +``` + +## Base Operation Costs + +| GasCosts Field | Typical Value | Affected Opcodes | +|---|---|---| +| `GAS_VERY_LOW` | 3 | ADD, SUB, CALLDATALOAD, LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR, SIGNEXTEND, PUSH1-PUSH32, DUP1-DUP16, SWAP1-SWAP16, MLOAD, MSTORE, MSTORE8 | +| `GAS_LOW` | 5 | MUL, DIV, SDIV, MOD, SMOD, CLZ | +| `GAS_MID` | 8 | ADDMOD, MULMOD, JUMP | +| `GAS_HIGH` | 10 | JUMPI | +| `GAS_BASE` | 2 | ADDRESS, ORIGIN, CALLER, CALLVALUE, CALLDATASIZE, CODESIZE, GASPRICE, COINBASE, TIMESTAMP, NUMBER, PREVRANDAO, GASLIMIT, POP, PC, MSIZE, GAS, RETURNDATASIZE, CHAINID, SELFBALANCE, BASEFEE, BLOBBASEFEE | +| `GAS_JUMPDEST` | 1 | JUMPDEST | +| `GAS_BLOCK_HASH` | 20 | BLOCKHASH | + +## Storage Costs + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_WARM_SLOAD` | 100 | SLOAD when slot is warm | +| `GAS_COLD_SLOAD` | 2100 | SLOAD when slot is cold | +| `GAS_STORAGE_SET` | 20000 | SSTORE: setting a slot from zero to non-zero | +| `GAS_STORAGE_UPDATE` | 2900 | SSTORE: updating existing non-zero slot | +| `GAS_STORAGE_RESET` | 2900 | SSTORE: resetting to original value | + +## Account Access Costs + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_WARM_ACCOUNT_ACCESS` | 100 | BALANCE, EXTCODESIZE, etc. when warm | +| `GAS_COLD_ACCOUNT_ACCESS` | 2600 | BALANCE, EXTCODESIZE, etc. when cold | +| `GAS_TX_ACCESS_LIST_ADDRESS` | 2400 | Per address in access list | +| `GAS_TX_ACCESS_LIST_STORAGE_KEY` | 1900 | Per storage key in access list | + +## Exponentiation + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_EXPONENTIATION` | 10 | EXP base cost | +| `GAS_EXPONENTIATION_PER_BYTE` | 50 | EXP per byte of exponent | + +## Memory and Copy + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_MEMORY` | 3 | Memory expansion cost coefficient | +| `GAS_COPY` | 3 | Per-word copy cost (CALLDATACOPY, CODECOPY, etc.) | +| `GAS_KECCAK256` | 30 | SHA3 base cost | +| `GAS_KECCAK256_PER_WORD` | 6 | SHA3 per 32-byte word | + +## Logging + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_LOG` | 375 | LOG base cost | +| `GAS_LOG_DATA_PER_BYTE` | 8 | LOG per byte of data | +| `GAS_LOG_TOPIC` | 375 | LOG per topic | + +## Transaction Costs + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_TX_BASE` | 21000 | Base transaction cost | +| `GAS_TX_CREATE` | 32000 | Additional cost for contract creation tx | +| `GAS_TX_DATA_PER_ZERO` | 4 | Per zero byte in tx data | +| `GAS_TX_DATA_PER_NON_ZERO` | 16 | Per non-zero byte in tx data | +| `GAS_TX_DATA_TOKEN_STANDARD` | 4 | Token cost per data element | +| `GAS_TX_DATA_TOKEN_FLOOR` | 0 | Minimum token cost | + +## Call and Create + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_CALL_VALUE` | 9000 | Additional cost when transferring value | +| `GAS_CALL_STIPEND` | 2300 | Gas stipend for calls with value | +| `GAS_NEW_ACCOUNT` | 25000 | Creating a new account via call | +| `GAS_CREATE` | 32000 | CREATE opcode base cost | +| `GAS_CODE_DEPOSIT_PER_BYTE` | 200 | Per byte of deployed code | +| `GAS_CODE_INIT_PER_WORD` | 2 | Per word of init code (EIP-3860) | +| `GAS_SELF_DESTRUCT` | 5000 | SELFDESTRUCT base cost | + +## Auth (EIP-3074) + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `GAS_AUTH_PER_EMPTY_ACCOUNT` | 25000 | AUTH cost for empty account | + +## Precompile Costs + +| GasCosts Field | Typical Value | Precompile | +|---|---|---| +| `GAS_PRECOMPILE_ECRECOVER` | 3000 | ecRecover (0x01) | +| `GAS_PRECOMPILE_SHA256_BASE` | 60 | SHA-256 base (0x02) | +| `GAS_PRECOMPILE_SHA256_PER_WORD` | 12 | SHA-256 per word (0x02) | +| `GAS_PRECOMPILE_RIPEMD160_BASE` | 600 | RIPEMD-160 base (0x03) | +| `GAS_PRECOMPILE_RIPEMD160_PER_WORD` | 120 | RIPEMD-160 per word (0x03) | +| `GAS_PRECOMPILE_IDENTITY_BASE` | 15 | Identity base (0x04) | +| `GAS_PRECOMPILE_IDENTITY_PER_WORD` | 3 | Identity per word (0x04) | +| `GAS_PRECOMPILE_ECADD` | 150 | BN256 add (0x06) | +| `GAS_PRECOMPILE_ECMUL` | 6000 | BN256 mul (0x07) | +| `GAS_PRECOMPILE_ECPAIRING_BASE` | 45000 | BN256 pairing base (0x08) | +| `GAS_PRECOMPILE_ECPAIRING_PER_POINT` | 34000 | BN256 pairing per point (0x08) | +| `GAS_PRECOMPILE_BLAKE2F_BASE` | 0 | BLAKE2 base (0x09) | +| `GAS_PRECOMPILE_BLAKE2F_PER_ROUND` | 1 | BLAKE2 per round (0x09) | +| `GAS_PRECOMPILE_POINT_EVALUATION` | 50000 | Point evaluation (0x0a) | +| `GAS_PRECOMPILE_BLS_G1ADD` | 500 | BLS G1 add (0x0b) | +| `GAS_PRECOMPILE_BLS_G1MUL` | 12000 | BLS G1 mul (0x0c) | +| `GAS_PRECOMPILE_BLS_G1MAP` | 5500 | BLS G1 map (0x12) | +| `GAS_PRECOMPILE_BLS_G2ADD` | 800 | BLS G2 add (0x0d) | +| `GAS_PRECOMPILE_BLS_G2MUL` | 45000 | BLS G2 mul (0x0e) | +| `GAS_PRECOMPILE_BLS_G2MAP` | 110000 | BLS G2 map (0x13) | +| `GAS_PRECOMPILE_BLS_PAIRING_BASE` | 115000 | BLS pairing base (0x11) | +| `GAS_PRECOMPILE_BLS_PAIRING_PER_PAIR` | 23000 | BLS pairing per pair (0x11) | +| `GAS_PRECOMPILE_P256VERIFY` | 6900 | P256 verify (0x100) | + +## Refund Constants + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `REFUND_STORAGE_CLEAR` | 4800 | Refund for clearing a storage slot | +| `REFUND_AUTH_PER_EXISTING_ACCOUNT` | 25000 | AUTH refund for existing account | + +## Dynamic Opcodes + +Some opcodes have dynamic gas costs that depend on multiple `GasCosts` fields +and runtime context: + +| Opcode | Relevant GasCosts Fields | Notes | +|---|---|---| +| EXP | `GAS_EXPONENTIATION`, `GAS_EXPONENTIATION_PER_BYTE` | Cost depends on exponent size | +| SLOAD | `GAS_WARM_SLOAD`, `GAS_COLD_SLOAD` | Warm vs cold access | +| SSTORE | `GAS_STORAGE_SET`, `GAS_STORAGE_UPDATE`, `GAS_STORAGE_RESET`, `GAS_WARM_SLOAD`, `GAS_COLD_SLOAD` | Complex rules based on original/current/new values | +| SHA3 | `GAS_KECCAK256`, `GAS_KECCAK256_PER_WORD` | Base + per-word cost | +| LOG0-LOG4 | `GAS_LOG`, `GAS_LOG_DATA_PER_BYTE`, `GAS_LOG_TOPIC` | Base + data + topics | +| CALL/CALLCODE | `GAS_WARM_ACCOUNT_ACCESS`, `GAS_COLD_ACCOUNT_ACCESS`, `GAS_CALL_VALUE`, `GAS_NEW_ACCOUNT` | Complex rules based on account state | +| CREATE/CREATE2 | `GAS_CREATE`, `GAS_CODE_INIT_PER_WORD` | Base + init code cost | +| BALANCE/EXTCODESIZE | `GAS_WARM_ACCOUNT_ACCESS`, `GAS_COLD_ACCOUNT_ACCESS` | Warm vs cold access | +| SELFDESTRUCT | `GAS_SELF_DESTRUCT`, `GAS_COLD_ACCOUNT_ACCESS`, `GAS_NEW_ACCOUNT` | Depends on target account state | + +## Generating Up-to-Date Mappings + +The tables above reflect typical values. Exact values vary by fork. + +For the authoritative mapping for a specific fork: + +```bash +# Full mapping +uv run gas-map --fork Osaka + +# Single opcode detail +uv run gas-map --opcode SLOAD --fork Osaka +``` diff --git a/docs/gas_repricing/repricing_guide.md b/docs/gas_repricing/repricing_guide.md new file mode 100644 index 00000000000..5fa1b1b5cfd --- /dev/null +++ b/docs/gas_repricing/repricing_guide.md @@ -0,0 +1,89 @@ +# Gas Repricing Guide + +## What is Gas Repricing? + +Gas repricing allows you to override the default gas cost constants for any fork +without modifying source code. This is useful for: + +- Experimenting with alternative gas schedules +- Testing the impact of proposed EIP gas changes +- Running "what-if" analyses on existing test suites + +## JSON Config Format + +Create a JSON file mapping fork names to `GasCosts` field overrides: + +```json +{ + "Osaka": { + "GAS_VERY_LOW": 4, + "GAS_COLD_SLOAD": 2200 + }, + "Prague": { + "GAS_WARM_SLOAD": 150 + } +} +``` + +Each key is a fork name (e.g., `Osaka`, `Prague`, `Cancun`). Each value is an +object mapping `GasCosts` field names to their new integer values. Only the +fields you want to change need to be specified; all others retain their +defaults. + +## Activation + +Set the `EELS_GAS_REPRICING_CONFIG` environment variable to the path of your +JSON config file: + +```bash +export EELS_GAS_REPRICING_CONFIG=./my_gas_repricing.json +uv run fill tests/osaka/ +``` + +The repricing config is loaded once (cached) and applied transparently whenever +`gas_costs()` is called on a fork. + +## Finding the Right Field Names + +The `GasCosts` dataclass has ~90 fields. To find which field controls a +particular opcode's gas cost, use the `gas-map` CLI tool: + +```bash +# Show full mapping for a fork +uv run gas-map --fork Osaka + +# Look up a specific opcode +uv run gas-map --opcode SLOAD +``` + +See [GasCosts Reference](reference.md) for a static reference table. + +## Example Workflow + +1. Identify the opcodes you want to reprice: + + ```bash + uv run gas-map --opcode SLOAD + ``` + + Output shows `GAS_WARM_SLOAD` and `GAS_COLD_SLOAD` are the relevant fields. + +2. Create a repricing config: + + ```json + { + "Osaka": { + "GAS_WARM_SLOAD": 150, + "GAS_COLD_SLOAD": 2500 + } + } + ``` + +3. Run tests with the new gas schedule: + + ```bash + EELS_GAS_REPRICING_CONFIG=./reprice.json uv run fill tests/osaka/ + ``` + +4. Compare results against the baseline to see which tests break or change + behavior under the new gas schedule. diff --git a/docs/navigation.md b/docs/navigation.md index 20bf2f8cd3b..3b0453fd771 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -74,6 +74,9 @@ * [Execute Eth Config](./running_tests/execute/eth_config.md) * [Transaction Metadata](./running_tests/execute/transaction_metadata.md) * [Useful Pytest Options](running_tests/useful_pytest_options.md) + * Gas Repricing + * [Repricing Guide](gas_repricing/repricing_guide.md) + * [GasCosts Reference](gas_repricing/reference.md) * [Developer Doc](dev/index.md) * [Managing Configurations](dev/configurations.md) * [Interactive Library Usage](dev/interactive_usage.md) diff --git a/packages/testing/pyproject.toml b/packages/testing/pyproject.toml index e6472809571..276e2f56436 100644 --- a/packages/testing/pyproject.toml +++ b/packages/testing/pyproject.toml @@ -95,6 +95,7 @@ order_fixtures = "execution_testing.cli.order_fixtures:order_fixtures" evm_bytes = "execution_testing.cli.evm_bytes:evm_bytes" hasher = "execution_testing.cli.hasher:main" eest = "execution_testing.cli.eest.cli:eest" +gas-map = "execution_testing.cli.eest.commands.gas_map:gas_map" fillerconvert = "execution_testing.cli.fillerconvert.fillerconvert:main" groupstats = "execution_testing.cli.show_pre_alloc_group_stats:main" extract_config = "execution_testing.cli.extract_config:extract_config" diff --git a/packages/testing/src/execution_testing/cli/eest/cli.py b/packages/testing/src/execution_testing/cli/eest/cli.py index b93469a5cb1..af2d4d1bf1c 100644 --- a/packages/testing/src/execution_testing/cli/eest/cli.py +++ b/packages/testing/src/execution_testing/cli/eest/cli.py @@ -6,6 +6,7 @@ import click from .commands import clean, info +from .commands.gas_map import gas_map from .make.cli import make @@ -33,3 +34,4 @@ def eest() -> None: eest.add_command(make) eest.add_command(clean) eest.add_command(info) +eest.add_command(gas_map, name="gas-map") diff --git a/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py b/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py new file mode 100644 index 00000000000..1bbe913fa2a --- /dev/null +++ b/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py @@ -0,0 +1,300 @@ +"""Display the mapping between EVM opcodes and GasCosts field names.""" + +import inspect +import re +from collections import defaultdict +from dataclasses import fields + +import click + +from execution_testing.forks.base_fork import BaseFork +from execution_testing.forks.gas_costs import GasCosts +from execution_testing.forks.helpers import get_forks + +OPCODE_TIER_FIELDS = ( + "GAS_JUMPDEST", + "GAS_BASE", + "GAS_VERY_LOW", + "GAS_LOW", + "GAS_MID", + "GAS_HIGH", + "GAS_BLOCK_HASH", + "GAS_WARM_SLOAD", +) + + +def _get_latest_fork() -> type[BaseFork]: + """Return the latest fork class.""" + return get_forks()[-1] + + +def _get_fork(fork_name: str) -> type[BaseFork]: + """Return the fork class matching fork_name, or exit with error.""" + for fork in get_forks(): + if fork.name().lower() == fork_name.lower(): + return fork + available = ", ".join(f.name() for f in get_forks()) + raise click.ClickException( + f"Unknown fork: {fork_name}. Available forks: {available}" + ) + + +def _build_tier_reverse_map(gas_costs: GasCosts) -> dict[int, list[str]]: + """Build a reverse map from gas value to opcode tier field names only.""" + reverse = defaultdict(list) + for name in OPCODE_TIER_FIELDS: + value = getattr(gas_costs, name) + reverse[value].append(name) + return dict(reverse) + + +def _get_opcode_gas_map_sources(fork_class: type[BaseFork]) -> str: + """Get source code of opcode_gas_map from the fork's MRO chain.""" + sources = [] + for cls in fork_class.__mro__: + if cls is BaseFork or cls is object: + continue + if "opcode_gas_map" in cls.__dict__: + try: + # `cls` is typed as `type` (from `__mro__`) + # The full chain up to python's default `object` is walked, and + # the guard above ensures that opcode_gas_map exists before + # adding the source. + method = cls.opcode_gas_map # type: ignore[attr-defined] + sources.append(inspect.getsource(method)) + except (OSError, TypeError): + pass + return "\n".join(sources) + + +def _get_helper_method_fields( + fork_class: type[BaseFork], +) -> dict[str, set[str]]: + """Extract GasCosts fields from helper methods on the fork class.""" + valid_fields = {f.name for f in fields(GasCosts)} + helper_fields = {} + for cls in fork_class.__mro__: + if cls is object: + continue + for name, method in cls.__dict__.items(): + if name.startswith("_with_") or name.startswith("_calculate_"): + try: + src = inspect.getsource(method) + except (OSError, TypeError): + continue + found = set() + for field_name in valid_fields: + if field_name in src: + found.add(field_name) + if name == "_with_memory_expansion": + found.add("GAS_MEMORY") + if found: + helper_fields[name] = found + return helper_fields + + +def _build_full_opcode_field_map( + fork_class: type[BaseFork], +) -> dict[str, list[str]]: + """Build complete opcode→GasCosts fields map using source analysis.""" + source = _get_opcode_gas_map_sources(fork_class) + valid_fields = {f.name for f in fields(GasCosts)} + helper_fields = _get_helper_method_fields(fork_class) + opcode_fields = defaultdict(set) + + lines_iter = iter(source.split("\n")) + opcode_re = re.compile(r"Opcodes\.(\w+)") + field_re = re.compile(r"gas_costs\.(\w+)") + helper_re = re.compile(r"cls\.(_with_\w+|_calculate_\w+)") + + current_opcode = None + brace_depth = 0 + + for line in lines_iter: + opcode_match = opcode_re.search(line) + if opcode_match: + current_opcode = opcode_match.group(1) + + if current_opcode: + for fm in field_re.finditer(line): + fn = fm.group(1) + if fn in valid_fields: + opcode_fields[current_opcode].add(fn) + + for hm in helper_re.finditer(line): + hname = hm.group(1) + if hname in helper_fields: + opcode_fields[current_opcode].update(helper_fields[hname]) + + brace_depth += line.count("(") - line.count(")") + if current_opcode and brace_depth <= 0 and "," in line: + current_opcode = None + brace_depth = 0 + + return {k: sorted(v) for k, v in opcode_fields.items()} + + +def _format_grouped_output(fork_class: type[BaseFork]) -> str: + """Format the full grouped-by-GasCosts-field output.""" + fork_name = fork_class.name() + gas_costs = fork_class.gas_costs() + opcode_map = fork_class.opcode_gas_map() + tier_reverse = _build_tier_reverse_map(gas_costs) + source_fields = _build_full_opcode_field_map(fork_class) + + field_to_opcodes = defaultdict(list) + dynamic_opcodes = [] + constant_opcodes = [] + + for opcode, cost in opcode_map.items(): + name = opcode._name_ + if callable(cost): + gas_fields = source_fields.get(name, []) + dynamic_opcodes.append((name, gas_fields)) + elif cost in tier_reverse: + for field_name in tier_reverse[cost]: + field_to_opcodes[field_name].append(name) + else: + constant_opcodes.append((name, cost)) + + lines = [ + f"Opcode-to-GasCosts mapping for {fork_name}", + "\u2550" * 50, + "", + ] + + for field_name in OPCODE_TIER_FIELDS: + if field_name not in field_to_opcodes: + continue + value = getattr(gas_costs, field_name) + opcodes = field_to_opcodes[field_name] + lines.append(f"{field_name} ({value})") + line = " " + for i, op in enumerate(opcodes): + suffix = ", " if i < len(opcodes) - 1 else "" + if len(line) + len(op) + len(suffix) > 72: + lines.append(line.rstrip(", ")) + line = " " + line += op + suffix + if line.strip(): + lines.append(line.rstrip(", ")) + lines.append("") + + if dynamic_opcodes: + lines.append("Dynamic (multiple GasCosts fields)") + for name, gas_fields in sorted(dynamic_opcodes): + if gas_fields: + fields_str = ", ".join(gas_fields) + lines.append(f" {name:<18}{fields_str}") + else: + lines.append(f" {name:<18}(unknown)") + lines.append("") + + if constant_opcodes: + lines.append("Constants (no GasCosts field)") + for name, value in sorted(constant_opcodes): + lines.append(f" {name:<18}{value}") + lines.append("") + + return "\n".join(lines) + + +def _format_single_opcode(fork_class: type[BaseFork], opcode_name: str) -> str: + """Format detailed output for a single opcode.""" + fork_name = fork_class.name() + gas_costs = fork_class.gas_costs() + opcode_map = fork_class.opcode_gas_map() + tier_reverse = _build_tier_reverse_map(gas_costs) + source_fields = _build_full_opcode_field_map(fork_class) + + target = None + for opcode in opcode_map: + if opcode._name_.upper() == opcode_name.upper(): + target = opcode + break + + if target is None: + available = sorted(op._name_ for op in opcode_map) + raise click.ClickException( + f"Unknown opcode: {opcode_name}. Available: {', '.join(available)}" + ) + + cost = opcode_map[target] + name = target._name_ + lines = [ + f"{name} \u2014 {fork_name}", + "\u2550" * 30, + ] + + if callable(cost): + gas_fields = source_fields.get(name, []) + lines.append("Type: dynamic") + if gas_fields: + parts = [] + for gf in gas_fields: + val = getattr(gas_costs, gf) + parts.append(f"{gf} ({val})") + lines.append(f"GasCosts: {', '.join(parts)}") + lines.append("") + lines.append("To reprice in gas_repricing.json:") + lines.append("{") + lines.append(f' "{fork_name}": {{') + for gf in gas_fields: + lines.append(f' "{gf}": ,') + lines.append(" }") + lines.append("}") + elif cost in tier_reverse: + field_names = tier_reverse[cost] + lines.append("Type: static") + parts = [f"{fn} ({cost})" for fn in field_names] + lines.append(f"GasCosts: {', '.join(parts)}") + lines.append("") + lines.append("To reprice in gas_repricing.json:") + lines.append("{") + lines.append(f' "{fork_name}": {{') + for fn in field_names: + lines.append(f' "{fn}": ,') + lines.append(" }") + lines.append("}") + else: + lines.append("Type: constant") + lines.append(f"Value: {cost}") + lines.append("") + lines.append( + "This opcode has a fixed cost not tied to a GasCosts field." + ) + + return "\n".join(lines) + + +@click.command( + name="gas-map", + short_help="Display opcode-to-GasCosts field mapping.", +) +@click.option( + "--fork", + "-f", + "fork_name", + default=None, + help="Fork name (default: latest fork).", +) +@click.option( + "--opcode", + "-o", + "opcode_name", + default=None, + help="Show detail for a single opcode.", +) +def gas_map(fork_name: str | None, opcode_name: str | None) -> None: + """Display the mapping between EVM opcodes and GasCosts field names.""" + if fork_name: + fork_class = _get_fork(fork_name) + else: + fork_class = _get_latest_fork() + + if opcode_name: + output = _format_single_opcode(fork_class, opcode_name) + else: + output = _format_grouped_output(fork_class) + + click.echo(output) diff --git a/packages/testing/src/execution_testing/forks/base_fork.py b/packages/testing/src/execution_testing/forks/base_fork.py index f6d5edb2bf9..288766fd88d 100644 --- a/packages/testing/src/execution_testing/forks/base_fork.py +++ b/packages/testing/src/execution_testing/forks/base_fork.py @@ -450,9 +450,26 @@ def header_slot_number_required(cls) -> bool: # Gas related abstract methods @classmethod - @abstractmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Return dataclass with the gas costs constants for the fork.""" + from .gas_repricing import apply_repricing + + base = cls._base_gas_costs( + block_number=block_number, timestamp=timestamp + ) + fork_name = cls.fork_at( + block_number=block_number, timestamp=timestamp + ).name() + return apply_repricing(fork_name, base) + + @classmethod + @abstractmethod + def _base_gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: + """Return base gas costs before repricing overrides.""" pass @classmethod diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py index d4b1b9a6e17..29a342b9135 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py @@ -30,10 +30,13 @@ def header_bal_hash_required(cls) -> bool: return True @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """ The cost per block access list item is introduced in EIP-7928. """ + del block_number, timestamp return replace( super(EIP7928, cls).gas_costs(), BLOCK_ACCESS_LIST_ITEM=2000, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py index 343e8ba9947..2e076571cc3 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py @@ -26,8 +26,11 @@ def precompiles(cls) -> List[Address]: ] + super(EIP196, cls).precompiles() @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Set gas costs for BN254 addition and multiplication.""" + del block_number, timestamp return replace( super(EIP196, cls).gas_costs(), PRECOMPILE_ECADD=500, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py index 4d18dc85b3e..bf44da449d2 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py @@ -25,8 +25,11 @@ def precompiles(cls) -> List[Address]: ] + super(EIP197, cls).precompiles() @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Set gas costs for BN254 pairing check.""" + del block_number, timestamp return replace( super(EIP197, cls).gas_costs(), PRECOMPILE_ECPAIRING_BASE=100_000, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py index 9774befab96..08f067c6971 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py @@ -185,8 +185,11 @@ def engine_new_payload_blob_hashes(cls) -> bool: return True @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """On Cancun, the point evaluation precompile gas cost is set.""" + del block_number, timestamp return replace( super(EIP4844, cls).gas_costs(), PRECOMPILE_POINT_EVALUATION=50_000, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py index 512a2c2c44f..520136a3c05 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py @@ -14,8 +14,11 @@ class EIP1108(BaseFork): """EIP-1108 class.""" @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Reduce BN254 precompile gas costs.""" + del block_number, timestamp return replace( super(EIP1108, cls).gas_costs(), PRECOMPILE_ECADD=150, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py index e8451f88fdf..5072addc8a6 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py @@ -24,8 +24,11 @@ def precompiles(cls) -> List[Address]: ] + super(EIP152, cls).precompiles() @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Set BLAKE2F per-round gas cost.""" + del block_number, timestamp return replace( super(EIP152, cls).gas_costs(), PRECOMPILE_BLAKE2F_PER_ROUND=1, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py index ca6fed863ed..4bdabb8e5e7 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py @@ -16,8 +16,11 @@ class EIP2028(BaseFork): """EIP-2028 class.""" @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Reduce non-zero calldata byte gas cost to 16.""" + del block_number, timestamp return replace( super(EIP2028, cls).gas_costs(), TX_DATA_PER_NON_ZERO=16, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py index e80674d2725..c59e3f9597d 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py @@ -31,8 +31,11 @@ def precompiles(cls) -> List[Address]: ] + super(EIP7951, cls).precompiles() @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Set the P256VERIFY precompile gas cost.""" + del block_number, timestamp return replace( super(EIP7951, cls).gas_costs(), PRECOMPILE_P256VERIFY=6_900, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py index d0cb4edc70b..ae3670a3e48 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py @@ -43,8 +43,11 @@ def precompiles(cls) -> List[Address]: ] + super(EIP2537, cls).precompiles() @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Add gas costs for BLS12-381 precompiles.""" + del block_number, timestamp return replace( super(EIP2537, cls).gas_costs(), PRECOMPILE_BLS_G1ADD=375, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py index d52c5601a16..a584ed4c6d1 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py @@ -25,8 +25,11 @@ class EIP7623(BaseFork): """EIP-7623 class.""" @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Add standard and floor token costs for calldata.""" + del block_number, timestamp return replace( super(EIP7623, cls).gas_costs(), TX_DATA_TOKEN_STANDARD=4, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py index 424c4ab2dab..7371f2ed67b 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py @@ -30,8 +30,11 @@ def tx_types(cls) -> List[int]: return [4] + super(EIP7702, cls).tx_types() @classmethod - def gas_costs(cls) -> GasCosts: + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Add gas costs for authorization operations.""" + del block_number, timestamp return replace( super(EIP7702, cls).gas_costs(), AUTH_PER_EMPTY_ACCOUNT=25_000, diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 748047b4827..7322c03fb54 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -94,10 +94,13 @@ def header_blob_gas_used_required(cls) -> bool: return False @classmethod - def gas_costs(cls) -> GasCosts: + def _base_gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """ Return dataclass with the defined gas costs constants for genesis. """ + del block_number, timestamp return GasCosts( # Tiers BASE=BASE, diff --git a/packages/testing/src/execution_testing/forks/gas_repricing.py b/packages/testing/src/execution_testing/forks/gas_repricing.py new file mode 100644 index 00000000000..69f0c0b84b5 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/gas_repricing.py @@ -0,0 +1,43 @@ +"""Gas repricing override loader for fast iteration on gas schedules.""" + +from dataclasses import fields, replace + +from ethereum.utils.gas_repricing import load_repricing_config + +from .gas_costs import GasCosts + +_VALID_FIELDS = frozenset(f.name for f in fields(GasCosts)) + + +def _validate_overrides( + fork_name: str, + overrides: dict, +) -> None: + for field_name, value in overrides.items(): + if field_name not in _VALID_FIELDS: + raise ValueError( + f"Unknown GasCosts field '{field_name}' " + f"in repricing config for fork " + f"'{fork_name}'. " + f"Valid fields: {sorted(_VALID_FIELDS)}" + ) + if not isinstance(value, int): + raise TypeError( + f"GasCosts field '{field_name}' for fork " + f"'{fork_name}' must be of type int, " + f"got {type(value).__name__}: {value!r}" + ) + + +def apply_repricing(fork_name: str, base_costs: GasCosts) -> GasCosts: + """Apply repricing overrides for fork_name to base_costs.""" + config = load_repricing_config() + if config is None: + return base_costs + + overrides = config.get(fork_name) + if overrides is None: + return base_costs + + _validate_overrides(fork_name, overrides) + return replace(base_costs, **overrides) diff --git a/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py b/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py new file mode 100644 index 00000000000..234aa602937 --- /dev/null +++ b/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py @@ -0,0 +1,305 @@ +"""Tests for gas repricing override mechanism.""" + +import json +from collections.abc import Generator +from pathlib import Path + +import pytest +from ethereum.utils.gas_repricing import ( + _ENV_VAR, + apply_spec_repricing, + load_repricing_config, +) + +from ..forks.forks import Osaka, Prague +from ..forks.transition import PragueToOsakaAtTime15k +from ..gas_costs import GasCosts +from ..gas_repricing import apply_repricing + + +@pytest.fixture(autouse=True) +def _clear_repricing_cache( + monkeypatch: pytest.MonkeyPatch, +) -> Generator[None, None, None]: + """Clear the lru_cache and env var before and after each test.""" + load_repricing_config.cache_clear() + monkeypatch.delenv(_ENV_VAR, raising=False) + yield + load_repricing_config.cache_clear() + + +def _default_osaka_costs() -> GasCosts: + return Osaka._base_gas_costs() + + +class TestLoadRepricingConfig: + """Tests for load_repricing_config.""" + + def test_no_env_var(self) -> None: + """Test that the config is not loaded when the env var is unset.""" + config = load_repricing_config() + assert config is None + + def test_empty_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that the config is not loaded when the env var is null.""" + monkeypatch.setenv(_ENV_VAR, "") + config = load_repricing_config() + assert config is None + + def test_missing_file(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Test that an error is raised when config is missing.""" + monkeypatch.setenv(_ENV_VAR, "/nonexistent/path.json") + with pytest.raises(FileNotFoundError): + load_repricing_config() + + def test_valid_config_with_unknown_field( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Test that unknown fields pass the shared loader.""" + config_file = tmp_path / "unknown.json" + config_file.write_text( + json.dumps({"Osaka": {"NOT_A_REAL_FIELD": 999}}) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + with pytest.warns(UserWarning): + config = load_repricing_config() + assert config is not None + + def test_valid_config( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Test that a minimal valid config loads correctly.""" + config_file = tmp_path / "good.json" + config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 25000}})) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + config = load_repricing_config() + assert config == {"Osaka": {"GAS_TX_BASE": 25000}} + + def test_warning_emitted( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Test that warnings are emitted when repricing has taken place.""" + config_file = tmp_path / "warn.json" + config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 1}})) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + with pytest.warns(UserWarning, match="Gas repricing config loaded"): + load_repricing_config() + + +class TestApplyRepricing: + """Tests for apply_repricing.""" + + def test_no_config(self) -> None: + """ + Test that costs are not altered when no repricing has taken place. + """ + base = _default_osaka_costs() + result = apply_repricing("Osaka", base) + assert result is base + + def test_fork_not_in_config( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """ + Test that applying repricing for a different fork does not affect the + values in Osaka. + """ + config_file = tmp_path / "other.json" + config_file.write_text( + json.dumps({"Amsterdam": {"GAS_TX_BASE": 25000}}) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + base = _default_osaka_costs() + with pytest.warns(UserWarning): + result = apply_repricing("Osaka", base) + assert result is base + + def test_single_field_override( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """ + Test that repricing a single field does not affect the other fields. + """ + config_file = tmp_path / "single.json" + config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 99999}})) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + base = _default_osaka_costs() + with pytest.warns(UserWarning): + result = apply_repricing("Osaka", base) + assert result.GAS_TX_BASE == 99999 + assert result.GAS_COLD_ACCOUNT_ACCESS == base.GAS_COLD_ACCOUNT_ACCESS + + def test_invalid_field_name( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """Test that an unknown field raises ValueError.""" + config_file = tmp_path / "bad.json" + config_file.write_text( + json.dumps({"Osaka": {"NOT_A_REAL_FIELD": 999}}) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + base = _default_osaka_costs() + with pytest.warns(UserWarning): + with pytest.raises(ValueError, match="NOT_A_REAL_FIELD"): + apply_repricing("Osaka", base) + + def test_non_int_value_type( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """Test that a non-int value raises TypeError.""" + config_file = tmp_path / "bad_type.json" + config_file.write_text( + json.dumps({"Osaka": {"GAS_TX_BASE": "not_a_number"}}) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + base = _default_osaka_costs() + with pytest.warns(UserWarning): + with pytest.raises(TypeError, match="must be of type int"): + apply_repricing("Osaka", base) + + +class TestIntegration: + """Integration tests using the full gas_costs() path.""" + + def test_osaka_gas_costs_with_override( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Test that Osaka gas costs are properly overwritten by repricing.""" + config_file = tmp_path / "osaka.json" + config_file.write_text( + json.dumps({"Osaka": {"GAS_COLD_ACCOUNT_ACCESS": 2100}}) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + with pytest.warns(UserWarning): + costs = Osaka.gas_costs() + assert costs.GAS_COLD_ACCOUNT_ACCESS == 2100 + assert costs.GAS_TX_BASE == _default_osaka_costs().GAS_TX_BASE + + def test_osaka_gas_costs_without_override(self) -> None: + """ + Test that default Osaka gas costs are not overwritten if gas costs + are not repriced. + """ + costs = Osaka.gas_costs() + assert costs == _default_osaka_costs() + + def test_transition_fork_with_override( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """ + Test repricing during fork transition. + """ + config_file = tmp_path / "transition.json" + config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 50000}})) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + with pytest.warns(UserWarning): + costs = PragueToOsakaAtTime15k.gas_costs(timestamp=15000) + assert costs.GAS_TX_BASE == 50000 + + def test_transition_fork_pre_transition( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """ + Test that repricing a value in Osaka does not affect pre-transition + prague cost. + """ + config_file = tmp_path / "transition.json" + config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 50000}})) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + with pytest.warns(UserWarning): + costs = PragueToOsakaAtTime15k.gas_costs(timestamp=0) + assert costs.GAS_TX_BASE == Prague._base_gas_costs().GAS_TX_BASE + + +class TestSpecSideRepricing: + """Tests for apply_spec_repricing (module globals mutation).""" + + def _make_globals(self) -> dict: + from ethereum_types.numeric import U64, Uint + + return { + "GAS_BASE": Uint(2), + "GAS_LOW": Uint(5), + "BLOB_SCHEDULE_TARGET": U64(6), + "__name__": "test_module", + } + + def test_no_config(self) -> None: + """Test no-op when env var is unset.""" + globs = self._make_globals() + original = dict(globs) + apply_spec_repricing("TestFork", globs) + assert globs == original + + def test_fork_not_in_config( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """Test no-op when fork is absent from config.""" + config_file = tmp_path / "other_fork.json" + config_file.write_text(json.dumps({"OtherFork": {"GAS_BASE": 99}})) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + globs = self._make_globals() + original = dict(globs) + with pytest.warns(UserWarning): + apply_spec_repricing("TestFork", globs) + assert globs == original + + def test_mutates_with_correct_type( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """Test that overrides preserve Uint/U64 type wrappers.""" + from ethereum_types.numeric import U64, Uint + + config_file = tmp_path / "typed.json" + config_file.write_text( + json.dumps( + { + "TestFork": { + "GAS_BASE": 99, + "BLOB_SCHEDULE_TARGET": 12, + } + } + ) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + globs = self._make_globals() + with pytest.warns(UserWarning): + apply_spec_repricing("TestFork", globs) + assert globs["GAS_BASE"] == Uint(99) + assert isinstance(globs["GAS_BASE"], Uint) + assert globs["BLOB_SCHEDULE_TARGET"] == U64(12) + assert isinstance(globs["BLOB_SCHEDULE_TARGET"], U64) + assert globs["GAS_LOW"] == Uint(5) + + def test_unknown_field_raises( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ) -> None: + """Test that unknown constant names raise ValueError.""" + config_file = tmp_path / "bad_spec.json" + config_file.write_text( + json.dumps({"TestFork": {"NOT_A_CONSTANT": 42}}) + ) + monkeypatch.setenv(_ENV_VAR, str(config_file)) + globs = self._make_globals() + with pytest.warns(UserWarning): + with pytest.raises(ValueError, match="NOT_A_CONSTANT"): + apply_spec_repricing("TestFork", globs) + + def test_nonexistent_file_raises( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Test that a missing config file raises FileNotFoundError.""" + monkeypatch.setenv(_ENV_VAR, "/nonexistent/config.json") + globs = self._make_globals() + with pytest.raises(FileNotFoundError): + apply_spec_repricing("TestFork", globs) diff --git a/packages/testing/src/execution_testing/forks/transition_base_fork.py b/packages/testing/src/execution_testing/forks/transition_base_fork.py index 3dca0cb1d31..4af5674037e 100644 --- a/packages/testing/src/execution_testing/forks/transition_base_fork.py +++ b/packages/testing/src/execution_testing/forks/transition_base_fork.py @@ -2,7 +2,7 @@ from typing import Any, Callable, ClassVar, Dict, Type -from .base_fork import BaseFork +from .base_fork import BaseFork, GasCosts class TransitionBaseMetaClass(type): @@ -106,6 +106,17 @@ def ruleset(cls) -> Dict[str, int]: """ raise Exception("Not implemented") + @classmethod + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: + """ + Return Gas Costs for the active fork. + """ + return cls.fork_at( + block_number=block_number, timestamp=timestamp + ).gas_costs(block_number=block_number, timestamp=timestamp) + def transition_fork( to_fork: Type[BaseFork], diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index f8ad377ff73..2d32d678a17 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -18,6 +18,7 @@ from ethereum.forks.bpo5.blocks import Header as PreviousHeader from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -527,3 +528,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("Amsterdam", globals()) diff --git a/src/ethereum/forks/arrow_glacier/vm/gas.py b/src/ethereum/forks/arrow_glacier/vm/gas.py index 2842b80b4d2..34d71b9d304 100644 --- a/src/ethereum/forks/arrow_glacier/vm/gas.py +++ b/src/ethereum/forks/arrow_glacier/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("ArrowGlacier", globals()) diff --git a/src/ethereum/forks/berlin/vm/gas.py b/src/ethereum/forks/berlin/vm/gas.py index 672dc2fd026..38c9a319f4c 100644 --- a/src/ethereum/forks/berlin/vm/gas.py +++ b/src/ethereum/forks/berlin/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("Berlin", globals()) diff --git a/src/ethereum/forks/bpo1/vm/gas.py b/src/ethereum/forks/bpo1/vm/gas.py index 2cd628097bc..2e4f455fd54 100644 --- a/src/ethereum/forks/bpo1/vm/gas.py +++ b/src/ethereum/forks/bpo1/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -500,3 +501,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("BPO1", globals()) diff --git a/src/ethereum/forks/bpo2/vm/gas.py b/src/ethereum/forks/bpo2/vm/gas.py index f2488b8e20a..89231620b3d 100644 --- a/src/ethereum/forks/bpo2/vm/gas.py +++ b/src/ethereum/forks/bpo2/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -500,3 +501,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("BPO2", globals()) diff --git a/src/ethereum/forks/bpo3/vm/gas.py b/src/ethereum/forks/bpo3/vm/gas.py index f2488b8e20a..653b50a7831 100644 --- a/src/ethereum/forks/bpo3/vm/gas.py +++ b/src/ethereum/forks/bpo3/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -500,3 +501,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("BPO3", globals()) diff --git a/src/ethereum/forks/bpo4/vm/gas.py b/src/ethereum/forks/bpo4/vm/gas.py index f2488b8e20a..bee4d0302b0 100644 --- a/src/ethereum/forks/bpo4/vm/gas.py +++ b/src/ethereum/forks/bpo4/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -500,3 +501,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("BPO4", globals()) diff --git a/src/ethereum/forks/bpo5/vm/gas.py b/src/ethereum/forks/bpo5/vm/gas.py index f2488b8e20a..cff93f2aab9 100644 --- a/src/ethereum/forks/bpo5/vm/gas.py +++ b/src/ethereum/forks/bpo5/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -500,3 +501,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("BPO5", globals()) diff --git a/src/ethereum/forks/byzantium/vm/gas.py b/src/ethereum/forks/byzantium/vm/gas.py index 0395403db01..eb3797d3b76 100644 --- a/src/ethereum/forks/byzantium/vm/gas.py +++ b/src/ethereum/forks/byzantium/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -333,3 +334,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("Byzantium", globals()) diff --git a/src/ethereum/forks/cancun/vm/gas.py b/src/ethereum/forks/cancun/vm/gas.py index 89ad479051f..ed97af64c8d 100644 --- a/src/ethereum/forks/cancun/vm/gas.py +++ b/src/ethereum/forks/cancun/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -471,3 +472,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("Cancun", globals()) diff --git a/src/ethereum/forks/constantinople/vm/gas.py b/src/ethereum/forks/constantinople/vm/gas.py index 770c5212461..34d8fe0250f 100644 --- a/src/ethereum/forks/constantinople/vm/gas.py +++ b/src/ethereum/forks/constantinople/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -337,3 +338,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("Constantinople", globals()) diff --git a/src/ethereum/forks/dao_fork/vm/gas.py b/src/ethereum/forks/dao_fork/vm/gas.py index cb663624a36..a3d83cc6c1c 100644 --- a/src/ethereum/forks/dao_fork/vm/gas.py +++ b/src/ethereum/forks/dao_fork/vm/gas.py @@ -18,6 +18,7 @@ from ethereum.state import Address from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from ..state_tracker import TransactionState, account_exists @@ -298,3 +299,6 @@ def calculate_message_call_gas( ) stipend = gas if value == 0 else GasCosts.CALL_STIPEND + gas return MessageCallGas(cost, stipend) + + +apply_spec_repricing("DAOFork", globals()) diff --git a/src/ethereum/forks/frontier/vm/gas.py b/src/ethereum/forks/frontier/vm/gas.py index 1974386b1c3..1fa0d8556dd 100644 --- a/src/ethereum/forks/frontier/vm/gas.py +++ b/src/ethereum/forks/frontier/vm/gas.py @@ -18,6 +18,7 @@ from ethereum.state import Address from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from ..state_tracker import TransactionState, account_exists @@ -296,3 +297,6 @@ def calculate_message_call_gas( ) stipend = gas if value == 0 else GasCosts.CALL_STIPEND + gas return MessageCallGas(cost, stipend) + + +apply_spec_repricing("Frontier", globals()) diff --git a/src/ethereum/forks/gray_glacier/vm/gas.py b/src/ethereum/forks/gray_glacier/vm/gas.py index 2842b80b4d2..023d46b57ab 100644 --- a/src/ethereum/forks/gray_glacier/vm/gas.py +++ b/src/ethereum/forks/gray_glacier/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("GrayGlacier", globals()) diff --git a/src/ethereum/forks/homestead/vm/gas.py b/src/ethereum/forks/homestead/vm/gas.py index cb663624a36..f26ae58efcf 100644 --- a/src/ethereum/forks/homestead/vm/gas.py +++ b/src/ethereum/forks/homestead/vm/gas.py @@ -18,6 +18,7 @@ from ethereum.state import Address from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from ..state_tracker import TransactionState, account_exists @@ -298,3 +299,6 @@ def calculate_message_call_gas( ) stipend = gas if value == 0 else GasCosts.CALL_STIPEND + gas return MessageCallGas(cost, stipend) + + +apply_spec_repricing("Homestead", globals()) diff --git a/src/ethereum/forks/istanbul/vm/gas.py b/src/ethereum/forks/istanbul/vm/gas.py index 25ff304ae00..cbdaa12d831 100644 --- a/src/ethereum/forks/istanbul/vm/gas.py +++ b/src/ethereum/forks/istanbul/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("Istanbul", globals()) diff --git a/src/ethereum/forks/london/vm/gas.py b/src/ethereum/forks/london/vm/gas.py index 2842b80b4d2..6d4d77457df 100644 --- a/src/ethereum/forks/london/vm/gas.py +++ b/src/ethereum/forks/london/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("London", globals()) diff --git a/src/ethereum/forks/muir_glacier/vm/gas.py b/src/ethereum/forks/muir_glacier/vm/gas.py index 25ff304ae00..1a2d8882782 100644 --- a/src/ethereum/forks/muir_glacier/vm/gas.py +++ b/src/ethereum/forks/muir_glacier/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("MuirGlacier", globals()) diff --git a/src/ethereum/forks/osaka/vm/gas.py b/src/ethereum/forks/osaka/vm/gas.py index 1d49b46098c..254fbd7e0a0 100644 --- a/src/ethereum/forks/osaka/vm/gas.py +++ b/src/ethereum/forks/osaka/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -500,3 +501,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("Osaka", globals()) diff --git a/src/ethereum/forks/paris/vm/gas.py b/src/ethereum/forks/paris/vm/gas.py index b4b9ab1e402..20577f83783 100644 --- a/src/ethereum/forks/paris/vm/gas.py +++ b/src/ethereum/forks/paris/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -340,3 +341,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("Paris", globals()) diff --git a/src/ethereum/forks/prague/vm/gas.py b/src/ethereum/forks/prague/vm/gas.py index e6cc23df1b8..467e8957b9c 100644 --- a/src/ethereum/forks/prague/vm/gas.py +++ b/src/ethereum/forks/prague/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U64, U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32, taylor_exponential from ..blocks import Header @@ -480,3 +481,6 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: return Uint(calculate_total_blob_gas(tx)) * calculate_blob_gas_price( excess_blob_gas ) + + +apply_spec_repricing("Prague", globals()) diff --git a/src/ethereum/forks/shanghai/vm/gas.py b/src/ethereum/forks/shanghai/vm/gas.py index f7b597f700f..70bdb0903d4 100644 --- a/src/ethereum/forks/shanghai/vm/gas.py +++ b/src/ethereum/forks/shanghai/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm diff --git a/src/ethereum/forks/spurious_dragon/vm/gas.py b/src/ethereum/forks/spurious_dragon/vm/gas.py index c4cd3ce528f..565b459888c 100644 --- a/src/ethereum/forks/spurious_dragon/vm/gas.py +++ b/src/ethereum/forks/spurious_dragon/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -326,3 +327,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("SpuriousDragon", globals()) diff --git a/src/ethereum/forks/tangerine_whistle/vm/gas.py b/src/ethereum/forks/tangerine_whistle/vm/gas.py index 93c81fae94d..ac92bdcab09 100644 --- a/src/ethereum/forks/tangerine_whistle/vm/gas.py +++ b/src/ethereum/forks/tangerine_whistle/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -326,3 +327,6 @@ def max_message_call_gas(gas: Uint) -> Uint: """ return gas - (gas // Uint(64)) + + +apply_spec_repricing("TangerineWhistle", globals()) diff --git a/src/ethereum/utils/gas_repricing.py b/src/ethereum/utils/gas_repricing.py new file mode 100644 index 00000000000..d929b56434f --- /dev/null +++ b/src/ethereum/utils/gas_repricing.py @@ -0,0 +1,66 @@ +"""Shared gas repricing config loader and spec-side applier.""" + +import json +import os +import warnings +from functools import lru_cache +from pathlib import Path +from typing import Any, Dict, Optional + +_ENV_VAR = "EELS_GAS_REPRICING_CONFIG" + + +@lru_cache(maxsize=1) +def load_repricing_config() -> Optional[Dict[str, Dict[str, Any]]]: + """ + Load gas repricing overrides from JSON config. + + Return None if env var is unset or empty. + """ + config_path = os.environ.get(_ENV_VAR, "") + if not config_path: + return None + + path = Path(config_path) + if not path.is_file(): + raise FileNotFoundError( + f"{_ENV_VAR} points to non-existent file: {config_path}" + ) + + with open(path) as f: + config = json.load(f) + + warnings.warn( + f"Gas repricing config loaded from {config_path}", + stacklevel=2, + ) + return config + + +def apply_spec_repricing( + fork_name: str, + module_globals: dict, +) -> None: + """ + Apply repricing overrides to module globals. + + Mutates module_globals in place, preserving the + original type wrapper (Uint, U64, etc.). + """ + config = load_repricing_config() + if config is None: + return + + overrides = config.get(fork_name) + if overrides is None: + return + + for name, value in overrides.items(): + if name not in module_globals: + raise ValueError( + f"Unknown gas constant '{name}' " + f"in repricing config for fork " + f"'{fork_name}'." + ) + original = module_globals[name] + module_globals[name] = type(original)(value) diff --git a/vulture_whitelist.py b/vulture_whitelist.py index 6b045fb9e8b..5b55f4d0cb9 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -8,6 +8,10 @@ from ethereum.cancun.blocks import Withdrawal from ethereum_spec_tools.evm_tools.t8n.transition_tool import EELST8N +from execution_testing.cli.eest.commands.gas_map import gas_map +from execution_testing.forks.tests.test_gas_repricing import ( + _clear_repricing_cache, +) from ethereum.ethash import * from ethereum.fork_criteria import Unscheduled @@ -155,6 +159,12 @@ CommentReplaceCommand CommentReplaceCommand.transform_module_impl +# packages/testing/src/execution_testing/cli/eest/commands/gas_map.py +gas_map # Click entry point registered in pyproject.toml + +# packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py +_clear_repricing_cache # pytest autouse fixture + _children # unused attribute (src/ethereum_spec_tools/docc.py:751) # evm_tools/loaders/fixture_loader.py - abstract methods From a4b6efdbb1679dc225709595c2b3f836b6ce50fe Mon Sep 17 00:00:00 2001 From: carsons-eels Date: Wed, 27 May 2026 00:56:20 -0400 Subject: [PATCH 2/4] fix: update & clean post rebase --- docs/gas_repricing/reference.md | 221 ++++++++++++------ docs/gas_repricing/repricing_guide.md | 12 +- docs/writing_tests/fork_methods.md | 2 +- .../cli/eest/commands/gas_map.py | 49 ++-- .../forks/forks/eips/amsterdam/eip_7928.py | 4 +- .../forks/forks/eips/amsterdam/eip_7976.py | 7 +- .../forks/forks/eips/byzantium/eip_196.py | 4 +- .../forks/forks/eips/byzantium/eip_197.py | 4 +- .../forks/forks/eips/byzantium/eip_211.py | 4 +- .../forks/forks/eips/cancun/eip_4844.py | 6 +- .../forks/forks/eips/cancun/eip_5656.py | 2 +- .../forks/eips/constantinople/eip_145.py | 6 +- .../forks/forks/eips/istanbul/eip_1108.py | 4 +- .../forks/forks/eips/istanbul/eip_152.py | 4 +- .../forks/forks/eips/istanbul/eip_2028.py | 4 +- .../forks/forks/eips/osaka/eip_7939.py | 2 +- .../forks/forks/eips/osaka/eip_7951.py | 4 +- .../forks/forks/eips/prague/eip_2537.py | 4 +- .../forks/forks/eips/prague/eip_7623.py | 4 +- .../forks/forks/eips/prague/eip_7702.py | 4 +- .../execution_testing/forks/forks/forks.py | 7 + .../forks/tests/test_gas_repricing.py | 78 +++++-- src/ethereum/forks/shanghai/vm/gas.py | 1 - .../lint/lints/glacier_forks_hygiene.py | 9 + 24 files changed, 284 insertions(+), 162 deletions(-) diff --git a/docs/gas_repricing/reference.md b/docs/gas_repricing/reference.md index 6f9e0ab69ef..8c169ebb480 100644 --- a/docs/gas_repricing/reference.md +++ b/docs/gas_repricing/reference.md @@ -14,113 +14,119 @@ uv run gas-map --fork | GasCosts Field | Typical Value | Affected Opcodes | |---|---|---| -| `GAS_VERY_LOW` | 3 | ADD, SUB, CALLDATALOAD, LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR, SIGNEXTEND, PUSH1-PUSH32, DUP1-DUP16, SWAP1-SWAP16, MLOAD, MSTORE, MSTORE8 | -| `GAS_LOW` | 5 | MUL, DIV, SDIV, MOD, SMOD, CLZ | -| `GAS_MID` | 8 | ADDMOD, MULMOD, JUMP | -| `GAS_HIGH` | 10 | JUMPI | -| `GAS_BASE` | 2 | ADDRESS, ORIGIN, CALLER, CALLVALUE, CALLDATASIZE, CODESIZE, GASPRICE, COINBASE, TIMESTAMP, NUMBER, PREVRANDAO, GASLIMIT, POP, PC, MSIZE, GAS, RETURNDATASIZE, CHAINID, SELFBALANCE, BASEFEE, BLOBBASEFEE | -| `GAS_JUMPDEST` | 1 | JUMPDEST | -| `GAS_BLOCK_HASH` | 20 | BLOCKHASH | +| `VERY_LOW` | 3 | ADD, SUB, CALLDATALOAD, LT, GT, SLT, SGT, EQ, ISZERO, AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR, SIGNEXTEND, PUSH1-PUSH32, DUP1-DUP16, SWAP1-SWAP16, MLOAD, MSTORE, MSTORE8 | +| `LOW` | 5 | MUL, DIV, SDIV, MOD, SMOD, CLZ | +| `MID` | 8 | ADDMOD, MULMOD, JUMP | +| `HIGH` | 10 | JUMPI | +| `BASE` | 2 | ADDRESS, ORIGIN, CALLER, CALLVALUE, CALLDATASIZE, CODESIZE, GASPRICE, COINBASE, TIMESTAMP, NUMBER, PREVRANDAO, GASLIMIT, POP, PC, MSIZE, GAS, RETURNDATASIZE, CHAINID, SELFBALANCE, BASEFEE, BLOBBASEFEE | +| `OPCODE_JUMPDEST` | 1 | JUMPDEST | +| `OPCODE_BLOCKHASH` | 20 | BLOCKHASH | ## Storage Costs | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_WARM_SLOAD` | 100 | SLOAD when slot is warm | -| `GAS_COLD_SLOAD` | 2100 | SLOAD when slot is cold | -| `GAS_STORAGE_SET` | 20000 | SSTORE: setting a slot from zero to non-zero | -| `GAS_STORAGE_UPDATE` | 2900 | SSTORE: updating existing non-zero slot | -| `GAS_STORAGE_RESET` | 2900 | SSTORE: resetting to original value | +| `WARM_SLOAD` | 100 | SLOAD when slot is warm | +| `COLD_STORAGE_ACCESS` | 2100 | SLOAD when slot is cold | +| `STORAGE_SET` | 20000 | SSTORE: setting a slot from zero to non-zero | +| `STORAGE_RESET` | 2900 | SSTORE: updating an existing non-zero slot or resetting to original value | +| `COLD_STORAGE_WRITE` | 5000 | SSTORE write involving a cold storage slot | ## Account Access Costs | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_WARM_ACCOUNT_ACCESS` | 100 | BALANCE, EXTCODESIZE, etc. when warm | -| `GAS_COLD_ACCOUNT_ACCESS` | 2600 | BALANCE, EXTCODESIZE, etc. when cold | -| `GAS_TX_ACCESS_LIST_ADDRESS` | 2400 | Per address in access list | -| `GAS_TX_ACCESS_LIST_STORAGE_KEY` | 1900 | Per storage key in access list | +| `WARM_ACCESS` | 100 | BALANCE, EXTCODESIZE, etc. when warm | +| `COLD_ACCOUNT_ACCESS` | 2600 | BALANCE, EXTCODESIZE, etc. when cold | +| `TX_ACCESS_LIST_ADDRESS` | 2400 | Per address in access list | +| `TX_ACCESS_LIST_STORAGE_KEY` | 1900 | Per storage key in access list | ## Exponentiation | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_EXPONENTIATION` | 10 | EXP base cost | -| `GAS_EXPONENTIATION_PER_BYTE` | 50 | EXP per byte of exponent | +| `OPCODE_EXP_BASE` | 10 | EXP base cost | +| `OPCODE_EXP_PER_BYTE` | 50 | EXP per byte of exponent | ## Memory and Copy | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_MEMORY` | 3 | Memory expansion cost coefficient | -| `GAS_COPY` | 3 | Per-word copy cost (CALLDATACOPY, CODECOPY, etc.) | -| `GAS_KECCAK256` | 30 | SHA3 base cost | -| `GAS_KECCAK256_PER_WORD` | 6 | SHA3 per 32-byte word | +| `MEMORY_PER_WORD` | 3 | Memory expansion cost coefficient | +| `OPCODE_COPY_PER_WORD` | 3 | Per-word copy cost (CALLDATACOPY, CODECOPY, etc.) | +| `OPCODE_KECCAK256_BASE` | 30 | SHA3 base cost | +| `OPCODE_KECCACK256_PER_WORD` | 6 | SHA3 per 32-byte word | ## Logging | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_LOG` | 375 | LOG base cost | -| `GAS_LOG_DATA_PER_BYTE` | 8 | LOG per byte of data | -| `GAS_LOG_TOPIC` | 375 | LOG per topic | +| `OPCODE_LOG_BASE` | 375 | LOG base cost | +| `OPCODE_LOG_DATA_PER_BYTE` | 8 | LOG per byte of data | +| `OPCODE_LOG_TOPIC` | 375 | LOG per topic | ## Transaction Costs | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_TX_BASE` | 21000 | Base transaction cost | -| `GAS_TX_CREATE` | 32000 | Additional cost for contract creation tx | -| `GAS_TX_DATA_PER_ZERO` | 4 | Per zero byte in tx data | -| `GAS_TX_DATA_PER_NON_ZERO` | 16 | Per non-zero byte in tx data | -| `GAS_TX_DATA_TOKEN_STANDARD` | 4 | Token cost per data element | -| `GAS_TX_DATA_TOKEN_FLOOR` | 0 | Minimum token cost | +| `TX_BASE` | 21000 | Base transaction cost | +| `TX_CREATE` | 32000 | Additional cost for contract creation tx | +| `TX_DATA_PER_ZERO` | 4 | Per zero byte in tx data | +| `TX_DATA_PER_NON_ZERO` | 16 | Per non-zero byte in tx data | +| `TX_DATA_TOKEN_STANDARD` | 4 | Token cost per data element | +| `TX_DATA_TOKEN_FLOOR` | 0 | Minimum token cost | ## Call and Create | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_CALL_VALUE` | 9000 | Additional cost when transferring value | -| `GAS_CALL_STIPEND` | 2300 | Gas stipend for calls with value | -| `GAS_NEW_ACCOUNT` | 25000 | Creating a new account via call | -| `GAS_CREATE` | 32000 | CREATE opcode base cost | -| `GAS_CODE_DEPOSIT_PER_BYTE` | 200 | Per byte of deployed code | -| `GAS_CODE_INIT_PER_WORD` | 2 | Per word of init code (EIP-3860) | -| `GAS_SELF_DESTRUCT` | 5000 | SELFDESTRUCT base cost | +| `CALL_VALUE` | 9000 | Additional cost when transferring value | +| `CALL_STIPEND` | 2300 | Gas stipend for calls with value | +| `NEW_ACCOUNT` | 25000 | Creating a new account via call | +| `OPCODE_CREATE_BASE` | 32000 | CREATE opcode base cost | +| `CODE_DEPOSIT_PER_BYTE` | 200 | Per byte of deployed code | +| `CODE_INIT_PER_WORD` | 2 | Per word of init code (EIP-3860) | +| `OPCODE_SELFDESTRUCT_BASE` | 5000 | SELFDESTRUCT base cost | ## Auth (EIP-3074) | GasCosts Field | Typical Value | Notes | |---|---|---| -| `GAS_AUTH_PER_EMPTY_ACCOUNT` | 25000 | AUTH cost for empty account | +| `AUTH_PER_EMPTY_ACCOUNT` | 25000 | AUTH cost for empty account | ## Precompile Costs | GasCosts Field | Typical Value | Precompile | |---|---|---| -| `GAS_PRECOMPILE_ECRECOVER` | 3000 | ecRecover (0x01) | -| `GAS_PRECOMPILE_SHA256_BASE` | 60 | SHA-256 base (0x02) | -| `GAS_PRECOMPILE_SHA256_PER_WORD` | 12 | SHA-256 per word (0x02) | -| `GAS_PRECOMPILE_RIPEMD160_BASE` | 600 | RIPEMD-160 base (0x03) | -| `GAS_PRECOMPILE_RIPEMD160_PER_WORD` | 120 | RIPEMD-160 per word (0x03) | -| `GAS_PRECOMPILE_IDENTITY_BASE` | 15 | Identity base (0x04) | -| `GAS_PRECOMPILE_IDENTITY_PER_WORD` | 3 | Identity per word (0x04) | -| `GAS_PRECOMPILE_ECADD` | 150 | BN256 add (0x06) | -| `GAS_PRECOMPILE_ECMUL` | 6000 | BN256 mul (0x07) | -| `GAS_PRECOMPILE_ECPAIRING_BASE` | 45000 | BN256 pairing base (0x08) | -| `GAS_PRECOMPILE_ECPAIRING_PER_POINT` | 34000 | BN256 pairing per point (0x08) | -| `GAS_PRECOMPILE_BLAKE2F_BASE` | 0 | BLAKE2 base (0x09) | -| `GAS_PRECOMPILE_BLAKE2F_PER_ROUND` | 1 | BLAKE2 per round (0x09) | -| `GAS_PRECOMPILE_POINT_EVALUATION` | 50000 | Point evaluation (0x0a) | -| `GAS_PRECOMPILE_BLS_G1ADD` | 500 | BLS G1 add (0x0b) | -| `GAS_PRECOMPILE_BLS_G1MUL` | 12000 | BLS G1 mul (0x0c) | -| `GAS_PRECOMPILE_BLS_G1MAP` | 5500 | BLS G1 map (0x12) | -| `GAS_PRECOMPILE_BLS_G2ADD` | 800 | BLS G2 add (0x0d) | -| `GAS_PRECOMPILE_BLS_G2MUL` | 45000 | BLS G2 mul (0x0e) | -| `GAS_PRECOMPILE_BLS_G2MAP` | 110000 | BLS G2 map (0x13) | -| `GAS_PRECOMPILE_BLS_PAIRING_BASE` | 115000 | BLS pairing base (0x11) | -| `GAS_PRECOMPILE_BLS_PAIRING_PER_PAIR` | 23000 | BLS pairing per pair (0x11) | -| `GAS_PRECOMPILE_P256VERIFY` | 6900 | P256 verify (0x100) | +| `PRECOMPILE_ECRECOVER` | 3000 | ecRecover (0x01) | +| `PRECOMPILE_SHA256_BASE` | 60 | SHA-256 base (0x02) | +| `PRECOMPILE_SHA256_PER_WORD` | 12 | SHA-256 per word (0x02) | +| `PRECOMPILE_RIPEMD160_BASE` | 600 | RIPEMD-160 base (0x03) | +| `PRECOMPILE_RIPEMD160_PER_WORD` | 120 | RIPEMD-160 per word (0x03) | +| `PRECOMPILE_IDENTITY_BASE` | 15 | Identity base (0x04) | +| `PRECOMPILE_IDENTITY_PER_WORD` | 3 | Identity per word (0x04) | +| `PRECOMPILE_ECADD` | 150 | BN256 add (0x06) | +| `PRECOMPILE_ECMUL` | 6000 | BN256 mul (0x07) | +| `PRECOMPILE_ECPAIRING_BASE` | 45000 | BN256 pairing base (0x08) | +| `PRECOMPILE_ECPAIRING_PER_POINT` | 34000 | BN256 pairing per point (0x08) | +| `PRECOMPILE_BLAKE2F_BASE` | 0 | BLAKE2 base (0x09) | +| `PRECOMPILE_BLAKE2F_PER_ROUND` | 1 | BLAKE2 per round (0x09) | +| `PRECOMPILE_POINT_EVALUATION` | 50000 | Point evaluation (0x0a) | +| `PRECOMPILE_BLS_G1ADD` | 500 | BLS G1 add (0x0b) | +| `PRECOMPILE_BLS_G1MUL` | 12000 | BLS G1 mul (0x0c) | +| `PRECOMPILE_BLS_G1MAP` | 5500 | BLS G1 map (0x12) | +| `PRECOMPILE_BLS_G2ADD` | 800 | BLS G2 add (0x0d) | +| `PRECOMPILE_BLS_G2MUL` | 45000 | BLS G2 mul (0x0e) | +| `PRECOMPILE_BLS_G2MAP` | 110000 | BLS G2 map (0x13) | +| `PRECOMPILE_BLS_PAIRING_BASE` | 115000 | BLS pairing base (0x11) | +| `PRECOMPILE_BLS_PAIRING_PER_PAIR` | 23000 | BLS pairing per pair (0x11) | +| `PRECOMPILE_P256VERIFY` | 6900 | P256 verify (0x100) | + +## Block Access Lists + +| GasCosts Field | Typical Value | Notes | +|---|---|---| +| `BLOCK_ACCESS_LIST_ITEM` | 2000 | Per-item cost for block access list entries (EIP-7928) | ## Refund Constants @@ -129,6 +135,79 @@ uv run gas-map --fork | `REFUND_STORAGE_CLEAR` | 4800 | Refund for clearing a storage slot | | `REFUND_AUTH_PER_EXISTING_ACCOUNT` | 25000 | AUTH refund for existing account | +## Opcode-Specific Fields + +Each opcode has a dedicated `OPCODE_` field. By default it equals the +opcode's tier value (see Base Operation Costs), but repricing it changes only +that opcode — so opcodes sharing a tier can be repriced independently. Values +below are typical (Osaka); exact values vary by fork, so `uv run gas-map` is the +authoritative source. + +### Static opcodes + +| GasCosts Field | Typical Value | Opcode | +|---|---|---| +| `OPCODE_ADD` | 3 | ADD | +| `OPCODE_SUB` | 3 | SUB | +| `OPCODE_MUL` | 5 | MUL | +| `OPCODE_DIV` | 5 | DIV | +| `OPCODE_SDIV` | 5 | SDIV | +| `OPCODE_MOD` | 5 | MOD | +| `OPCODE_SMOD` | 5 | SMOD | +| `OPCODE_ADDMOD` | 8 | ADDMOD | +| `OPCODE_MULMOD` | 8 | MULMOD | +| `OPCODE_SIGNEXTEND` | 5 | SIGNEXTEND | +| `OPCODE_LT` | 3 | LT | +| `OPCODE_GT` | 3 | GT | +| `OPCODE_SLT` | 3 | SLT | +| `OPCODE_SGT` | 3 | SGT | +| `OPCODE_EQ` | 3 | EQ | +| `OPCODE_ISZERO` | 3 | ISZERO | +| `OPCODE_AND` | 3 | AND | +| `OPCODE_OR` | 3 | OR | +| `OPCODE_XOR` | 3 | XOR | +| `OPCODE_NOT` | 3 | NOT | +| `OPCODE_BYTE` | 3 | BYTE | +| `OPCODE_SHL` | 3 | SHL | +| `OPCODE_SHR` | 3 | SHR | +| `OPCODE_SAR` | 3 | SAR | +| `OPCODE_CLZ` | 5 | CLZ | +| `OPCODE_CALLDATALOAD` | 3 | CALLDATALOAD | +| `OPCODE_COINBASE` | 2 | COINBASE | +| `OPCODE_BLOCKHASH` | 20 | BLOCKHASH | +| `OPCODE_BLOBHASH` | 3 | BLOBHASH | +| `OPCODE_JUMP` | 8 | JUMP | +| `OPCODE_JUMPI` | 10 | JUMPI | +| `OPCODE_JUMPDEST` | 1 | JUMPDEST | +| `OPCODE_PUSH` | 3 | PUSH1–PUSH32 | +| `OPCODE_DUP` | 3 | DUP1–DUP16 | +| `OPCODE_SWAP` | 3 | SWAP1–SWAP16 | + +### Dynamic opcode base components + +These set the base/per-unit cost of opcodes whose total cost also depends on +runtime context (memory expansion, data length, etc.). + +| GasCosts Field | Typical Value | Opcode / Notes | +|---|---|---| +| `OPCODE_MLOAD_BASE` | 3 | MLOAD base | +| `OPCODE_MSTORE_BASE` | 3 | MSTORE base | +| `OPCODE_MSTORE8_BASE` | 3 | MSTORE8 base | +| `OPCODE_CALLDATACOPY_BASE` | 3 | CALLDATACOPY base | +| `OPCODE_CODECOPY_BASE` | 3 | CODECOPY base | +| `OPCODE_RETURNDATACOPY_BASE` | 3 | RETURNDATACOPY base | +| `OPCODE_MCOPY_BASE` | 3 | MCOPY base | +| `OPCODE_COPY_PER_WORD` | 3 | Per-word cost for copy opcodes | +| `OPCODE_CREATE_BASE` | 32000 | CREATE / CREATE2 base | +| `OPCODE_SELFDESTRUCT_BASE` | 5000 | SELFDESTRUCT base | +| `OPCODE_EXP_BASE` | 10 | EXP base | +| `OPCODE_EXP_PER_BYTE` | 50 | EXP per exponent byte | +| `OPCODE_KECCAK256_BASE` | 30 | KECCAK256 (SHA3) base | +| `OPCODE_KECCACK256_PER_WORD` | 6 | KECCAK256 (SHA3) per word (note: field name is misspelled in source) | +| `OPCODE_LOG_BASE` | 375 | LOG0–LOG4 base | +| `OPCODE_LOG_DATA_PER_BYTE` | 8 | LOG per data byte | +| `OPCODE_LOG_TOPIC` | 375 | LOG per topic | + ## Dynamic Opcodes Some opcodes have dynamic gas costs that depend on multiple `GasCosts` fields @@ -136,15 +215,15 @@ and runtime context: | Opcode | Relevant GasCosts Fields | Notes | |---|---|---| -| EXP | `GAS_EXPONENTIATION`, `GAS_EXPONENTIATION_PER_BYTE` | Cost depends on exponent size | -| SLOAD | `GAS_WARM_SLOAD`, `GAS_COLD_SLOAD` | Warm vs cold access | -| SSTORE | `GAS_STORAGE_SET`, `GAS_STORAGE_UPDATE`, `GAS_STORAGE_RESET`, `GAS_WARM_SLOAD`, `GAS_COLD_SLOAD` | Complex rules based on original/current/new values | -| SHA3 | `GAS_KECCAK256`, `GAS_KECCAK256_PER_WORD` | Base + per-word cost | -| LOG0-LOG4 | `GAS_LOG`, `GAS_LOG_DATA_PER_BYTE`, `GAS_LOG_TOPIC` | Base + data + topics | -| CALL/CALLCODE | `GAS_WARM_ACCOUNT_ACCESS`, `GAS_COLD_ACCOUNT_ACCESS`, `GAS_CALL_VALUE`, `GAS_NEW_ACCOUNT` | Complex rules based on account state | -| CREATE/CREATE2 | `GAS_CREATE`, `GAS_CODE_INIT_PER_WORD` | Base + init code cost | -| BALANCE/EXTCODESIZE | `GAS_WARM_ACCOUNT_ACCESS`, `GAS_COLD_ACCOUNT_ACCESS` | Warm vs cold access | -| SELFDESTRUCT | `GAS_SELF_DESTRUCT`, `GAS_COLD_ACCOUNT_ACCESS`, `GAS_NEW_ACCOUNT` | Depends on target account state | +| EXP | `OPCODE_EXP_BASE`, `OPCODE_EXP_PER_BYTE` | Cost depends on exponent size | +| SLOAD | `WARM_SLOAD`, `COLD_STORAGE_ACCESS` | Warm vs cold access | +| SSTORE | `STORAGE_SET`, `STORAGE_RESET`, `WARM_SLOAD`, `COLD_STORAGE_ACCESS` | Complex rules based on original/current/new values | +| SHA3 | `OPCODE_KECCAK256_BASE`, `OPCODE_KECCACK256_PER_WORD` | Base + per-word cost | +| LOG0-LOG4 | `OPCODE_LOG_BASE`, `OPCODE_LOG_DATA_PER_BYTE`, `OPCODE_LOG_TOPIC` | Base + data + topics | +| CALL/CALLCODE | `WARM_ACCESS`, `COLD_ACCOUNT_ACCESS`, `CALL_VALUE`, `NEW_ACCOUNT` | Complex rules based on account state | +| CREATE/CREATE2 | `OPCODE_CREATE_BASE`, `CODE_INIT_PER_WORD` | Base + init code cost | +| BALANCE/EXTCODESIZE | `WARM_ACCESS`, `COLD_ACCOUNT_ACCESS` | Warm vs cold access | +| SELFDESTRUCT | `OPCODE_SELFDESTRUCT_BASE`, `COLD_ACCOUNT_ACCESS`, `NEW_ACCOUNT` | Depends on target account state | ## Generating Up-to-Date Mappings diff --git a/docs/gas_repricing/repricing_guide.md b/docs/gas_repricing/repricing_guide.md index 5fa1b1b5cfd..fe110817a41 100644 --- a/docs/gas_repricing/repricing_guide.md +++ b/docs/gas_repricing/repricing_guide.md @@ -16,11 +16,11 @@ Create a JSON file mapping fork names to `GasCosts` field overrides: ```json { "Osaka": { - "GAS_VERY_LOW": 4, - "GAS_COLD_SLOAD": 2200 + "VERY_LOW": 4, + "COLD_STORAGE_ACCESS": 2200 }, "Prague": { - "GAS_WARM_SLOAD": 150 + "WARM_SLOAD": 150 } } ``` @@ -66,15 +66,15 @@ See [GasCosts Reference](reference.md) for a static reference table. uv run gas-map --opcode SLOAD ``` - Output shows `GAS_WARM_SLOAD` and `GAS_COLD_SLOAD` are the relevant fields. + Output shows `WARM_SLOAD` and `COLD_STORAGE_ACCESS` are the relevant fields. 2. Create a repricing config: ```json { "Osaka": { - "GAS_WARM_SLOAD": 150, - "GAS_COLD_SLOAD": 2500 + "WARM_SLOAD": 150, + "COLD_STORAGE_ACCESS": 2500 } } ``` diff --git a/docs/writing_tests/fork_methods.md b/docs/writing_tests/fork_methods.md index efb4c90d4e3..3af264f77ba 100644 --- a/docs/writing_tests/fork_methods.md +++ b/docs/writing_tests/fork_methods.md @@ -38,7 +38,7 @@ def test_some_feature(fork): ```python def test_transaction_gas(fork, state_test): - gas_cost = fork.gas_costs().GAS_TX_BASE + gas_cost = fork.gas_costs().TX_BASE # Create a transaction with the correct gas parameters for this fork tx = Transaction( diff --git a/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py b/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py index 1bbe913fa2a..2bc3630a1a1 100644 --- a/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py +++ b/packages/testing/src/execution_testing/cli/eest/commands/gas_map.py @@ -1,6 +1,7 @@ """Display the mapping between EVM opcodes and GasCosts field names.""" import inspect +import json import re from collections import defaultdict from dataclasses import fields @@ -12,14 +13,14 @@ from execution_testing.forks.helpers import get_forks OPCODE_TIER_FIELDS = ( - "GAS_JUMPDEST", - "GAS_BASE", - "GAS_VERY_LOW", - "GAS_LOW", - "GAS_MID", - "GAS_HIGH", - "GAS_BLOCK_HASH", - "GAS_WARM_SLOAD", + "OPCODE_JUMPDEST", + "BASE", + "VERY_LOW", + "LOW", + "MID", + "HIGH", + "OPCODE_BLOCKHASH", + "WARM_SLOAD", ) @@ -87,7 +88,7 @@ def _get_helper_method_fields( if field_name in src: found.add(field_name) if name == "_with_memory_expansion": - found.add("GAS_MEMORY") + found.add("MEMORY_PER_WORD") if found: helper_fields[name] = found return helper_fields @@ -226,8 +227,10 @@ def _format_single_opcode(fork_class: type[BaseFork], opcode_name: str) -> str: "\u2550" * 30, ] + gas_fields = source_fields.get(name, []) + repricing_fields: list[str] = [] if callable(cost): - gas_fields = source_fields.get(name, []) + repricing_fields = gas_fields lines.append("Type: dynamic") if gas_fields: parts = [] @@ -235,27 +238,11 @@ def _format_single_opcode(fork_class: type[BaseFork], opcode_name: str) -> str: val = getattr(gas_costs, gf) parts.append(f"{gf} ({val})") lines.append(f"GasCosts: {', '.join(parts)}") - lines.append("") - lines.append("To reprice in gas_repricing.json:") - lines.append("{") - lines.append(f' "{fork_name}": {{') - for gf in gas_fields: - lines.append(f' "{gf}": ,') - lines.append(" }") - lines.append("}") elif cost in tier_reverse: - field_names = tier_reverse[cost] + repricing_fields = tier_reverse[cost] lines.append("Type: static") - parts = [f"{fn} ({cost})" for fn in field_names] + parts = [f"{fn} ({cost})" for fn in repricing_fields] lines.append(f"GasCosts: {', '.join(parts)}") - lines.append("") - lines.append("To reprice in gas_repricing.json:") - lines.append("{") - lines.append(f' "{fork_name}": {{') - for fn in field_names: - lines.append(f' "{fn}": ,') - lines.append(" }") - lines.append("}") else: lines.append("Type: constant") lines.append(f"Value: {cost}") @@ -264,6 +251,12 @@ def _format_single_opcode(fork_class: type[BaseFork], opcode_name: str) -> str: "This opcode has a fixed cost not tied to a GasCosts field." ) + if repricing_fields: + snippet = {fork_name: dict.fromkeys(repricing_fields, 0)} + lines.append("") + lines.append("To reprice in gas_repricing.json:") + lines.extend(json.dumps(snippet, indent=2).splitlines()) + return "\n".join(lines) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py index 29a342b9135..dd722691f76 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7928.py @@ -30,7 +30,7 @@ def header_bal_hash_required(cls) -> bool: return True @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """ @@ -38,7 +38,7 @@ def gas_costs( """ del block_number, timestamp return replace( - super(EIP7928, cls).gas_costs(), + super(EIP7928, cls)._base_gas_costs(), BLOCK_ACCESS_LIST_ITEM=2000, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7976.py b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7976.py index b126395a4c7..d387ee27871 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7976.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/amsterdam/eip_7976.py @@ -20,10 +20,13 @@ class EIP7976(BaseFork): """EIP-7976 class.""" @classmethod - def gas_costs(cls) -> GasCosts: + def _base_gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: """Transaction data floor token cost is increased from 10 to 16.""" + del block_number, timestamp return replace( - super(EIP7976, cls).gas_costs(), + super(EIP7976, cls)._base_gas_costs(), TX_DATA_TOKEN_FLOOR=16, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py index 2e076571cc3..05e0d73627d 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_196.py @@ -26,13 +26,13 @@ def precompiles(cls) -> List[Address]: ] + super(EIP196, cls).precompiles() @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Set gas costs for BN254 addition and multiplication.""" del block_number, timestamp return replace( - super(EIP196, cls).gas_costs(), + super(EIP196, cls)._base_gas_costs(), PRECOMPILE_ECADD=500, PRECOMPILE_ECMUL=40_000, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py index bf44da449d2..f7d348a91fe 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_197.py @@ -25,13 +25,13 @@ def precompiles(cls) -> List[Address]: ] + super(EIP197, cls).precompiles() @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Set gas costs for BN254 pairing check.""" del block_number, timestamp return replace( - super(EIP197, cls).gas_costs(), + super(EIP197, cls)._base_gas_costs(), PRECOMPILE_ECPAIRING_BASE=100_000, PRECOMPILE_ECPAIRING_PER_POINT=80_000, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py index d8e053c26ef..c47a3a057a5 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/byzantium/eip_211.py @@ -26,7 +26,9 @@ def opcode_gas_map( **base_map, Opcodes.RETURNDATASIZE: gas_costs.BASE, Opcodes.RETURNDATACOPY: cls._with_memory_expansion( - cls._with_data_copy(gas_costs.VERY_LOW, gas_costs), + cls._with_data_copy( + gas_costs.OPCODE_RETURNDATACOPY_BASE, gas_costs + ), memory_expansion_calculator, ), } diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py index 08f067c6971..40cb0f59f66 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_4844.py @@ -185,13 +185,13 @@ def engine_new_payload_blob_hashes(cls) -> bool: return True @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """On Cancun, the point evaluation precompile gas cost is set.""" del block_number, timestamp return replace( - super(EIP4844, cls).gas_costs(), + super(EIP4844, cls)._base_gas_costs(), PRECOMPILE_POINT_EVALUATION=50_000, ) @@ -211,7 +211,7 @@ def opcode_gas_map( base_map = super(EIP4844, cls).opcode_gas_map() # Add Cancun-specific opcodes - return {**base_map, Opcodes.BLOBHASH: gas_costs.VERY_LOW} + return {**base_map, Opcodes.BLOBHASH: gas_costs.OPCODE_BLOBHASH} @classmethod def valid_opcodes(cls) -> List[Opcodes]: diff --git a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py index 3365f3c2296..0fd801b6095 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/cancun/eip_5656.py @@ -27,7 +27,7 @@ def opcode_gas_map( return { **base_map, Opcodes.MCOPY: cls._with_memory_expansion( - cls._with_data_copy(gas_costs.VERY_LOW, gas_costs), + cls._with_data_copy(gas_costs.OPCODE_MCOPY_BASE, gas_costs), memory_expansion_calculator, ), } diff --git a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py index 6a1b1cd854a..0ce8da924a8 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/constantinople/eip_145.py @@ -25,9 +25,9 @@ def opcode_gas_map( base_map = super(EIP145, cls).opcode_gas_map() return { **base_map, - Opcodes.SHL: gas_costs.VERY_LOW, - Opcodes.SHR: gas_costs.VERY_LOW, - Opcodes.SAR: gas_costs.VERY_LOW, + Opcodes.SHL: gas_costs.OPCODE_SHL, + Opcodes.SHR: gas_costs.OPCODE_SHR, + Opcodes.SAR: gas_costs.OPCODE_SAR, } @classmethod diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py index 520136a3c05..1b56ed499e0 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_1108.py @@ -14,13 +14,13 @@ class EIP1108(BaseFork): """EIP-1108 class.""" @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Reduce BN254 precompile gas costs.""" del block_number, timestamp return replace( - super(EIP1108, cls).gas_costs(), + super(EIP1108, cls)._base_gas_costs(), PRECOMPILE_ECADD=150, PRECOMPILE_ECMUL=6000, PRECOMPILE_ECPAIRING_BASE=45_000, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py index 5072addc8a6..462c919630f 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_152.py @@ -24,12 +24,12 @@ def precompiles(cls) -> List[Address]: ] + super(EIP152, cls).precompiles() @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Set BLAKE2F per-round gas cost.""" del block_number, timestamp return replace( - super(EIP152, cls).gas_costs(), + super(EIP152, cls)._base_gas_costs(), PRECOMPILE_BLAKE2F_PER_ROUND=1, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py index 4bdabb8e5e7..d8554136062 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/istanbul/eip_2028.py @@ -16,12 +16,12 @@ class EIP2028(BaseFork): """EIP-2028 class.""" @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Reduce non-zero calldata byte gas cost to 16.""" del block_number, timestamp return replace( - super(EIP2028, cls).gas_costs(), + super(EIP2028, cls)._base_gas_costs(), TX_DATA_PER_NON_ZERO=16, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py index c53935a4d4d..87c7c704d56 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7939.py @@ -25,7 +25,7 @@ def opcode_gas_map( base_map = super(EIP7939, cls).opcode_gas_map() return { **base_map, - Opcodes.CLZ: gas_costs.LOW, + Opcodes.CLZ: gas_costs.OPCODE_CLZ, } @classmethod diff --git a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py index c59e3f9597d..cafa2175963 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/osaka/eip_7951.py @@ -31,12 +31,12 @@ def precompiles(cls) -> List[Address]: ] + super(EIP7951, cls).precompiles() @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Set the P256VERIFY precompile gas cost.""" del block_number, timestamp return replace( - super(EIP7951, cls).gas_costs(), + super(EIP7951, cls)._base_gas_costs(), PRECOMPILE_P256VERIFY=6_900, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py index ae3670a3e48..3dbff023097 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_2537.py @@ -43,13 +43,13 @@ def precompiles(cls) -> List[Address]: ] + super(EIP2537, cls).precompiles() @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Add gas costs for BLS12-381 precompiles.""" del block_number, timestamp return replace( - super(EIP2537, cls).gas_costs(), + super(EIP2537, cls)._base_gas_costs(), PRECOMPILE_BLS_G1ADD=375, PRECOMPILE_BLS_G1MUL=12_000, PRECOMPILE_BLS_G1MAP=5_500, diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py index a584ed4c6d1..a8ea38ba831 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7623.py @@ -25,13 +25,13 @@ class EIP7623(BaseFork): """EIP-7623 class.""" @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Add standard and floor token costs for calldata.""" del block_number, timestamp return replace( - super(EIP7623, cls).gas_costs(), + super(EIP7623, cls)._base_gas_costs(), TX_DATA_TOKEN_STANDARD=4, TX_DATA_TOKEN_FLOOR=10, ) diff --git a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py index 7371f2ed67b..68b0e5f4bbe 100644 --- a/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py +++ b/packages/testing/src/execution_testing/forks/forks/eips/prague/eip_7702.py @@ -30,13 +30,13 @@ def tx_types(cls) -> List[int]: return [4] + super(EIP7702, cls).tx_types() @classmethod - def gas_costs( + def _base_gas_costs( cls, *, block_number: int = 0, timestamp: int = 0 ) -> GasCosts: """Add gas costs for authorization operations.""" del block_number, timestamp return replace( - super(EIP7702, cls).gas_costs(), + super(EIP7702, cls)._base_gas_costs(), AUTH_PER_EMPTY_ACCOUNT=25_000, REFUND_AUTH_PER_EXISTING_ACCOUNT=12_500, ) diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index 7322c03fb54..898f1aed1e3 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -193,6 +193,13 @@ def _base_gas_costs( OPCODE_LOG_TOPIC=375, OPCODE_KECCAK256_BASE=30, OPCODE_KECCACK256_PER_WORD=6, + OPCODE_SHL=VERY_LOW, + OPCODE_SHR=VERY_LOW, + OPCODE_SAR=VERY_LOW, + OPCODE_CLZ=LOW, + OPCODE_BLOBHASH=VERY_LOW, + OPCODE_MCOPY_BASE=VERY_LOW, + OPCODE_RETURNDATACOPY_BASE=VERY_LOW, # Zero-initialized: introduced in later forks, set via # replace() in the fork that activates them. TX_DATA_TOKEN_STANDARD=0, diff --git a/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py b/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py index 234aa602937..7cc4e31dc1f 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py +++ b/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py @@ -1,7 +1,9 @@ """Tests for gas repricing override mechanism.""" import json +import re from collections.abc import Generator +from dataclasses import fields from pathlib import Path import pytest @@ -70,17 +72,17 @@ def test_valid_config( ) -> None: """Test that a minimal valid config loads correctly.""" config_file = tmp_path / "good.json" - config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 25000}})) + config_file.write_text(json.dumps({"Osaka": {"TX_BASE": 25000}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) config = load_repricing_config() - assert config == {"Osaka": {"GAS_TX_BASE": 25000}} + assert config == {"Osaka": {"TX_BASE": 25000}} def test_warning_emitted( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """Test that warnings are emitted when repricing has taken place.""" config_file = tmp_path / "warn.json" - config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 1}})) + config_file.write_text(json.dumps({"Osaka": {"TX_BASE": 1}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) with pytest.warns(UserWarning, match="Gas repricing config loaded"): load_repricing_config() @@ -105,9 +107,7 @@ def test_fork_not_in_config( values in Osaka. """ config_file = tmp_path / "other.json" - config_file.write_text( - json.dumps({"Amsterdam": {"GAS_TX_BASE": 25000}}) - ) + config_file.write_text(json.dumps({"Amsterdam": {"TX_BASE": 25000}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) base = _default_osaka_costs() with pytest.warns(UserWarning): @@ -121,13 +121,13 @@ def test_single_field_override( Test that repricing a single field does not affect the other fields. """ config_file = tmp_path / "single.json" - config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 99999}})) + config_file.write_text(json.dumps({"Osaka": {"TX_BASE": 99999}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) base = _default_osaka_costs() with pytest.warns(UserWarning): result = apply_repricing("Osaka", base) - assert result.GAS_TX_BASE == 99999 - assert result.GAS_COLD_ACCOUNT_ACCESS == base.GAS_COLD_ACCOUNT_ACCESS + assert result.TX_BASE == 99999 + assert result.COLD_ACCOUNT_ACCESS == base.COLD_ACCOUNT_ACCESS def test_invalid_field_name( self, @@ -153,7 +153,7 @@ def test_non_int_value_type( """Test that a non-int value raises TypeError.""" config_file = tmp_path / "bad_type.json" config_file.write_text( - json.dumps({"Osaka": {"GAS_TX_BASE": "not_a_number"}}) + json.dumps({"Osaka": {"TX_BASE": "not_a_number"}}) ) monkeypatch.setenv(_ENV_VAR, str(config_file)) base = _default_osaka_costs() @@ -171,13 +171,13 @@ def test_osaka_gas_costs_with_override( """Test that Osaka gas costs are properly overwritten by repricing.""" config_file = tmp_path / "osaka.json" config_file.write_text( - json.dumps({"Osaka": {"GAS_COLD_ACCOUNT_ACCESS": 2100}}) + json.dumps({"Osaka": {"COLD_ACCOUNT_ACCESS": 2100}}) ) monkeypatch.setenv(_ENV_VAR, str(config_file)) with pytest.warns(UserWarning): costs = Osaka.gas_costs() - assert costs.GAS_COLD_ACCOUNT_ACCESS == 2100 - assert costs.GAS_TX_BASE == _default_osaka_costs().GAS_TX_BASE + assert costs.COLD_ACCOUNT_ACCESS == 2100 + assert costs.TX_BASE == _default_osaka_costs().TX_BASE def test_osaka_gas_costs_without_override(self) -> None: """ @@ -194,11 +194,11 @@ def test_transition_fork_with_override( Test repricing during fork transition. """ config_file = tmp_path / "transition.json" - config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 50000}})) + config_file.write_text(json.dumps({"Osaka": {"TX_BASE": 50000}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) with pytest.warns(UserWarning): costs = PragueToOsakaAtTime15k.gas_costs(timestamp=15000) - assert costs.GAS_TX_BASE == 50000 + assert costs.TX_BASE == 50000 def test_transition_fork_pre_transition( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path @@ -208,11 +208,11 @@ def test_transition_fork_pre_transition( prague cost. """ config_file = tmp_path / "transition.json" - config_file.write_text(json.dumps({"Osaka": {"GAS_TX_BASE": 50000}})) + config_file.write_text(json.dumps({"Osaka": {"TX_BASE": 50000}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) with pytest.warns(UserWarning): costs = PragueToOsakaAtTime15k.gas_costs(timestamp=0) - assert costs.GAS_TX_BASE == Prague._base_gas_costs().GAS_TX_BASE + assert costs.TX_BASE == Prague._base_gas_costs().TX_BASE class TestSpecSideRepricing: @@ -222,8 +222,8 @@ def _make_globals(self) -> dict: from ethereum_types.numeric import U64, Uint return { - "GAS_BASE": Uint(2), - "GAS_LOW": Uint(5), + "BASE": Uint(2), + "LOW": Uint(5), "BLOB_SCHEDULE_TARGET": U64(6), "__name__": "test_module", } @@ -242,7 +242,7 @@ def test_fork_not_in_config( ) -> None: """Test no-op when fork is absent from config.""" config_file = tmp_path / "other_fork.json" - config_file.write_text(json.dumps({"OtherFork": {"GAS_BASE": 99}})) + config_file.write_text(json.dumps({"OtherFork": {"BASE": 99}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) globs = self._make_globals() original = dict(globs) @@ -263,7 +263,7 @@ def test_mutates_with_correct_type( json.dumps( { "TestFork": { - "GAS_BASE": 99, + "BASE": 99, "BLOB_SCHEDULE_TARGET": 12, } } @@ -273,11 +273,11 @@ def test_mutates_with_correct_type( globs = self._make_globals() with pytest.warns(UserWarning): apply_spec_repricing("TestFork", globs) - assert globs["GAS_BASE"] == Uint(99) - assert isinstance(globs["GAS_BASE"], Uint) + assert globs["BASE"] == Uint(99) + assert isinstance(globs["BASE"], Uint) assert globs["BLOB_SCHEDULE_TARGET"] == U64(12) assert isinstance(globs["BLOB_SCHEDULE_TARGET"], U64) - assert globs["GAS_LOW"] == Uint(5) + assert globs["LOW"] == Uint(5) def test_unknown_field_raises( self, @@ -303,3 +303,33 @@ def test_nonexistent_file_raises( globs = self._make_globals() with pytest.raises(FileNotFoundError): apply_spec_repricing("TestFork", globs) + + +class TestReferenceDocFreshness: + """Keep docs/gas_repricing/reference.md in sync with GasCosts.""" + + @staticmethod + def _reference_doc() -> Path | None: + for parent in Path(__file__).resolve().parents: + candidate = parent / "docs" / "gas_repricing" / "reference.md" + if candidate.is_file(): + return candidate + return None + + def test_all_gas_costs_fields_documented(self) -> None: + """ + Every GasCosts field must appear in the reference table so the doc + does not drift when fields are added or renamed. + """ + doc = self._reference_doc() + if doc is None: + pytest.skip("reference.md not present in this checkout") + documented = set(re.findall(r"`([A-Z][A-Z0-9_]+)`", doc.read_text())) + missing = sorted( + f.name for f in fields(GasCosts) if f.name not in documented + ) + assert not missing, ( + "GasCosts fields missing from " + "docs/gas_repricing/reference.md: " + f"{missing}. Run `uv run gas-map` to find their values." + ) diff --git a/src/ethereum/forks/shanghai/vm/gas.py b/src/ethereum/forks/shanghai/vm/gas.py index 70bdb0903d4..f7b597f700f 100644 --- a/src/ethereum/forks/shanghai/vm/gas.py +++ b/src/ethereum/forks/shanghai/vm/gas.py @@ -17,7 +17,6 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace -from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm diff --git a/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py b/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py index 55d58c6a264..3ad5f9df089 100644 --- a/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py +++ b/src/ethereum_spec_tools/lint/lints/glacier_forks_hygiene.py @@ -231,6 +231,15 @@ def visit_Expr(self, expr: ast.Expr) -> None: ): return + # Ignore the module-level apply_spec_repricing() hook appended to + # each fork's gas.py; it is identical machinery across forks. + if ( + isinstance(expr.value, ast.Call) + and isinstance(expr.value.func, ast.Name) + and expr.value.func.id == "apply_spec_repricing" + ): + return + print(f"The expression {type(expr)} has been ignored.") def visit_AsyncFunctionDef(self, function: ast.AsyncFunctionDef) -> None: From ce10d2a2913b69a12a85a172db35a6ec0d1c0357 Mon Sep 17 00:00:00 2001 From: carsons-eels Date: Wed, 27 May 2026 10:46:45 -0400 Subject: [PATCH 3/4] fix: retarget repricing to GasCosts class Also add a scenario to repricing guide to illustrate use --- docs/gas_repricing/repricing_guide.md | 50 ++++++++++++++ .../forks/tests/test_gas_repricing.py | 66 ++++++++++--------- src/ethereum/forks/amsterdam/vm/gas.py | 2 +- src/ethereum/forks/arrow_glacier/vm/gas.py | 2 +- src/ethereum/forks/berlin/vm/gas.py | 2 +- src/ethereum/forks/bpo1/vm/gas.py | 2 +- src/ethereum/forks/bpo2/vm/gas.py | 2 +- src/ethereum/forks/bpo3/vm/gas.py | 2 +- src/ethereum/forks/bpo4/vm/gas.py | 2 +- src/ethereum/forks/bpo5/vm/gas.py | 2 +- src/ethereum/forks/byzantium/vm/gas.py | 2 +- src/ethereum/forks/cancun/vm/gas.py | 2 +- src/ethereum/forks/constantinople/vm/gas.py | 2 +- src/ethereum/forks/dao_fork/vm/gas.py | 2 +- src/ethereum/forks/frontier/vm/gas.py | 2 +- src/ethereum/forks/gray_glacier/vm/gas.py | 2 +- src/ethereum/forks/homestead/vm/gas.py | 2 +- src/ethereum/forks/istanbul/vm/gas.py | 2 +- src/ethereum/forks/london/vm/gas.py | 2 +- src/ethereum/forks/muir_glacier/vm/gas.py | 2 +- src/ethereum/forks/osaka/vm/gas.py | 2 +- src/ethereum/forks/paris/vm/gas.py | 2 +- src/ethereum/forks/prague/vm/gas.py | 2 +- src/ethereum/forks/shanghai/vm/gas.py | 4 ++ src/ethereum/forks/spurious_dragon/vm/gas.py | 2 +- .../forks/tangerine_whistle/vm/gas.py | 2 +- src/ethereum/utils/gas_repricing.py | 12 ++-- 27 files changed, 119 insertions(+), 59 deletions(-) diff --git a/docs/gas_repricing/repricing_guide.md b/docs/gas_repricing/repricing_guide.md index fe110817a41..5a60502734c 100644 --- a/docs/gas_repricing/repricing_guide.md +++ b/docs/gas_repricing/repricing_guide.md @@ -87,3 +87,53 @@ See [GasCosts Reference](reference.md) for a static reference table. 4. Compare results against the baseline to see which tests break or change behavior under the new gas schedule. + +## Regenerating Fixtures Under a New Schedule + +A repricing config changes gas costs, so existing fixtures — which encode the +default schedule — will fail once it is active. That is expected, not a bug. +How you regenerate depends on your intent. + +!!! warning "Protect the canonical fixtures" + - Never run `fill --clean` over the canonical `fixtures/` directory with a + config active: it replaces mainnet-correct fixtures with "what-if" ones. + Always fill experiments into a separate `--output` directory. + - Unset `EELS_GAS_REPRICING_CONFIG` before normal test or fill runs, or + every run silently reprices. + +### What-if comparison (the intended use) + +Fill into a scratch directory with the config active, then diff against a +baseline fill to see how gas usage shifts: + +```bash +# Repriced +EELS_GAS_REPRICING_CONFIG=./reprice.json \ + uv run fill tests/amsterdam/ --fork Amsterdam --output /tmp/fx_repriced --clean + +# Baseline (config unset) +uv run fill tests/amsterdam/ --fork Amsterdam --output /tmp/fx_baseline --clean + +diff -r /tmp/fx_baseline /tmp/fx_repriced +``` + +Differences in `cumulativeGasUsed`, balances, and state/block hashes confirm the +new schedule took effect. Nothing here is committed. + +### Adopting the schedule permanently + +The config is for iteration, not a source of truth. To make a schedule +permanent, bake the values into the spec (`src/ethereum/forks//vm/gas.py`) +and the testing framework (`forks/forks.py`), remove the config, then fill the +canonical `fixtures/` directory without any config set. + +### Quick sanity check + +To confirm the pipeline works under repricing, scope a fill to a few +gas-sensitive tests into a scratch directory: + +```bash +EELS_GAS_REPRICING_CONFIG=./reprice.json uv run fill \ + tests/berlin/eip2929_gas_cost_increases/test_warm_status_revert.py \ + --fork Amsterdam --output /tmp/fx_check --clean +``` diff --git a/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py b/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py index 7cc4e31dc1f..85240740a5d 100644 --- a/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py +++ b/packages/testing/src/execution_testing/forks/tests/test_gas_repricing.py @@ -12,6 +12,7 @@ apply_spec_repricing, load_repricing_config, ) +from ethereum_types.numeric import U64, Uint from ..forks.forks import Osaka, Prague from ..forks.transition import PragueToOsakaAtTime15k @@ -215,25 +216,32 @@ def test_transition_fork_pre_transition( assert costs.TX_BASE == Prague._base_gas_costs().TX_BASE +class _SpecGasCosts: + """Minimal stand-in for a fork's spec-side ``GasCosts`` class.""" + + BASE = Uint(2) + LOW = Uint(5) + BLOB_SCHEDULE_TARGET = U64(6) + + class TestSpecSideRepricing: - """Tests for apply_spec_repricing (module globals mutation).""" + """Tests for apply_spec_repricing (GasCosts class mutation).""" - def _make_globals(self) -> dict: - from ethereum_types.numeric import U64, Uint + @staticmethod + def _make_gas_costs() -> type[_SpecGasCosts]: + # Fresh subclass per call so setattr-based overrides don't leak + # between tests. + class GasCosts(_SpecGasCosts): + pass - return { - "BASE": Uint(2), - "LOW": Uint(5), - "BLOB_SCHEDULE_TARGET": U64(6), - "__name__": "test_module", - } + return GasCosts def test_no_config(self) -> None: """Test no-op when env var is unset.""" - globs = self._make_globals() - original = dict(globs) - apply_spec_repricing("TestFork", globs) - assert globs == original + gc = self._make_gas_costs() + before = (gc.BASE, gc.LOW, gc.BLOB_SCHEDULE_TARGET) + apply_spec_repricing("TestFork", gc) + assert (gc.BASE, gc.LOW, gc.BLOB_SCHEDULE_TARGET) == before def test_fork_not_in_config( self, @@ -244,11 +252,11 @@ def test_fork_not_in_config( config_file = tmp_path / "other_fork.json" config_file.write_text(json.dumps({"OtherFork": {"BASE": 99}})) monkeypatch.setenv(_ENV_VAR, str(config_file)) - globs = self._make_globals() - original = dict(globs) + gc = self._make_gas_costs() + before = (gc.BASE, gc.LOW, gc.BLOB_SCHEDULE_TARGET) with pytest.warns(UserWarning): - apply_spec_repricing("TestFork", globs) - assert globs == original + apply_spec_repricing("TestFork", gc) + assert (gc.BASE, gc.LOW, gc.BLOB_SCHEDULE_TARGET) == before def test_mutates_with_correct_type( self, @@ -256,8 +264,6 @@ def test_mutates_with_correct_type( tmp_path: Path, ) -> None: """Test that overrides preserve Uint/U64 type wrappers.""" - from ethereum_types.numeric import U64, Uint - config_file = tmp_path / "typed.json" config_file.write_text( json.dumps( @@ -270,14 +276,14 @@ def test_mutates_with_correct_type( ) ) monkeypatch.setenv(_ENV_VAR, str(config_file)) - globs = self._make_globals() + gc = self._make_gas_costs() with pytest.warns(UserWarning): - apply_spec_repricing("TestFork", globs) - assert globs["BASE"] == Uint(99) - assert isinstance(globs["BASE"], Uint) - assert globs["BLOB_SCHEDULE_TARGET"] == U64(12) - assert isinstance(globs["BLOB_SCHEDULE_TARGET"], U64) - assert globs["LOW"] == Uint(5) + apply_spec_repricing("TestFork", gc) + assert gc.BASE == Uint(99) + assert isinstance(gc.BASE, Uint) + assert gc.BLOB_SCHEDULE_TARGET == U64(12) + assert isinstance(gc.BLOB_SCHEDULE_TARGET, U64) + assert gc.LOW == Uint(5) def test_unknown_field_raises( self, @@ -290,19 +296,19 @@ def test_unknown_field_raises( json.dumps({"TestFork": {"NOT_A_CONSTANT": 42}}) ) monkeypatch.setenv(_ENV_VAR, str(config_file)) - globs = self._make_globals() + gc = self._make_gas_costs() with pytest.warns(UserWarning): with pytest.raises(ValueError, match="NOT_A_CONSTANT"): - apply_spec_repricing("TestFork", globs) + apply_spec_repricing("TestFork", gc) def test_nonexistent_file_raises( self, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that a missing config file raises FileNotFoundError.""" monkeypatch.setenv(_ENV_VAR, "/nonexistent/config.json") - globs = self._make_globals() + gc = self._make_gas_costs() with pytest.raises(FileNotFoundError): - apply_spec_repricing("TestFork", globs) + apply_spec_repricing("TestFork", gc) class TestReferenceDocFreshness: diff --git a/src/ethereum/forks/amsterdam/vm/gas.py b/src/ethereum/forks/amsterdam/vm/gas.py index 2d32d678a17..3dc83f02913 100644 --- a/src/ethereum/forks/amsterdam/vm/gas.py +++ b/src/ethereum/forks/amsterdam/vm/gas.py @@ -530,4 +530,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("Amsterdam", globals()) +apply_spec_repricing("Amsterdam", GasCosts) diff --git a/src/ethereum/forks/arrow_glacier/vm/gas.py b/src/ethereum/forks/arrow_glacier/vm/gas.py index 34d71b9d304..c62c62dfa42 100644 --- a/src/ethereum/forks/arrow_glacier/vm/gas.py +++ b/src/ethereum/forks/arrow_glacier/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("ArrowGlacier", globals()) +apply_spec_repricing("ArrowGlacier", GasCosts) diff --git a/src/ethereum/forks/berlin/vm/gas.py b/src/ethereum/forks/berlin/vm/gas.py index 38c9a319f4c..1050dff8918 100644 --- a/src/ethereum/forks/berlin/vm/gas.py +++ b/src/ethereum/forks/berlin/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("Berlin", globals()) +apply_spec_repricing("Berlin", GasCosts) diff --git a/src/ethereum/forks/bpo1/vm/gas.py b/src/ethereum/forks/bpo1/vm/gas.py index 2e4f455fd54..3f4cb0d60fc 100644 --- a/src/ethereum/forks/bpo1/vm/gas.py +++ b/src/ethereum/forks/bpo1/vm/gas.py @@ -503,4 +503,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("BPO1", globals()) +apply_spec_repricing("BPO1", GasCosts) diff --git a/src/ethereum/forks/bpo2/vm/gas.py b/src/ethereum/forks/bpo2/vm/gas.py index 89231620b3d..e0c57bd3bfd 100644 --- a/src/ethereum/forks/bpo2/vm/gas.py +++ b/src/ethereum/forks/bpo2/vm/gas.py @@ -503,4 +503,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("BPO2", globals()) +apply_spec_repricing("BPO2", GasCosts) diff --git a/src/ethereum/forks/bpo3/vm/gas.py b/src/ethereum/forks/bpo3/vm/gas.py index 653b50a7831..b4fe3d9263a 100644 --- a/src/ethereum/forks/bpo3/vm/gas.py +++ b/src/ethereum/forks/bpo3/vm/gas.py @@ -503,4 +503,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("BPO3", globals()) +apply_spec_repricing("BPO3", GasCosts) diff --git a/src/ethereum/forks/bpo4/vm/gas.py b/src/ethereum/forks/bpo4/vm/gas.py index bee4d0302b0..0e5e3c3366f 100644 --- a/src/ethereum/forks/bpo4/vm/gas.py +++ b/src/ethereum/forks/bpo4/vm/gas.py @@ -503,4 +503,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("BPO4", globals()) +apply_spec_repricing("BPO4", GasCosts) diff --git a/src/ethereum/forks/bpo5/vm/gas.py b/src/ethereum/forks/bpo5/vm/gas.py index cff93f2aab9..723ef50c122 100644 --- a/src/ethereum/forks/bpo5/vm/gas.py +++ b/src/ethereum/forks/bpo5/vm/gas.py @@ -503,4 +503,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("BPO5", globals()) +apply_spec_repricing("BPO5", GasCosts) diff --git a/src/ethereum/forks/byzantium/vm/gas.py b/src/ethereum/forks/byzantium/vm/gas.py index eb3797d3b76..975bb5b6026 100644 --- a/src/ethereum/forks/byzantium/vm/gas.py +++ b/src/ethereum/forks/byzantium/vm/gas.py @@ -336,4 +336,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("Byzantium", globals()) +apply_spec_repricing("Byzantium", GasCosts) diff --git a/src/ethereum/forks/cancun/vm/gas.py b/src/ethereum/forks/cancun/vm/gas.py index ed97af64c8d..8b51646dd5a 100644 --- a/src/ethereum/forks/cancun/vm/gas.py +++ b/src/ethereum/forks/cancun/vm/gas.py @@ -474,4 +474,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("Cancun", globals()) +apply_spec_repricing("Cancun", GasCosts) diff --git a/src/ethereum/forks/constantinople/vm/gas.py b/src/ethereum/forks/constantinople/vm/gas.py index 34d8fe0250f..3e1d5c8d9b5 100644 --- a/src/ethereum/forks/constantinople/vm/gas.py +++ b/src/ethereum/forks/constantinople/vm/gas.py @@ -340,4 +340,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("Constantinople", globals()) +apply_spec_repricing("Constantinople", GasCosts) diff --git a/src/ethereum/forks/dao_fork/vm/gas.py b/src/ethereum/forks/dao_fork/vm/gas.py index a3d83cc6c1c..9c839ec127c 100644 --- a/src/ethereum/forks/dao_fork/vm/gas.py +++ b/src/ethereum/forks/dao_fork/vm/gas.py @@ -301,4 +301,4 @@ def calculate_message_call_gas( return MessageCallGas(cost, stipend) -apply_spec_repricing("DAOFork", globals()) +apply_spec_repricing("DAOFork", GasCosts) diff --git a/src/ethereum/forks/frontier/vm/gas.py b/src/ethereum/forks/frontier/vm/gas.py index 1fa0d8556dd..01d9518f27d 100644 --- a/src/ethereum/forks/frontier/vm/gas.py +++ b/src/ethereum/forks/frontier/vm/gas.py @@ -299,4 +299,4 @@ def calculate_message_call_gas( return MessageCallGas(cost, stipend) -apply_spec_repricing("Frontier", globals()) +apply_spec_repricing("Frontier", GasCosts) diff --git a/src/ethereum/forks/gray_glacier/vm/gas.py b/src/ethereum/forks/gray_glacier/vm/gas.py index 023d46b57ab..6b3d5a6ccab 100644 --- a/src/ethereum/forks/gray_glacier/vm/gas.py +++ b/src/ethereum/forks/gray_glacier/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("GrayGlacier", globals()) +apply_spec_repricing("GrayGlacier", GasCosts) diff --git a/src/ethereum/forks/homestead/vm/gas.py b/src/ethereum/forks/homestead/vm/gas.py index f26ae58efcf..2e8eb727fe6 100644 --- a/src/ethereum/forks/homestead/vm/gas.py +++ b/src/ethereum/forks/homestead/vm/gas.py @@ -301,4 +301,4 @@ def calculate_message_call_gas( return MessageCallGas(cost, stipend) -apply_spec_repricing("Homestead", globals()) +apply_spec_repricing("Homestead", GasCosts) diff --git a/src/ethereum/forks/istanbul/vm/gas.py b/src/ethereum/forks/istanbul/vm/gas.py index cbdaa12d831..c6a89b4de29 100644 --- a/src/ethereum/forks/istanbul/vm/gas.py +++ b/src/ethereum/forks/istanbul/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("Istanbul", globals()) +apply_spec_repricing("Istanbul", GasCosts) diff --git a/src/ethereum/forks/london/vm/gas.py b/src/ethereum/forks/london/vm/gas.py index 6d4d77457df..13e3065ddf2 100644 --- a/src/ethereum/forks/london/vm/gas.py +++ b/src/ethereum/forks/london/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("London", globals()) +apply_spec_repricing("London", GasCosts) diff --git a/src/ethereum/forks/muir_glacier/vm/gas.py b/src/ethereum/forks/muir_glacier/vm/gas.py index 1a2d8882782..590d835357b 100644 --- a/src/ethereum/forks/muir_glacier/vm/gas.py +++ b/src/ethereum/forks/muir_glacier/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("MuirGlacier", globals()) +apply_spec_repricing("MuirGlacier", GasCosts) diff --git a/src/ethereum/forks/osaka/vm/gas.py b/src/ethereum/forks/osaka/vm/gas.py index 254fbd7e0a0..e744ef20043 100644 --- a/src/ethereum/forks/osaka/vm/gas.py +++ b/src/ethereum/forks/osaka/vm/gas.py @@ -503,4 +503,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("Osaka", globals()) +apply_spec_repricing("Osaka", GasCosts) diff --git a/src/ethereum/forks/paris/vm/gas.py b/src/ethereum/forks/paris/vm/gas.py index 20577f83783..5353d887b86 100644 --- a/src/ethereum/forks/paris/vm/gas.py +++ b/src/ethereum/forks/paris/vm/gas.py @@ -343,4 +343,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("Paris", globals()) +apply_spec_repricing("Paris", GasCosts) diff --git a/src/ethereum/forks/prague/vm/gas.py b/src/ethereum/forks/prague/vm/gas.py index 467e8957b9c..d805c795a34 100644 --- a/src/ethereum/forks/prague/vm/gas.py +++ b/src/ethereum/forks/prague/vm/gas.py @@ -483,4 +483,4 @@ def calculate_data_fee(excess_blob_gas: U64, tx: Transaction) -> Uint: ) -apply_spec_repricing("Prague", globals()) +apply_spec_repricing("Prague", GasCosts) diff --git a/src/ethereum/forks/shanghai/vm/gas.py b/src/ethereum/forks/shanghai/vm/gas.py index f7b597f700f..f9071246a7a 100644 --- a/src/ethereum/forks/shanghai/vm/gas.py +++ b/src/ethereum/forks/shanghai/vm/gas.py @@ -17,6 +17,7 @@ from ethereum_types.numeric import U256, Uint, ulen from ethereum.trace import GasAndRefund, evm_trace +from ethereum.utils.gas_repricing import apply_spec_repricing from ethereum.utils.numeric import ceil32 from . import Evm @@ -362,3 +363,6 @@ def init_code_cost(init_code_length: Uint) -> Uint: """ return GasCosts.CODE_INIT_PER_WORD * ceil32(init_code_length) // Uint(32) + + +apply_spec_repricing("Shanghai", GasCosts) diff --git a/src/ethereum/forks/spurious_dragon/vm/gas.py b/src/ethereum/forks/spurious_dragon/vm/gas.py index 565b459888c..c79a0449292 100644 --- a/src/ethereum/forks/spurious_dragon/vm/gas.py +++ b/src/ethereum/forks/spurious_dragon/vm/gas.py @@ -329,4 +329,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("SpuriousDragon", globals()) +apply_spec_repricing("SpuriousDragon", GasCosts) diff --git a/src/ethereum/forks/tangerine_whistle/vm/gas.py b/src/ethereum/forks/tangerine_whistle/vm/gas.py index ac92bdcab09..e91899f4e1e 100644 --- a/src/ethereum/forks/tangerine_whistle/vm/gas.py +++ b/src/ethereum/forks/tangerine_whistle/vm/gas.py @@ -329,4 +329,4 @@ def max_message_call_gas(gas: Uint) -> Uint: return gas - (gas // Uint(64)) -apply_spec_repricing("TangerineWhistle", globals()) +apply_spec_repricing("TangerineWhistle", GasCosts) diff --git a/src/ethereum/utils/gas_repricing.py b/src/ethereum/utils/gas_repricing.py index d929b56434f..9dfd3a8d21e 100644 --- a/src/ethereum/utils/gas_repricing.py +++ b/src/ethereum/utils/gas_repricing.py @@ -39,12 +39,12 @@ def load_repricing_config() -> Optional[Dict[str, Dict[str, Any]]]: def apply_spec_repricing( fork_name: str, - module_globals: dict, + gas_costs: type, ) -> None: """ - Apply repricing overrides to module globals. + Apply repricing overrides to a fork's ``GasCosts`` class. - Mutates module_globals in place, preserving the + Mutate the class attributes in place, preserving the original type wrapper (Uint, U64, etc.). """ config = load_repricing_config() @@ -56,11 +56,11 @@ def apply_spec_repricing( return for name, value in overrides.items(): - if name not in module_globals: + if not hasattr(gas_costs, name): raise ValueError( f"Unknown gas constant '{name}' " f"in repricing config for fork " f"'{fork_name}'." ) - original = module_globals[name] - module_globals[name] = type(original)(value) + original = getattr(gas_costs, name) + setattr(gas_costs, name, type(original)(value)) From d16ba62b0e398802bdd6867c74e59583d6379499 Mon Sep 17 00:00:00 2001 From: carsons-eels Date: Wed, 27 May 2026 21:37:42 -0400 Subject: [PATCH 4/4] fix: ensure lint-spec doesn't use repriced values --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index 350ba08ab61..cf595d93f22 100644 --- a/Justfile +++ b/Justfile @@ -69,7 +69,7 @@ typecheck *args: # Check EELS import isolation [group('static analysis')] lint-spec: - uv run ethereum-spec-lint + EELS_GAS_REPRICING_CONFIG= uv run ethereum-spec-lint # Verify uv.lock is up to date [group('static analysis')]