Skip to content

Instantly share code, notes, and snippets.

@ajsutton
Created July 30, 2026 22:04
Show Gist options
  • Select an option

  • Save ajsutton/d52d1dd89af037f5c89c7a27d6f18c03 to your computer and use it in GitHub Desktop.

Select an option

Save ajsutton/d52d1dd89af037f5c89c7a27d6f18c03 to your computer and use it in GitHub Desktop.
8,10d7
< import { Types } from "src/libraries/Types.sol";
< import { Hashing } from "src/libraries/Hashing.sol";
< import { RLPReader } from "src/libraries/rlp/RLPReader.sol";
24a22
> import { Types } from "src/libraries/Types.sol";
42d39
< L2BlockNumberChallenged,
49,50d45
< BlockNumberMatches,
< InvalidHeaderRLP,
54d48
< InvalidOutputRootProof,
62a57,58
> import { Hashing } from "src/libraries/Hashing.sol";
> import { Encoding } from "src/libraries/Encoding.sol";
71,73c67,74
< /// @title FaultDisputeGame
< /// @notice An implementation of the `IFaultDisputeGame` interface.
< contract FaultDisputeGame is Clone, ISemver {
---
> /// @title SuperFaultDisputeGame
> /// @notice An implementation of the `IFaultDisputeGame` interface for interop.
> contract SuperFaultDisputeGame is Clone, ISemver {
> /// @dev Error to prevent initializing a dispute game with the reserved invalid root claim.
> error SuperFaultDisputeGameInvalidRootClaim();
> /// @dev Error to prevent passing a chainId to this dispute game
> error NoChainIdNeeded();
>
97c98
< /// @notice Parameters for creating a new FaultDisputeGame. We place this into a struct to
---
> /// @notice Parameters for creating a new SuperFaultDisputeGame. We place this into a struct to
126a128,130
> /// @notice The constant for invalid root claims.
> bytes32 internal constant INVALID_ROOT_CLAIM = keccak256("invalid");
>
144,147c148,154
< /// @notice The index of the block number in the RLP-encoded block header.
< /// @dev Consensus encoding reference:
< /// https://github.com/paradigmxyz/reth/blob/5f82993c23164ce8ccdc7bf3ae5085205383a5c8/crates/primitives/src/header.rs#L368
< uint256 internal constant HEADER_BLOCK_NUMBER_INDEX = 8;
---
> /// @notice The byte count of the data before the extra data in the CWIA payload.
> /// Expected length: 88 bytes
> /// - 20 bytes: creator address
> /// - 32 bytes: root claim
> /// - 32 bytes: l1 head
> /// - 4 bytes: game type
> uint256 internal constant PRE_EXTRA_DATA_BYTE_COUNT = 88;
149,150c156,166
< /// @notice Semantic version.
< /// @custom:semver 2.4.2
---
> /// @notice The byte count of the game implementation args for this contract.
> /// Expected length: 124 bytes
> /// - 32 bytes: absolutePrestate
> /// - 20 bytes: vm address
> /// - 20 bytes: anchorStateRegistry address
> /// - 20 bytes: weth address
> /// - 32 bytes: l2ChainId (unused)
> uint256 internal constant GAME_IMPL_ARGS_BYTE_COUNT = 124;
>
> /// @notice Semantic version.
> /// @custom:semver 0.8.0
152c168
< return "2.4.2";
---
> return "0.8.0";
167,173d182
< /// @notice Flag for whether or not the L2 block number claim has been invalidated via `challengeRootL2Block`.
< bool public l2BlockNumberChallenged;
<
< /// @notice The challenger of the L2 block number claim. Should always be `address(0)` if `l2BlockNumberChallenged`
< /// is `false`. Should be the address of the challenger if `l2BlockNumberChallenged` is `true`.
< address public l2BlockNumberChallenger;
<
193c202
< Proposal public startingOutputRoot;
---
> Proposal public startingProposal;
207c216
< /// @param _params Parameters for creating a new FaultDisputeGame.
---
> /// @param _params Parameters for creating a new SuperFaultDisputeGame.
252c261
< // - An output root cannot be proposed at or before the starting block number.
---
> // - A super root cannot be proposed at or before the starting l2SequenceNumber.
261,262c270,271
< // output proposal to be created.
< if (msg.data.length != expectedInitCallDataLength()) revert BadExtraData();
---
> // super proposal to be created.
> if (!_verifyInitCallDataLength()) revert BadExtraData();
263a273,280
> // Sanity check to prevent initializing right away with an invalid claim state that is used as convention
> // Should be impossible to find a valid Super preimage of INVALID_ROOT_CLAIM
> if (rootClaim().raw() == INVALID_ROOT_CLAIM) revert SuperFaultDisputeGameInvalidRootClaim();
>
> // Revert if the super root proof in extraData does not match the root claim.
> Types.SuperRootProof memory superRootProof = Encoding.decodeSuperRootProof(extraData());
> if (Hashing.hashSuperRootProof(superRootProof) != rootClaim().raw()) revert BadExtraData();
>
265c282
< (Hash root, uint256 rootBlockNumber) = anchorStateRegistry().getAnchorRoot();
---
> (Hash root, uint256 rootL2SequenceNumber) = anchorStateRegistry().getAnchorRoot();
270,276c287,288
< // Set the starting proposal.
< startingOutputRoot = Proposal({ l2SequenceNumber: rootBlockNumber, root: root });
<
< // Do not allow the game to be initialized if the root claim corresponds to a block at or before the
< // configured starting block number.
< if (l2BlockNumber() <= rootBlockNumber) revert UnexpectedRootClaim(rootClaim());
< if (l2BlockNumber() > type(uint64).max) revert UnexpectedRootClaim(rootClaim());
---
> // Set the starting Proposal.
> startingProposal = Proposal({ l2SequenceNumber: rootL2SequenceNumber, root: root });
278d289
< // Validate parameters that require access to the VM.
279a291,293
> // Runtime check was added instead of changing the ABI since the contract is already
> // deployed in production. We perform the same check within the PreimageOracle for the
> // benefit of developers but also perform this check here defensively.
285c299
< uint256 maxGameDepthExtension = uint256(CLOCK_EXTENSION.raw()) + uint64(vm().oracle().challengePeriod());
---
> uint256 maxGameDepthExtension = uint256(CLOCK_EXTENSION.raw()) + uint256(vm().oracle().challengePeriod());
293a308,314
> // The l2ChainId must still exist, yet zero-valued, in the game args for legacy reasons.
> if (_l2ChainId() != 0) revert NoChainIdNeeded();
>
> // Do not allow the game to be initialized if the root claim corresponds to a l2 sequence number (timestamp) at
> // or before the configured starting sequence number.
> if (l2SequenceNumber() <= rootL2SequenceNumber) revert UnexpectedRootClaim(rootClaim());
>
322,328c343,369
< /// @notice Returns the expected calldata length for the initialize method
< function expectedInitCallDataLength() internal pure returns (uint256) {
< // Expected length: 6 bytes + immutable args byte count
< // - 4 bytes: selector
< // - 2 bytes: CWIA length prefix
< // - n bytes: Immutable args data
< return 6 + immutableArgsByteCount();
---
> /// @notice Validates the expected length of msg.data for the initialize() call.
> /// @dev This function must only be called by initialize().
> ///
> /// Expected msg.data structure:
> /// ┌────────────────────────────────────────────────────────────────────┐
> /// │ 4 bytes │ Function selector (initialize()) │
> /// │ 2 bytes │ CWIA length prefix │
> /// │===================│ ============ pre extra data ================== │
> /// │ 20 bytes │ creator address │
> /// │ 32 bytes │ root claim │
> /// │ 32 bytes │ l1 head │
> /// │ 4 bytes │ game type │
> /// │===================│ ============ extra data ====================== │
> /// │ 1 byte │ super version │
> /// │ 8 bytes │ super timestamp (seqnr) │
> /// │ n * (32+32) bytes │ (chainId, outputRoot) tuples │
> /// │===================│ ============ end extra data ================== │
> /// │ 124 bytes │ game impl args │
> /// └────────────────────────────────────────────────────────────────────┘
> function _verifyInitCallDataLength() internal pure returns (bool) {
> uint256 preExtraDataLen = 4 + 2 + PRE_EXTRA_DATA_BYTE_COUNT;
> uint256 minLen = preExtraDataLen + 9 + GAME_IMPL_ARGS_BYTE_COUNT;
> if (msg.data.length < minLen) return false;
>
> uint256 superLen = msg.data.length - preExtraDataLen - GAME_IMPL_ARGS_BYTE_COUNT;
> uint256 rem = superLen - 9;
> return rem != 0 && rem % 64 == 0;
331,344c372,378
< /// @notice Returns the byte count of the immutable args for this contract.
< function immutableArgsByteCount() internal pure virtual returns (uint256) {
< // Expected length: 244 bytes
< // - 20 bytes: creator address
< // - 32 bytes: root claim
< // - 32 bytes: l1 head
< // - 4 bytes: game type
< // - 32 bytes: extraData
< // - 32 bytes: absolutePrestate
< // - 20 bytes: vm address
< // - 20 bytes: anchorStateRegistry address
< // - 20 bytes: weth address
< // - 32 bytes: l2ChainId
< return 244;
---
> /// @notice Returns the length of the super extra data in the initialize() call.
> /// @dev Precondition: msg.data has a valid length.
> function _extraDataByteCount() internal pure returns (uint256) {
> // The CWIA runtime appends the immutable args and a 2-byte length suffix to every call;
> // strip the original calldata and suffix so offsets stay correct for functions with params.
> uint256 immutableArgsLength = msg.data.length - _getImmutableArgsOffset() - 2;
> return immutableArgsLength - PRE_EXTRA_DATA_BYTE_COUNT - GAME_IMPL_ARGS_BYTE_COUNT;
471,474d504
< // INVARIANT: No moves against the root claim can be made after it has been challenged with
< // `challengeRootL2Block`.`
< if (l2BlockNumberChallenged && _challengeIndex == 0) revert L2BlockNumberChallenged();
<
600,613c630
< // Load the disputed proposal's L2 block number as a big-endian uint64 in the
< // high order 8 bytes of the word.
<
< // We add the index at depth + 1 to the starting block number to get the disputed L2
< // block number.
< uint256 l2Number = startingOutputRoot.l2SequenceNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1;
<
< // Choose the minimum between the `l2BlockNumber` claim and the bisected-to L2 block number.
< l2Number = l2Number < l2BlockNumber() ? l2Number : l2BlockNumber();
<
< oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2Number << 0xC0), 8, _partOffset);
< } else if (_ident == LocalPreimageKey.CHAIN_ID) {
< // Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word.
< oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2ChainId() << 0xC0), 8, _partOffset);
---
> oracle.loadLocalData(_ident, uuid.raw(), bytes32(l2SequenceNumber() << 0xC0), 8, _partOffset);
631,636c648
< /// @notice The l2BlockNumber of the disputed output root in the `L2OutputOracle`.
< function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) {
< l2BlockNumber_ = _getArgUint256(88);
< }
<
< /// @notice The l2SequenceNumber of the disputed output root in the `L2OutputOracle` (in this case - block number).
---
> /// @notice The l2SequenceNumber (timestamp) of the disputed super root in game root claim.
638c650
< l2SequenceNumber_ = l2BlockNumber();
---
> l2SequenceNumber_ = _getArgUint64(PRE_EXTRA_DATA_BYTE_COUNT + 1);
641,643c653,655
< /// @notice Only the starting block number of the game.
< function startingBlockNumber() external view returns (uint256 startingBlockNumber_) {
< startingBlockNumber_ = startingOutputRoot.l2SequenceNumber;
---
> /// @notice Only the starting sequence number (timestamp) of the game.
> function startingSequenceNumber() external view returns (uint256 startingSequenceNumber_) {
> startingSequenceNumber_ = startingProposal.l2SequenceNumber;
646c658
< /// @notice Starting output root and block number of the game.
---
> /// @notice Starting super root of the game.
648c660
< startingRootHash_ = startingOutputRoot.root;
---
> startingRootHash_ = startingProposal.root;
651,698d662
< /// @notice Challenges the root L2 block number by providing the preimage of the output root and the L2 block header
< /// and showing that the committed L2 block number is incorrect relative to the claimed L2 block number.
< /// @param _outputRootProof The output root proof.
< /// @param _headerRLP The RLP-encoded L2 block header.
< function challengeRootL2Block(
< Types.OutputRootProof calldata _outputRootProof,
< bytes calldata _headerRLP
< )
< external
< {
< // INVARIANT: Moves cannot be made unless the game is currently in progress.
< if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
<
< // The root L2 block claim can only be challenged once.
< if (l2BlockNumberChallenged) revert L2BlockNumberChallenged();
<
< // Verify the output root preimage.
< if (Hashing.hashOutputRootProof(_outputRootProof) != rootClaim().raw()) revert InvalidOutputRootProof();
<
< // Verify the block hash preimage.
< if (keccak256(_headerRLP) != _outputRootProof.latestBlockhash) revert InvalidHeaderRLP();
<
< // Decode the header RLP to find the number of the block. In the consensus encoding, the timestamp
< // is the 9th element in the list that represents the block header.
< RLPReader.RLPItem[] memory headerContents = RLPReader.readList(RLPReader.toRLPItem(_headerRLP));
< bytes memory rawBlockNumber = RLPReader.readBytes(headerContents[HEADER_BLOCK_NUMBER_INDEX]);
<
< // Sanity check the block number string length.
< if (rawBlockNumber.length > 32) revert InvalidHeaderRLP();
<
< // Convert the raw, left-aligned block number to a uint256 by aligning it as a big-endian
< // number in the low-order bytes of a 32-byte word.
< //
< // SAFETY: The length of `rawBlockNumber` is checked above to ensure it is at most 32 bytes.
< uint256 blockNumber;
< assembly {
< blockNumber := shr(shl(0x03, sub(0x20, mload(rawBlockNumber))), mload(add(rawBlockNumber, 0x20)))
< }
<
< // Ensure the block number does not match the block number claimed in the dispute game.
< if (blockNumber == l2BlockNumber()) revert BlockNumberMatches();
<
< // Issue a special counter to the root claim. This counter will always win the root claim subgame, and receive
< // the bond from the root claimant.
< l2BlockNumberChallenger = msg.sender;
< l2BlockNumberChallenged = true;
< }
<
818,827c782,784
< if (_claimIndex == 0 && l2BlockNumberChallenged) {
< // Special case: If the root claim has been challenged with the `challengeRootL2Block` function,
< // the bond is always paid out to the issuer of that challenge.
< address challenger = l2BlockNumberChallenger;
< _distributeBond(challenger, subgameRootClaim);
< subgameRootClaim.counteredBy = challenger;
< } else {
< // If the parent was not successfully countered, pay out the parent's bond to the claimant.
< // If the parent was successfully countered, pay out the parent's bond to the challenger.
< _distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim);
---
> // If the parent was not successfully countered, pay out the parent's bond to the claimant.
> // If the parent was successfully countered, pay out the parent's bond to the challenger.
> _distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim);
829,832c786,788
< // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
< // resolveClaim will not need to traverse this subgame.
< subgameRootClaim.counteredBy = countered;
< }
---
> // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
> // resolveClaim will not need to traverse this subgame.
> subgameRootClaim.counteredBy = countered;
850,855c806,818
< /// @notice Getter for the root claim for a given L2 chain ID.
< /// @param _chainId The L2 chain ID to get the root claim for.
< /// @return rootClaim_ The root claim of the DisputeGame.
< function rootClaimByChainId(uint256 _chainId) public pure returns (Claim rootClaim_) {
< if (_chainId != l2ChainId()) revert UnknownChainId();
< rootClaim_ = rootClaim();
---
> /// @notice Returns the output root in the root claim for the specified L2 chain ID.
> /// @param _chainId The L2 chain ID to get the output root claim for.
> /// @return outputRootClaim_ The output root claim for the specified L2 chain ID.
> function rootClaimByChainId(uint256 _chainId) public pure returns (Claim outputRootClaim_) {
> Types.SuperRootProof memory superRootProof = Encoding.decodeSuperRootProof(extraData());
> Types.OutputRootWithChainId[] memory outputRoots = superRootProof.outputRoots;
>
> for (uint256 i = 0; i < outputRoots.length; i++) {
> if (outputRoots[i].chainId == _chainId) {
> return Claim.wrap(outputRoots[i].root);
> }
> }
> revert UnknownChainId();
873c836
< /// @dev `clones-with-immutable-args` argument #5
---
> /// @dev `clones-with-immutable-args` argument #4
876,878c839
< // The extra data starts at the second word within the cwia calldata and
< // is 32 bytes long.
< extraData_ = _getArgBytes(88, 32);
---
> extraData_ = _getArgBytes(PRE_EXTRA_DATA_BYTE_COUNT, _extraDataByteCount());
885c846
< absolutePrestate_ = Claim.wrap(_getArgBytes32(120));
---
> absolutePrestate_ = Claim.wrap(_getArgBytes32(PRE_EXTRA_DATA_BYTE_COUNT + _extraDataByteCount()));
892c853
< vm_ = IBigStepper(_getArgAddress(152));
---
> vm_ = IBigStepper(_getArgAddress(PRE_EXTRA_DATA_BYTE_COUNT + _extraDataByteCount() + 32));
899c860
< registry_ = IAnchorStateRegistry(_getArgAddress(172));
---
> registry_ = IAnchorStateRegistry(_getArgAddress(PRE_EXTRA_DATA_BYTE_COUNT + _extraDataByteCount() + 52));
906c867
< weth_ = IDelayedWETH(payable(_getArgAddress(192)));
---
> weth_ = IDelayedWETH(payable(_getArgAddress(PRE_EXTRA_DATA_BYTE_COUNT + _extraDataByteCount() + 72)));
912,913c873,874
< function l2ChainId() public pure returns (uint256 l2ChainId_) {
< l2ChainId_ = _getArgUint256(212);
---
> function _l2ChainId() internal pure returns (uint256 l2ChainId_) {
> l2ChainId_ = _getArgUint256(PRE_EXTRA_DATA_BYTE_COUNT + _extraDataByteCount() + 92);
1026a988,996
> // We won't close the game if the system is currently paused. Paused games are temporarily
> // invalid which would cause the game to go into refund mode and potentially cause some
> // confusion for honest challengers. By blocking the game from being closed while the
> // system is paused, the game will only go into refund mode if it ends up being explicitly
> // invalidated in the AnchorStateRegistry.
> if (anchorStateRegistry().paused()) {
> revert GamePaused();
> }
>
1035,1044d1004
< }
<
< // We won't close the game if the system is currently paused. Paused games are temporarily
< // invalid which would cause the game to go into refund mode and potentially cause some
< // confusion for honest challengers. By blocking the game from being closed while the
< // system is paused, the game will only go into refund mode if it ends up being explicitly
< // invalidated in the AnchorStateRegistry. If the game has already been closed and a refund
< // mode has been selected, we'll already have returned and we won't hit this revert.
< if (anchorStateRegistry().paused()) {
< revert GamePaused();
1169c1129
< // 1. Signal that the VM panicked or resulted in an invalid transition if the disputed output root
---
> // 1. Signal that the VM panicked or resulted in an invalid transition if the disputed super root
1271,1272c1231,1232
< // If this is an attack on the first output root (the block directly after the starting
< // block number), the starting claim nor position exists in the tree. We leave these as
---
> // If this is an attack on the first super root (the block directly after the starting
> // timestamp), the starting claim nor position exists in the tree. We leave these as
1278c1238
< startingClaim_ = Claim.wrap(startingOutputRoot.root.raw());
---
> startingClaim_ = Claim.wrap(startingProposal.root.raw());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment