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 }