github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/test/actors/FaultDisputeActors.sol (about)

     1  // SPDX-License-Identifier: MIT
     2  pragma solidity ^0.8.15;
     3  
     4  import { CommonBase } from "forge-std/Base.sol";
     5  
     6  import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
     7  import { IFaultDisputeGame } from "src/dispute/interfaces/IFaultDisputeGame.sol";
     8  
     9  import "src/libraries/DisputeTypes.sol";
    10  
    11  /// @title GameSolver
    12  /// @notice The `GameSolver` contract is a contract that can produce an array of available
    13  ///         moves for a given `FaultDisputeGame` contract, from the eyes of an honest
    14  ///         actor. The `GameSolver` does not implement functionality for acting on the `Move`s
    15  ///         it suggests.
    16  abstract contract GameSolver is CommonBase {
    17      /// @notice The `FaultDisputeGame` proxy that the `GameSolver` will be solving.
    18      FaultDisputeGame public immutable GAME;
    19      /// @notice The split depth of the game
    20      uint256 internal immutable SPLIT_DEPTH;
    21      /// @notice The max depth of the game
    22      uint256 internal immutable MAX_DEPTH;
    23      /// @notice The maximum L2 block number that the output bisection portion of the position tree
    24      ///         can handle.
    25      uint256 internal immutable MAX_L2_BLOCK_NUMBER;
    26  
    27      /// @notice The L2 outputs that the `GameSolver` will be representing, keyed by L2 block number - 1.
    28      uint256[] public l2Outputs;
    29      /// @notice The execution trace that the `GameSolver` will be representing.
    30      bytes public trace;
    31      /// @notice The raw absolute prestate data.
    32      bytes public absolutePrestateData;
    33      /// @notice The offset of previously processed claims in the `GAME` contract's `claimData` array.
    34      ///         Starts at 0 and increments by 1 for each claim processed.
    35      uint256 public processedBuf;
    36      /// @notice Signals whether or not the `GameSolver` agrees with the root claim of the
    37      ///         `GAME` contract.
    38      bool public agreeWithRoot;
    39  
    40      /// @notice The `MoveKind` enum represents a kind of interaction with the `FaultDisputeGame` contract.
    41      enum MoveKind {
    42          Attack,
    43          Defend,
    44          Step,
    45          AddLocalData
    46      }
    47  
    48      /// @notice The `Move` struct represents a move in the game, and contains information
    49      ///         about the kind of move, the sender of the move, and the calldata to be sent
    50      ///         to the `FaultDisputeGame` contract by a consumer of this contract.
    51      struct Move {
    52          MoveKind kind;
    53          bytes data;
    54          uint256 value;
    55      }
    56  
    57      constructor(
    58          FaultDisputeGame _gameProxy,
    59          uint256[] memory _l2Outputs,
    60          bytes memory _trace,
    61          bytes memory _preStateData
    62      ) {
    63          GAME = _gameProxy;
    64          SPLIT_DEPTH = GAME.splitDepth();
    65          MAX_DEPTH = GAME.maxGameDepth();
    66          MAX_L2_BLOCK_NUMBER = 2 ** (MAX_DEPTH - SPLIT_DEPTH);
    67  
    68          l2Outputs = _l2Outputs;
    69          trace = _trace;
    70          absolutePrestateData = _preStateData;
    71      }
    72  
    73      /// @notice Returns an array of `Move`s that can be taken from the perspective of an honest
    74      ///         actor in the `FaultDisputeGame` contract.
    75      function solveGame() external virtual returns (Move[] memory moves_);
    76  }
    77  
    78  /// @title HonestGameSolver
    79  /// @notice The `HonestGameSolver` is an implementation of `GameSolver` which responds accordingly depending
    80  ///         on the state of the `FaultDisputeGame` contract in relation to their local opinion of the correct
    81  ///         order of output roots and the execution trace between each block `n` -> `n + 1` state transition.
    82  contract HonestGameSolver is GameSolver {
    83      /// @notice The `Direction` enum represents the direction of a proposed move in the game,
    84      ///         or a lack thereof.
    85      enum Direction {
    86          Defend,
    87          Attack,
    88          Noop
    89      }
    90  
    91      constructor(
    92          FaultDisputeGame _gameProxy,
    93          uint256[] memory _l2Outputs,
    94          bytes memory _trace,
    95          bytes memory _preStateData
    96      )
    97          GameSolver(_gameProxy, _l2Outputs, _trace, _preStateData)
    98      {
    99          // Mark agreement with the root claim if the local opinion of the root claim is the same as the
   100          // observed root claim.
   101          agreeWithRoot = Claim.unwrap(outputAt(MAX_L2_BLOCK_NUMBER)) == Claim.unwrap(_gameProxy.rootClaim());
   102      }
   103  
   104      ////////////////////////////////////////////////////////////////
   105      //                          EXTERNAL                          //
   106      ////////////////////////////////////////////////////////////////
   107  
   108      /// @notice Returns an array of `Move`s that can be taken from the perspective of an honest
   109      ///         actor in the `FaultDisputeGame` contract.
   110      function solveGame() external override returns (Move[] memory moves_) {
   111          uint256 numClaims = GAME.claimDataLen();
   112  
   113          // Pre-allocate the `moves_` array to the maximum possible length. Test environment, so
   114          // over-allocation is fine, and more easy to read than making a linked list in asm.
   115          moves_ = new Move[](numClaims - processedBuf);
   116  
   117          uint256 numMoves = 0;
   118          for (uint256 i = processedBuf; i < numClaims; i++) {
   119              // Grab the observed claim.
   120              IFaultDisputeGame.ClaimData memory observed = getClaimData(i);
   121  
   122              // Determine the direction of the next move to be taken.
   123              (Direction moveDirection, Position movePos) = determineDirection(observed);
   124  
   125              // Continue if there is no move to be taken against the observed claim.
   126              if (moveDirection == Direction.Noop) continue;
   127  
   128              if (movePos.depth() <= MAX_DEPTH) {
   129                  // bisection
   130                  moves_[numMoves++] = handleBisectionMove(moveDirection, movePos, i);
   131              } else {
   132                  // instruction step
   133                  moves_[numMoves++] = handleStepMove(moveDirection, observed.position, movePos, i);
   134              }
   135          }
   136  
   137          // Update the length of the `moves_` array to the number of moves that were added. This is
   138          // always a no-op or a truncation operation.
   139          assembly {
   140              mstore(moves_, numMoves)
   141          }
   142  
   143          // Increment `processedBuf` by the number of claims processed, so that next time around,
   144          // we don't attempt to process the same claims again.
   145          processedBuf += numClaims - processedBuf;
   146      }
   147  
   148      ////////////////////////////////////////////////////////////////
   149      //                          INTERNAL                          //
   150      ////////////////////////////////////////////////////////////////
   151  
   152      /// @dev Helper function to determine the direction of the next move to be taken.
   153      function determineDirection(IFaultDisputeGame.ClaimData memory _claimData)
   154          internal
   155          view
   156          returns (Direction direction_, Position movePos_)
   157      {
   158          bool rightLevel = isRightLevel(_claimData.position);
   159          bool localAgree = Claim.unwrap(claimAt(_claimData.position)) == Claim.unwrap(_claimData.claim);
   160          if (_claimData.parentIndex == type(uint32).max) {
   161              // If we agree with the parent claim and it is on a level we agree with, ignore it.
   162              if (localAgree && rightLevel) {
   163                  return (Direction.Noop, Position.wrap(0));
   164              }
   165  
   166              // The parent claim is the root claim. We must attack if we disagree per the game rules.
   167              direction_ = Direction.Attack;
   168              movePos_ = _claimData.position.move(true);
   169          } else {
   170              // Never attempt to defend an execution trace subgame root. Only attack if we disagree with it,
   171              // otherwise do nothing.
   172              // NOTE: This is not correct behavior in the context of the honest actor; The alphabet game has
   173              //       a constant status byte, and is not safe from someone being dishonest in output bisection
   174              //       and then posting a correct execution trace bisection root claim.
   175              if (_claimData.position.depth() == SPLIT_DEPTH + 1 && localAgree) {
   176                  return (Direction.Noop, Position.wrap(0));
   177              }
   178  
   179              // If the parent claim is not the root claim, first check if the observed claim is on a level that
   180              // agrees with the local view of the root claim. If it is, noop. If it is not, perform an attack or
   181              // defense depending on the local view of the observed claim.
   182              if (rightLevel) {
   183                  // Never move against a claim on the right level. Even if it's wrong, if it's uncountered, it furthers
   184                  // our goals.
   185                  return (Direction.Noop, Position.wrap(0));
   186              } else {
   187                  // Fetch the local opinion of the parent claim.
   188                  Claim localParent = claimAt(_claimData.position);
   189  
   190                  // NOTE: Poison not handled.
   191                  if (Claim.unwrap(localParent) != Claim.unwrap(_claimData.claim)) {
   192                      // If we disagree with the observed claim, we must attack it.
   193                      movePos_ = _claimData.position.move(true);
   194                      direction_ = Direction.Attack;
   195                  } else {
   196                      // If we agree with the observed claim, we must defend the observed claim.
   197                      movePos_ = _claimData.position.move(false);
   198                      direction_ = Direction.Defend;
   199                  }
   200              }
   201          }
   202      }
   203  
   204      /// @notice Returns a `Move` struct that represents an attack or defense move in the bisection portion
   205      ///         of the game.
   206      ///
   207      /// @dev Note: This function assumes that the `movePos` and `challengeIndex` are valid within the
   208      ///            output bisection context. This is enforced by the `solveGame` function.
   209      function handleBisectionMove(
   210          Direction _direction,
   211          Position _movePos,
   212          uint256 _challengeIndex
   213      )
   214          internal
   215          view
   216          returns (Move memory move_)
   217      {
   218          bool isAttack = _direction == Direction.Attack;
   219  
   220          uint256 bond = GAME.getRequiredBond(_movePos);
   221  
   222          move_ = Move({
   223              kind: isAttack ? MoveKind.Attack : MoveKind.Defend,
   224              value: bond,
   225              data: abi.encodeCall(FaultDisputeGame.move, (_challengeIndex, claimAt(_movePos), isAttack))
   226          });
   227      }
   228  
   229      /// @notice Returns a `Move` struct that represents a step move in the execution trace
   230      ///         bisection portion of the dispute game.
   231      /// @dev Note: This function assumes that the `movePos` and `challengeIndex` are valid within the
   232      ///            execution trace bisection context. This is enforced by the `solveGame` function.
   233      function handleStepMove(
   234          Direction _direction,
   235          Position _parentPos,
   236          Position _movePos,
   237          uint256 _challengeIndex
   238      )
   239          internal
   240          view
   241          returns (Move memory move_)
   242      {
   243          bool isAttack = _direction == Direction.Attack;
   244          bytes memory preStateTrace;
   245  
   246          // First, we need to find the pre/post state index depending on whether we
   247          // are making an attack step or a defense step. If the relative index at depth of the
   248          // move position is 0, the prestate is the absolute prestate and we need to
   249          // do nothing.
   250          if ((_movePos.indexAtDepth() % (2 ** (MAX_DEPTH - SPLIT_DEPTH))) != 0) {
   251              // Grab the trace up to the prestate's trace index.
   252              if (isAttack) {
   253                  Position leafPos = Position.wrap(Position.unwrap(_parentPos) - 1);
   254                  preStateTrace = abi.encode(leafPos.traceIndex(MAX_DEPTH), stateAt(leafPos));
   255              } else {
   256                  preStateTrace = abi.encode(_parentPos.traceIndex(MAX_DEPTH), stateAt(_parentPos));
   257              }
   258          } else {
   259              preStateTrace = absolutePrestateData;
   260          }
   261  
   262          move_ = Move({
   263              kind: MoveKind.Step,
   264              value: 0,
   265              data: abi.encodeCall(FaultDisputeGame.step, (_challengeIndex, isAttack, preStateTrace, hex""))
   266          });
   267      }
   268  
   269      ////////////////////////////////////////////////////////////////
   270      //                          HELPERS                           //
   271      ////////////////////////////////////////////////////////////////
   272  
   273      /// @dev Helper function to get the `ClaimData` struct at a given index in the `GAME` contract's
   274      ///      `claimData` array.
   275      function getClaimData(uint256 _claimIndex) internal view returns (IFaultDisputeGame.ClaimData memory claimData_) {
   276          // thanks, solc
   277          (
   278              uint32 parentIndex,
   279              address countered,
   280              address claimant,
   281              uint128 bond,
   282              Claim claim,
   283              Position position,
   284              Clock clock
   285          ) = GAME.claimData(_claimIndex);
   286          claimData_ = IFaultDisputeGame.ClaimData({
   287              parentIndex: parentIndex,
   288              counteredBy: countered,
   289              claimant: claimant,
   290              bond: bond,
   291              claim: claim,
   292              position: position,
   293              clock: clock
   294          });
   295      }
   296  
   297      /// @notice Returns the player's claim that commits to a given position, swapping between
   298      ///         output bisection claims and execution trace bisection claims depending on the depth.
   299      /// @dev Prefer this function over `outputAt` or `statehashAt` directly.
   300      function claimAt(Position _position) internal view returns (Claim claim_) {
   301          return _position.depth() > SPLIT_DEPTH ? statehashAt(_position) : outputAt(_position);
   302      }
   303  
   304      /// @notice Returns the mock output at the given position.
   305      function outputAt(Position _position) internal view returns (Claim claim_) {
   306          // Don't allow for positions that are deeper than the split depth.
   307          if (_position.depth() > SPLIT_DEPTH) {
   308              revert("GameSolver: invalid position depth");
   309          }
   310  
   311          return outputAt(_position.traceIndex(SPLIT_DEPTH) + 1);
   312      }
   313  
   314      /// @notice Returns the mock output at the given L2 block number.
   315      function outputAt(uint256 _l2BlockNumber) internal view returns (Claim claim_) {
   316          return Claim.wrap(bytes32(l2Outputs[_l2BlockNumber - 1]));
   317      }
   318  
   319      /// @notice Returns the player's claim that commits to a given trace index.
   320      function statehashAt(uint256 _traceIndex) internal view returns (Claim claim_) {
   321          bytes32 hash =
   322              keccak256(abi.encode(_traceIndex >= trace.length ? trace.length - 1 : _traceIndex, stateAt(_traceIndex)));
   323          assembly {
   324              claim_ := or(and(hash, not(shl(248, 0xFF))), shl(248, 1))
   325          }
   326      }
   327  
   328      /// @notice Returns the player's claim that commits to a given trace index.
   329      function statehashAt(Position _position) internal view returns (Claim claim_) {
   330          return statehashAt(_position.traceIndex(MAX_DEPTH));
   331      }
   332  
   333      /// @notice Returns the state at the trace index within the player's trace.
   334      function stateAt(Position _position) internal view returns (uint256 state_) {
   335          return stateAt(_position.traceIndex(MAX_DEPTH));
   336      }
   337  
   338      /// @notice Returns the state at the trace index within the player's trace.
   339      function stateAt(uint256 _traceIndex) internal view returns (uint256 state_) {
   340          return uint256(uint8(_traceIndex >= trace.length ? trace[trace.length - 1] : trace[_traceIndex]));
   341      }
   342  
   343      /// @notice Returns whether or not the position is on a level which opposes the local opinion of the
   344      ///         root claim.
   345      function isRightLevel(Position _position) internal view returns (bool isRightLevel_) {
   346          isRightLevel_ = agreeWithRoot == (_position.depth() % 2 == 0);
   347      }
   348  }
   349  
   350  /// @title DisputeActor
   351  /// @notice The `DisputeActor` contract is an abstract contract that represents an actor
   352  ///         that consumes the suggested moves from a `GameSolver` contract.
   353  abstract contract DisputeActor {
   354      /// @notice The `GameSolver` contract used to determine the moves to be taken.
   355      GameSolver public solver;
   356  
   357      /// @notice Performs all available moves deemed by the attached solver.
   358      /// @return numMoves_ The number of moves that the actor took.
   359      /// @return success_ True if all moves were successful, false otherwise.
   360      function move() external virtual returns (uint256 numMoves_, bool success_);
   361  }
   362  
   363  /// @title HonestDisputeActor
   364  /// @notice An actor that consumes the suggested moves from an `HonestGameSolver` contract. Note
   365  ///         that this actor *can* be dishonest if the trace is faulty, but it will always follow
   366  ///         the rules of the honest actor.
   367  contract HonestDisputeActor is DisputeActor {
   368      FaultDisputeGame public immutable GAME;
   369  
   370      constructor(
   371          FaultDisputeGame _gameProxy,
   372          uint256[] memory _l2Outputs,
   373          bytes memory _trace,
   374          bytes memory _preStateData
   375      ) {
   376          GAME = _gameProxy;
   377          solver = GameSolver(new HonestGameSolver(_gameProxy, _l2Outputs, _trace, _preStateData));
   378      }
   379  
   380      /// @inheritdoc DisputeActor
   381      function move() external override returns (uint256 numMoves_, bool success_) {
   382          GameSolver.Move[] memory moves = solver.solveGame();
   383          numMoves_ = moves.length;
   384  
   385          // Optimistically assume success, will be set to false if any move fails.
   386          success_ = true;
   387  
   388          // Perform all available moves given to the actor by the solver.
   389          for (uint256 i = 0; i < moves.length; i++) {
   390              GameSolver.Move memory localMove = moves[i];
   391  
   392              // If the move is a step, we first need to add the starting L2 block number to the `PreimageOracle`
   393              // via the `FaultDisputeGame` contract.
   394              // TODO: This is leaky. Could be another move kind.
   395              if (localMove.kind == GameSolver.MoveKind.Step) {
   396                  bytes memory moveData = localMove.data;
   397                  uint256 challengeIndex;
   398                  assembly {
   399                      challengeIndex := mload(add(moveData, 0x24))
   400                  }
   401                  GAME.addLocalData({
   402                      _ident: LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER,
   403                      _execLeafIdx: challengeIndex,
   404                      _partOffset: 0
   405                  });
   406              }
   407  
   408              (bool innerSuccess,) = address(GAME).call{ value: localMove.value }(localMove.data);
   409              assembly {
   410                  success_ := and(success_, innerSuccess)
   411              }
   412          }
   413      }
   414  
   415      fallback() external payable { }
   416  
   417      receive() external payable { }
   418  }