ethereum / ethereum/execution-specs
feat(all): Repricing Tool
- Dominant language
- Python
- Stars
- 1.2k
- Forks
- 505
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 116
Description
# Gas Repricing Support in Specs + Testing Framework
## Summary
We want a minimal, robust way to experiment with gas repricing across both the specs and the testing framework, without constantly touching/dirtying spec files or adding heavy configuration plumbing.
The core idea is:
- Define all baseline gas costs as usual in specs and the testing framework (no behaviour change by default).
- Introduce an optional, local **repricing configuration file** (e.g. `gas_repricings.json`) that, when present, is used to override gas costs at **import/load time**.
- Have both:
- the testing framework (forks in `forks.py` / testing side), and
- the spec side (per-fork `gas.py` / equivalent)
consult this config on import and transparently update the gas cost data structures.
- Keep the mechanism as small and localized as possible (ideally one entry point per side, wired through base classes).
This should support:
- Fast local iterations on candidate gas schedules (e.g. for 7904 / Amsterdam and beyond).
- Running both `fill` and `execute` (including `execute-remote`) with modified gas schedules.
- Eventually feeding finalized repricing back into the canonical spec constants and tests.
## Goals
1. **Fast iteration loop for repricing**
- Change gas costs in one JSON file.
- Re-run tests/benchmarks against the new schedule.
- No need to hand-edit spec/test constants repeatedly.
2. **Minimal intrusion into existing code**
- Centralize repricing logic in a base layer (e.g. base fork class on testing side; single hook on spec side).
- Default behaviour (no config file) is exactly current behaviour.
3. **Shared mechanism across specs and tests**
- Single config format that can be consumed by:
- Testing framework (forks)
- EELS spec modules
- No duplication of repricing logic.
4. **Safe and explicit**
- Opt-in only: repricing happens only if the config is present (and possibly guarded by an env var).
- Emit a clear warning when repricing is active to avoid “silent” surprises.
- If we have a default repricing JSON file, it should be added to the .gitignore
## Non-goals
- This is **not** a permanent source of truth for gas costs.
- Final gas schedules must still be reflected back into normal spec/test constants.
- This is **not** intended to replace Pitstop or client-side tooling.
- It’s complementary: helps us generate/validate repricing test data and spec constants.
## Prerequisites
Before wiring in the repricing tool, we need:
1. **Benchmark refactor complete** ✔️
- All benchmark tests use the gas cost calculator, not hard-coded values.
- (Louis: this is already done.)
2. **Unified/renamed gas constants between specs and tests** ✔️
- Carson’s existing [PR](https://github.com/ethereum/execution-specs/pull/2094) to align naming between `forks.py` and the spec constants needs:
- Rebase on current `main` (`forks/amsterdam`).
- Adjustments for recent refactors (new constructs in `forks.py`, etc.).
- Any remaining spec-side changes (e.g., replacing hard-coded numeric gas charges with references to the unified constants) should be handled in/alongside that PR.
3. Source feedback on approach from stakeholders:
- Maria
- Kamil
- Jochem
Once these are done, repricing logic can be implemented on top.
## Proposed Design
### 1. Repricing Config File
- **Location**: Current working directory where `uv run fill` / `pytest` / `execute` is invoked.
- **Name** (suggested): `gas_repricings.json`
- **Git**: Add to `.gitignore` so it’s treated as ephemeral/local by default.
- **Format** (example):
```json
{
"amsterdam": {
"DUP": 1000,
"SSTORE": 5000
},
"osaka": {
"CALL": 800
}
}
```
Details to be finalized, but high-level:
- Top-level keys: fork identifiers (e.g. `"amsterdam"`, `"osaka"`).
- Values: `{ opcode_name: new_gas_value }` maps.
- Could optionally support more structured overrides later if needed (e.g. multiple cost categories per opcode).
### 2. Testing side (forks / testing framework)
#### Base idea
- Introduce a **single hook** in the base fork class that always passes gas maps through a repricing function.
- Fork-specific classes only define their “internal” / baseline gas maps.
- At import time, if the correct environment variable is set, `gas_repricings.json` exists and is non-empty, apply overrides to the gas map.
#### Sketch
- In `forks.py`:
class BaseFork:
@classmethod
@abstractmethod
def _internal_opcode_gas_map(
cls, *, block_number: int = 0, timestamp: int = 0
) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]:
# Definition meant to be overloaded in each subclass
raise NotImplementedError
@classmethod
def opcode_gas_map(
cls, *, block_number: int = 0, timestamp: int = 0
) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]:
base_map = cls._internal_opcode_gas_map()
return apply_gas_repricings(cls, base_map)
- For each fork (e.g. `Amsterdam`), rename all current `opcode_gas_map` to `_internal_opcode_gas_map`:
class Amsterdam(BPO2):
"""Amsterdam fork."""
@classmethod
def _internal_opcode_gas_map(
cls, *, block_number: int = 0, timestamp: int = 0
) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]:
return {
Op.DUP1: 3,
Op.SWAP1: 3,
# ...
}
- `base_fork.py`:
- On import:
- Resolve current working directory.
- Look for `gas_repricings.json`.
- Optionally check an env var gate (e.g. `EELS_GAS_REPRICINGS=1`) for extra safety (We can also set `EELS_GAS_REPRICINGS` to be a path and then look for that file, up to implementer)
- If file exists, pydantic-load the file into a global variable `GAS_REPRICINGS`.
- If file doesn’t exist or is empty, set the same global variable as an empty dictionary.
- If it exists:
- Load JSON.
- Identify the current fork key (e.g. `"amsterdam"`).
- For entries in that fork’s map, `base_map.update(overrides)`.
- Should be designed so that:
- No repricing occurs unless config is present (and optionally the env var is set).
- A warning is emitted when repricings are applied:
- `apply_gas_repricings(cls, base_map)`:
- On call:
- Check if `GAS_REPRICINGS` global variable contains the current fork of `cls`
- Modify each gas constant in the provided `base_map` and return the modified map.
### 3. Spec side (EELS fork gas definitions)
The spec side needs a similar mechanism, but with care for readability and “spec purity”.
#### Constraints
- Specs should remain readable and look like “normal” spec code.
- By default (no config), they must behave exactly as now.
- Repricing must not require every spec file to know about JSON/config.
- Everything must happen on import. This way we don't need to touch T8N interface.
#### Approach
** TBD **
### 4. Activation / safety
To reduce accidental use:
- **Config file presence**: repricing only happens if `gas_repricings.json` exists and contains relevant entries.
- **Optional env var**: e.g. `EELS_GAS_REPRICINGS=1`
- If not set, even with a JSON file present, we can:
- Either skip repricing entirely, or
- Emit a warning that the config is ignored.
- **Warnings**:
- Emit a `warnings.warn` (or logging) on **first use** per process:
- Which file is being used.
- For which fork(s) repricings are active.
### 5. Developer workflow examples
#### a) Local repricing experiment
1. Create `gas_repricings.json` in the repo root:
{
"amsterdam": {
"DUP": 1000
}
}
Note: We can do a trick here where instead of adding DUP1, DUP2, etc, we add a single DUP, but up to implementer.
2. Set env var (if we decide to gate by env):
export EELS_GAS_REPRICINGS=1
3. Run benchmarks / tests:
uv run fill
uv run execute --remote ...
4. Observe breakage / results with `DUP` repriced for Amsterdam (Ideally, thanks to renamings and use of `gas_cost`, no breakage should occur).
5. When done, delete or clear `gas_repricings.json` (or unset env var).
#### b) Finalizing repricing
1. After converging on a final gas schedule via the above workflow:
- Update:
- The spec constants (per-fork `gas.py`).
- The testing framework’s fork definitions.
- Remove/ignore the JSON overrides.
2. Regenerate fixtures/benchmarks (`fill`).
3. Sync with client tooling (e.g. Pitstop) for updating client-side constants.
## Open Questions / Details to Decide
1. **Exact JSON schema**
- Just flat `{ fork: { opcode: gas } }`?
- Do we need multiple “cost dimensions” (base, warm, cold, etc.) in the first version?
2. **Shared vs duplicated repricing helper**
- Single helper module shared across specs and tests?
- Or two separate but identical helpers that both read `gas_repricings.json`? (Probably this)
3. **Fork naming / mapping**
- How do we map fork names used in JSON to actual classes/modules?
- Should we enforce a canonical string (e.g. `"amsterdam"`, `"osaka"`)?
4. **Env var gating**
- Do we require `EELS_GAS_REPRICINGS=1` (or similar) or just rely on the presence of the JSON file?
- Any additional safety checks we want?
5. **Spec readability**
- Do we prefer small helper wrappers (e.g. `OPCODE_GAS_COSTS = fork_gas("amsterdam", {...})`) over calling `apply_gas_repricings` directly?
- How much “config awareness” are we okay with in spec modules?
## Next Steps
- [x] Finish benchmark refactoring (Louis: stateful + compute benchmarks already migrated to gas calculator).
- Rebase and update Carson’s **gas-constant renaming/alignment** PR:
- [ ] Ensure spec and testing constants are in sync.
- [ ] Replace remaining hardcoded usages with references to unified constants where appropriate.
- Get design feedback from stakeholders:
- [ ] Maria
- [ ] Kamil
- [ ] Jochem
- Design and implement `apply_gas_repricings` helpers, specs and test since they will possibly be different:
- [ ] Decide JSON schema and env var behavior.
- [ ] Add warnings and tests.
- Wire repricing helper into:
- [ ] Base fork class on testing side (`BaseFork.opcode_gas_map`).
- [ ] Per-fork spec gas definitions.
- Document the workflow (README / docs):
- [ ] How to create and use `gas_repricings.json`.
- [ ] Example for repricing a few opcodes on Amsterdam.
- [ ] Caveats and cleanup steps.
Contributor guide
Assessment
This issue has not been assessed yet.