Migration applies to Mainnet only, and it has not happened yet. The legacy L1 mainnet, chain ID
1875, is live and producing blocks. Whitechain Sepolia started from its own genesis and carries no legacy state. If you are a user or an integrator asking what you have to do, read Migration to L2 instead: this page is the mechanics, not the checklist.The strategy
The migration is a freeze and dump. L1 block production stops at a snapshot block. The full state trie at that block is exported, the export is transformed into an OP Stack genesis allocation, and the L2 boots from it.
Final state migrates. History does not, because the L2 starts a new chain whose block 0 holds that state rather than replaying the blocks that produced it.
The three allocations
Three files in this procedure are all called an allocation at some point, and confusing them is the most common way to produce a chain that will not boot. They are kept in separate files with distinct names.
The final
genesis.json and rollup.json are emitted together by one op-node genesis l2 run. They are a pair: op-node recomputes the L2 block 0 hash from the allocation and writes it into rollup.json, so a rollup.json from an earlier run paired with a later genesis.json fails at boot with a genesis hash mismatch.
Why the merge step needs its own tool
op-node genesis l2 --l2-allocs reads its input as an OP Stack allocations document. Each key is an address and each value is that account, with balance and nonce as hex quantities, code as 0x prefixed bytes, and every storage key and value a full 32 byte 0x prefixed hash. It also replaces the allocation rather than merging into it, so the predeploys have to be inside the file it is given.
A geth dump satisfies neither requirement.
Upstream OP Stack tooling ships no converter for this.
scripts/dump-to-alloc.mjs in the migration repository does exactly those two jobs, the format conversion and the merge, in a single streaming pass. Only the base allocation is held in memory, so peak memory does not grow with the size of the dump. Everything before and after that step is stock op-deployer and op-node.
Address collisions
An address defined by both the base allocation and the legacy state dump is a collision, and the dump account wins. The one exception is a dump account that is itself empty and gets pruned, which leaves the base entry standing. Two groups are exposed, and they carry different risk.dump-to-alloc.mjs reports every collision on stderr as it happens, repeats the full list at the end of the run, and states explicitly when there were none. The authoritative list of addresses at risk for a given deployment is the base allocation itself, read with jq -r '.alloc | keys[]' genesis.base.json.
Empty accounts, meaning zero balance, zero nonce, no code and no storage, are pruned by default, so account counts between the dump and the allocation differ by that number. --no-prune-empty keeps them.
The phases
The L1 is read only from the moment production stops until the L2 is serving, so everything in phase 2 is time boxed.
What each step produces:
Two details decide whether the boot succeeds. The L1 node that is dumped must be a full, non pruned node synced with
--cache.preimages, stopped gracefully so the trie cache is flushed, and the dump header state root must equal the snapshot block state root. The execution client volumes must be empty before the first start, because booting on top of an old database is the most common cause of a genesis hash mismatch.
Where the tooling lives
The migration repository carries two runbooks. The default one builds the genesis with
op-deployer, dump-to-alloc.mjs and op-node genesis l2, and is used for any migration that carries real L1 state. The second uses upstream OP Stack tooling only, and applies when the allocation is small and hand authored rather than dumped from a chain.

