Upgrade 20: Preparing the Fault Proof System for Interop

Upgrade 20 delivers a major upgrade to the OP Stack fault proof system to lay the groundwork for supporting interop. Proposals move from the single-chain output roots to the multi-chain capable super roots. For U20 each chain will continue to use separate dispute games, but it will be ready to support the cross-chain interop future. Doing the rollout incrementally means the fault proofs changes get audited, deployed and soaked on real mainnet chains separately to the protocol changes that enable interop. Smaller releases, lower risk.
The reason interop needs a different format at all is that once chains can send each other messages, you can't honestly prove one of them on its own. Its blocks depend on what its peers did in the same second, so validating them means deriving those chains too and then checking the messages between them. That's the work a super root game can express and an output root game can't.
What is a super root?
A super root is a keccak hash of a version byte, a big-endian uint64 timestamp, and a list of (chainID, outputRoot) pairs sorted ascending by chain ID. That's it. With one chain it's 73 bytes before hashing. The format is deliberately variable length so it provides a snapshot of the state across any number of chains at a specific timestamp. For chains that don't produce a block every second, the included output root must be from the latest block at or before the super root's timestamp.

Timestamps, not block numbers
The game's L2 sequence number is now the super root timestamp. l2SequenceNumber() returns it, the anchor state registry compares anchors on it, and a game can't be created at or before the anchor's timestamp.
This falls out of the format. Chains in a dependency set don't share a block height, and with different block times they don't even produce a block every second. This is the one change that affects how to prove withdrawals. Previously to prove your withdrawal, you would need a dispute game with a L2 sequence number greater than or equal to the number of the block the initiating transaction was included in on L2. Now with super roots, you need a dispute game with a L2 sequence number greater than or equal to the timestamp of the block. viem 2.51.0 and later handles it for you.
The trace is the point
The natural instinct with interop is to add more bisection: bisect the timestamp, then bisect which chain, then bisect blocks, then bisect Cannon. Four split depths, sub-games inside sub-games, and a lot of very hard reasoning about which level you're at.
That's the wrong lever. The bisection is only search. What has to be exactly defined is the trace, and there are still only two of them: a state transition per step above the split depth, and a MIPS instruction per step below it. So instead of adding tree structure, the top-half trace becomes a slightly richer state machine.
With super dispute games, there are 128 steps per timestamp. Step 0 is the super root at timestamp T. With N chains in the super root, steps 1 to N each derive one chain's next block optimistically, ignoring its interop dependencies entirely (even after interop is enabled in a future hard fork), and append that block's hash and output root to a pending list. Chains are processed in ascending chain ID order. Steps N+1 to 127 are no-ops that bump a counter and change nothing else, which is what makes the step count independent of the number of chains. The step out of 127 lands back on a super root, at timestamp T+1, and that one is the consolidation step: check every cross-chain message, replace any block that depended on an invalid message with a deposits-only block, and hash what's left into the next super root.
The intermediate states are their own type. TransitionState is a version byte 255 followed by RLP of the previous super root, the pending optimistic blocks, and the step number. Below the split depth the fault proof program executes one of these steps rather than one block. This ensures that each execution of the fault proof program still only executes a single block keeping execution times down.
Everything else is the dispute game you already have. Same bisection, same bonds, same clocks, same resolution rules, and slightly less contract code than before.
Running out of L1 data
FaultDisputeGame has an ugly corner. Trace extension repeats the final claim to fill out the rest of the trace, which is right for a proposal that legitimately reaches its claimed block early. But it also fired when a proposer claimed block 10 while only block 5's batch data was on L1. The program derived to block 5, repeated it, and the trace happily agreed with a root claim of block 5's output root sitting at a position that claimed block 10. Those two cases were indistinguishable.
The fix was challengeRootL2Block: a separate entry point letting anyone invalidate the root claim by proving the real block number, with its own storage flag and its own special case in resolution. It worked, but it was a second way to win a game that had to be reasoned about everywhere.
Super games have a simpler solution. When a step needs a chain's next block and the L1 data required to derive it isn't at or below the game's L1 head, the transition produces keccak256("invalid") instead of repeating anything. Invalid transitions to invalid, forever. initialize rejects keccak256("invalid") as a root claim outright, so a proposal that runs past the end of available L1 data can only ever be countered: the honest challenger bisects to the step where the trace turned invalid and wins there, through the ordinary game. Trace extension survives for its original job, repeating the super root once the claimed timestamp is reached.

One chain
With a single chain the trace is nearly empty. Step 1 appends the chain's optimistic block. Steps 2 through 127 do nothing at all. The consolidation step runs with no cross-chain messages to check, so it can't invalidate anything, the optimistic block stands unchanged, and the result is a super root holding one output root. The fixed number of steps per timestamp exists so that the trace layout doesn't change when a chain joins or leaves the dependency set.
Withdrawals still prove output roots
OptimismPortal2.proveWithdrawalTransaction is unchanged. It still takes a Types.OutputRootProof and a Merkle proof against the L2ToL1MessagePasser storage root, and it still compares hashOutputRootProof to a claim.
This works because the portal fetches the chain specific output root with rootClaimByChainId(systemConfig.l2ChainId()) rather than using the root claim directly. So while the root claim is a super root, withdrawals continue to be proven against output roots for simplicity and backwards compatibility.
Other changes
Upgrade 20 also includes a collection of other contract changes to simplify the system. ProtocolVersions is gone entirely. SystemConfig loses batchInbox(), which held a second copy of an address the OP Stack already reads from the rollup config, and the long-deprecated setGasConfig().
Chain operators need to update op-proposer, op-challenger and op-dispute-mon configuration to work with super dispute games. Node operators don't need to take any action. The notice page has the exact steps.
What's next?
The road to interop continues on from here with a future hard fork actually enabling cross-chain messages. With that, the fault proof system for chains in the interop dependency set will be merged into a shared DisputeGameFactory with multiple chains in the super roots and a new prestate that includes support for the interop hard fork - all using the same super dispute game that is shipping now with upgrade 20.
Authored by
Adrian Sutton
