ethereum / ethereum/execution-specs

feat(all): Repricing Tool

Open
#2,200 7 comments 0 reactions 1 assignee Assigned to @LouisTsai-Csie View on GitHub
A-spec-specs A-spec-tests C-feat E-medium P-high
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.