ethereum-optimism / ethereum-optimism/optimism

opgeth-decoupling: replace op-chain-ops/script's op-geth EVM with a Rust script engine

Open
#21,743 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
6.5k
Forks
4k
Avg merge
2d 15h
Merged PRs (30d)
145

Description

Part of #20257. See §16 of the [op-geth decoupling doc](https://github.com/ethereum-optimism/optimism/blob/develop/docs/ai/opgeth-decoupling.md). **Blocks #20266.**

## Problem

`op-chain-ops/script` is a Foundry-style in-process forge-script executor; op-deployer runs all
its deployment/genesis scripts through it (~40 importing files). Semantically it is a **plain L1
EVM** — its chain config activates only Ethereum forks (OP fork fields are explicitly nil), so
there is no OP state transition to preserve. But its *mechanism* is fork-bound: the cheatcode
machinery runs on op-geth-only EVM hooks that upstream go-ethereum does not have and will not get
(we don't open upstream PRs):

- `vm.Config.PrecompileOverrides` — cheatcode precompile injection
- `vm.Config.CallerOverride` — prank support
- `vm.Config.NoMaxCodeSize`
- `vm.BlockContext.L1CostFunc` (trivial: set to nil today)
- exported `state.StateUpdate` — upstream's `state.Database.Commit` takes the *unexported*
`*state.stateUpdate`, so `script/forking.ForkDB` cannot implement the interface outside the fork

So "swap call sites to op-core" does not exist as an option here.

## Decision: Rust engine, no interim module split

Rewrite script execution as a **Rust engine reusing foundry crates** (forge is the reference
executor for these scripts; revm underneath), consumed by op-deployer — subprocess/sidecar
following the op-reth-test-engine pattern (#20415), or an equivalent embedding.

A transitional Go module split (op-chain-ops+op-deployer keeping the op-geth replace in their own
`go.mod`) was considered and **rejected**: the value of this epic only materialises when we can
stop maintaining the op-geth fork entirely. Any in-repo module still depending on it keeps the
fork alive.

## Design constraints

- **Distribution**: op-deployer must keep working without a system-installed foundry — the engine
is version-pinned and shipped/built with our tooling, not discovered from the user's PATH.
- **Cheatcode surface**: only what our scripts actually use. Derive the current surface from the
cheatcode dispatch in `op-chain-ops/script/cheatcodes*.go` rather than reimplementing all of
forge.
- **Fork mode**: `script/forking` (RPC-backed state) is part of the surface; foundry crates
already provide this.
- **Parity gate**: before switching, run both engines over the deployment/genesis scripts of a
reference config and require identical resulting state/allocs.

## Spike results (2026-07, branch `seb/spike-op-script-engine` — three completed spikes)

The strategy is **spike-proven end-to-end**; what remains is landing, bake time, and the final
Go-host deletion. Spike 1 built the engine core and passed the first byte-for-byte parity gate;
spike 2 made Rust the op-deployer default for non-forked hosts (L2Genesis `ForgeAllocs`
byte-parity across all 5 alloc modes, OPCM `RunScript*` path, interopgen, CI binary provisioning,
~2.6× faster than the Go host); spike 3 added fork mode (lazy RPC-backed `AlloyDB` underlay
pinned by block hash) and deleted the per-host-kind boundary, making `--script-engine=go` the
only remaining Go-engine path — forked A/B parity verified against a hermetic fixture and live
Sepolia archive. Each spike passed independent adversarial no-shortcut audits (no skips, no
weakened assertions, no regenerated goldens).

Key facts from spike 1 (still the architectural foundation):

- **Hand-rolled on the already-pinned revm 40 — no foundry crates.** Sidesteps foundry's
revm/alloy version-skew risk entirely; no second workspace or lockfile. All four fork hooks
have proven revm equivalents (inspector call hook, `CallInputs.caller`,
`CfgEnv.limit_contract_code_size`, `CacheDB` iteration).
- **The headline risk is dead**: revm==geth gas/nonce accounting at the L1 fork is empirically
equal on the golden sequences.
- Transport per #20415: reth-ipc/jsonrpsee UDS server, `rpc.DialIPC`-compatible.
- **Honest gap**: ~13 of ~153 cheatcodes implemented; L2Genesis parity (the true go/no-go
milestone) not yet attempted; fork mode (~2k LOC Go equivalent) absent; OPCM
input/output-precompile path designed but unbuilt; artifact plumbing minimal.
- **Known hazard to fix first**: the unimplemented-cheatcode fallback returns empty-success
instead of reverting — silent-wrong-state risk; must become a loud revert before any further
parity work.
- **Estimated production effort**: ~7–9.5 engineer-weeks, sequenced so L2Genesis parity comes
first as the go/no-go milestone, then fork mode, OPCM path, op-deployer cutover,
distribution/packaging.

## Acceptance criteria

- op-deployer executes its scripts via the Rust engine; `op-chain-ops/script` (Go EVM host) is
deleted.
- No monorepo Go package imports `go-ethereum/core/vm` for script execution.
- Parity run against the old engine documented in the PR.

🤖 *Generated by Claude Code*

Contributor guide

Open the contributing guide

Research direction

Read §16 of docs/ai/opgeth-decoupling.md and inspect the cheatcode dispatch in op-chain-ops/script/cheatcodes*.go to establish the required surface. Use the spike branch and its parity work as the starting point, then compare both engines on deployment/genesis scripts for a reference config. Done means op-deployer uses the Rust engine, the Go EVM host is deleted, and parity is documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, rust
Domain
backend, tooling
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.