This document analyses the dependencies of the optimism monorepo Go services on op-geth–specific APIs, and proposes decoupling strategies for each. The goal is to depend on upstream go-ethereum instead of op-geth without opening upstream PRs. Tracking issue: #20257. Last full revisit: 2026-07 (verified by an upstream-build spike; added §§15–19).
Sections are numbered stably (§0–§19) because issues reference them. Done sections keep their
context and decisions but not their implementation detail — the landed code in op-core/* is its
own reference. Work-list detail (exact files, counts) is deliberately absent: it goes stale, and
an agent re-derives it in minutes from the grep recipes given per section.
Scope: the whole monorepo. The monorepo is a single Go module with one go-ethereum
replace directive, so at the flip everything in the tree must build — and behave — against
upstream go-ethereum; components can only leave scope by deletion. Each component falls into one
of three fates:
- Swap to
op-core/*(+ upstream go-ethereum) — code that stays in Go; where it used op-geth-specific types/helpers they swap toop-core/*, and some members need no changes at all (§12). The bulk: op-node, op-service, op-batcher, op-proposer, op-challenger, op-faucet, op-supernode, cannon; the test suites (op-e2e, op-acceptance-tests, op-devstack) and the Go proof tests underrust/kona/tests/proofs/;op-chain-ops/genesis(#21281) and the survivingop-chain-ops/cmd/check-*checkers (§18); op-sync-tester — a CL-sync EL mock that does no execution (it proxies a real op-reth EL and gates visibility), whose one non-swap bit, the OP-awarePayloadIDhash, is tracked in #21525; and the fork's infrastructure extensions to geth'slog/rpcpackages, re-homed into owned op-service layers (§15). - Migrate execution to op-reth / Rust — anything that builds or executes blocks, or needs
fork-only EVM hooks. op-acceptance-tests sequences op-reth-only for Karst+ (#21182);
op-e2e/actions moves onto an op-reth-test-engine subprocess EL (#20415, #21196); op-e2e system
tests and op-devstack/sysgo retire their in-process op-geth L2 EL (§17); and
op-chain-ops/script+ op-deployer move to a Rust script engine (§16). - Delete — geth-as-library tools with no remaining need, to be reimplemented in Rust against
op-reth if ever needed again: op-simulate and op-run-block (#21282),
op-wheel/cheatand the stale pre-Holocenecheck-*tools (§18).
Once every component is in fate 1, 2, or 3, the go.mod replace flips to upstream go-ethereum
(#20266). A scheduled CI ratchet job rehearses the flip continuously (see §19).
Compile-compat is necessary, not sufficient. Upstream go-ethereum does not implement the OP state transition, so code can compile and silently behave wrongly. The classifying question for every geth-as-library consumer: does it execute OP state transitions? Yes → op-reth or delete (fate 2/3). No — plain-EVM script execution, storage-level surgery, header/RLP work → upstream is behaviorally fine (fate 1).
The op-geth diff vs. upstream go-ethereum (currently based on v1.17.2) can be summarised in four kinds of change:
- New standalone types/files –
DepositTx,PostExecTx,RollupCostData, thesuperchain/package, eip1559 Holocene/Jovian helpers. - Fields/methods added to existing upstream types –
Transactionmethods (IsDepositTx,SourceHash,Mint,IsSystemTx,RollupCostData),ReceiptL1-cost fields,ChainConfigOP hardfork fields and methods,PayloadAttributesextensions. - Infrastructure hooks in general-purpose packages –
logcontext methods,rpcrecording hooks, EVM override hooks invm.Config(§15, §16). Having a fork made "just add a hook to geth" cheap; the decoupling converts each such shortcut into a layer we own or a feature decision. - Config/CLI wiring – how op-geth starts: not relevant to the monorepo.
All op-geth-specific code lives in op-core/ packages (alongside the pre-existing
op-core/forks/ and op-core/predeploys/). All of these have landed:
| op-geth source | Monorepo package | Content |
|---|---|---|
core/types/deposit_tx.go, post_exec_tx.go, receipt_opstack.go |
op-core/types/ |
DepositTx, PostExecTx, Receipt + helpers |
core/types/rollup_cost.go |
op-core/fees/ |
L1 cost + operator-fee math |
params/config_op.go, params/superchain.go |
op-core/params/ |
OptimismConfig, standalone ChainConfig, loaders |
superchain/ + sync-superchain.sh |
op-core/superchain/ |
embedded registry (absorbed op-service/superutil) |
consensus/misc/eip1559/eip1559_optimism.go |
op-core/eip1559/ |
Holocene/Jovian extraData helpers |
- Monorepo-best, not op-geth-faithful. Names and shapes diverge freely; only computed values and wire formats must match. Equivalence is pinned by live differential tests that compare against op-geth while the build still resolves go-ethereum to the fork; all differential tests are deleted at the cutover (§ #20266).
- Naming: inside
op-core/theOptimism/OPStackprefix is dropped (redundant), with two deliberate exceptions:OptimismConfig(pairs with the JSON-wire-load-bearingChainConfig.Optimismfield name) and the{Validate,Decode,Encode}OptimismExtraDatahelpers (there "Optimism" names the extraData format and avoids collision with per-fork helpers). - Files needing both upstream and monorepo variants use import aliases
optypes,opparams,opfees. - Wire compatibility where types serialise: JSON tags identical to op-geth
(
OptimismConfig), byte-identical encodings (DepositTx:0x7E || RLP(struct)), verified by the differential tests.
Rather than port the deprecated protocol-versions signalling mechanism (op-node watching the L1
ProtocolVersions contract, engine_signalSuperchainV1, halt-when-outdated) into op-core, the
feature was removed entirely — op-node, kona/op-reth, contracts, and all Go wiring (#20258,
#20311, #20317, #20441, #20527). This shrank the surface to decouple and avoided carrying dead
code.
Careful when grepping: op-service/apis/p2p.go / op-node/p2p/rpc_server.go contain an
unrelated libp2p ProtocolVersion string — same name, different concept, left alone.
op-core/types ships DepositTx (0x7E || RLP(struct)) and PostExecTx with
MarshalBinary/Unmarshal* and differential tests (#20262). Read the package for the API.
Two wire facts worth keeping in mind (both encoded in the landed implementation):
PostExecTx(0x7D) has no RLP envelope —0x7D || dataverbatim; op-geth treats the payload as opaque bytes (op-alloy/kona parse it as a versionedPostExecPayload). The op-core type mirrors the opaque handling.PostExecTxis a canonical block transaction: on Lagoon+ it can be the last tx of a block (sequencer rebates). So an L2 block's tx list is[L1-info deposit (0x7E), …user txs…, optional post-exec (0x7D)]— under upstream go-ethereum a full-block decode fails on both synthetic types, which is why the OP-aware client (§11) must route both out.
Open work (#20269, #20263): swap the construction/decoding call sites. The op-geth pattern
types.NewTx(&types.DepositTx{...}).MarshalBinary() becomes
optypes.DepositTx{...}.MarshalBinary() — the *types.Transaction wrapper is eliminated, struct
straight to bytes. UnmarshalDepositLogEvent returns *optypes.DepositTx. Sites cluster in
op-node derivation (deposits, one *_upgrade_transactions.go per fork — the set grows every
fork) and span-batch code (PostExecTxType); derive the current list with
grep -rn 'types\.\(DepositTx\|PostExecTx\)' --include='*.go' over the go-ethereum import.
op-geth adds methods to *types.Transaction (IsDepositTx, IsSystemTx, SourceHash, Mint,
RollupCostData) that the monorepo calls on txs received via Engine API / RPC. tx.Type() and
tx.Data() are standard upstream — no change. The replacements exist in op-core/types (free
functions and UnmarshalDepositTx-then-read-fields) and op-core/fees.TxRollupCostData.
These *types.Transaction helpers are transition scaffolding, not the destination. They only
work while the build resolves go-ethereum to op-geth (where a *types.Transaction can carry a
0x7E/0x7D tx); they let call sites drop op-geth's methods before the underlying
representation changes. After the cutover an upstream *types.Transaction can never hold a
deposit or post-exec tx, so the helpers and their differential tests are deleted at the cutover
(§ #20266). The durable shape never wraps an OP tx in *types.Transaction: raw bytes decode
into optypes.DepositTx/optypes.PostExecTx, and code never asks "is this a deposit?" of a
generic tx because the sources accessors already partition by class (§11).
op-core/types.Receipt (landed, #20262) embeds upstream types.Receipt and adds op-geth's
complete OP receipt field set (L1 fee fields, operator fee, deposit nonce, DA footprint) with
a custom JSON unmarshaler and a differential test. Complete deliberately: the e2e/acceptance
suites read more fields than the one production consumer, and a partial set would silently drop
fields at cutover — invisible while the replace still points at op-geth.
Production consumer: op-service/txinclude reads the fee fields off receipts fetched via RPC.
Open work: its EL.TransactionReceipt (and apis.EthClient / FetchReceipts generally)
return *optypes.Receipt instead of *types.Receipt — part of §11 (#20264). Under upstream,
the standard unmarshaler drops the OP fields silently; only the raw-RPC path through
op-service/sources + optypes.Receipt preserves them.
Context: op-node itself never reads receipt fields — it reads L1BlockInfo from deposit-tx
calldata; txinclude's cost oracle reads the L1Block predeploy via eth_call. Receipts matter
mainly for txinclude's post-inclusion accounting and the test suites (§13).
Landed (#20261) and wired into op-service/txinclude. API is monorepo-best (see design
principles): plain per-era free functions (L1CostBedrock/Ecotone/Fjord + L1FeeParams) instead
of op-geth's closure factories, OperatorCostIsthmus/Jovian replacing inline math,
TxRollupCostData(tx) replacing the fork's Transaction method. Values pinned by differential
tests. Notable choice: both txinclude sites use the Jovian operator-fee formula
unconditionally — exact on all production chains, and on pre-Jovian chains it only
over-estimates, which over-reserves budget rather than under-budgeting.
Remaining: the op-batcher tx.RollupCostData() site swaps to opfees.TxRollupCostData as part
of the op-batcher → sources migration (§11), not here.
op-core/params ships OptimismConfig, a standalone ChainConfig, and GethChainConfig().
Why standalone, not embedding upstream's params.ChainConfig (decision worth remembering):
embedding would promote go-ethereum's OP-unaware EIP-1559 methods (ElasticityMultiplier,
BaseFeeChangeDenominator, LatestFork), which would read the embedded (nil) fields and
silently return L1 defaults — a footgun on consensus-relevant fee params, present both during the
transition and after the cutover. A standalone type cannot promote them. Code that needs a
concrete go-ethereum config converts explicitly via GethChainConfig() (Ethereum schedule
derived from the OP schedule: Shanghai=Canyon, Cancun=Ecotone, Prague=Isthmus, Osaka=Karst —
op-geth's own loader is missing Osaka=Karst; the monorepo follows op-reth, which is correct).
While the build still resolves go-ethereum to op-geth, GethChainConfig() also carries the OP
fork fields and Optimism struct on the produced config (in-process test ELs need them). Those
fields don't exist upstream — shedding them is a cutover edit (#20266).
Open work (#20270): rollup.Config.ChainOpConfig changes type to *opparams.OptimismConfig
(JSON tags identical → wire-compatible); op-service/eth.BlockAsPayload's
*params.ChainConfig parameter becomes a small local HardforkConfig interface
(IsCanyon/IsIsthmus) that rollup.Config already satisfies; remaining direct OP-hardfork
reads on go-ethereum's *params.ChainConfig go through rollup.Config or
*opparams.ChainConfig. Grep recipe: files importing go-ethereum/params that call
Is<OpFork>( or reference OptimismConfig.
Moved to op-core/superchain: (*superchain.ChainConfig).OpChainConfig() plus the by-chain-ID
loader superchain.LoadOpChainConfig (ex-superutil). §7 has the layering rationale.
The registry package moved verbatim to op-core/superchain (embedded zip regenerated from the
repo-root superchain-registry submodule by sync-superchain.sh; the zip is gitignored, pinned
by a committed .sha256). op-service/superutil was deleted; all consumers import
op-core/superchain.
Layering decision worth remembering: op-core/params holds pure config types and must stay
free of op-core/superchain, which embeds the bundle — anything in its build closure needs the
bundle generated before it compiles, and external module consumers (the superchain-registry ops
tooling) cannot generate it at all. The registry→ChainConfig conversion (OpChainConfig,
LoadOpChainConfig) therefore lives in op-core/superchain, which imports op-core/params
one-way. Transitive guard tests in op-node/rollup, op-chain-ops/script, and
op-fetcher/pkg/fetcher/fetch/script enforce the boundary (op-service/testutils/depguard).
rollup.Config carries the full OP hardfork schedule itself (loaded from the registry,
bypassing any geth ChainConfig); future forks extend rollup.Config directly. Any remaining
gaps ride #20270.
8. consensus/misc/eip1559 – Holocene/Jovian helpers — op-node DONE (#20268), helpers missing for §13/§14/§18
op-core/eip1559 landed and op-node is fully switched (no op-node production code imports geth's
eip1559; one test file still does — a fork-comparison import that goes away at cutover).
Signatures operate on []byte/uint64 only.
Still absent from op-core/eip1559: only EncodeOptimismExtraData and DecodeHoloceneExtraData
(the Holocene/Jovian encode helpers are already present) — add them when §13 migrates the op-e2e
helpers. The geth-eip1559 imports remaining in genesis (§14) and the check-* tools (§18) are pure
import swaps onto the existing op-core helpers.
engine.PayloadID is standard upstream. (op-sync-tester's OP-aware PayloadID hash is different
— see #21525.)
op-service defines its own Engine-API types with the OP extensions. BlockAsPayload touches only
upstream-stable block/header fields; it just needs the HardforkConfig change from §5.
Most ethclient usage in the monorepo is safe (L1-only, or standard scalar/receipt-log reads).
Exactly two failure modes appear when go-ethereum resolves upstream, and both only bite against
L2 endpoints:
- L2 block decoding fails: upstream
Transaction.UnmarshalJSONrejects the deposit (0x7E) and post-exec (0x7D) txs present in every L2 block. - OP receipt fields silently drop: upstream
types.Receiptdoesn't know the L1-cost / operator-fee fields (§3).
The audited unsafe production paths are the op-batcher L2 block fetch (driver.go's
L2Client) and txinclude receipt fetch; op-faucet, op-proposer, op-challenger (headers
only), op-supernode (receipts via sources; reads only Logs) and cannon (no blockchain-layer
coupling at all — see §12) need nothing. Re-derive with
grep -rl 'go-ethereum/ethclient' and classify each client by endpoint.
The sources clients already speak raw JSON-RPC (not ethclient). Extend them; do not
introduce an "OP ethclient" or a go-ethereum-style Transaction+TxData wrapper. The codebase's
dominant L2-tx currency is already opaque []hexutil.Bytes; where typed access is needed it is
almost always class-specific, and a wrapper would re-import the signer/hashing machinery we are
shedding. Instead, partition the apis.EthClient accessors by tx class:
InfoAndUserTxs(...) (eth.BlockInfo, types.Transactions, error)— excludes the synthetic OP types (0x7E/0x7D); the remainder are standard Ethereum txs, so the list is plain upstreamtypes.Transactionsand decodes fine post-cutover. Serves the dominant "skip deposits, operate on user txs" pattern (op-batcher DA estimation, most test iterators).InfoAndDeposits(...) (eth.BlockInfo, []*optypes.DepositTx, error)— deposits as op-core structs.InfoAndFirstDeposit(...)— header + the L1-info deposit only, for hot paths (pattern prototyped in the closed PR #20532; revive).- No
InfoAndPostExecTxyet, deliberately: every known consumer of post-exec data reads it from batch/flashblock bytes (span-batch decode,op-chain-ops/pkg/sdm, the flashblock client), not from L2 block bodies — block-fetching code only needs the exclusion thatInfoAndUserTxsprovides. Add the accessor when a consumer (e.g. a rebate checker) first needs to read the trailing0x7Dtx off a block.
InfoAndTxsBy* stays as-is for L1. Internally RPCBlock keeps txs as raw per-tx bytes: the
transactions-trie root is recomputed directly from those bytes (a typed tx's trie leaf is its
opaque encoding), synthetic-tx detection is a type-byte check, and each class decodes on demand.
Index-correlation caveat (for §13): callers that iterate the full tx list and correlate by
position with a receipts list misalign once InfoAndUserTxs drops the synthetic txs — such
tests must filter receipts in lockstep or use a paired accessor.
Concrete migrations in this issue: op-batcher's L2Client moves to InfoAndUserTxs (it only
filters deposits for DA estimation; RollupCostData → opfees.TxRollupCostData);
op-service/dial's L2EndpointProvider.EthClient returns a sources-shaped client (or the
interface is phased out); txinclude's EL.TransactionReceipt → *optypes.Receipt via raw RPC.
- op-proposer — pure L1; contract calls via
sources/batching.MultiCaller; no deposit txs, OP receipt fields, or OP params anywhere. - op-challenger — ~90% L1; its single L2 access fetches headers only (no txs → no decode issue). If it ever needs L2 block bodies, migrate per §11 then.
- op-supernode — no direct
ethclient; receipts via sources interfaces, reads onlyreceipt.Logs. Picks up §11 improvements transparently. - cannon — imports only
common/hexutil/log; the preimage oracle treats bytes opaquely. Zero blockchain-layer coupling.
Dependency profiles: op-acceptance-tests has zero direct ethclient imports — everything
goes through the op-devstack DSL to apis.EthClient, so it picks up §11's implementation swap
automatically (this is why §11 rejects a parallel wrapper type). op-e2e uses ethclient and
OP receipt fields directly in dozens of files. op-devstack's DSL layer is clean;
sysgo/ calls ethclient.Dial directly in a handful of files (mostly L1 contract/dispute-game
setup).
apis.EthClient.TransactionReceipt/FetchReceiptsreturn*optypes.Receipt(§3); the many test sites reading OP receipt fields keep working unchanged.- L2 tx iteration migrates to the class-partitioned accessors from §11 (
InfoAndUserTxsremoves the per-txIsDepositTx()checks entirely). Mind the index-correlation caveat (§11). op-e2e/e2eutils/geth/wait.go(the central wait helpers, ~26 importers): none of them semantically need OP specifics — the failure is transport-level (full-block decode on L2). Split into header-only variants (HeaderByNumber-based, L2-safe) for the majority of callers that discard the block, keep a full-block variant backed byapis.EthClientfor the few that read the body;WaitForTransactionreturns*optypes.Receipt.e2esys.SystemConfig.L2Clientbecomesapis.EthClient; L1 stays*ethclient.Client.op-devstack/sysgo: per-call audit; L1 callers stay onethclient, L2 callers migrate to sources.- Hardfork checks on geth
*params.ChainConfigin tests migrate to*opparams.ChainConfig(mostly automatic once the loaders return the op-core type). op-e2e/opgeth/is deleted (#20275), not migrated — it intentionally exercises op-geth engine internals; op-reth equivalents are introduced separately.
L2-endpoint full-block/tx fetches were ~20 sites at the 2025 audit — re-derive before working
(grep -rn 'BlockByNumber\|BlockByHash\|TransactionByHash' op-e2e/, classify by target
endpoint); L1 fetches are safe; the generic utilities (geth/find.go, wait.go) are fixed once
via item 3 and cover both call patterns. The simulated backend (ethclient/simulated) uses only
upstream-stable APIs.
Removing op-geth has two parts: op-geth as the execution engine (in-process ELs — deleted or
replaced by op-reth; §17, #21196) and op-geth as a library in offline tooling. The genuine
in-scope library consumer is op-chain-ops/genesis (BuildL2Genesis: genesis state-root +
genesis.json). op-simulate/op-run-block are deleted instead (#21282, no importers).
The only op-geth diff the genesis path relies on: for Isthmus+ chains the genesis block's
WithdrawalsHash is the storage root of the L2ToL1MessagePasser predeploy (via the upstream
GetStorageRoot primitive) instead of the empty hash. Migration (#21281): genesis builds an
opparams.ChainConfig and serialises that into genesis.json (it carries the OP fields;
upstream's config cannot); the block/state-root is computed against upstream via
GethChainConfig(); the ~15-line Isthmus withdrawals-root rule is ported on top of upstream
state/triedb + op-core/predeploys. This is why GethChainConfig() is a real bridge rather
than throwaway.
op-geth doesn't only add OP protocol code; having a fork made it cheap to add general-purpose hooks to geth's infrastructure packages, and op-service foundations grew to depend on them. They sit under every service, so they break the whole tree at cutover, and two of them are features rather than symbols. (Found by the 2026-07 upstream-build spike; §19 keeps finding such uses.)
Log context extensions — fork adds Logger.SetContext, WriteCtx, LogAttrs, and the
Trace/…/ErrorContext methods; op-service/log's logfilter feature and op-service/testlog
build on them. Strategy: own the log layer — depending on an internal log API is better
layering than importing geth/log everywhere anyway.
- Phase 1 (pre-cutover, mechanical, per-component):
oplog.Loggerbecomes a type alias of gethlog.Logger, plus re-exports of the package-level API in use (Root,SetDefault,NewLogger, level parsing). Sweep everygo-ethereum/logimport toop-service/log— a no-op rename while the alias holds. - Phase 2 (at cutover): flip the alias to an owned interface (upstream's method set + the
context methods) with an slog-backed implementation. Implement over
slog— don't copy upstream's LGPL log package; the fork's context-extension logic is OP-authored and ports. The owned interface is a superset of upstream's, so our loggers still satisfylog.Loggerwhere we hand one into geth code (e.g. the in-process L1 geth in op-e2e).
RPC recording hooks — fork adds rpc.Recorder/RecordedMsg/RecordDone/WithRecorder
inside the geth RPC client and server; op-service/metrics (RPC metrics), op-service/rpc,
and op-service/client build on them. Client-side recording moves into our own client wrappers
(a seam we own); server-side needs a new interception point (HTTP middleware or handler
wrapping) — small design task, metric names/labels must be preserved. rpc.JsonError
(op-test-sequencer) is the same family, trivially replaced by a local error type.
One-off fork symbols in the same spirit — Transaction.SetBlobTxSidecar (op-service/signer),
types.NewIsthmusSigner, the fork-changed types.NewBlock(…, *BlockConfig) signature
(op-service/testutils), types.LogForStorage (op-chain-ops/crossdomain; deleted upstream),
params.InteropCrossL2InboxAddress (used by op-core/interop/messages — switch to
op-core/predeploys) — ride the §2-style call-site swaps (#20263 family). Derive current sites
by grepping the symbol; listed file snapshots go stale.
op-chain-ops/script is the Foundry-style in-process forge-script executor; op-deployer runs all
deployment/genesis scripts through it. Semantically it is plain L1 EVM — its chain config
activates only Ethereum forks (the OP fork fields are explicitly nil) — so by §"scope" rules it
could be fate 1. But its cheatcode mechanism runs on fork-only EVM hooks that upstream has no
equivalent for: vm.Config.PrecompileOverrides (cheatcode precompiles), vm.Config.CallerOverride
(pranks), vm.Config.NoMaxCodeSize, and the exported state.StateUpdate without which
script/forking.ForkDB cannot implement upstream's state.Database (its Commit takes an
unexported type). "Swap to op-core" does not exist here.
Decision: 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 per the op-reth-test-engine precedent (#20415), or equivalent embedding. Constraints:
op-deployer keeps working without a system-installed foundry (engine version-pinned and shipped
with our tooling); cheatcode surface limited to what our scripts use (derive from the cheatcode
dispatch in op-chain-ops/script); parity-gate against the Go engine on reference deployments
before switching.
No interim module split. Splitting op-chain-ops+op-deployer into their own Go module that
keeps the op-geth replace was considered and rejected: the epic's value only materialises when we
stop maintaining the op-geth fork entirely — any in-repo module still depending on it keeps the
fork alive. (The same lens applies to the superchain-registry repo's ops module, which pins its
own op-geth — outside this repo, flagged to that team.)
This is a hard blocker of #20266 and the longest pole alongside #20415/#21196.
Beyond op-e2e/actions (§ scope fate 2, #21196), op-e2e system tests and op-devstack/sysgo can
run op-geth in-process as the L2 EL: op-e2e/e2eutils/geth.InitL2 behind the
e2eutils/el.InitL2 factory's ELKindOpGeth, and op-devstack/sysgo/l2_el_opgeth.go. After the
go.mod flip these would silently run upstream geth as the L2 EL — wrong OP state transition,
with no compile error to warn us. The in-process L1 geth miner is unaffected (plain Ethereum
semantics are exactly right for it under upstream) and stays.
Plan: make op-reth the only L2 EL kind — delete ELKindOpGeth, geth.InitL2,
l2_el_opgeth.go, and the geth-only knob plumbing (GethOptions); resolve each geth-pinned test
case by case (migrate or delete with rationale; derive the pin list by grepping ELKindOpGeth /
GethOption). The pre-Regolith tests (#21451: op-reth returns IsSystemTx as a string on
pre-Regolith blocks) fold into this — likely resolved by deleting them and requiring
bedrock/regolith co-activation. Precedent: #21182 (acceptance tests op-reth-only for Karst+).
cmd/check-* per-fork checkers verify fork activation on live chains using fork symbols
(types.DepositTx, receipt DepositReceiptVersion, L1-cost funcs, types.CalcDAFootprint, geth
eip1559 Jovian helpers). Plan: delete the checkers for long-activated forks (proposal:
pre-Holocene — team confirms the list on the issue) and swap the survivors to op-core, adding
the one missing symbol family with differential tests — DA-footprint calculation
(types.CalcDAFootprint) to op-core/fees; the eip1559 uses are import swaps onto the existing
op-core/eip1559 helpers (§8).
op-wheel splits cleanly: the engine commands drive a live EL over Engine-API RPC —
upstream-safe, stay. The cheat commands do offline surgery on op-geth datadirs (raw
rawdb/core/state, fork-only StateDB.OpenStorageTrie, in-process block processing) — not
upstream-portable, and pointless in an op-reth fleet whose datadirs aren't geth-format. Plan:
delete op-wheel/cheat (op-simulate precedent, #21282).
A scheduled CI job makes decoupling progress measurable and regression-proof: in a throwaway
checkout it drops the go-ethereum replace, pins upstream at the current op-geth base version,
runs go mod tidy + go build/go vet, and compares the set of failing packages against a
committed baseline. A failure outside the baseline is a regression (a fork-API use crept into a
clean package) and fails the job; a baselined package turning green shrinks the baseline — it
only ever tightens. #20266 becomes executable exactly when the baseline is empty, and the flip PR
deletes the job. The op-core differential tests are expected baseline members until then (they
import op-geth-only symbols by design).
| Area | Target | Status |
|---|---|---|
| ProtocolVersions mechanism (§0) | deleted end-to-end | done |
DepositTx / PostExecTx types + helpers (§1, §2) |
op-core/types |
done (#20262); call-site swaps open (#20269, #20263) |
OP Receipt (§3) |
op-core/types.Receipt |
done; wiring rides §11 (#20264) |
| L1-cost / operator-fee math (§4) | op-core/fees |
done (#20261); op-batcher site rides §11 |
OptimismConfig / standalone ChainConfig / GethChainConfig (§5) |
op-core/params |
done (#20260); swaps + HardforkConfig open (#20270) |
| Superchain registry + loaders (§6, §7) | op-core/superchain + op-core/params |
done (#20267, #21487) |
| eip1559 Holocene/Jovian helpers (§8) | op-core/eip1559 |
done for op-node; EncodeOptimismExtraData/DecodeHoloceneExtraData pending (§13) |
PayloadID, Engine-API types (§9, §10) |
upstream / op-service | no change needed |
OP-aware eth client: class-partitioned accessors, optypes.Receipt returns, op-batcher + txinclude migration (§11) |
op-service/sources / apis.EthClient |
open (#20264) |
| op-proposer / op-challenger / op-supernode / cannon (§12) | — | audited, no work |
Test migration: wait.go split, L2 call sites, L2Client type, sysgo audit, delete op-e2e/opgeth (§13) |
apis.EthClient + header-only variants |
open (#20265 + subs) |
| op-e2e/actions in-process EL | op-reth-test-engine subprocess |
open (#20415 Rust, #21196 Go) |
| Genesis tooling (§14) | upstream geth as library + opparams |
open (#21281) |
| op-simulate / op-run-block (§14) | delete | done (#21282) |
| op-sync-tester PayloadID hash | OP-aware Id() reimplementation |
open (#21525) |
| Log context extensions (§15) | owned op-service/log layer (alias sweep → owned interface) |
open |
RPC recorder hooks + JsonError (§15) |
client wrappers + server-side interception | open |
| One-off fork symbols (§15) | per-symbol swaps, ride #20263 family | open |
op-chain-ops/script + op-deployer (§16) |
Rust script engine (foundry crates) | open |
| In-process op-geth L2 EL in system tests + sysgo (§17) | op-reth-only; folds #21451 | open |
cmd/check-* (§18) |
delete pre-Holocene; swap survivors to op-core | open |
op-wheel/cheat (§18) |
delete (engine stays) |
done (#21747) |
| CI ratchet (§19) | scheduled upstream-build job + tightening baseline | open |
Final cutover: flip replace, shed GethChainConfig OP fields, delete differential tests + §2 scaffolding |
go.mod | open (#20266) |