From b52c5f6051d95d1de1e51b2ece51913daeb8fef3 Mon Sep 17 00:00:00 2001 From: billxu Date: Thu, 2 May 2024 18:18:22 +0800 Subject: [PATCH 1/2] Copy FaultDisputeGame2.sol and FaultDisputGame2.t.sol and FaultDisputeActors2.sol --- .../src/dispute/FaultDisputeGame2.sol | 945 ++++++++ .../test/actors/FaultDisputeActors2.sol | 418 ++++ .../test/dispute/FaultDisputeGame2.t.sol | 2045 +++++++++++++++++ 3 files changed, 3408 insertions(+) create mode 100644 packages/contracts-bedrock/src/dispute/FaultDisputeGame2.sol create mode 100644 packages/contracts-bedrock/test/actors/FaultDisputeActors2.sol create mode 100644 packages/contracts-bedrock/test/dispute/FaultDisputeGame2.t.sol diff --git a/packages/contracts-bedrock/src/dispute/FaultDisputeGame2.sol b/packages/contracts-bedrock/src/dispute/FaultDisputeGame2.sol new file mode 100644 index 000000000000..24f46ed1d9fb --- /dev/null +++ b/packages/contracts-bedrock/src/dispute/FaultDisputeGame2.sol @@ -0,0 +1,945 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { FixedPointMathLib } from "@solady/utils/FixedPointMathLib.sol"; + +import { IDelayedWETH } from "src/dispute/interfaces/IDelayedWETH.sol"; +import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol"; +import { IFaultDisputeGame } from "src/dispute/interfaces/IFaultDisputeGame.sol"; +import { IInitializable } from "src/dispute/interfaces/IInitializable.sol"; +import { IBigStepper, IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol"; +import { IAnchorStateRegistry } from "src/dispute/interfaces/IAnchorStateRegistry.sol"; + +import { Clone } from "@solady/utils/Clone.sol"; +import { Types } from "src/libraries/Types.sol"; +import { ISemver } from "src/universal/ISemver.sol"; + +import "src/dispute/lib/Types.sol"; +import "src/dispute/lib/Errors.sol"; + +/// @title FaultDisputeGame +/// @notice An implementation of the `IFaultDisputeGame` interface. +contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { + //////////////////////////////////////////////////////////////// + // State Vars // + //////////////////////////////////////////////////////////////// + + /// @notice The absolute prestate of the instruction trace. This is a constant that is defined + /// by the program that is being used to execute the trace. + Claim internal immutable ABSOLUTE_PRESTATE; + + /// @notice The max depth of the game. + uint256 internal immutable MAX_GAME_DEPTH; + + /// @notice The max depth of the output bisection portion of the position tree. Immediately beneath + /// this depth, execution trace bisection begins. + uint256 internal immutable SPLIT_DEPTH; + + /// @notice The maximum duration that may accumulate on a team's chess clock before they may no longer respond. + Duration internal immutable MAX_CLOCK_DURATION; + + /// @notice An onchain VM that performs single instruction steps on a fault proof program trace. + IBigStepper internal immutable VM; + + /// @notice The game type ID. + GameType internal immutable GAME_TYPE; + + /// @notice WETH contract for holding ETH. + IDelayedWETH internal immutable WETH; + + /// @notice The anchor state registry. + IAnchorStateRegistry internal immutable ANCHOR_STATE_REGISTRY; + + /// @notice The chain ID of the L2 network this contract argues about. + uint256 internal immutable L2_CHAIN_ID; + + /// @notice The duration of the clock extension. Will be doubled if the grandchild is the root claim of an execution + /// trace bisection subgame. + Duration internal immutable CLOCK_EXTENSION; + + /// @notice The global root claim's position is always at gindex 1. + Position internal constant ROOT_POSITION = Position.wrap(1); + + /// @notice Semantic version. + /// @custom:semver 0.18.1 + string public constant version = "0.18.1"; + + /// @notice The starting timestamp of the game + Timestamp public createdAt; + + /// @notice The timestamp of the game's global resolution. + Timestamp public resolvedAt; + + /// @inheritdoc IDisputeGame + GameStatus public status; + + /// @notice Flag for the `initialize` function to prevent re-initialization. + bool internal initialized; + + /// @notice An append-only array of all claims made during the dispute game. + ClaimData[] public claimData; + + /// @notice Credited balances for winning participants. + mapping(address => uint256) public credit; + + /// @notice A mapping to allow for constant-time lookups of existing claims. + mapping(Hash => bool) public claims; + + /// @notice A mapping of subgames rooted at a claim index to other claim indices in the subgame. + mapping(uint256 => uint256[]) public subgames; + + /// @notice A mapping of resolved subgames rooted at a claim index. + mapping(uint256 => bool) public resolvedSubgames; + + /// @notice A mapping of claim indices to resolution checkpoints. + mapping(uint256 => ResolutionCheckpoint) public resolutionCheckpoints; + + /// @notice The latest finalized output root, serving as the anchor for output bisection. + OutputRoot public startingOutputRoot; + + /// @param _gameType The type ID of the game. + /// @param _absolutePrestate The absolute prestate of the instruction trace. + /// @param _maxGameDepth The maximum depth of bisection. + /// @param _splitDepth The final depth of the output bisection portion of the game. + /// @param _clockExtension The clock extension to perform when the remaining duration is less than the extension. + /// @param _maxClockDuration The maximum amount of time that may accumulate on a team's chess clock. + /// @param _vm An onchain VM that performs single instruction steps on an FPP trace. + /// @param _weth WETH contract for holding ETH. + /// @param _anchorStateRegistry The contract that stores the anchor state for each game type. + /// @param _l2ChainId Chain ID of the L2 network this contract argues about. + constructor( + GameType _gameType, + Claim _absolutePrestate, + uint256 _maxGameDepth, + uint256 _splitDepth, + Duration _clockExtension, + Duration _maxClockDuration, + IBigStepper _vm, + IDelayedWETH _weth, + IAnchorStateRegistry _anchorStateRegistry, + uint256 _l2ChainId + ) { + // The max game depth may not be greater than `LibPosition.MAX_POSITION_BITLEN - 1`. + if (_maxGameDepth > LibPosition.MAX_POSITION_BITLEN - 1) revert MaxDepthTooLarge(); + // The split depth cannot be greater than or equal to the max game depth. + if (_splitDepth >= _maxGameDepth) revert InvalidSplitDepth(); + // The clock extension may not be greater than the max clock duration. + if (_clockExtension.raw() > _maxClockDuration.raw()) revert InvalidClockExtension(); + + GAME_TYPE = _gameType; + ABSOLUTE_PRESTATE = _absolutePrestate; + MAX_GAME_DEPTH = _maxGameDepth; + SPLIT_DEPTH = _splitDepth; + CLOCK_EXTENSION = _clockExtension; + MAX_CLOCK_DURATION = _maxClockDuration; + VM = _vm; + WETH = _weth; + ANCHOR_STATE_REGISTRY = _anchorStateRegistry; + L2_CHAIN_ID = _l2ChainId; + } + + /// @inheritdoc IInitializable + function initialize() public payable virtual { + // SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and + // prevent the game from being created. + // + // Implicit assumptions: + // - The `gameStatus` state variable defaults to 0, which is `GameStatus.IN_PROGRESS` + // - The dispute game factory will enforce the required bond to initialize the game. + // + // Explicit checks: + // - The game must not have already been initialized. + // - An output root cannot be proposed at or before the starting block number. + + // INVARIANT: The game must not have already been initialized. + if (initialized) revert AlreadyInitialized(); + + // Grab the latest anchor root. + (Hash root, uint256 rootBlockNumber) = ANCHOR_STATE_REGISTRY.anchors(GAME_TYPE); + + // Should only happen if this is a new game type that hasn't been set up yet. + if (root.raw() == bytes32(0)) revert AnchorRootNotFound(); + + // Set the starting output root. + startingOutputRoot = OutputRoot({ l2BlockNumber: rootBlockNumber, root: root }); + + // Revert if the calldata size is not the expected length. + // + // This is to prevent adding extra or omitting bytes from to `extraData` that result in a different game UUID + // in the factory, but are not used by the game, which would allow for multiple dispute games for the same + // output proposal to be created. + // + // Expected length: 0x7A + // - 0x04 selector + // - 0x14 creator address + // - 0x20 root claim + // - 0x20 l1 head + // - 0x20 extraData + // - 0x02 CWIA bytes + assembly { + if iszero(eq(calldatasize(), 0x7A)) { + // Store the selector for `BadExtraData()` & revert + mstore(0x00, 0x9824bdab) + revert(0x1C, 0x04) + } + } + + // 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()); + + // Set the root claim + claimData.push( + ClaimData({ + parentIndex: type(uint32).max, + counteredBy: address(0), + claimant: gameCreator(), + bond: uint128(msg.value), + claim: rootClaim(), + position: ROOT_POSITION, + clock: LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))) + }) + ); + + // Set the game as initialized. + initialized = true; + + // Deposit the bond. + WETH.deposit{ value: msg.value }(); + + // Set the game's starting timestamp + createdAt = Timestamp.wrap(uint64(block.timestamp)); + } + + //////////////////////////////////////////////////////////////// + // `IFaultDisputeGame` impl // + //////////////////////////////////////////////////////////////// + + /// @inheritdoc IFaultDisputeGame + function step( + uint256 _claimIndex, + bool _isAttack, + bytes calldata _stateData, + bytes calldata _proof + ) + public + virtual + { + // INVARIANT: Steps cannot be made unless the game is currently in progress. + if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); + + // Get the parent. If it does not exist, the call will revert with OOB. + ClaimData storage parent = claimData[_claimIndex]; + + // Pull the parent position out of storage. + Position parentPos = parent.position; + // Determine the position of the step. + Position stepPos = parentPos.move(_isAttack); + + // INVARIANT: A step cannot be made unless the move position is 1 below the `MAX_GAME_DEPTH` + if (stepPos.depth() != MAX_GAME_DEPTH + 1) revert InvalidParent(); + + // Determine the expected pre & post states of the step. + Claim preStateClaim; + ClaimData storage postState; + if (_isAttack) { + // If the step position's index at depth is 0, the prestate is the absolute + // prestate. + // If the step is an attack at a trace index > 0, the prestate exists elsewhere in + // the game state. + // NOTE: We localize the `indexAtDepth` for the current execution trace subgame by finding + // the remainder of the index at depth divided by 2 ** (MAX_GAME_DEPTH - SPLIT_DEPTH), + // which is the number of leaves in each execution trace subgame. This is so that we can + // determine whether or not the step position is represents the `ABSOLUTE_PRESTATE`. + preStateClaim = (stepPos.indexAtDepth() % (1 << (MAX_GAME_DEPTH - SPLIT_DEPTH))) == 0 + ? ABSOLUTE_PRESTATE + : _findTraceAncestor(Position.wrap(parentPos.raw() - 1), parent.parentIndex, false).claim; + // For all attacks, the poststate is the parent claim. + postState = parent; + } else { + // If the step is a defense, the poststate exists elsewhere in the game state, + // and the parent claim is the expected pre-state. + preStateClaim = parent.claim; + postState = _findTraceAncestor(Position.wrap(parentPos.raw() + 1), parent.parentIndex, false); + } + + // INVARIANT: The prestate is always invalid if the passed `_stateData` is not the + // preimage of the prestate claim hash. + // We ignore the highest order byte of the digest because it is used to + // indicate the VM Status and is added after the digest is computed. + if (keccak256(_stateData) << 8 != preStateClaim.raw() << 8) revert InvalidPrestate(); + + // Compute the local preimage context for the step. + Hash uuid = _findLocalContext(_claimIndex); + + // INVARIANT: If a step is an attack, the poststate is valid if the step produces + // the same poststate hash as the parent claim's value. + // If a step is a defense: + // 1. If the parent claim and the found post state agree with each other + // (depth diff % 2 == 0), the step is valid if it produces the same + // state hash as the post state's claim. + // 2. If the parent claim and the found post state disagree with each other + // (depth diff % 2 != 0), the parent cannot be countered unless the step + // produces the same state hash as `postState.claim`. + // SAFETY: While the `attack` path does not need an extra check for the post + // state's depth in relation to the parent, we don't need another + // branch because (n - n) % 2 == 0. + bool validStep = VM.step(_stateData, _proof, uuid.raw()) == postState.claim.raw(); + bool parentPostAgree = (parentPos.depth() - postState.position.depth()) % 2 == 0; + if (parentPostAgree == validStep) revert ValidStep(); + + // INVARIANT: A step cannot be made against a claim for a second time. + if (parent.counteredBy != address(0)) revert DuplicateStep(); + + // Set the parent claim as countered. We do not need to append a new claim to the game; + // instead, we can just set the existing parent as countered. + parent.counteredBy = msg.sender; + } + + /// @notice Generic move function, used for both `attack` and `defend` moves. + /// @param _challengeIndex The index of the claim being moved against. + /// @param _claim The claim at the next logical position in the game. + /// @param _isAttack Whether or not the move is an attack or defense. + function move(uint256 _challengeIndex, Claim _claim, bool _isAttack) public payable virtual { + // INVARIANT: Moves cannot be made unless the game is currently in progress. + if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); + + // Get the parent. If it does not exist, the call will revert with OOB. + ClaimData memory parent = claimData[_challengeIndex]; + + // Compute the position that the claim commits to. Because the parent's position is already + // known, we can compute the next position by moving left or right depending on whether + // or not the move is an attack or defense. + Position parentPos = parent.position; + Position nextPosition = parentPos.move(_isAttack); + uint256 nextPositionDepth = nextPosition.depth(); + + // INVARIANT: A defense can never be made against the root claim of either the output root game or any + // of the execution trace bisection subgames. This is because the root claim commits to the + // entire state. Therefore, the only valid defense is to do nothing if it is agreed with. + if ((_challengeIndex == 0 || nextPositionDepth == SPLIT_DEPTH + 2) && !_isAttack) { + revert CannotDefendRootClaim(); + } + + // INVARIANT: A move can never surpass the `MAX_GAME_DEPTH`. The only option to counter a + // claim at this depth is to perform a single instruction step on-chain via + // the `step` function to prove that the state transition produces an unexpected + // post-state. + if (nextPositionDepth > MAX_GAME_DEPTH) revert GameDepthExceeded(); + + // When the next position surpasses the split depth (i.e., it is the root claim of an execution + // trace bisection sub-game), we need to perform some extra verification steps. + if (nextPositionDepth == SPLIT_DEPTH + 1) { + _verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack); + } + + // INVARIANT: The `msg.value` must exactly equal the required bond. + if (getRequiredBond(nextPosition) != msg.value) revert IncorrectBondAmount(); + + // Compute the duration of the next clock. This is done by adding the duration of the + // grandparent claim to the difference between the current block timestamp and the + // parent's clock timestamp. + Duration nextDuration = getChallengerDuration(_challengeIndex); + + // INVARIANT: A move can never be made once its clock has exceeded `MAX_CLOCK_DURATION` + // seconds of time. + if (nextDuration.raw() == MAX_CLOCK_DURATION.raw()) revert ClockTimeExceeded(); + + // If the remaining clock time has less than `CLOCK_EXTENSION` seconds remaining, grant the potential + // grandchild's clock `CLOCK_EXTENSION` seconds. This is to ensure that, even if a player has to inherit another + // team's clock to counter a freeloader claim, they will always have enough time to to respond. This extension + // is bounded by the depth of the tree. If the potential grandchild is an execution trace bisection root, the + // clock extension is doubled. This is to allow for extra time for the off-chain challenge agent to generate + // the initial instruction trace on the native FPVM. + if (nextDuration.raw() > MAX_CLOCK_DURATION.raw() - CLOCK_EXTENSION.raw()) { + // If the potential grandchild is an execution trace bisection root, double the clock extension. + uint64 extensionPeriod = + nextPositionDepth == SPLIT_DEPTH - 1 ? CLOCK_EXTENSION.raw() * 2 : CLOCK_EXTENSION.raw(); + nextDuration = Duration.wrap(MAX_CLOCK_DURATION.raw() - extensionPeriod); + } + + // Construct the next clock with the new duration and the current block timestamp. + Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp))); + + // INVARIANT: There cannot be multiple identical claims with identical moves on the same challengeIndex. Multiple + // claims at the same position may dispute the same challengeIndex. However, they must have different + // values. + Hash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex); + if (claims[claimHash]) revert ClaimAlreadyExists(); + claims[claimHash] = true; + + // Create the new claim. + claimData.push( + ClaimData({ + parentIndex: uint32(_challengeIndex), + // This is updated during subgame resolution + counteredBy: address(0), + claimant: msg.sender, + bond: uint128(msg.value), + claim: _claim, + position: nextPosition, + clock: nextClock + }) + ); + + // Update the subgame rooted at the parent claim. + subgames[_challengeIndex].push(claimData.length - 1); + + // Deposit the bond. + WETH.deposit{ value: msg.value }(); + + // Emit the appropriate event for the attack or defense. + emit Move(_challengeIndex, _claim, msg.sender); + } + + /// @inheritdoc IFaultDisputeGame + function attack(uint256 _parentIndex, Claim _claim) external payable { + move(_parentIndex, _claim, true); + } + + /// @inheritdoc IFaultDisputeGame + function defend(uint256 _parentIndex, Claim _claim) external payable { + move(_parentIndex, _claim, false); + } + + /// @inheritdoc IFaultDisputeGame + function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external { + // INVARIANT: Local data can only be added if the game is currently in progress. + if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); + + (Claim starting, Position startingPos, Claim disputed, Position disputedPos) = + _findStartingAndDisputedOutputs(_execLeafIdx); + Hash uuid = _computeLocalContext(starting, startingPos, disputed, disputedPos); + + IPreimageOracle oracle = VM.oracle(); + if (_ident == LocalPreimageKey.L1_HEAD_HASH) { + // Load the L1 head hash + oracle.loadLocalData(_ident, uuid.raw(), l1Head().raw(), 32, _partOffset); + } else if (_ident == LocalPreimageKey.STARTING_OUTPUT_ROOT) { + // Load the starting proposal's output root. + oracle.loadLocalData(_ident, uuid.raw(), starting.raw(), 32, _partOffset); + } else if (_ident == LocalPreimageKey.DISPUTED_OUTPUT_ROOT) { + // Load the disputed proposal's output root + oracle.loadLocalData(_ident, uuid.raw(), disputed.raw(), 32, _partOffset); + } else if (_ident == LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER) { + // 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.l2BlockNumber + disputedPos.traceIndex(SPLIT_DEPTH) + 1; + + 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(L2_CHAIN_ID << 0xC0), 8, _partOffset); + } else { + revert InvalidLocalIdent(); + } + } + + /// @inheritdoc IFaultDisputeGame + function getNumToResolve(uint256 _claimIndex) public view returns (uint256 numRemainingChildren_) { + ResolutionCheckpoint storage checkpoint = resolutionCheckpoints[_claimIndex]; + uint256[] storage challengeIndices = subgames[_claimIndex]; + uint256 challengeIndicesLen = challengeIndices.length; + + numRemainingChildren_ = challengeIndicesLen - checkpoint.subgameIndex; + } + + /// @inheritdoc IFaultDisputeGame + function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) { + l2BlockNumber_ = _getArgUint256(0x54); + } + + /// @inheritdoc IFaultDisputeGame + function startingBlockNumber() external view returns (uint256 startingBlockNumber_) { + startingBlockNumber_ = startingOutputRoot.l2BlockNumber; + } + + /// @inheritdoc IFaultDisputeGame + function startingRootHash() external view returns (Hash startingRootHash_) { + startingRootHash_ = startingOutputRoot.root; + } + + //////////////////////////////////////////////////////////////// + // `IDisputeGame` impl // + //////////////////////////////////////////////////////////////// + + /// @inheritdoc IDisputeGame + function resolve() external returns (GameStatus status_) { + // INVARIANT: Resolution cannot occur unless the game is currently in progress. + if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); + + // INVARIANT: Resolution cannot occur unless the absolute root subgame has been resolved. + if (!resolvedSubgames[0]) revert OutOfOrderResolution(); + + // Update the global game status; The dispute has concluded. + status_ = claimData[0].counteredBy == address(0) ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS; + resolvedAt = Timestamp.wrap(uint64(block.timestamp)); + + // Update the status and emit the resolved event, note that we're performing an assignment here. + emit Resolved(status = status_); + + // Try to update the anchor state, this should not revert. + ANCHOR_STATE_REGISTRY.tryUpdateAnchorState(); + } + + /// @inheritdoc IFaultDisputeGame + function resolveClaim(uint256 _claimIndex, uint256 _numToResolve) external { + // INVARIANT: Resolution cannot occur unless the game is currently in progress. + if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); + + ClaimData storage subgameRootClaim = claimData[_claimIndex]; + Duration challengeClockDuration = getChallengerDuration(_claimIndex); + + // INVARIANT: Cannot resolve a subgame unless the clock of its would-be counter has expired + // INVARIANT: Assuming ordered subgame resolution, challengeClockDuration is always >= MAX_CLOCK_DURATION if all + // descendant subgames are resolved + if (challengeClockDuration.raw() < MAX_CLOCK_DURATION.raw()) revert ClockNotExpired(); + + // INVARIANT: Cannot resolve a subgame twice. + if (resolvedSubgames[_claimIndex]) revert ClaimAlreadyResolved(); + + uint256[] storage challengeIndices = subgames[_claimIndex]; + uint256 challengeIndicesLen = challengeIndices.length; + + // Uncontested claims are resolved implicitly unless they are the root claim. Pay out the bond to the claimant + // and return early. + if (challengeIndicesLen == 0 && _claimIndex != 0) { + // In the event that the parent claim is at the max depth, there will always be 0 subgames. If the + // `counteredBy` field is set and there are no subgames, this implies that the parent claim was successfully + // stepped against. In this case, we pay out the bond to the party that stepped against the parent claim. + // Otherwise, the parent claim is uncontested, and the bond is returned to the claimant. + address counteredBy = subgameRootClaim.counteredBy; + address recipient = counteredBy == address(0) ? subgameRootClaim.claimant : counteredBy; + _distributeBond(recipient, subgameRootClaim); + resolvedSubgames[_claimIndex] = true; + return; + } + + // Fetch the resolution checkpoint from storage. + ResolutionCheckpoint memory checkpoint = resolutionCheckpoints[_claimIndex]; + + // If the checkpoint does not currently exist, initialize the current left most position as max u128. + if (!checkpoint.initialCheckpointComplete) { + checkpoint.leftmostPosition = Position.wrap(type(uint128).max); + checkpoint.initialCheckpointComplete = true; + + // If `_numToResolve == 0`, assume that we can check all child subgames in this one callframe. + if (_numToResolve == 0) _numToResolve = challengeIndicesLen; + } + + // Assume parent is honest until proven otherwise + uint256 lastToResolve = checkpoint.subgameIndex + _numToResolve; + uint256 finalCursor = lastToResolve > challengeIndicesLen ? challengeIndicesLen : lastToResolve; + for (uint256 i = checkpoint.subgameIndex; i < finalCursor; i++) { + uint256 challengeIndex = challengeIndices[i]; + + // INVARIANT: Cannot resolve a subgame containing an unresolved claim + if (!resolvedSubgames[challengeIndex]) revert OutOfOrderResolution(); + + ClaimData storage claim = claimData[challengeIndex]; + + // If the child subgame is uncountered and further left than the current left-most counter, + // update the parent subgame's `countered` address and the current `leftmostCounter`. + // The left-most correct counter is preferred in bond payouts in order to discourage attackers + // from countering invalid subgame roots via an invalid defense position. As such positions + // cannot be correctly countered. + // Note that correctly positioned defense, but invalid claimes can still be successfully countered. + if (claim.counteredBy == address(0) && checkpoint.leftmostPosition.raw() > claim.position.raw()) { + checkpoint.counteredBy = claim.claimant; + checkpoint.leftmostPosition = claim.position; + } + } + + // Increase the checkpoint's cursor position by the number of children that were checked. + checkpoint.subgameIndex = uint32(finalCursor); + + // Persist the checkpoint and allow for continuing in a separate transaction, if resolution is not already + // complete. + resolutionCheckpoints[_claimIndex] = checkpoint; + + // If all children have been traversed in the above loop, the subgame may be resolved. Otherwise, persist the + // checkpoint and allow for continuation in a separate transaction. + if (checkpoint.subgameIndex == challengeIndicesLen) { + address countered = checkpoint.counteredBy; + + // 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; + + // Mark the subgame as resolved. + resolvedSubgames[_claimIndex] = true; + + // 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); + } + } + + /// @inheritdoc IDisputeGame + function gameType() public view override returns (GameType gameType_) { + gameType_ = GAME_TYPE; + } + + /// @inheritdoc IDisputeGame + function gameCreator() public pure returns (address creator_) { + creator_ = _getArgAddress(0x00); + } + + /// @inheritdoc IDisputeGame + function rootClaim() public pure returns (Claim rootClaim_) { + rootClaim_ = Claim.wrap(_getArgBytes32(0x14)); + } + + /// @inheritdoc IDisputeGame + function l1Head() public pure returns (Hash l1Head_) { + l1Head_ = Hash.wrap(_getArgBytes32(0x34)); + } + + /// @inheritdoc IDisputeGame + function extraData() public pure returns (bytes memory extraData_) { + // The extra data starts at the second word within the cwia calldata and + // is 32 bytes long. + extraData_ = _getArgBytes(0x54, 0x20); + } + + /// @inheritdoc IDisputeGame + function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) { + gameType_ = gameType(); + rootClaim_ = rootClaim(); + extraData_ = extraData(); + } + + //////////////////////////////////////////////////////////////// + // MISC EXTERNAL // + //////////////////////////////////////////////////////////////// + + /// @notice Returns the required bond for a given move kind. + /// @param _position The position of the bonded interaction. + /// @return requiredBond_ The required ETH bond for the given move, in wei. + function getRequiredBond(Position _position) public view returns (uint256 requiredBond_) { + uint256 depth = uint256(_position.depth()); + if (depth > MAX_GAME_DEPTH) revert GameDepthExceeded(); + + // Values taken from Big Bonds v1.5 (TM) spec. + uint256 assumedBaseFee = 200 gwei; + uint256 baseGasCharged = 400_000; + uint256 highGasCharged = 300_000_000; + + // Goal here is to compute the fixed multiplier that will be applied to the base gas + // charged to get the required gas amount for the given depth. We apply this multiplier + // some `n` times where `n` is the depth of the position. We are looking for some number + // that, when multiplied by itself `MAX_GAME_DEPTH` times and then multiplied by the base + // gas charged, will give us the maximum gas that we want to charge. + // We want to solve for (highGasCharged/baseGasCharged) ** (1/MAX_GAME_DEPTH). + // We know that a ** (b/c) is equal to e ** (ln(a) * (b/c)). + // We can compute e ** (ln(a) * (b/c)) quite easily with FixedPointMathLib. + + // Set up a, b, and c. + uint256 a = highGasCharged / baseGasCharged; + uint256 b = FixedPointMathLib.WAD; + uint256 c = MAX_GAME_DEPTH * FixedPointMathLib.WAD; + + // Compute ln(a). + // slither-disable-next-line divide-before-multiply + uint256 lnA = uint256(FixedPointMathLib.lnWad(int256(a * FixedPointMathLib.WAD))); + + // Computes (b / c) with full precision using WAD = 1e18. + uint256 bOverC = FixedPointMathLib.divWad(b, c); + + // Compute e ** (ln(a) * (b/c)) + // sMulWad can be used here since WAD = 1e18 maintains the same precision. + uint256 numerator = FixedPointMathLib.mulWad(lnA, bOverC); + int256 base = FixedPointMathLib.expWad(int256(numerator)); + + // Compute the required gas amount. + int256 rawGas = FixedPointMathLib.powWad(base, int256(depth * FixedPointMathLib.WAD)); + uint256 requiredGas = FixedPointMathLib.mulWad(baseGasCharged, uint256(rawGas)); + + // Compute the required bond. + requiredBond_ = assumedBaseFee * requiredGas; + } + + /// @notice Claim the credit belonging to the recipient address. + /// @param _recipient The owner and recipient of the credit. + function claimCredit(address _recipient) external { + // Remove the credit from the recipient prior to performing the external call. + uint256 recipientCredit = credit[_recipient]; + credit[_recipient] = 0; + + // Revert if the recipient has no credit to claim. + if (recipientCredit == 0) { + revert NoCreditToClaim(); + } + + // Try to withdraw the WETH amount so it can be used here. + WETH.withdraw(_recipient, recipientCredit); + + // Transfer the credit to the recipient. + (bool success,) = _recipient.call{ value: recipientCredit }(hex""); + if (!success) revert BondTransferFailed(); + } + + /// @notice Returns the amount of time elapsed on the potential challenger to `_claimIndex`'s chess clock. Maxes + /// out at `MAX_CLOCK_DURATION`. + /// @param _claimIndex The index of the subgame root claim. + /// @return duration_ The time elapsed on the potential challenger to `_claimIndex`'s chess clock. + function getChallengerDuration(uint256 _claimIndex) public view returns (Duration duration_) { + // INVARIANT: The game must be in progress to query the remaining time to respond to a given claim. + if (status != GameStatus.IN_PROGRESS) { + revert GameNotInProgress(); + } + + // Fetch the subgame root claim. + ClaimData storage subgameRootClaim = claimData[_claimIndex]; + + // Fetch the parent of the subgame root's clock, if it exists. + Clock parentClock; + if (subgameRootClaim.parentIndex != type(uint32).max) { + parentClock = claimData[subgameRootClaim.parentIndex].clock; + } + + // Compute the duration elapsed of the potential challenger's clock. + uint64 challengeDuration = + uint64(parentClock.duration().raw() + (block.timestamp - subgameRootClaim.clock.timestamp().raw())); + duration_ = challengeDuration > MAX_CLOCK_DURATION.raw() ? MAX_CLOCK_DURATION : Duration.wrap(challengeDuration); + } + + /// @notice Returns the length of the `claimData` array. + function claimDataLen() external view returns (uint256 len_) { + len_ = claimData.length; + } + + //////////////////////////////////////////////////////////////// + // IMMUTABLE GETTERS // + //////////////////////////////////////////////////////////////// + + /// @notice Returns the absolute prestate of the instruction trace. + function absolutePrestate() external view returns (Claim absolutePrestate_) { + absolutePrestate_ = ABSOLUTE_PRESTATE; + } + + /// @notice Returns the max game depth. + function maxGameDepth() external view returns (uint256 maxGameDepth_) { + maxGameDepth_ = MAX_GAME_DEPTH; + } + + /// @notice Returns the split depth. + function splitDepth() external view returns (uint256 splitDepth_) { + splitDepth_ = SPLIT_DEPTH; + } + + /// @notice Returns the max clock duration. + function maxClockDuration() external view returns (Duration maxClockDuration_) { + maxClockDuration_ = MAX_CLOCK_DURATION; + } + + /// @notice Returns the clock extension constant. + function clockExtension() external view returns (Duration clockExtension_) { + clockExtension_ = CLOCK_EXTENSION; + } + + /// @notice Returns the address of the VM. + function vm() external view returns (IBigStepper vm_) { + vm_ = VM; + } + + /// @notice Returns the WETH contract for holding ETH. + function weth() external view returns (IDelayedWETH weth_) { + weth_ = WETH; + } + + /// @notice Returns the anchor state registry contract. + function anchorStateRegistry() external view returns (IAnchorStateRegistry registry_) { + registry_ = ANCHOR_STATE_REGISTRY; + } + + /// @notice Returns the chain ID of the L2 network this contract argues about. + function l2ChainId() external view returns (uint256 l2ChainId_) { + l2ChainId_ = L2_CHAIN_ID; + } + + //////////////////////////////////////////////////////////////// + // HELPERS // + //////////////////////////////////////////////////////////////// + + /// @notice Pays out the bond of a claim to a given recipient. + /// @param _recipient The recipient of the bond. + /// @param _bonded The claim to pay out the bond of. + function _distributeBond(address _recipient, ClaimData storage _bonded) internal { + // Set all bits in the bond value to indicate that the bond has been paid out. + uint256 bond = _bonded.bond; + + // Increase the recipient's credit. + credit[_recipient] += bond; + + // Unlock the bond. + WETH.unlock(_recipient, bond); + } + + /// @notice Verifies the integrity of an execution bisection subgame's root claim. Reverts if the claim + /// is invalid. + /// @param _rootClaim The root claim of the execution bisection subgame. + function _verifyExecBisectionRoot( + Claim _rootClaim, + uint256 _parentIdx, + Position _parentPos, + bool _isAttack + ) + internal + view + { + // The root claim of an execution trace bisection sub-game must: + // 1. Signal that the VM panicked or resulted in an invalid transition if the disputed output root + // was made by the opposing party. + // 2. Signal that the VM resulted in a valid transition if the disputed output root was made by the same party. + + // If the move is a defense, the disputed output could have been made by either party. In this case, we + // need to search for the parent output to determine what the expected status byte should be. + Position disputedLeafPos = Position.wrap(_parentPos.raw() + 1); + ClaimData storage disputed = _findTraceAncestor({ _pos: disputedLeafPos, _start: _parentIdx, _global: true }); + uint8 vmStatus = uint8(_rootClaim.raw()[0]); + + if (_isAttack || disputed.position.depth() % 2 == SPLIT_DEPTH % 2) { + // If the move is an attack, the parent output is always deemed to be disputed. In this case, we only need + // to check that the root claim signals that the VM panicked or resulted in an invalid transition. + // If the move is a defense, and the disputed output and creator of the execution trace subgame disagree, + // the root claim should also signal that the VM panicked or resulted in an invalid transition. + if (!(vmStatus == VMStatuses.INVALID.raw() || vmStatus == VMStatuses.PANIC.raw())) { + revert UnexpectedRootClaim(_rootClaim); + } + } else if (vmStatus != VMStatuses.VALID.raw()) { + // The disputed output and the creator of the execution trace subgame agree. The status byte should + // have signaled that the VM succeeded. + revert UnexpectedRootClaim(_rootClaim); + } + } + + /// @notice Finds the trace ancestor of a given position within the DAG. + /// @param _pos The position to find the trace ancestor claim of. + /// @param _start The index to start searching from. + /// @param _global Whether or not to search the entire dag or just within an execution trace subgame. If set to + /// `true`, and `_pos` is at or above the split depth, this function will revert. + /// @return ancestor_ The ancestor claim that commits to the same trace index as `_pos`. + function _findTraceAncestor( + Position _pos, + uint256 _start, + bool _global + ) + internal + view + returns (ClaimData storage ancestor_) + { + // Grab the trace ancestor's expected position. + Position traceAncestorPos = _global ? _pos.traceAncestor() : _pos.traceAncestorBounded(SPLIT_DEPTH); + + // Walk up the DAG to find a claim that commits to the same trace index as `_pos`. It is + // guaranteed that such a claim exists. + ancestor_ = claimData[_start]; + while (ancestor_.position.raw() != traceAncestorPos.raw()) { + ancestor_ = claimData[ancestor_.parentIndex]; + } + } + + /// @notice Finds the starting and disputed output root for a given `ClaimData` within the DAG. This + /// `ClaimData` must be below the `SPLIT_DEPTH`. + /// @param _start The index within `claimData` of the claim to start searching from. + /// @return startingClaim_ The starting output root claim. + /// @return startingPos_ The starting output root position. + /// @return disputedClaim_ The disputed output root claim. + /// @return disputedPos_ The disputed output root position. + function _findStartingAndDisputedOutputs(uint256 _start) + internal + view + returns (Claim startingClaim_, Position startingPos_, Claim disputedClaim_, Position disputedPos_) + { + // Fatch the starting claim. + uint256 claimIdx = _start; + ClaimData storage claim = claimData[claimIdx]; + + // If the starting claim's depth is less than or equal to the split depth, we revert as this is UB. + if (claim.position.depth() <= SPLIT_DEPTH) revert ClaimAboveSplit(); + + // We want to: + // 1. Find the first claim at the split depth. + // 2. Determine whether it was the starting or disputed output for the exec game. + // 3. Find the complimentary claim depending on the info from #2 (pre or post). + + // Walk up the DAG until the ancestor's depth is equal to the split depth. + uint256 currentDepth; + ClaimData storage execRootClaim = claim; + while ((currentDepth = claim.position.depth()) > SPLIT_DEPTH) { + uint256 parentIndex = claim.parentIndex; + + // If we're currently at the split depth + 1, we're at the root of the execution sub-game. + // We need to keep track of the root claim here to determine whether the execution sub-game was + // started with an attack or defense against the output leaf claim. + if (currentDepth == SPLIT_DEPTH + 1) execRootClaim = claim; + + claim = claimData[parentIndex]; + claimIdx = parentIndex; + } + + // Determine whether the start of the execution sub-game was an attack or defense to the output root + // above. This is important because it determines which claim is the starting output root and which + // is the disputed output root. + (Position execRootPos, Position outputPos) = (execRootClaim.position, claim.position); + bool wasAttack = execRootPos.parent().raw() == outputPos.raw(); + + // Determine the starting and disputed output root indices. + // 1. If it was an attack, the disputed output root is `claim`, and the starting output root is + // elsewhere in the DAG (it must commit to the block # index at depth of `outputPos - 1`). + // 2. If it was a defense, the starting output root is `claim`, and the disputed output root is + // elsewhere in the DAG (it must commit to the block # index at depth of `outputPos + 1`). + if (wasAttack) { + // 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 + // 0, which can be easily identified due to 0 being an invalid Gindex. + if (outputPos.indexAtDepth() > 0) { + ClaimData storage starting = _findTraceAncestor(Position.wrap(outputPos.raw() - 1), claimIdx, true); + (startingClaim_, startingPos_) = (starting.claim, starting.position); + } else { + startingClaim_ = Claim.wrap(startingOutputRoot.root.raw()); + } + (disputedClaim_, disputedPos_) = (claim.claim, claim.position); + } else { + ClaimData storage disputed = _findTraceAncestor(Position.wrap(outputPos.raw() + 1), claimIdx, true); + (startingClaim_, startingPos_) = (claim.claim, claim.position); + (disputedClaim_, disputedPos_) = (disputed.claim, disputed.position); + } + } + + /// @notice Finds the local context hash for a given claim index that is present in an execution trace subgame. + /// @param _claimIndex The index of the claim to find the local context hash for. + /// @return uuid_ The local context hash. + function _findLocalContext(uint256 _claimIndex) internal view returns (Hash uuid_) { + (Claim starting, Position startingPos, Claim disputed, Position disputedPos) = + _findStartingAndDisputedOutputs(_claimIndex); + uuid_ = _computeLocalContext(starting, startingPos, disputed, disputedPos); + } + + /// @notice Computes the local context hash for a set of starting/disputed claim values and positions. + /// @param _starting The starting claim. + /// @param _startingPos The starting claim's position. + /// @param _disputed The disputed claim. + /// @param _disputedPos The disputed claim's position. + /// @return uuid_ The local context hash. + function _computeLocalContext( + Claim _starting, + Position _startingPos, + Claim _disputed, + Position _disputedPos + ) + internal + pure + returns (Hash uuid_) + { + // A position of 0 indicates that the starting claim is the absolute prestate. In this special case, + // we do not include the starting claim within the local context hash. + uuid_ = _startingPos.raw() == 0 + ? Hash.wrap(keccak256(abi.encode(_disputed, _disputedPos))) + : Hash.wrap(keccak256(abi.encode(_starting, _startingPos, _disputed, _disputedPos))); + } +} diff --git a/packages/contracts-bedrock/test/actors/FaultDisputeActors2.sol b/packages/contracts-bedrock/test/actors/FaultDisputeActors2.sol new file mode 100644 index 000000000000..c1cbb45cecd8 --- /dev/null +++ b/packages/contracts-bedrock/test/actors/FaultDisputeActors2.sol @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { CommonBase } from "forge-std/Base.sol"; + +import { FaultDisputeGame } from "src/dispute/FaultDisputeGame2.sol"; +import { IFaultDisputeGame } from "src/dispute/interfaces/IFaultDisputeGame.sol"; + +import "src/dispute/lib/Types.sol"; + +/// @title GameSolver +/// @notice The `GameSolver` contract is a contract that can produce an array of available +/// moves for a given `FaultDisputeGame` contract, from the eyes of an honest +/// actor. The `GameSolver` does not implement functionality for acting on the `Move`s +/// it suggests. +abstract contract GameSolver is CommonBase { + /// @notice The `FaultDisputeGame` proxy that the `GameSolver` will be solving. + FaultDisputeGame public immutable GAME; + /// @notice The split depth of the game + uint256 internal immutable SPLIT_DEPTH; + /// @notice The max depth of the game + uint256 internal immutable MAX_DEPTH; + /// @notice The maximum L2 block number that the output bisection portion of the position tree + /// can handle. + uint256 internal immutable MAX_L2_BLOCK_NUMBER; + + /// @notice The L2 outputs that the `GameSolver` will be representing, keyed by L2 block number - 1. + uint256[] public l2Outputs; + /// @notice The execution trace that the `GameSolver` will be representing. + bytes public trace; + /// @notice The raw absolute prestate data. + bytes public absolutePrestateData; + /// @notice The offset of previously processed claims in the `GAME` contract's `claimData` array. + /// Starts at 0 and increments by 1 for each claim processed. + uint256 public processedBuf; + /// @notice Signals whether or not the `GameSolver` agrees with the root claim of the + /// `GAME` contract. + bool public agreeWithRoot; + + /// @notice The `MoveKind` enum represents a kind of interaction with the `FaultDisputeGame` contract. + enum MoveKind { + Attack, + Defend, + Step, + AddLocalData + } + + /// @notice The `Move` struct represents a move in the game, and contains information + /// about the kind of move, the sender of the move, and the calldata to be sent + /// to the `FaultDisputeGame` contract by a consumer of this contract. + struct Move { + MoveKind kind; + bytes data; + uint256 value; + } + + constructor( + FaultDisputeGame _gameProxy, + uint256[] memory _l2Outputs, + bytes memory _trace, + bytes memory _preStateData + ) { + GAME = _gameProxy; + SPLIT_DEPTH = GAME.splitDepth(); + MAX_DEPTH = GAME.maxGameDepth(); + MAX_L2_BLOCK_NUMBER = 2 ** (MAX_DEPTH - SPLIT_DEPTH); + + l2Outputs = _l2Outputs; + trace = _trace; + absolutePrestateData = _preStateData; + } + + /// @notice Returns an array of `Move`s that can be taken from the perspective of an honest + /// actor in the `FaultDisputeGame` contract. + function solveGame() external virtual returns (Move[] memory moves_); +} + +/// @title HonestGameSolver +/// @notice The `HonestGameSolver` is an implementation of `GameSolver` which responds accordingly depending +/// on the state of the `FaultDisputeGame` contract in relation to their local opinion of the correct +/// order of output roots and the execution trace between each block `n` -> `n + 1` state transition. +contract HonestGameSolver is GameSolver { + /// @notice The `Direction` enum represents the direction of a proposed move in the game, + /// or a lack thereof. + enum Direction { + Defend, + Attack, + Noop + } + + constructor( + FaultDisputeGame _gameProxy, + uint256[] memory _l2Outputs, + bytes memory _trace, + bytes memory _preStateData + ) + GameSolver(_gameProxy, _l2Outputs, _trace, _preStateData) + { + // Mark agreement with the root claim if the local opinion of the root claim is the same as the + // observed root claim. + agreeWithRoot = Claim.unwrap(outputAt(MAX_L2_BLOCK_NUMBER)) == Claim.unwrap(_gameProxy.rootClaim()); + } + + //////////////////////////////////////////////////////////////// + // EXTERNAL // + //////////////////////////////////////////////////////////////// + + /// @notice Returns an array of `Move`s that can be taken from the perspective of an honest + /// actor in the `FaultDisputeGame` contract. + function solveGame() external override returns (Move[] memory moves_) { + uint256 numClaims = GAME.claimDataLen(); + + // Pre-allocate the `moves_` array to the maximum possible length. Test environment, so + // over-allocation is fine, and more easy to read than making a linked list in asm. + moves_ = new Move[](numClaims - processedBuf); + + uint256 numMoves = 0; + for (uint256 i = processedBuf; i < numClaims; i++) { + // Grab the observed claim. + IFaultDisputeGame.ClaimData memory observed = getClaimData(i); + + // Determine the direction of the next move to be taken. + (Direction moveDirection, Position movePos) = determineDirection(observed); + + // Continue if there is no move to be taken against the observed claim. + if (moveDirection == Direction.Noop) continue; + + if (movePos.depth() <= MAX_DEPTH) { + // bisection + moves_[numMoves++] = handleBisectionMove(moveDirection, movePos, i); + } else { + // instruction step + moves_[numMoves++] = handleStepMove(moveDirection, observed.position, movePos, i); + } + } + + // Update the length of the `moves_` array to the number of moves that were added. This is + // always a no-op or a truncation operation. + assembly { + mstore(moves_, numMoves) + } + + // Increment `processedBuf` by the number of claims processed, so that next time around, + // we don't attempt to process the same claims again. + processedBuf += numClaims - processedBuf; + } + + //////////////////////////////////////////////////////////////// + // INTERNAL // + //////////////////////////////////////////////////////////////// + + /// @dev Helper function to determine the direction of the next move to be taken. + function determineDirection(IFaultDisputeGame.ClaimData memory _claimData) + internal + view + returns (Direction direction_, Position movePos_) + { + bool rightLevel = isRightLevel(_claimData.position); + bool localAgree = Claim.unwrap(claimAt(_claimData.position)) == Claim.unwrap(_claimData.claim); + if (_claimData.parentIndex == type(uint32).max) { + // If we agree with the parent claim and it is on a level we agree with, ignore it. + if (localAgree && rightLevel) { + return (Direction.Noop, Position.wrap(0)); + } + + // The parent claim is the root claim. We must attack if we disagree per the game rules. + direction_ = Direction.Attack; + movePos_ = _claimData.position.move(true); + } else { + // Never attempt to defend an execution trace subgame root. Only attack if we disagree with it, + // otherwise do nothing. + // NOTE: This is not correct behavior in the context of the honest actor; The alphabet game has + // a constant status byte, and is not safe from someone being dishonest in output bisection + // and then posting a correct execution trace bisection root claim. + if (_claimData.position.depth() == SPLIT_DEPTH + 1 && localAgree) { + return (Direction.Noop, Position.wrap(0)); + } + + // If the parent claim is not the root claim, first check if the observed claim is on a level that + // agrees with the local view of the root claim. If it is, noop. If it is not, perform an attack or + // defense depending on the local view of the observed claim. + if (rightLevel) { + // Never move against a claim on the right level. Even if it's wrong, if it's uncountered, it furthers + // our goals. + return (Direction.Noop, Position.wrap(0)); + } else { + // Fetch the local opinion of the parent claim. + Claim localParent = claimAt(_claimData.position); + + // NOTE: Poison not handled. + if (Claim.unwrap(localParent) != Claim.unwrap(_claimData.claim)) { + // If we disagree with the observed claim, we must attack it. + movePos_ = _claimData.position.move(true); + direction_ = Direction.Attack; + } else { + // If we agree with the observed claim, we must defend the observed claim. + movePos_ = _claimData.position.move(false); + direction_ = Direction.Defend; + } + } + } + } + + /// @notice Returns a `Move` struct that represents an attack or defense move in the bisection portion + /// of the game. + /// + /// @dev Note: This function assumes that the `movePos` and `challengeIndex` are valid within the + /// output bisection context. This is enforced by the `solveGame` function. + function handleBisectionMove( + Direction _direction, + Position _movePos, + uint256 _challengeIndex + ) + internal + view + returns (Move memory move_) + { + bool isAttack = _direction == Direction.Attack; + + uint256 bond = GAME.getRequiredBond(_movePos); + + move_ = Move({ + kind: isAttack ? MoveKind.Attack : MoveKind.Defend, + value: bond, + data: abi.encodeCall(FaultDisputeGame.move, (_challengeIndex, claimAt(_movePos), isAttack)) + }); + } + + /// @notice Returns a `Move` struct that represents a step move in the execution trace + /// bisection portion of the dispute game. + /// @dev Note: This function assumes that the `movePos` and `challengeIndex` are valid within the + /// execution trace bisection context. This is enforced by the `solveGame` function. + function handleStepMove( + Direction _direction, + Position _parentPos, + Position _movePos, + uint256 _challengeIndex + ) + internal + view + returns (Move memory move_) + { + bool isAttack = _direction == Direction.Attack; + bytes memory preStateTrace; + + // First, we need to find the pre/post state index depending on whether we + // are making an attack step or a defense step. If the relative index at depth of the + // move position is 0, the prestate is the absolute prestate and we need to + // do nothing. + if ((_movePos.indexAtDepth() % (2 ** (MAX_DEPTH - SPLIT_DEPTH))) != 0) { + // Grab the trace up to the prestate's trace index. + if (isAttack) { + Position leafPos = Position.wrap(Position.unwrap(_parentPos) - 1); + preStateTrace = abi.encode(leafPos.traceIndex(MAX_DEPTH), stateAt(leafPos)); + } else { + preStateTrace = abi.encode(_parentPos.traceIndex(MAX_DEPTH), stateAt(_parentPos)); + } + } else { + preStateTrace = absolutePrestateData; + } + + move_ = Move({ + kind: MoveKind.Step, + value: 0, + data: abi.encodeCall(FaultDisputeGame.step, (_challengeIndex, isAttack, preStateTrace, hex"")) + }); + } + + //////////////////////////////////////////////////////////////// + // HELPERS // + //////////////////////////////////////////////////////////////// + + /// @dev Helper function to get the `ClaimData` struct at a given index in the `GAME` contract's + /// `claimData` array. + function getClaimData(uint256 _claimIndex) internal view returns (IFaultDisputeGame.ClaimData memory claimData_) { + // thanks, solc + ( + uint32 parentIndex, + address countered, + address claimant, + uint128 bond, + Claim claim, + Position position, + Clock clock + ) = GAME.claimData(_claimIndex); + claimData_ = IFaultDisputeGame.ClaimData({ + parentIndex: parentIndex, + counteredBy: countered, + claimant: claimant, + bond: bond, + claim: claim, + position: position, + clock: clock + }); + } + + /// @notice Returns the player's claim that commits to a given position, swapping between + /// output bisection claims and execution trace bisection claims depending on the depth. + /// @dev Prefer this function over `outputAt` or `statehashAt` directly. + function claimAt(Position _position) internal view returns (Claim claim_) { + return _position.depth() > SPLIT_DEPTH ? statehashAt(_position) : outputAt(_position); + } + + /// @notice Returns the mock output at the given position. + function outputAt(Position _position) internal view returns (Claim claim_) { + // Don't allow for positions that are deeper than the split depth. + if (_position.depth() > SPLIT_DEPTH) { + revert("GameSolver: invalid position depth"); + } + + return outputAt(_position.traceIndex(SPLIT_DEPTH) + 1); + } + + /// @notice Returns the mock output at the given L2 block number. + function outputAt(uint256 _l2BlockNumber) internal view returns (Claim claim_) { + return Claim.wrap(bytes32(l2Outputs[_l2BlockNumber - 1])); + } + + /// @notice Returns the player's claim that commits to a given trace index. + function statehashAt(uint256 _traceIndex) internal view returns (Claim claim_) { + bytes32 hash = + keccak256(abi.encode(_traceIndex >= trace.length ? trace.length - 1 : _traceIndex, stateAt(_traceIndex))); + assembly { + claim_ := or(and(hash, not(shl(248, 0xFF))), shl(248, 1)) + } + } + + /// @notice Returns the player's claim that commits to a given trace index. + function statehashAt(Position _position) internal view returns (Claim claim_) { + return statehashAt(_position.traceIndex(MAX_DEPTH)); + } + + /// @notice Returns the state at the trace index within the player's trace. + function stateAt(Position _position) internal view returns (uint256 state_) { + return stateAt(_position.traceIndex(MAX_DEPTH)); + } + + /// @notice Returns the state at the trace index within the player's trace. + function stateAt(uint256 _traceIndex) internal view returns (uint256 state_) { + return uint256(uint8(_traceIndex >= trace.length ? trace[trace.length - 1] : trace[_traceIndex])); + } + + /// @notice Returns whether or not the position is on a level which opposes the local opinion of the + /// root claim. + function isRightLevel(Position _position) internal view returns (bool isRightLevel_) { + isRightLevel_ = agreeWithRoot == (_position.depth() % 2 == 0); + } +} + +/// @title DisputeActor +/// @notice The `DisputeActor` contract is an abstract contract that represents an actor +/// that consumes the suggested moves from a `GameSolver` contract. +abstract contract DisputeActor { + /// @notice The `GameSolver` contract used to determine the moves to be taken. + GameSolver public solver; + + /// @notice Performs all available moves deemed by the attached solver. + /// @return numMoves_ The number of moves that the actor took. + /// @return success_ True if all moves were successful, false otherwise. + function move() external virtual returns (uint256 numMoves_, bool success_); +} + +/// @title HonestDisputeActor +/// @notice An actor that consumes the suggested moves from an `HonestGameSolver` contract. Note +/// that this actor *can* be dishonest if the trace is faulty, but it will always follow +/// the rules of the honest actor. +contract HonestDisputeActor is DisputeActor { + FaultDisputeGame public immutable GAME; + + constructor( + FaultDisputeGame _gameProxy, + uint256[] memory _l2Outputs, + bytes memory _trace, + bytes memory _preStateData + ) { + GAME = _gameProxy; + solver = GameSolver(new HonestGameSolver(_gameProxy, _l2Outputs, _trace, _preStateData)); + } + + /// @inheritdoc DisputeActor + function move() external override returns (uint256 numMoves_, bool success_) { + GameSolver.Move[] memory moves = solver.solveGame(); + numMoves_ = moves.length; + + // Optimistically assume success, will be set to false if any move fails. + success_ = true; + + // Perform all available moves given to the actor by the solver. + for (uint256 i = 0; i < moves.length; i++) { + GameSolver.Move memory localMove = moves[i]; + + // If the move is a step, we first need to add the starting L2 block number to the `PreimageOracle` + // via the `FaultDisputeGame` contract. + // TODO: This is leaky. Could be another move kind. + if (localMove.kind == GameSolver.MoveKind.Step) { + bytes memory moveData = localMove.data; + uint256 challengeIndex; + assembly { + challengeIndex := mload(add(moveData, 0x24)) + } + GAME.addLocalData({ + _ident: LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, + _execLeafIdx: challengeIndex, + _partOffset: 0 + }); + } + + (bool innerSuccess,) = address(GAME).call{ value: localMove.value }(localMove.data); + assembly { + success_ := and(success_, innerSuccess) + } + } + } + + fallback() external payable { } + + receive() external payable { } +} diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame2.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame2.t.sol new file mode 100644 index 000000000000..9a25b02b3940 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame2.t.sol @@ -0,0 +1,2045 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol"; +import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; +import { FaultDisputeGame } from "src/dispute/FaultDisputeGame2.sol"; +import { DelayedWETH } from "src/dispute/weth/DelayedWETH.sol"; +import { PreimageOracle } from "src/cannon/PreimageOracle.sol"; + +import "src/dispute/lib/Types.sol"; +import "src/dispute/lib/Errors.sol"; +import { LibClock } from "src/dispute/lib/LibUDT.sol"; +import { LibPosition } from "src/dispute/lib/LibPosition.sol"; +import { IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol"; +import { IAnchorStateRegistry } from "src/dispute/interfaces/IAnchorStateRegistry.sol"; +import { AlphabetVM } from "test/mocks/AlphabetVM.sol"; + +import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors2.sol"; + +contract FaultDisputeGame_Init is DisputeGameFactory_Init { + /// @dev The type of the game being tested. + GameType internal constant GAME_TYPE = GameType.wrap(0); + + /// @dev The implementation of the game. + FaultDisputeGame internal gameImpl; + /// @dev The `Clone` proxy of the game. + FaultDisputeGame internal gameProxy; + + /// @dev The extra data passed to the game for initialization. + bytes internal extraData; + + event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); + + event ReceiveETH(uint256 amount); + + function init(Claim rootClaim, Claim absolutePrestate, uint256 l2BlockNumber) public { + // Set the time to a realistic date. + vm.warp(1690906994); + + // Set the extra data for the game creation + extraData = abi.encode(l2BlockNumber); + + AlphabetVM _vm = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + // Deploy an implementation of the fault game + gameImpl = new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: 2 ** 3, + _splitDepth: 2 ** 2, + _clockExtension: Duration.wrap(3 hours), + _maxClockDuration: Duration.wrap(3.5 days), + _vm: _vm, + _weth: delayedWeth, + _anchorStateRegistry: anchorStateRegistry, + _l2ChainId: 10 + }); + + // Register the game implementation with the factory. + disputeGameFactory.setImplementation(GAME_TYPE, gameImpl); + // Create a new game. + gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, rootClaim, extraData)))); + + // Check immutables + assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); + assertEq(gameProxy.absolutePrestate().raw(), absolutePrestate.raw()); + assertEq(gameProxy.maxGameDepth(), 2 ** 3); + assertEq(gameProxy.splitDepth(), 2 ** 2); + assertEq(gameProxy.clockExtension().raw(), 3 hours); + assertEq(gameProxy.maxClockDuration().raw(), 3.5 days); + assertEq(address(gameProxy.weth()), address(delayedWeth)); + assertEq(address(gameProxy.anchorStateRegistry()), address(anchorStateRegistry)); + assertEq(address(gameProxy.vm()), address(_vm)); + + // Label the proxy + vm.label(address(gameProxy), "FaultDisputeGame_Clone"); + } + + fallback() external payable { } + + receive() external payable { } +} + +contract FaultDisputeGame2_Test is FaultDisputeGame_Init { + /// @dev The root claim of the game. + Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32((uint256(1) << 248) | uint256(10))); + + /// @dev The preimage of the absolute prestate claim + bytes internal absolutePrestateData; + /// @dev The absolute prestate of the trace. + Claim internal absolutePrestate; + + function setUp() public override { + absolutePrestateData = abi.encode(0); + absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); + + super.setUp(); + super.init({ rootClaim: ROOT_CLAIM, absolutePrestate: absolutePrestate, l2BlockNumber: 0x10 }); + } + + //////////////////////////////////////////////////////////////// + // `IDisputeGame` Implementation Tests // + //////////////////////////////////////////////////////////////// + + /// @dev Tests that the constructor of the `FaultDisputeGame` reverts when the `MAX_GAME_DEPTH` parameter is + /// greater than `LibPosition.MAX_POSITION_BITLEN - 1`. + function testFuzz_constructor_maxDepthTooLarge_reverts(uint256 _maxGameDepth) public { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + _maxGameDepth = bound(_maxGameDepth, LibPosition.MAX_POSITION_BITLEN, type(uint256).max - 1); + vm.expectRevert(MaxDepthTooLarge.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: _maxGameDepth, + _splitDepth: _maxGameDepth + 1, + _clockExtension: Duration.wrap(3 hours), + _maxClockDuration: Duration.wrap(3.5 days), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _anchorStateRegistry: IAnchorStateRegistry(address(0)), + _l2ChainId: 10 + }); + } + + /// @dev Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` + /// parameter is greater than or equal to the `MAX_GAME_DEPTH` + function testFuzz_constructor_invalidSplitDepth_reverts(uint256 _splitDepth) public { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + _splitDepth = bound(_splitDepth, 2 ** 3, type(uint256).max); + vm.expectRevert(InvalidSplitDepth.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: 2 ** 3, + _splitDepth: _splitDepth, + _clockExtension: Duration.wrap(3 hours), + _maxClockDuration: Duration.wrap(3.5 days), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _anchorStateRegistry: IAnchorStateRegistry(address(0)), + _l2ChainId: 10 + }); + } + + /// @dev Tests that the constructor of the `FaultDisputeGame` reverts when clock extension is greater than the + /// max clock duration. + function testFuzz_constructor_clockExtensionTooLong_reverts( + uint64 _maxClockDuration, + uint64 _clockExtension + ) + public + { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + _maxClockDuration = uint64(bound(_maxClockDuration, 0, type(uint64).max - 1)); + _clockExtension = uint64(bound(_clockExtension, _maxClockDuration + 1, type(uint64).max)); + vm.expectRevert(InvalidClockExtension.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _maxGameDepth: 16, + _splitDepth: 8, + _clockExtension: Duration.wrap(_clockExtension), + _maxClockDuration: Duration.wrap(_maxClockDuration), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _anchorStateRegistry: IAnchorStateRegistry(address(0)), + _l2ChainId: 10 + }); + } + + /// @dev Tests that the game's root claim is set correctly. + function test_rootClaim_succeeds() public view { + assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); + } + + /// @dev Tests that the game's extra data is set correctly. + function test_extraData_succeeds() public view { + assertEq(gameProxy.extraData(), extraData); + } + + /// @dev Tests that the game's starting timestamp is set correctly. + function test_createdAt_succeeds() public view { + assertEq(gameProxy.createdAt().raw(), block.timestamp); + } + + /// @dev Tests that the game's type is set correctly. + function test_gameType_succeeds() public view { + assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); + } + + /// @dev Tests that the game's data is set correctly. + function test_gameData_succeeds() public view { + (GameType gameType, Claim rootClaim, bytes memory _extraData) = gameProxy.gameData(); + + assertEq(gameType.raw(), GAME_TYPE.raw()); + assertEq(rootClaim.raw(), ROOT_CLAIM.raw()); + assertEq(_extraData, extraData); + } + + //////////////////////////////////////////////////////////////// + // `IFaultDisputeGame` Implementation Tests // + //////////////////////////////////////////////////////////////// + + /// @dev Tests that the game cannot be initialized with an output root that commits to <= the configured starting + /// block number + function testFuzz_initialize_cannotProposeGenesis_reverts(uint256 _blockNumber) public { + (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); + _blockNumber = bound(_blockNumber, 0, startingL2Block); + + Claim claim = _dummyClaim(); + vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, claim)); + gameProxy = + FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, abi.encode(_blockNumber))))); + } + + /// @dev Tests that the proxy receives ETH from the dispute game factory. + function test_initialize_receivesETH_succeeds() public { + uint256 _value = disputeGameFactory.initBonds(GAME_TYPE); + vm.deal(address(this), _value); + + assertEq(address(gameProxy).balance, 0); + gameProxy = FaultDisputeGame( + payable(address(disputeGameFactory.create{ value: _value }(GAME_TYPE, ROOT_CLAIM, abi.encode(1)))) + ); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); + } + + /// @dev Tests that the game cannot be initialized with extra data of the incorrect length (must be 32 bytes) + function testFuzz_initialize_badExtraData_reverts(uint256 _extraDataLen) public { + // The `DisputeGameFactory` will pack the root claim and the extra data into a single array, which is enforced + // to be at least 64 bytes long. + // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the contract size limit + // in this test, as CWIA proxies store the immutable args in their bytecode. + // [0 bytes, 31 bytes] u [33 bytes, 23.5 KB] + _extraDataLen = bound(_extraDataLen, 0, 23_500); + if (_extraDataLen == 32) { + _extraDataLen++; + } + bytes memory _extraData = new bytes(_extraDataLen); + + // Assign the first 32 bytes in `extraData` to a valid L2 block number passed the starting block. + (, uint256 startingL2Block) = gameProxy.startingOutputRoot(); + assembly { + mstore(add(_extraData, 0x20), add(startingL2Block, 1)) + } + + Claim claim = _dummyClaim(); + vm.expectRevert(abi.encodeWithSelector(BadExtraData.selector)); + gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, _extraData)))); + } + + /// @dev Tests that the game is initialized with the correct data. + function test_initialize_correctData_succeeds() public view { + // Assert that the root claim is initialized correctly. + ( + uint32 parentIndex, + address counteredBy, + address claimant, + uint128 bond, + Claim claim, + Position position, + Clock clock + ) = gameProxy.claimData(0); + assertEq(parentIndex, type(uint32).max); + assertEq(counteredBy, address(0)); + assertEq(claimant, address(this)); + assertEq(bond, 0); + assertEq(claim.raw(), ROOT_CLAIM.raw()); + assertEq(position.raw(), 1); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); + + // Assert that the `createdAt` timestamp is correct. + assertEq(gameProxy.createdAt().raw(), block.timestamp); + + // Assert that the blockhash provided is correct. + assertEq(gameProxy.l1Head().raw(), blockhash(block.number - 1)); + } + + /// @dev Tests that the game cannot be initialized twice. + function test_initialize_onlyOnce_succeeds() public { + vm.expectRevert(AlreadyInitialized.selector); + gameProxy.initialize(); + } + + /// @dev Tests that the user cannot control the first 4 bytes of the CWIA data, disallowing them to control the + /// entrypoint when no calldata is provided to a call. + function test_cwiaCalldata_userCannotControlSelector_succeeds() public { + // Construct the expected CWIA data that the proxy will pass to the implementation, alongside any extra + // calldata passed by the user. + Hash l1Head = gameProxy.l1Head(); + bytes memory cwiaData = abi.encodePacked(address(this), gameProxy.rootClaim(), l1Head, gameProxy.extraData()); + + // We expect a `ReceiveETH` event to be emitted when 0 bytes of calldata are sent; The fallback is always + // reached *within the minimal proxy* in `LibClone`'s version of `clones-with-immutable-args` + vm.expectEmit(false, false, false, true); + emit ReceiveETH(0); + // We expect no delegatecall to the implementation contract if 0 bytes are sent. Assert that this happens + // 0 times. + vm.expectCall(address(gameImpl), cwiaData, 0); + (bool successA,) = address(gameProxy).call(hex""); + assertTrue(successA); + + // When calldata is forwarded, we do expect a delegatecall to the implementation. + bytes memory data = abi.encodePacked(gameProxy.l1Head.selector); + vm.expectCall(address(gameImpl), abi.encodePacked(data, cwiaData), 1); + (bool successB, bytes memory returnData) = address(gameProxy).call(data); + assertTrue(successB); + assertEq(returnData, abi.encode(l1Head)); + } + + /// @dev Tests that the bond during the bisection game depths is correct. + function test_getRequiredBond_succeeds() public view { + for (uint8 i = 0; i < uint8(gameProxy.splitDepth()); i++) { + Position pos = LibPosition.wrap(i, 0); + uint256 bond = gameProxy.getRequiredBond(pos); + + // Reasonable approximation for a max depth of 8. + uint256 expected = 0.08 ether; + for (uint64 j = 0; j < i; j++) { + expected = expected * 22876; + expected = expected / 10000; + } + + assertApproxEqAbs(bond, expected, 0.01 ether); + } + } + + /// @dev Tests that the bond at a depth greater than the maximum game depth reverts. + function test_getRequiredBond_outOfBounds_reverts() public { + Position pos = LibPosition.wrap(uint8(gameProxy.maxGameDepth() + 1), 0); + vm.expectRevert(GameDepthExceeded.selector); + gameProxy.getRequiredBond(pos); + } + + /// @dev Tests that a move while the game status is not `IN_PROGRESS` causes the call to revert + /// with the `GameNotInProgress` error + function test_move_gameNotInProgress_reverts() public { + uint256 chalWins = uint256(GameStatus.CHALLENGER_WINS); + + // Replace the game status in storage. It exists in slot 0 at offset 16. + uint256 slot = uint256(vm.load(address(gameProxy), bytes32(0))); + uint256 offset = 16 << 3; + uint256 mask = 0xFF << offset; + // Replace the byte in the slot value with the challenger wins status. + slot = (slot & ~mask) | (chalWins << offset); + vm.store(address(gameProxy), bytes32(0), bytes32(slot)); + + // Ensure that the game status was properly updated. + GameStatus status = gameProxy.status(); + assertEq(uint256(status), chalWins); + + // Attempt to make a move. Should revert. + vm.expectRevert(GameNotInProgress.selector); + gameProxy.attack(0, Claim.wrap(0)); + } + + /// @dev Tests that an attempt to defend the root claim reverts with the `CannotDefendRootClaim` error. + function test_move_defendRoot_reverts() public { + vm.expectRevert(CannotDefendRootClaim.selector); + gameProxy.defend(0, _dummyClaim()); + } + + /// @dev Tests that an attempt to move against a claim that does not exist reverts with the + /// `ParentDoesNotExist` error. + function test_move_nonExistentParent_reverts() public { + Claim claim = _dummyClaim(); + + // Expect an out of bounds revert for an attack + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x32)); + gameProxy.attack(1, claim); + + // Expect an out of bounds revert for a defense + vm.expectRevert(abi.encodeWithSignature("Panic(uint256)", 0x32)); + gameProxy.defend(1, claim); + } + + /// @dev Tests that an attempt to move at the maximum game depth reverts with the + /// `GameDepthExceeded` error. + function test_move_gameDepthExceeded_reverts() public { + Claim claim = _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC); + + uint256 maxDepth = gameProxy.maxGameDepth(); + + for (uint256 i = 0; i <= maxDepth; i++) { + // At the max game depth, the `_move` function should revert with + // the `GameDepthExceeded` error. + if (i == maxDepth) { + vm.expectRevert(GameDepthExceeded.selector); + gameProxy.attack{ value: 100 ether }(i, claim); + } else { + gameProxy.attack{ value: _getRequiredBond(i) }(i, claim); + } + } + } + + /// @dev Tests that a move made after the clock time has exceeded reverts with the + /// `ClockTimeExceeded` error. + function test_move_clockTimeExceeded_reverts() public { + // Warp ahead past the clock time for the first move (3 1/2 days) + vm.warp(block.timestamp + 3 days + 12 hours + 1); + uint256 bond = _getRequiredBond(0); + vm.expectRevert(ClockTimeExceeded.selector); + gameProxy.attack{ value: bond }(0, _dummyClaim()); + } + + /// @notice Static unit test for the correctness of the chess clock incrementation. + function test_move_clockCorrectness_succeeds() public { + (,,,,,, Clock clock) = gameProxy.claimData(0); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); + + Claim claim = _dummyClaim(); + + vm.warp(block.timestamp + 15); + uint256 bond = _getRequiredBond(0); + gameProxy.attack{ value: bond }(0, claim); + (,,,,,, clock) = gameProxy.claimData(1); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(15), Timestamp.wrap(uint64(block.timestamp))).raw()); + + vm.warp(block.timestamp + 10); + bond = _getRequiredBond(1); + gameProxy.attack{ value: bond }(1, claim); + (,,,,,, clock) = gameProxy.claimData(2); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(10), Timestamp.wrap(uint64(block.timestamp))).raw()); + + // We are at the split depth, so we need to set the status byte of the claim + // for the next move. + claim = _changeClaimStatus(claim, VMStatuses.PANIC); + + vm.warp(block.timestamp + 10); + bond = _getRequiredBond(2); + gameProxy.attack{ value: bond }(2, claim); + (,,,,,, clock) = gameProxy.claimData(3); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(25), Timestamp.wrap(uint64(block.timestamp))).raw()); + + vm.warp(block.timestamp + 10); + bond = _getRequiredBond(3); + gameProxy.attack{ value: bond }(3, claim); + (,,,,,, clock) = gameProxy.claimData(4); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(20), Timestamp.wrap(uint64(block.timestamp))).raw()); + } + + /// @notice Static unit test that checks proper clock extension. + function test_move_clockExtensionCorrectness_succeeds() public { + (,,,,,, Clock clock) = gameProxy.claimData(0); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); + + Claim claim = _dummyClaim(); + uint256 splitDepth = gameProxy.splitDepth(); + uint64 halfGameDuration = gameProxy.maxClockDuration().raw(); + uint64 clockExtension = gameProxy.clockExtension().raw(); + + // Make an initial attack against the root claim with 1 second left on the clock. The grandchild should be + // allocated exactly `clockExtension` seconds remaining on their potential clock. + vm.warp(block.timestamp + halfGameDuration - 1 seconds); + uint256 bond = _getRequiredBond(0); + gameProxy.attack{ value: bond }(0, claim); + (,,,,,, clock) = gameProxy.claimData(1); + assertEq(clock.duration().raw(), halfGameDuration - clockExtension); + + // Warp ahead to the last second of the root claim defender's clock, and bisect all the way down to the move + // above the `SPLIT_DEPTH`. This warp guarantees that all moves from here on out will have clock extensions. + vm.warp(block.timestamp + halfGameDuration - 1 seconds); + for (uint256 i = 1; i < splitDepth - 2; i++) { + bond = _getRequiredBond(i); + gameProxy.attack{ value: bond }(i, claim); + } + + // Warp ahead 1 seconds to have `clockExtension - 1 seconds` left on the next move's clock. + vm.warp(block.timestamp + 1 seconds); + + // The move above the split depth's grand child is the execution trace bisection root. The grandchild should + // be allocated `clockExtension * 2` seconds on their potential clock, if currently they have less than + // `clockExtension` seconds left. + bond = _getRequiredBond(splitDepth - 2); + gameProxy.attack{ value: bond }(splitDepth - 2, claim); + (,,,,,, clock) = gameProxy.claimData(splitDepth - 1); + assertEq(clock.duration().raw(), halfGameDuration - clockExtension * 2); + } + + /// @dev Tests that an identical claim cannot be made twice. The duplicate claim attempt should + /// revert with the `ClaimAlreadyExists` error. + function test_move_duplicateClaim_reverts() public { + Claim claim = _dummyClaim(); + + // Make the first move. This should succeed. + uint256 bond = _getRequiredBond(0); + gameProxy.attack{ value: bond }(0, claim); + + // Attempt to make the same move again. + vm.expectRevert(ClaimAlreadyExists.selector); + gameProxy.attack{ value: bond }(0, claim); + } + + /// @dev Static unit test asserting that identical claims at the same position can be made in different subgames. + function test_move_duplicateClaimsDifferentSubgames_succeeds() public { + Claim claimA = _dummyClaim(); + Claim claimB = _dummyClaim(); + + // Make the first moves. This should succeed. + uint256 bond = _getRequiredBond(0); + gameProxy.attack{ value: bond }(0, claimA); + gameProxy.attack{ value: bond }(0, claimB); + + // Perform an attack at the same position with the same claim value in both subgames. + // These both should succeed. + bond = _getRequiredBond(1); + gameProxy.attack{ value: bond }(1, claimA); + bond = _getRequiredBond(2); + gameProxy.attack{ value: bond }(2, claimA); + } + + /// @dev Static unit test for the correctness of an opening attack. + function test_move_simpleAttack_succeeds() public { + // Warp ahead 5 seconds. + vm.warp(block.timestamp + 5); + + Claim counter = _dummyClaim(); + + // Perform the attack. + uint256 reqBond = _getRequiredBond(0); + vm.expectEmit(true, true, true, false); + emit Move(0, counter, address(this)); + gameProxy.attack{ value: reqBond }(0, counter); + + // Grab the claim data of the attack. + ( + uint32 parentIndex, + address counteredBy, + address claimant, + uint128 bond, + Claim claim, + Position position, + Clock clock + ) = gameProxy.claimData(1); + + // Assert correctness of the attack claim's data. + assertEq(parentIndex, 0); + assertEq(counteredBy, address(0)); + assertEq(claimant, address(this)); + assertEq(bond, reqBond); + assertEq(claim.raw(), counter.raw()); + assertEq(position.raw(), Position.wrap(1).move(true).raw()); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(5), Timestamp.wrap(uint64(block.timestamp))).raw()); + + // Grab the claim data of the parent. + (parentIndex, counteredBy, claimant, bond, claim, position, clock) = gameProxy.claimData(0); + + // Assert correctness of the parent claim's data. + assertEq(parentIndex, type(uint32).max); + assertEq(counteredBy, address(0)); + assertEq(claimant, address(this)); + assertEq(bond, 0); + assertEq(claim.raw(), ROOT_CLAIM.raw()); + assertEq(position.raw(), 1); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp - 5))).raw()); + } + + /// @dev Tests that making a claim at the execution trace bisection root level with an invalid status + /// byte reverts with the `UnexpectedRootClaim` error. + function test_move_incorrectStatusExecRoot_reverts() public { + for (uint256 i; i < 4; i++) { + gameProxy.attack{ value: _getRequiredBond(i) }(i, _dummyClaim()); + } + + uint256 bond = _getRequiredBond(4); + vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, bytes32(0))); + gameProxy.attack{ value: bond }(4, Claim.wrap(bytes32(0))); + } + + /// @dev Tests that making a claim at the execution trace bisection root level with a valid status + /// byte succeeds. + function test_move_correctStatusExecRoot_succeeds() public { + for (uint256 i; i < 4; i++) { + uint256 bond = _getRequiredBond(i); + gameProxy.attack{ value: bond }(i, _dummyClaim()); + } + uint256 lastBond = _getRequiredBond(4); + gameProxy.attack{ value: lastBond }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + } + + /// @dev Static unit test asserting that a move reverts when the bonded amount is incorrect. + function test_move_incorrectBondAmount_reverts() public { + vm.expectRevert(IncorrectBondAmount.selector); + gameProxy.attack{ value: 0 }(0, _dummyClaim()); + } + + /// @dev Tests that a claim cannot be stepped against twice. + function test_step_duplicateStep_reverts() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + // Make claims all the way down the tree. + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(2) }(2, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(3) }(3, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(4) }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + gameProxy.attack{ value: _getRequiredBond(5) }(5, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(6) }(6, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(7) }(7, _dummyClaim()); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + gameProxy.step(8, true, absolutePrestateData, hex""); + + vm.expectRevert(DuplicateStep.selector); + gameProxy.step(8, true, absolutePrestateData, hex""); + } + + /// @dev Tests that successfully step with true attacking claim when there is a true defend claim(claim5) in the + /// middle of the dispute game. + function test_stepAttackDummyClaim_defendTrueClaimInTheMiddle_succeeds() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + // Make claims all the way down the tree. + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(2) }(2, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(3) }(3, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(4) }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + bytes memory claimData5 = abi.encode(5, 5); + Claim claim5 = Claim.wrap(keccak256(claimData5)); + gameProxy.attack{ value: _getRequiredBond(5) }(5, claim5); + gameProxy.defend{ value: _getRequiredBond(6) }(6, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(7) }(7, _dummyClaim()); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + gameProxy.step(8, true, claimData5, hex""); + } + + /// @dev Tests that step reverts with false attacking claim when there is a true defend claim(claim5) in the middle + /// of the dispute game. + function test_stepAttackTrueClaim_defendTrueClaimInTheMiddle_reverts() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + // Make claims all the way down the tree. + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(2) }(2, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(3) }(3, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(4) }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + bytes memory claimData5 = abi.encode(5, 5); + Claim claim5 = Claim.wrap(keccak256(claimData5)); + gameProxy.attack{ value: _getRequiredBond(5) }(5, claim5); + gameProxy.defend{ value: _getRequiredBond(6) }(6, _dummyClaim()); + Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData5, hex"", bytes32(0))); + gameProxy.attack{ value: _getRequiredBond(7) }(7, postState_); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + + vm.expectRevert(ValidStep.selector); + gameProxy.step(8, true, claimData5, hex""); + } + + /// @dev Tests that step reverts with false defending claim when there is a true defend claim(postState_) in the + /// middle of the dispute game. + function test_stepDefendDummyClaim_defendTrueClaimInTheMiddle_reverts() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + // Make claims all the way down the tree. + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(2) }(2, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(3) }(3, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(4) }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + + bytes memory claimData7 = abi.encode(5, 5); + Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData7, hex"", bytes32(0))); + + gameProxy.attack{ value: _getRequiredBond(5) }(5, postState_); + gameProxy.defend{ value: _getRequiredBond(6) }(6, _dummyClaim()); + + bytes memory _dummyClaimData = abi.encode(gasleft(), gasleft()); + Claim dummyClaim7 = Claim.wrap(keccak256(_dummyClaimData)); + gameProxy.attack{ value: _getRequiredBond(7) }(7, dummyClaim7); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + vm.expectRevert(ValidStep.selector); + gameProxy.step(8, false, _dummyClaimData, hex""); + } + + /// @dev Tests that step reverts with true defending claim when there is a true defend claim(postState_) in the + /// middle of the dispute game. + function test_stepDefendTrueClaim_defendTrueClaimInTheMiddle_reverts() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + // Make claims all the way down the tree. + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(2) }(2, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(3) }(3, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(4) }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + + bytes memory claimData7 = abi.encode(5, 5); + Claim claim7 = Claim.wrap(keccak256(claimData7)); + Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData7, hex"", bytes32(0))); + + gameProxy.attack{ value: _getRequiredBond(5) }(5, postState_); + gameProxy.defend{ value: _getRequiredBond(6) }(6, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(7) }(7, claim7); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + + vm.expectRevert(ValidStep.selector); + gameProxy.step(8, false, claimData7, hex""); + } + + /// @dev Static unit test for the correctness an uncontested root resolution. + function test_resolve_rootUncontested_succeeds() public { + vm.warp(block.timestamp + 3 days + 12 hours); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + } + + /// @dev Static unit test for the correctness an uncontested root resolution. + function test_resolve_rootUncontestedClockNotExpired_succeeds() public { + vm.warp(block.timestamp + 3 days + 12 hours - 1 seconds); + vm.expectRevert(ClockNotExpired.selector); + gameProxy.resolveClaim(0, 0); + } + + /// @dev Static unit test for the correctness of a multi-part resolution of a single claim. + function test_resolve_multiPart_succeeds() public { + vm.deal(address(this), 10_000 ether); + + uint256 bond = _getRequiredBond(0); + for (uint256 i = 0; i < 2048; i++) { + gameProxy.attack{ value: bond }(0, Claim.wrap(bytes32(i))); + } + + // Warp past the clock period. + vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds); + + // Resolve all children of the root subgame. Every single one of these will be uncontested. + for (uint256 i = 1; i <= 2048; i++) { + gameProxy.resolveClaim(i, 0); + } + + // Resolve the first half of the root claim subgame. + gameProxy.resolveClaim(0, 1024); + + // Fetch the resolution checkpoint for the root subgame and assert correctness. + (bool initCheckpoint, uint32 subgameIndex, Position leftmostPosition, address counteredBy) = + gameProxy.resolutionCheckpoints(0); + assertTrue(initCheckpoint); + assertEq(subgameIndex, 1024); + assertEq(leftmostPosition.raw(), Position.wrap(1).move(true).raw()); + assertEq(counteredBy, address(this)); + + // The root subgame should not be resolved. + assertFalse(gameProxy.resolvedSubgames(0)); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolve(); + + // Resolve the second half of the root claim subgame. + uint256 numToResolve = gameProxy.getNumToResolve(0); + assertEq(numToResolve, 1024); + gameProxy.resolveClaim(0, numToResolve); + + // Fetch the resolution checkpoint for the root subgame and assert correctness. + (initCheckpoint, subgameIndex, leftmostPosition, counteredBy) = gameProxy.resolutionCheckpoints(0); + assertTrue(initCheckpoint); + assertEq(subgameIndex, 2048); + assertEq(leftmostPosition.raw(), Position.wrap(1).move(true).raw()); + assertEq(counteredBy, address(this)); + + // The root subgame should now be resolved + assertTrue(gameProxy.resolvedSubgames(0)); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); + } + + /// @dev Static unit test asserting that resolve reverts when the absolute root + /// subgame has not been resolved. + function test_resolve_rootUncontestedButUnresolved_reverts() public { + vm.warp(block.timestamp + 3 days + 12 hours); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolve(); + } + + /// @dev Static unit test asserting that resolve reverts when the game state is + /// not in progress. + function test_resolve_notInProgress_reverts() public { + uint256 chalWins = uint256(GameStatus.CHALLENGER_WINS); + + // Replace the game status in storage. It exists in slot 0 at offset 16. + uint256 slot = uint256(vm.load(address(gameProxy), bytes32(0))); + uint256 offset = 16 << 3; + uint256 mask = 0xFF << offset; + // Replace the byte in the slot value with the challenger wins status. + slot = (slot & ~mask) | (chalWins << offset); + + vm.store(address(gameProxy), bytes32(uint256(0)), bytes32(slot)); + vm.expectRevert(GameNotInProgress.selector); + gameProxy.resolveClaim(0, 0); + } + + /// @dev Static unit test for the correctness of resolving a single attack game state. + function test_resolve_rootContested_succeeds() public { + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + + vm.warp(block.timestamp + 3 days + 12 hours); + + gameProxy.resolveClaim(1, 0); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); + } + + /// @dev Static unit test for the correctness of resolving a game with a contested challenge claim. + function test_resolve_challengeContested_succeeds() public { + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.defend{ value: _getRequiredBond(1) }(1, _dummyClaim()); + + vm.warp(block.timestamp + 3 days + 12 hours); + + gameProxy.resolveClaim(2, 0); + gameProxy.resolveClaim(1, 0); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + } + + /// @dev Static unit test for the correctness of resolving a game with multiplayer moves. + function test_resolve_teamDeathmatch_succeeds() public { + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.defend{ value: _getRequiredBond(1) }(1, _dummyClaim()); + gameProxy.defend{ value: _getRequiredBond(1) }(1, _dummyClaim()); + + vm.warp(block.timestamp + 3 days + 12 hours); + + gameProxy.resolveClaim(4, 0); + gameProxy.resolveClaim(3, 0); + gameProxy.resolveClaim(2, 0); + gameProxy.resolveClaim(1, 0); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); + } + + /// @dev Static unit test for the correctness of resolving a game that reaches max game depth. + function test_resolve_stepReached_succeeds() public { + Claim claim = _dummyClaim(); + for (uint256 i; i < gameProxy.splitDepth(); i++) { + gameProxy.attack{ value: _getRequiredBond(i) }(i, claim); + } + + claim = _changeClaimStatus(claim, VMStatuses.PANIC); + for (uint256 i = gameProxy.claimDataLen() - 1; i < gameProxy.maxGameDepth(); i++) { + gameProxy.attack{ value: _getRequiredBond(i) }(i, claim); + } + + vm.warp(block.timestamp + 3 days + 12 hours); + + for (uint256 i = 9; i > 0; i--) { + gameProxy.resolveClaim(i - 1, 0); + } + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + } + + /// @dev Static unit test asserting that resolve reverts when attempting to resolve a subgame multiple times + function test_resolve_claimAlreadyResolved_reverts() public { + Claim claim = _dummyClaim(); + uint256 firstBond = _getRequiredBond(0); + vm.deal(address(this), firstBond); + gameProxy.attack{ value: firstBond }(0, claim); + uint256 secondBond = _getRequiredBond(1); + vm.deal(address(this), secondBond); + gameProxy.attack{ value: secondBond }(1, claim); + + vm.warp(block.timestamp + 3 days + 12 hours); + + assertEq(address(this).balance, 0); + gameProxy.resolveClaim(2, 0); + gameProxy.resolveClaim(1, 0); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + assertEq(address(this).balance, firstBond + secondBond); + + vm.expectRevert(ClaimAlreadyResolved.selector); + gameProxy.resolveClaim(1, 0); + assertEq(address(this).balance, firstBond + secondBond); + } + + /// @dev Static unit test asserting that resolve reverts when attempting to resolve a subgame at max depth + function test_resolve_claimAtMaxDepthAlreadyResolved_reverts() public { + Claim claim = _dummyClaim(); + for (uint256 i; i < gameProxy.splitDepth(); i++) { + gameProxy.attack{ value: _getRequiredBond(i) }(i, claim); + } + + vm.deal(address(this), 10000 ether); + claim = _changeClaimStatus(claim, VMStatuses.PANIC); + for (uint256 i = gameProxy.claimDataLen() - 1; i < gameProxy.maxGameDepth(); i++) { + gameProxy.attack{ value: _getRequiredBond(i) }(i, claim); + } + + vm.warp(block.timestamp + 3 days + 12 hours); + + // Resolve to claim bond + uint256 balanceBefore = address(this).balance; + gameProxy.resolveClaim(8, 0); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + assertEq(address(this).balance, balanceBefore + _getRequiredBond(7)); + + vm.expectRevert(ClaimAlreadyResolved.selector); + gameProxy.resolveClaim(8, 0); + } + + /// @dev Static unit test asserting that resolve reverts when attempting to resolve subgames out of order + function test_resolve_outOfOrderResolution_reverts() public { + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + + vm.warp(block.timestamp + 3 days + 12 hours); + + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(0, 0); + } + + /// @dev Static unit test asserting that resolve pays out bonds on step, output bisection, and execution trace + /// moves. + function test_resolve_bondPayouts_succeeds() public { + // Give the test contract some ether + uint256 bal = 1000 ether; + vm.deal(address(this), bal); + + // Make claims all the way down the tree. + uint256 bond = _getRequiredBond(0); + uint256 totalBonded = bond; + gameProxy.attack{ value: bond }(0, _dummyClaim()); + bond = _getRequiredBond(1); + totalBonded += bond; + gameProxy.attack{ value: bond }(1, _dummyClaim()); + bond = _getRequiredBond(2); + totalBonded += bond; + gameProxy.attack{ value: bond }(2, _dummyClaim()); + bond = _getRequiredBond(3); + totalBonded += bond; + gameProxy.attack{ value: bond }(3, _dummyClaim()); + bond = _getRequiredBond(4); + totalBonded += bond; + gameProxy.attack{ value: bond }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + bond = _getRequiredBond(5); + totalBonded += bond; + gameProxy.attack{ value: bond }(5, _dummyClaim()); + bond = _getRequiredBond(6); + totalBonded += bond; + gameProxy.attack{ value: bond }(6, _dummyClaim()); + bond = _getRequiredBond(7); + totalBonded += bond; + gameProxy.attack{ value: bond }(7, _dummyClaim()); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + gameProxy.step(8, true, absolutePrestateData, hex""); + + // Ensure that the step successfully countered the leaf claim. + (, address counteredBy,,,,,) = gameProxy.claimData(8); + assertEq(counteredBy, address(this)); + + // Ensure we bonded the correct amounts + assertEq(address(this).balance, bal - totalBonded); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), totalBonded); + + // Resolve all claims + vm.warp(block.timestamp + 3 days + 12 hours); + for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { + (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); + assertTrue(success); + } + gameProxy.resolve(); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + + // Ensure that bonds were paid out correctly. + assertEq(address(this).balance, bal); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), 0); + + // Ensure that the init bond for the game is 0, in case we change it in the test suite in the future. + assertEq(disputeGameFactory.initBonds(GAME_TYPE), 0); + } + + /// @dev Static unit test asserting that resolve pays out bonds on step, output bisection, and execution trace + /// moves with 2 actors and a dishonest root claim. + function test_resolve_bondPayoutsSeveralActors_succeeds() public { + // Give the test contract and bob some ether + uint256 bal = 1000 ether; + address bob = address(0xb0b); + vm.deal(address(this), bal); + vm.deal(bob, bal); + + // Make claims all the way down the tree, trading off between bob and the test contract. + uint256 firstBond = _getRequiredBond(0); + uint256 thisBonded = firstBond; + gameProxy.attack{ value: firstBond }(0, _dummyClaim()); + + uint256 secondBond = _getRequiredBond(1); + uint256 bobBonded = secondBond; + vm.prank(bob); + gameProxy.attack{ value: secondBond }(1, _dummyClaim()); + + uint256 thirdBond = _getRequiredBond(2); + thisBonded += thirdBond; + gameProxy.attack{ value: thirdBond }(2, _dummyClaim()); + + uint256 fourthBond = _getRequiredBond(3); + bobBonded += fourthBond; + vm.prank(bob); + gameProxy.attack{ value: fourthBond }(3, _dummyClaim()); + + uint256 fifthBond = _getRequiredBond(4); + thisBonded += fifthBond; + gameProxy.attack{ value: fifthBond }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + + uint256 sixthBond = _getRequiredBond(5); + bobBonded += sixthBond; + vm.prank(bob); + gameProxy.attack{ value: sixthBond }(5, _dummyClaim()); + + uint256 seventhBond = _getRequiredBond(6); + thisBonded += seventhBond; + gameProxy.attack{ value: seventhBond }(6, _dummyClaim()); + + uint256 eighthBond = _getRequiredBond(7); + bobBonded += eighthBond; + vm.prank(bob); + gameProxy.attack{ value: eighthBond }(7, _dummyClaim()); + + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 8, 0); + gameProxy.step(8, true, absolutePrestateData, hex""); + + // Ensure that the step successfully countered the leaf claim. + (, address counteredBy,,,,,) = gameProxy.claimData(8); + assertEq(counteredBy, address(this)); + + // Ensure we bonded the correct amounts + assertEq(address(this).balance, bal - thisBonded); + assertEq(bob.balance, bal - bobBonded); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), thisBonded + bobBonded); + + // Resolve all claims + vm.warp(block.timestamp + 3 days + 12 hours); + for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { + (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); + assertTrue(success); + } + gameProxy.resolve(); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + + // Bob's claim should revert since it's value is 0 + vm.expectRevert(NoCreditToClaim.selector); + gameProxy.claimCredit(bob); + + // Ensure that bonds were paid out correctly. + assertEq(address(this).balance, bal + bobBonded); + assertEq(bob.balance, bal - bobBonded); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), 0); + + // Ensure that the init bond for the game is 0, in case we change it in the test suite in the future. + assertEq(disputeGameFactory.initBonds(GAME_TYPE), 0); + } + + /// @dev Static unit test asserting that resolve pays out bonds on moves to the leftmost actor + /// in subgames containing successful counters. + function test_resolve_leftmostBondPayout_succeeds() public { + uint256 bal = 1000 ether; + address alice = address(0xa11ce); + address bob = address(0xb0b); + address charlie = address(0xc0c); + vm.deal(address(this), bal); + vm.deal(alice, bal); + vm.deal(bob, bal); + vm.deal(charlie, bal); + + // Make claims with bob, charlie and the test contract on defense, and alice as the challenger + // charlie is successfully countered by alice + // alice is successfully countered by both bob and the test contract + uint256 firstBond = _getRequiredBond(0); + vm.prank(alice); + gameProxy.attack{ value: firstBond }(0, _dummyClaim()); + + uint256 secondBond = _getRequiredBond(1); + vm.prank(bob); + gameProxy.defend{ value: secondBond }(1, _dummyClaim()); + vm.prank(charlie); + gameProxy.attack{ value: secondBond }(1, _dummyClaim()); + gameProxy.attack{ value: secondBond }(1, _dummyClaim()); + + uint256 thirdBond = _getRequiredBond(3); + vm.prank(alice); + gameProxy.attack{ value: thirdBond }(3, _dummyClaim()); + + // Resolve all claims + vm.warp(block.timestamp + 3 days + 12 hours); + for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { + (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); + assertTrue(success); + } + gameProxy.resolve(); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + gameProxy.claimCredit(address(this)); + gameProxy.claimCredit(alice); + gameProxy.claimCredit(bob); + + // Charlie's claim should revert since it's value is 0 + vm.expectRevert(NoCreditToClaim.selector); + gameProxy.claimCredit(charlie); + + // Ensure that bonds were paid out correctly. + uint256 aliceLosses = firstBond; + uint256 charlieLosses = secondBond; + assertEq(address(this).balance, bal + aliceLosses, "incorrect this balance"); + assertEq(alice.balance, bal - aliceLosses + charlieLosses, "incorrect alice balance"); + assertEq(bob.balance, bal, "incorrect bob balance"); + assertEq(charlie.balance, bal - charlieLosses, "incorrect charlie balance"); + assertEq(address(gameProxy).balance, 0); + + // Ensure that the init bond for the game is 0, in case we change it in the test suite in the future. + assertEq(disputeGameFactory.initBonds(GAME_TYPE), 0); + } + + /// @dev Static unit test asserting that the anchor state updates when the game resolves in + /// favor of the defender and the anchor state is older than the game state. + function test_resolve_validNewerStateUpdatesAnchor_succeeds() public { + // Confirm that the anchor state is older than the game state. + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assert(l2BlockNumber < gameProxy.l2BlockNumber()); + + // Resolve the game. + vm.warp(block.timestamp + 3 days + 12 hours); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + + // Confirm that the anchor state is now the same as the game state. + (root, l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assertEq(l2BlockNumber, gameProxy.l2BlockNumber()); + assertEq(root.raw(), gameProxy.rootClaim().raw()); + } + + /// @dev Static unit test asserting that the anchor state does not change when the game + /// resolves in favor of the defender but the game state is not newer than the anchor state. + function test_resolve_validOlderStateSameAnchor_succeeds() public { + // Mock the game block to be older than the game state. + vm.mockCall(address(gameProxy), abi.encodeWithSelector(gameProxy.l2BlockNumber.selector), abi.encode(0)); + + // Confirm that the anchor state is newer than the game state. + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assert(l2BlockNumber >= gameProxy.l2BlockNumber()); + + // Resolve the game. + vm.mockCall(address(gameProxy), abi.encodeWithSelector(gameProxy.l2BlockNumber.selector), abi.encode(0)); + vm.warp(block.timestamp + 3 days + 12 hours); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + + // Confirm that the anchor state is the same as the initial anchor state. + (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assertEq(updatedL2BlockNumber, l2BlockNumber); + assertEq(updatedRoot.raw(), root.raw()); + } + + /// @dev Static unit test asserting that the anchor state does not change when the game + /// resolves in favor of the challenger, even if the game state is newer than the anchor. + function test_resolve_invalidStateSameAnchor_succeeds() public { + // Confirm that the anchor state is older than the game state. + (Hash root, uint256 l2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assert(l2BlockNumber < gameProxy.l2BlockNumber()); + + // Challenge the claim and resolve it. + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + vm.warp(block.timestamp + 3 days + 12 hours); + gameProxy.resolveClaim(1, 0); + gameProxy.resolveClaim(0, 0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); + + // Confirm that the anchor state is the same as the initial anchor state. + (Hash updatedRoot, uint256 updatedL2BlockNumber) = anchorStateRegistry.anchors(gameProxy.gameType()); + assertEq(updatedL2BlockNumber, l2BlockNumber); + assertEq(updatedRoot.raw(), root.raw()); + } + + /// @dev Static unit test asserting that credit may not be drained past allowance through reentrancy. + function test_claimCredit_claimAlreadyResolved_reverts() public { + ClaimCreditReenter reenter = new ClaimCreditReenter(gameProxy, vm); + vm.startPrank(address(reenter)); + + // Give the game proxy 1 extra ether, unregistered. + vm.deal(address(gameProxy), 1 ether); + + // Perform a bonded move. + Claim claim = _dummyClaim(); + uint256 firstBond = _getRequiredBond(0); + vm.deal(address(reenter), firstBond); + gameProxy.attack{ value: firstBond }(0, claim); + uint256 secondBond = _getRequiredBond(1); + vm.deal(address(reenter), secondBond); + gameProxy.attack{ value: secondBond }(1, claim); + uint256 reenterBond = firstBond + secondBond; + + // Warp past the finalization period + vm.warp(block.timestamp + 3 days + 12 hours); + + // Ensure that we bonded all the test contract's ETH + assertEq(address(reenter).balance, 0); + // Ensure the game proxy has 1 ether in it. + assertEq(address(gameProxy).balance, 1 ether); + // Ensure the game has a balance of reenterBond in the delayedWeth contract. + assertEq(delayedWeth.balanceOf(address(gameProxy)), reenterBond); + + // Resolve the claim at index 2 first so that index 1 can be resolved. + gameProxy.resolveClaim(2, 0); + + // Resolve the claim at index 1 and claim the reenter contract's credit. + gameProxy.resolveClaim(1, 0); + + // Ensure that the game registered the `reenter` contract's credit. + assertEq(gameProxy.credit(address(reenter)), reenterBond); + + // Wait for the withdrawal delay. + vm.warp(block.timestamp + delayedWeth.delay() + 1 seconds); + + // Initiate the reentrant credit claim. + reenter.claimCredit(address(reenter)); + + // The reenter contract should have performed 2 calls to `claimCredit`. + // Once all the credit is claimed, all subsequent calls will revert since there is 0 credit left to claim. + // The claimant must only have received the amount bonded for the gindex 1 subgame. + // The root claim bond and the unregistered ETH should still exist in the game proxy. + assertEq(reenter.numCalls(), 2); + assertEq(address(reenter).balance, reenterBond); + assertEq(address(gameProxy).balance, 1 ether); + assertEq(delayedWeth.balanceOf(address(gameProxy)), 0); + + vm.stopPrank(); + } + + /// @dev Tests that adding local data with an out of bounds identifier reverts. + function testFuzz_addLocalData_oob_reverts(uint256 _ident) public { + // Get a claim below the split depth so that we can add local data for an execution trace subgame. + for (uint256 i; i < 4; i++) { + uint256 bond = _getRequiredBond(i); + gameProxy.attack{ value: bond }(i, _dummyClaim()); + } + uint256 lastBond = _getRequiredBond(4); + gameProxy.attack{ value: lastBond }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + + // [1, 5] are valid local data identifiers. + if (_ident <= 5) _ident = 0; + + vm.expectRevert(InvalidLocalIdent.selector); + gameProxy.addLocalData(_ident, 5, 0); + } + + /// @dev Tests that local data is loaded into the preimage oracle correctly in the subgame + /// that is disputing the transition from `GENESIS -> GENESIS + 1` + function test_addLocalDataGenesisTransition_static_succeeds() public { + IPreimageOracle oracle = IPreimageOracle(address(gameProxy.vm().oracle())); + + // Get a claim below the split depth so that we can add local data for an execution trace subgame. + for (uint256 i; i < 4; i++) { + uint256 bond = _getRequiredBond(i); + gameProxy.attack{ value: bond }(i, Claim.wrap(bytes32(i))); + } + uint256 lastBond = _getRequiredBond(4); + gameProxy.attack{ value: lastBond }(4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + + // Expected start/disputed claims + (Hash root,) = gameProxy.startingOutputRoot(); + bytes32 startingClaim = root.raw(); + bytes32 disputedClaim = bytes32(uint256(3)); + Position disputedPos = LibPosition.wrap(4, 0); + + // Expected local data + bytes32[5] memory data = [ + gameProxy.l1Head().raw(), + startingClaim, + disputedClaim, + bytes32(uint256(1) << 0xC0), + bytes32(gameProxy.l2ChainId() << 0xC0) + ]; + + for (uint256 i = 1; i <= 5; i++) { + uint256 expectedLen = i > 3 ? 8 : 32; + bytes32 key = _getKey(i, keccak256(abi.encode(disputedClaim, disputedPos))); + + gameProxy.addLocalData(i, 5, 0); + (bytes32 dat, uint256 datLen) = oracle.readPreimage(key, 0); + assertEq(dat >> 0xC0, bytes32(expectedLen)); + // Account for the length prefix if i > 3 (the data stored + // at identifiers i <= 3 are 32 bytes long, so the expected + // length is already correct. If i > 3, the data is only 8 + // bytes long, so the length prefix + the data is 16 bytes + // total.) + assertEq(datLen, expectedLen + (i > 3 ? 8 : 0)); + + gameProxy.addLocalData(i, 5, 8); + (dat, datLen) = oracle.readPreimage(key, 8); + assertEq(dat, data[i - 1]); + assertEq(datLen, expectedLen); + } + } + + /// @dev Tests that local data is loaded into the preimage oracle correctly. + function test_addLocalDataMiddle_static_succeeds() public { + IPreimageOracle oracle = IPreimageOracle(address(gameProxy.vm().oracle())); + + // Get a claim below the split depth so that we can add local data for an execution trace subgame. + for (uint256 i; i < 4; i++) { + uint256 bond = _getRequiredBond(i); + gameProxy.attack{ value: bond }(i, Claim.wrap(bytes32(i))); + } + uint256 lastBond = _getRequiredBond(4); + gameProxy.defend{ value: lastBond }(4, _changeClaimStatus(ROOT_CLAIM, VMStatuses.VALID)); + + // Expected start/disputed claims + bytes32 startingClaim = bytes32(uint256(3)); + Position startingPos = LibPosition.wrap(4, 0); + bytes32 disputedClaim = bytes32(uint256(2)); + Position disputedPos = LibPosition.wrap(3, 0); + + // Expected local data + bytes32[5] memory data = [ + gameProxy.l1Head().raw(), + startingClaim, + disputedClaim, + bytes32(uint256(2) << 0xC0), + bytes32(gameProxy.l2ChainId() << 0xC0) + ]; + + for (uint256 i = 1; i <= 5; i++) { + uint256 expectedLen = i > 3 ? 8 : 32; + bytes32 key = _getKey(i, keccak256(abi.encode(startingClaim, startingPos, disputedClaim, disputedPos))); + + gameProxy.addLocalData(i, 5, 0); + (bytes32 dat, uint256 datLen) = oracle.readPreimage(key, 0); + assertEq(dat >> 0xC0, bytes32(expectedLen)); + // Account for the length prefix if i > 3 (the data stored + // at identifiers i <= 3 are 32 bytes long, so the expected + // length is already correct. If i > 3, the data is only 8 + // bytes long, so the length prefix + the data is 16 bytes + // total.) + assertEq(datLen, expectedLen + (i > 3 ? 8 : 0)); + + gameProxy.addLocalData(i, 5, 8); + (dat, datLen) = oracle.readPreimage(key, 8); + assertEq(dat, data[i - 1]); + assertEq(datLen, expectedLen); + } + } + + /// @dev Static unit test asserting that resolveClaim isn't possible if there's time + /// left for a counter. + function test_resolution_lastSecondDisputes_succeeds() public { + // The honest proposer created an honest root claim during setup - node 0 + + // Defender's turn + vm.warp(block.timestamp + 3.5 days - 1 seconds); + gameProxy.attack{ value: _getRequiredBond(0) }(0, _dummyClaim()); + // Chess clock time accumulated: + assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days - 1 seconds); + assertEq(gameProxy.getChallengerDuration(1).raw(), 0); + + // Advance time by 1 second, so that the root claim challenger clock is expired. + vm.warp(block.timestamp + 1 seconds); + // Attempt a second attack against the root claim. This should revert since the challenger clock is expired. + uint256 expectedBond = _getRequiredBond(0); + vm.expectRevert(ClockTimeExceeded.selector); + gameProxy.attack{ value: expectedBond }(0, _dummyClaim()); + // Chess clock time accumulated: + assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); + assertEq(gameProxy.getChallengerDuration(1).raw(), 1 seconds); + + // Should not be able to resolve the root claim or second counter yet. + vm.expectRevert(ClockNotExpired.selector); + gameProxy.resolveClaim(1, 0); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(0, 0); + + // Warp to the last second of the root claim defender clock. + vm.warp(block.timestamp + 3.5 days - 2 seconds); + // Attack the challenge to the root claim. This should succeed, since the defender clock is not expired. + gameProxy.attack{ value: _getRequiredBond(1) }(1, _dummyClaim()); + // Chess clock time accumulated: + assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); + assertEq(gameProxy.getChallengerDuration(1).raw(), 3.5 days - 1 seconds); + assertEq(gameProxy.getChallengerDuration(2).raw(), 3.5 days - gameProxy.clockExtension().raw()); + + // Should not be able to resolve any claims yet. + vm.expectRevert(ClockNotExpired.selector); + gameProxy.resolveClaim(2, 0); + vm.expectRevert(ClockNotExpired.selector); + gameProxy.resolveClaim(1, 0); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(0, 0); + + vm.warp(block.timestamp + gameProxy.clockExtension().raw() - 1 seconds); + + // Should not be able to resolve any claims yet. + vm.expectRevert(ClockNotExpired.selector); + gameProxy.resolveClaim(2, 0); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(1, 0); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(0, 0); + + // Chess clock time accumulated: + assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); + assertEq(gameProxy.getChallengerDuration(1).raw(), 3.5 days); + assertEq(gameProxy.getChallengerDuration(2).raw(), 3.5 days - 1 seconds); + + // Warp past the challenge period for the root claim defender. Defending the root claim should now revert. + vm.warp(block.timestamp + 1 seconds); + expectedBond = _getRequiredBond(1); + vm.expectRevert(ClockTimeExceeded.selector); // no further move can be made + gameProxy.attack{ value: expectedBond }(1, _dummyClaim()); + expectedBond = _getRequiredBond(2); + vm.expectRevert(ClockTimeExceeded.selector); // no further move can be made + gameProxy.attack{ value: expectedBond }(2, _dummyClaim()); + // Chess clock time accumulated: + assertEq(gameProxy.getChallengerDuration(0).raw(), 3.5 days); + assertEq(gameProxy.getChallengerDuration(1).raw(), 3.5 days); + assertEq(gameProxy.getChallengerDuration(2).raw(), 3.5 days); + + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(1, 0); + vm.expectRevert(OutOfOrderResolution.selector); + gameProxy.resolveClaim(0, 0); + + // All clocks are expired. Resolve the game. + gameProxy.resolveClaim(2, 0); // Node 2 is resolved as UNCOUNTERED by default since it has no children + gameProxy.resolveClaim(1, 0); // Node 1 is resolved as COUNTERED since it has an UNCOUNTERED child + gameProxy.resolveClaim(0, 0); // Node 0 is resolved as UNCOUNTERED since it has no UNCOUNTERED children + + // Defender wins game since the root claim is uncountered + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); + } + + /// @dev Helper to get the required bond for the given claim index. + function _getRequiredBond(uint256 _claimIndex) internal view returns (uint256 bond_) { + (,,,,, Position parent,) = gameProxy.claimData(_claimIndex); + Position pos = parent.move(true); + bond_ = gameProxy.getRequiredBond(pos); + } + + /// @dev Helper to return a pseudo-random claim + function _dummyClaim() internal view returns (Claim) { + return Claim.wrap(keccak256(abi.encode(gasleft()))); + } + + /// @dev Helper to get the localized key for an identifier in the context of the game proxy. + function _getKey(uint256 _ident, bytes32 _localContext) internal view returns (bytes32) { + bytes32 h = keccak256(abi.encode(_ident | (1 << 248), address(gameProxy), _localContext)); + return bytes32((uint256(h) & ~uint256(0xFF << 248)) | (1 << 248)); + } +} + +contract FaultDispute_1v1_Actors_Test is FaultDisputeGame_Init { + /// @dev The honest actor + DisputeActor internal honest; + /// @dev The dishonest actor + DisputeActor internal dishonest; + + function setUp() public override { + // Setup the `FaultDisputeGame` + super.setUp(); + } + + /// @notice Fuzz test for a 1v1 output bisection dispute. + /// @dev The alphabet game has a constant status byte, and is not safe from someone being dishonest in + /// output bisection and then posting a correct execution trace bisection root claim. This test + /// does not cover this case (i.e. root claim of output bisection is dishonest, root claim of + /// execution trace bisection is made by the dishonest actor but is honest, honest actor cannot + /// attack it without risk of losing). + function testFuzz_outputBisection1v1honestRoot_succeeds(uint8 _divergeOutput, uint8 _divergeStep) public { + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + uint256 divergeAtOutput = bound(_divergeOutput, 0, 15); + uint256 divergeAtStep = bound(_divergeStep, 0, 7); + uint256 divergeStepOffset = (divergeAtOutput << 4) + divergeAtStep; + + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i >= divergeAtOutput ? 0xFF : i + 1; + } + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i >= divergeStepOffset ? bytes1(uint8(0xFF)) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 16, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.DEFENDER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1honestRootGenesisAbsolutePrestate_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are from [2, 17] in this game. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i + 2; + } + // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of all set bits. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = bytes1(0xFF); + } + + // Run the actor test + _actorTest({ + _rootClaim: 16, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.DEFENDER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1dishonestRootGenesisAbsolutePrestate_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are from [2, 17] in this game. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i + 2; + } + // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of all set bits. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = bytes1(0xFF); + } + + // Run the actor test + _actorTest({ + _rootClaim: 17, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.CHALLENGER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1honestRoot_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are from [2, 17] in this game. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i + 2; + } + // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of all zeros. + bytes memory dishonestTrace = new bytes(256); + + // Run the actor test + _actorTest({ + _rootClaim: 16, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.DEFENDER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1dishonestRoot_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are from [2, 17] in this game. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i + 2; + } + // The dishonest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of all zeros. + bytes memory dishonestTrace = new bytes(256); + + // Run the actor test + _actorTest({ + _rootClaim: 17, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.CHALLENGER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1correctRootHalfWay_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are half correct, half incorrect. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + } + // The dishonest trace is half correct, half incorrect. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i > (127 + 4) ? bytes1(0xFF) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 16, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.DEFENDER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1dishonestRootHalfWay_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are half correct, half incorrect. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + } + // The dishonest trace is half correct, half incorrect. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i > (127 + 4) ? bytes1(0xFF) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 0xFF, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.CHALLENGER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1correctAbsolutePrestate_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are half correct, half incorrect. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + } + // The dishonest trace correct is half correct, half incorrect. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i > 127 ? bytes1(0xFF) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 16, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.DEFENDER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1dishonestAbsolutePrestate_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are half correct, half incorrect. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + } + // The dishonest trace correct is half correct, half incorrect. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i > 127 ? bytes1(0xFF) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 0xFF, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.CHALLENGER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1honestRootFinalInstruction_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are half correct, half incorrect. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + } + // The dishonest trace is half correct, and correct all the way up to the final instruction of the exec + // subgame. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i > (127 + 7) ? bytes1(0xFF) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 16, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.DEFENDER_WINS + }); + } + + /// @notice Static unit test for a 1v1 output bisection dispute. + function test_static_1v1dishonestRootFinalInstruction_succeeds() public { + // The honest l2 outputs are from [1, 16] in this game. + uint256[] memory honestL2Outputs = new uint256[](16); + for (uint256 i; i < honestL2Outputs.length; i++) { + honestL2Outputs[i] = i + 1; + } + // The honest trace covers all block -> block + 1 transitions, and is 256 bytes long, consisting + // of bytes [0, 255]. + bytes memory honestTrace = new bytes(256); + for (uint256 i; i < honestTrace.length; i++) { + honestTrace[i] = bytes1(uint8(i)); + } + + // The dishonest l2 outputs are half correct, half incorrect. + uint256[] memory dishonestL2Outputs = new uint256[](16); + for (uint256 i; i < dishonestL2Outputs.length; i++) { + dishonestL2Outputs[i] = i > 7 ? 0xFF : i + 1; + } + // The dishonest trace is half correct, and correct all the way up to the final instruction of the exec + // subgame. + bytes memory dishonestTrace = new bytes(256); + for (uint256 i; i < dishonestTrace.length; i++) { + dishonestTrace[i] = i > (127 + 7) ? bytes1(0xFF) : bytes1(uint8(i)); + } + + // Run the actor test + _actorTest({ + _rootClaim: 0xFF, + _absolutePrestateData: 0, + _honestTrace: honestTrace, + _honestL2Outputs: honestL2Outputs, + _dishonestTrace: dishonestTrace, + _dishonestL2Outputs: dishonestL2Outputs, + _expectedStatus: GameStatus.CHALLENGER_WINS + }); + } + + //////////////////////////////////////////////////////////////// + // HELPERS // + //////////////////////////////////////////////////////////////// + + /// @dev Helper to run a 1v1 actor test + function _actorTest( + uint256 _rootClaim, + uint256 _absolutePrestateData, + bytes memory _honestTrace, + uint256[] memory _honestL2Outputs, + bytes memory _dishonestTrace, + uint256[] memory _dishonestL2Outputs, + GameStatus _expectedStatus + ) + internal + { + // Setup the environment + bytes memory absolutePrestateData = + _setup({ _absolutePrestateData: _absolutePrestateData, _rootClaim: _rootClaim }); + + // Create actors + _createActors({ + _honestTrace: _honestTrace, + _honestPreStateData: absolutePrestateData, + _honestL2Outputs: _honestL2Outputs, + _dishonestTrace: _dishonestTrace, + _dishonestPreStateData: absolutePrestateData, + _dishonestL2Outputs: _dishonestL2Outputs + }); + + // Exhaust all moves from both actors + _exhaustMoves(); + + // Resolve the game and assert that the defender won + _warpAndResolve(); + assertEq(uint8(gameProxy.status()), uint8(_expectedStatus)); + } + + /// @dev Helper to setup the 1v1 test + function _setup( + uint256 _absolutePrestateData, + uint256 _rootClaim + ) + internal + returns (bytes memory absolutePrestateData_) + { + absolutePrestateData_ = abi.encode(_absolutePrestateData); + Claim absolutePrestateExec = + _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData_)), VMStatuses.UNFINISHED); + Claim rootClaim = Claim.wrap(bytes32(uint256(_rootClaim))); + super.init({ rootClaim: rootClaim, absolutePrestate: absolutePrestateExec, l2BlockNumber: _rootClaim }); + } + + /// @dev Helper to create actors for the 1v1 dispute. + function _createActors( + bytes memory _honestTrace, + bytes memory _honestPreStateData, + uint256[] memory _honestL2Outputs, + bytes memory _dishonestTrace, + bytes memory _dishonestPreStateData, + uint256[] memory _dishonestL2Outputs + ) + internal + { + honest = new HonestDisputeActor({ + _gameProxy: gameProxy, + _l2Outputs: _honestL2Outputs, + _trace: _honestTrace, + _preStateData: _honestPreStateData + }); + dishonest = new HonestDisputeActor({ + _gameProxy: gameProxy, + _l2Outputs: _dishonestL2Outputs, + _trace: _dishonestTrace, + _preStateData: _dishonestPreStateData + }); + + vm.deal(address(honest), 100 ether); + vm.deal(address(dishonest), 100 ether); + vm.label(address(honest), "HonestActor"); + vm.label(address(dishonest), "DishonestActor"); + } + + /// @dev Helper to exhaust all moves from both actors. + function _exhaustMoves() internal { + while (true) { + // Allow the dishonest actor to make their moves, and then the honest actor. + (uint256 numMovesA,) = dishonest.move(); + (uint256 numMovesB, bool success) = honest.move(); + + require(success, "Honest actor's moves should always be successful"); + + // If both actors have run out of moves, we're done. + if (numMovesA == 0 && numMovesB == 0) break; + } + } + + /// @dev Helper to warp past the chess clock and resolve all claims within the dispute game. + function _warpAndResolve() internal { + // Warp past the chess clock + vm.warp(block.timestamp + 3 days + 12 hours); + + // Resolve all claims in reverse order. We allow `resolveClaim` calls to fail due to + // the check that prevents claims with no subgames attached from being passed to + // `resolveClaim`. There's also a check in `resolve` to ensure all children have been + // resolved before global resolution, which catches any unresolved subgames here. + for (uint256 i = gameProxy.claimDataLen(); i > 0; i--) { + (bool success,) = address(gameProxy).call(abi.encodeCall(gameProxy.resolveClaim, (i - 1, 0))); + assertTrue(success); + } + gameProxy.resolve(); + } +} + +contract ClaimCreditReenter { + Vm internal immutable vm; + FaultDisputeGame internal immutable GAME; + uint256 public numCalls; + + constructor(FaultDisputeGame _gameProxy, Vm _vm) { + GAME = _gameProxy; + vm = _vm; + } + + function claimCredit(address _recipient) public { + numCalls += 1; + if (numCalls > 1) { + vm.expectRevert(NoCreditToClaim.selector); + } + GAME.claimCredit(_recipient); + } + + receive() external payable { + if (numCalls == 5) { + return; + } + claimCredit(address(this)); + } +} + +/// @dev Helper to change the VM status byte of a claim. +function _changeClaimStatus(Claim _claim, VMStatus _status) pure returns (Claim out_) { + assembly { + out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status)) + } +} From c1d702fbe10ce28d6a6ccf4ea483056048ace048 Mon Sep 17 00:00:00 2001 From: Qi Zhou Date: Wed, 29 May 2024 13:31:32 +0800 Subject: [PATCH 2/2] add LibDA.sol --- .../src/dispute/lib/LibDA.sol | 74 +++++++++++++++++++ .../test/dispute/lib/LibDA.t.sol | 65 ++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 packages/contracts-bedrock/src/dispute/lib/LibDA.sol create mode 100644 packages/contracts-bedrock/test/dispute/lib/LibDA.t.sol diff --git a/packages/contracts-bedrock/src/dispute/lib/LibDA.sol b/packages/contracts-bedrock/src/dispute/lib/LibDA.sol new file mode 100644 index 000000000000..e5318df13d7c --- /dev/null +++ b/packages/contracts-bedrock/src/dispute/lib/LibDA.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +library LibDA { + uint256 constant DA_TYPE_CALLDATA = 0; + uint256 constant DA_TYPE_EIP4844 = 1; + + function getClaimsHash(uint256 daType, uint256 nelemebts, bytes memory data) internal view returns (bytes32 root) { + if (daType == DA_TYPE_EIP4844) { + // TODO: may specify which blob? + root = blobhash(0); + require(root != bytes32(0), "root must not zero"); + return root; + } + + require(daType == DA_TYPE_CALLDATA, "unsupported DA type"); + require(nelemebts * 32 == data.length, "data must 32 * n"); + require(nelemebts > 0, "data must not empty"); + + while (nelemebts != 1) { + for (uint256 i = 0 ; i < nelemebts / 2; i++) { + bytes32 hash; + uint256 roff = i * 32 * 2; + uint256 woff = i * 32; + assembly { + hash := keccak256(add(add(data, 0x20), roff), 64) + mstore(add(add(data, 0x20), woff), hash) + } + } + + // directly copy the last item + if (nelemebts % 2 == 1) { + uint256 roff = (nelemebts - 1) * 32; + uint256 woff = (nelemebts / 2) * 32; + bytes32 hash; + assembly { + hash := mload(add(add(data, 0x20), roff)) + mstore(add(add(data, 0x20), woff), hash) + } + } + + nelemebts = (nelemebts + 1) / 2; + } + + assembly { + root := mload(add(data, 0x20)) + } + } + + function verifyClaimHash(uint256 daType, bytes32 root, uint256 nelements, uint256 idx, bytes32 claimHash, bytes memory proof) internal pure { + require(daType == 0, "unsupported DA type"); + bytes32 hash = claimHash; + uint256 proofOff = 0; + while (nelements != 1) { + if (idx != nelements - 1 || nelements % 2 == 0) { + bytes32 pHash; + require(proofOff < proof.length, "no enough proof"); + assembly { + pHash := mload(add(add(proof, 0x20), proofOff)) + } + proofOff += 32; + if (idx % 2 == 0) { + hash = keccak256(abi.encode(hash, pHash)); + } else { + hash = keccak256(abi.encode(pHash, hash)); + } + } + nelements = (nelements + 1) / 2; + idx = idx / 2; + } + require(root == hash, "proof failed"); + } +} + diff --git a/packages/contracts-bedrock/test/dispute/lib/LibDA.t.sol b/packages/contracts-bedrock/test/dispute/lib/LibDA.t.sol new file mode 100644 index 000000000000..09b4e61ec638 --- /dev/null +++ b/packages/contracts-bedrock/test/dispute/lib/LibDA.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; +import { LibDA } from "src/dispute/lib/LibDA.sol"; + +/// @notice Tests for `LibDA` +contract LibDA_Test is Test { + function test_calldata_one() public view { + bytes32 root; + bytes memory input = "00000000000000000000000000000000"; + root = LibDA.getClaimsHash(LibDA.DA_TYPE_CALLDATA, 1, input); + assertEq(root, bytes32("00000000000000000000000000000000")); + input = "10000000000000000000000000000001"; + root = LibDA.getClaimsHash(LibDA.DA_TYPE_CALLDATA, 1, input); + assertEq(root, bytes32("10000000000000000000000000000001")); + } + + function test_calldata_two() public view { + bytes32 root; + bytes memory input = "0000000000000000000000000000000010000000000000000000000000000001"; + root = LibDA.getClaimsHash(LibDA.DA_TYPE_CALLDATA, 2, input); + assertEq(root, keccak256(abi.encode(bytes32("00000000000000000000000000000000"), bytes32("10000000000000000000000000000001")))); + } + + function test_calldata_three() public view { + bytes32 root; + bytes memory input = "000000000000000000000000000000001000000000000000000000000000000120000000000000000000000000000002"; + root = LibDA.getClaimsHash(LibDA.DA_TYPE_CALLDATA, 3, input); + assertEq(root, keccak256(abi.encode( + keccak256(abi.encode(bytes32("00000000000000000000000000000000"), bytes32("10000000000000000000000000000001"))), + bytes32("20000000000000000000000000000002") + ))); + } + + function test_calldata_seven() public view { + bytes32 root; + bytes memory input = "00000000000000000000000000000000100000000000000000000000000000012000000000000000000000000000000230000000000000000000000000000003400000000000000000000000000000045000000000000000000000000000000560000000000000000000000000000006"; + root = LibDA.getClaimsHash(LibDA.DA_TYPE_CALLDATA, 7, input); + assertEq(root, + keccak256(abi.encode( + keccak256(abi.encode( + keccak256(abi.encode( + bytes32("00000000000000000000000000000000"), + bytes32("10000000000000000000000000000001"))), + keccak256(abi.encode( + bytes32("20000000000000000000000000000002"), + bytes32("30000000000000000000000000000003"))))), + keccak256(abi.encode( + keccak256(abi.encode( + bytes32("40000000000000000000000000000004"), + bytes32("50000000000000000000000000000005"))), + bytes32("60000000000000000000000000000006"))) + ))); + } + + function test_calldata_prove_three() public view { + bytes32 root; + bytes memory input = "000000000000000000000000000000001000000000000000000000000000000120000000000000000000000000000002"; + root = LibDA.getClaimsHash(LibDA.DA_TYPE_CALLDATA, 3, input); + LibDA.verifyClaimHash(LibDA.DA_TYPE_CALLDATA, root, 3, 0, "00000000000000000000000000000000", "1000000000000000000000000000000120000000000000000000000000000002"); + LibDA.verifyClaimHash(LibDA.DA_TYPE_CALLDATA, root, 3, 1, "10000000000000000000000000000001", "0000000000000000000000000000000020000000000000000000000000000002"); + LibDA.verifyClaimHash(LibDA.DA_TYPE_CALLDATA, root, 3, 2, "20000000000000000000000000000002", bytes.concat(keccak256(abi.encode(bytes32("00000000000000000000000000000000"), bytes32("10000000000000000000000000000001"))))); + } +}