ethereum-optimism / ethereum-optimism/optimism
opgeth-decoupling: replace op-chain-ops/script's op-geth EVM with a Rust script engine
- 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
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