Commit 6d07c4d6 authored by clabby's avatar clabby Committed by GitHub

Merge pull request #6080 from ethereum-optimism/clabby/dispute/alphabet-vm

feat(ctb): Alphabet `step` function in `FaultDisputeGame`
parents ee255c37 cac7a904
...@@ -32,25 +32,30 @@ DisputeGameFactory_SetImplementation_Test:test_setImplementation_notOwner_revert ...@@ -32,25 +32,30 @@ DisputeGameFactory_SetImplementation_Test:test_setImplementation_notOwner_revert
DisputeGameFactory_SetImplementation_Test:test_setImplementation_succeeds() (gas: 44243) DisputeGameFactory_SetImplementation_Test:test_setImplementation_succeeds() (gas: 44243)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_notOwner_reverts() (gas: 15950) DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_notOwner_reverts() (gas: 15950)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_succeeds() (gas: 18642) DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_succeeds() (gas: 18642)
FaultDisputeGame_Test:test_clockTimeExceeded_reverts() (gas: 26496) FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 498304)
FaultDisputeGame_Test:test_defendRoot_reverts() (gas: 13236) FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 500038)
FaultDisputeGame_Test:test_duplicateClaim_reverts() (gas: 103425) FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 490057)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 17478) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 495173)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 17859) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 496907)
FaultDisputeGame_Test:test_gameDepthExceeded_reverts() (gas: 5907231) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 486926)
FaultDisputeGame_Test:test_gameStart_succeeds() (gas: 10337) FaultDisputeGame_Test:test_clockTimeExceeded_reverts() (gas: 26444)
FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8259) FaultDisputeGame_Test:test_defendRoot_reverts() (gas: 13281)
FaultDisputeGame_Test:test_initialRootClaimData_succeeds() (gas: 17624) FaultDisputeGame_Test:test_duplicateClaim_reverts() (gas: 103343)
FaultDisputeGame_Test:test_moveAgainstNonexistentParent_reverts() (gas: 24632) FaultDisputeGame_Test:test_extraData_succeeds() (gas: 17431)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10945) FaultDisputeGame_Test:test_gameData_succeeds() (gas: 17834)
FaultDisputeGame_Test:test_resolve_challengeContested() (gas: 222383) FaultDisputeGame_Test:test_gameDepthExceeded_reverts() (gas: 408128)
FaultDisputeGame_Test:test_resolve_reverts() (gas: 27121) FaultDisputeGame_Test:test_gameStart_succeeds() (gas: 10359)
FaultDisputeGame_Test:test_resolve_rootContested() (gas: 107225) FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8281)
FaultDisputeGame_Test:test_resolve_rootUncontested() (gas: 24459) FaultDisputeGame_Test:test_initialRootClaimData_succeeds() (gas: 17669)
FaultDisputeGame_Test:test_resolve_teamDeathmatch() (gas: 393609) FaultDisputeGame_Test:test_moveAgainstNonexistentParent_reverts() (gas: 24611)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8169) FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10901)
FaultDisputeGame_Test:test_simpleAttack_succeeds() (gas: 107389) FaultDisputeGame_Test:test_resolve_challengeContested() (gas: 221222)
FaultDisputeGame_Test:test_version_succeeds() (gas: 9780) FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9657)
FaultDisputeGame_Test:test_resolve_rootContested() (gas: 106144)
FaultDisputeGame_Test:test_resolve_rootUncontested() (gas: 23697)
FaultDisputeGame_Test:test_resolve_teamDeathmatch() (gas: 391960)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8225)
FaultDisputeGame_Test:test_simpleAttack_succeeds() (gas: 107438)
FeeVault_Test:test_constructor_succeeds() (gas: 18185) FeeVault_Test:test_constructor_succeeds() (gas: 18185)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352135) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 352135)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2950342) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2950342)
......
...@@ -27,12 +27,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -27,12 +27,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
/** /**
* @notice The current Semver of the FaultDisputeGame implementation. * @notice The current Semver of the FaultDisputeGame implementation.
*/ */
string internal constant VERSION = "0.0.1"; string internal constant VERSION = "0.0.2";
/**
* @notice The max depth of the game.
*/
uint256 internal constant MAX_GAME_DEPTH = 63;
/** /**
* @notice The duration of the game. * @notice The duration of the game.
...@@ -45,6 +40,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -45,6 +40,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
*/ */
Position internal constant ROOT_POSITION = Position.wrap(1); Position internal constant ROOT_POSITION = Position.wrap(1);
/**
* @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 public immutable ABSOLUTE_PRESTATE;
/**
* @notice The max depth of the game.
*/
uint256 public immutable MAX_GAME_DEPTH;
/** /**
* @notice The starting timestamp of the game * @notice The starting timestamp of the game
*/ */
...@@ -70,6 +76,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -70,6 +76,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
*/ */
mapping(ClaimHash => bool) internal claims; mapping(ClaimHash => bool) internal claims;
/**
* @param _absolutePrestate The absolute prestate of the instruction trace.
*/
constructor(Claim _absolutePrestate, uint256 _maxGameDepth) {
ABSOLUTE_PRESTATE = _absolutePrestate;
MAX_GAME_DEPTH = _maxGameDepth;
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// External Logic // // External Logic //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -92,12 +106,76 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -92,12 +106,76 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
* @inheritdoc IFaultDisputeGame * @inheritdoc IFaultDisputeGame
*/ */
function step( function step(
uint256 _prestateIndex, uint256 _stateIndex,
uint256 _parentIndex, uint256 _claimIndex,
bytes calldata _stateData, bool _isAttack,
bytes calldata _proof bytes calldata,
bytes calldata
) external { ) external {
// TODO - Call the VM to perform the execution step. // 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 = _isAttack ? parentPos.attack() : parentPos.defend();
// Ensure that the step position is 1 deeper than the maximum game depth.
if (stepPos.depth() != MAX_GAME_DEPTH + 1) {
revert InvalidParent();
}
// Determine the expected pre & post states of the step.
Claim preStateClaim;
Claim postStateClaim;
if (stepPos.indexAtDepth() == 0) {
// If the step position's index at depth is 0, the prestate is the absolute prestate
// and the post state is the parent claim.
preStateClaim = ABSOLUTE_PRESTATE;
postStateClaim = claimData[_claimIndex].claim;
} else {
Position preStatePos;
Position postStatePos;
if (_isAttack) {
// If the step is an attack, the prestate exists elsewhere in the game state,
// and the parent claim is the expected post-state.
preStatePos = claimData[_stateIndex].position;
preStateClaim = claimData[_stateIndex].claim;
postStatePos = parentPos;
postStateClaim = parent.claim;
} else {
// If the step is a defense, the poststate exists elsewhere in the game state,
// and the parent claim is the expected pre-state.
preStatePos = parent.position;
preStateClaim = parent.claim;
postStatePos = claimData[_stateIndex].position;
postStateClaim = claimData[_stateIndex].claim;
}
// Assert that the given prestate commits to the instruction at `gindex - 1`.
if (
Position.unwrap(preStatePos.rightIndex(MAX_GAME_DEPTH)) !=
Position.unwrap(postStatePos.rightIndex(MAX_GAME_DEPTH)) - 1
) {
revert InvalidPrestate();
}
}
// TODO: Call `MIPS.sol#step` to verify the step.
// For now, we just use a simple state transition function that increments the prestate,
// `s_p`, by 1.
if (uint256(Claim.unwrap(preStateClaim)) + 1 == uint256(Claim.unwrap(postStateClaim))) {
revert ValidStep();
}
// 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.countered = true;
} }
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -141,7 +219,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -141,7 +219,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
// At the leaf nodes of the game, the only option is to run a step to prove or disprove // At the leaf nodes of the game, the only option is to run a step to prove or disprove
// the above claim. At this depth, the parent claim commits to the state after a single // the above claim. At this depth, the parent claim commits to the state after a single
// instruction step. // instruction step.
if (nextPosition.depth() >= MAX_GAME_DEPTH) { if (nextPosition.depth() > MAX_GAME_DEPTH) {
revert GameDepthExceeded(); revert GameDepthExceeded();
} }
...@@ -197,6 +275,13 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -197,6 +275,13 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
emit Move(_challengeIndex, _pivot, msg.sender); emit Move(_challengeIndex, _pivot, msg.sender);
} }
/**
* @inheritdoc IFaultDisputeGame
*/
function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) {
l2BlockNumber_ = _getArgUint256(0x20);
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `IDisputeGame` impl // // `IDisputeGame` impl //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -219,17 +304,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -219,17 +304,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
* @inheritdoc IDisputeGame * @inheritdoc IDisputeGame
*/ */
function resolve() external returns (GameStatus status_) { function resolve() external returns (GameStatus status_) {
// TODO: Do not allow resolution before clocks run out.
if (status != GameStatus.IN_PROGRESS) { if (status != GameStatus.IN_PROGRESS) {
// If the game is not in progress, it cannot be resolved.
revert GameNotInProgress(); revert GameNotInProgress();
} }
// Search for the left-most dangling non-bottom node // Search for the left-most dangling non-bottom node
// The most recent claim is always a dangling, non-bottom node so we start with that // The most recent claim is always a dangling, non-bottom node so we start with that
uint256 leftMostIndex = claimData.length - 1; uint256 leftMostIndex = claimData.length - 1;
uint256 leftMostTraceIndex = LibPosition.rightIndex( Position leftMostTraceIndex = Position.wrap(type(uint128).max);
claimData[leftMostIndex].position,
MAX_GAME_DEPTH
);
for (uint256 i = leftMostIndex; i < type(uint64).max; ) { for (uint256 i = leftMostIndex; i < type(uint64).max; ) {
// Fetch the claim at the current index. // Fetch the claim at the current index.
ClaimData storage claim = claimData[i]; ClaimData storage claim = claimData[i];
...@@ -243,24 +328,29 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -243,24 +328,29 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
// If the claim is not a dangling node above the bottom of the tree, // If the claim is not a dangling node above the bottom of the tree,
// we can skip over it. These nodes are not relevant to the game resolution. // we can skip over it. These nodes are not relevant to the game resolution.
Position claimPos = claim.position; Position claimPos = claim.position;
if (LibPosition.depth(claimPos) == MAX_GAME_DEPTH || claim.countered) { if (claim.countered) {
continue; continue;
} }
// If the claim is a dangling node, we can check if it is the left-most // If the claim is a dangling node, we can check if it is the left-most
// dangling node we've come across so far. If it is, we can update the // dangling node we've come across so far. If it is, we can update the
// left-most trace index. // left-most trace index.
uint256 traceIndex = LibPosition.rightIndex(claimPos, MAX_GAME_DEPTH); Position traceIndex = claimPos.rightIndex(MAX_GAME_DEPTH);
if (traceIndex < leftMostTraceIndex) { if (Position.unwrap(traceIndex) < Position.unwrap(leftMostTraceIndex)) {
leftMostTraceIndex = traceIndex; leftMostTraceIndex = traceIndex;
unchecked {
leftMostIndex = i + 1; leftMostIndex = i + 1;
} }
} }
}
// If the left-most dangling node is at an even depth, the defender wins. // If the left-most dangling node is at an even depth, the defender wins.
// Otherwise, the challenger wins and the root claim is deemed invalid. // Otherwise, the challenger wins and the root claim is deemed invalid.
if (
// slither-disable-next-line weak-prng // slither-disable-next-line weak-prng
if (LibPosition.depth(claimData[leftMostIndex].position) % 2 == 0) { claimData[leftMostIndex].position.depth() % 2 == 0 &&
Position.unwrap(leftMostTraceIndex) != type(uint128).max
) {
status_ = GameStatus.DEFENDER_WINS; status_ = GameStatus.DEFENDER_WINS;
} else { } else {
status_ = GameStatus.CHALLENGER_WINS; status_ = GameStatus.CHALLENGER_WINS;
......
...@@ -51,15 +51,24 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -51,15 +51,24 @@ interface IFaultDisputeGame is IDisputeGame {
* a step in the fault proof program on-chain. The interface of the fault proof * a step in the fault proof program on-chain. The interface of the fault proof
* processor contract should be generic enough such that we can use different * processor contract should be generic enough such that we can use different
* fault proof VMs (MIPS, RiscV5, etc.) * fault proof VMs (MIPS, RiscV5, etc.)
* @param _prestateIndex The index of the prestate of the step within `claimData`. * @param _stateIndex The index of the pre/post state of the step within `claimData`.
* @param _parentIndex The index of the parent claim within `claimData`. * @param _claimIndex The index of the challenged claim within `claimData`.
* @param _isAttack Whether or not the step is an attack or a defense.
* @param _stateData The stateData of the step is the preimage of the claim @ `prestateIndex` * @param _stateData The stateData of the step is the preimage of the claim @ `prestateIndex`
* @param _proof Proof to access memory leaf nodes in the VM. * @param _proof Proof to access memory leaf nodes in the VM.
*/ */
function step( function step(
uint256 _prestateIndex, uint256 _stateIndex,
uint256 _parentIndex, uint256 _claimIndex,
bool _isAttack,
bytes calldata _stateData, bytes calldata _stateData,
bytes calldata _proof bytes calldata _proof
) external; ) external;
/**
* @notice The l2BlockNumber that the `rootClaim` commits to. The trace being bisected within
* the game is from `l2BlockNumber - 1` -> `l2BlockNumber`.
* @return l2BlockNumber_ The l2BlockNumber that the `rootClaim` commits to.
*/
function l2BlockNumber() external view returns (uint256 l2BlockNumber_);
} }
...@@ -99,7 +99,7 @@ library LibPosition { ...@@ -99,7 +99,7 @@ library LibPosition {
function rightIndex( function rightIndex(
Position _position, Position _position,
uint256 _maxDepth uint256 _maxDepth
) internal pure returns (uint64 rightIndex_) { ) internal pure returns (Position rightIndex_) {
uint256 msb = depth(_position); uint256 msb = depth(_position);
assembly { assembly {
switch eq(msb, _maxDepth) switch eq(msb, _maxDepth)
...@@ -110,7 +110,6 @@ library LibPosition { ...@@ -110,7 +110,6 @@ library LibPosition {
let remaining := sub(_maxDepth, msb) let remaining := sub(_maxDepth, msb)
rightIndex_ := or(shl(remaining, _position), sub(shl(remaining, 1), 1)) rightIndex_ := or(shl(remaining, _position), sub(shl(remaining, 1), 1))
} }
rightIndex_ := sub(rightIndex_, shl(_maxDepth, 1))
} }
} }
......
...@@ -60,6 +60,21 @@ error ClockTimeExceeded(); ...@@ -60,6 +60,21 @@ error ClockTimeExceeded();
*/ */
error GameDepthExceeded(); error GameDepthExceeded();
/**
* @notice Thrown when a step is attempted above the maximum game depth.
*/
error InvalidParent();
/**
* @notice Thrown when an invalid prestate is supplied to `step`.
*/
error InvalidPrestate();
/**
* @notice Thrown when a step is made that computes the expected post state correctly.
*/
error ValidStep();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `AttestationDisputeGame` Errors // // `AttestationDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
pragma solidity ^0.8.15; pragma solidity ^0.8.15;
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol";
import { DisputeGameFactory_Init } from "./DisputeGameFactory.t.sol"; import { DisputeGameFactory_Init } from "./DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "../dispute/DisputeGameFactory.sol"; import { DisputeGameFactory } from "../dispute/DisputeGameFactory.sol";
import { FaultDisputeGame } from "../dispute/FaultDisputeGame.sol"; import { FaultDisputeGame } from "../dispute/FaultDisputeGame.sol";
...@@ -11,11 +12,7 @@ import "../libraries/DisputeErrors.sol"; ...@@ -11,11 +12,7 @@ import "../libraries/DisputeErrors.sol";
import { LibClock } from "../dispute/lib/LibClock.sol"; import { LibClock } from "../dispute/lib/LibClock.sol";
import { LibPosition } from "../dispute/lib/LibPosition.sol"; import { LibPosition } from "../dispute/lib/LibPosition.sol";
contract FaultDisputeGame_Test is DisputeGameFactory_Init { contract FaultDisputeGame_Init is DisputeGameFactory_Init {
/**
* @dev The root claim of the game.
*/
Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10)));
/** /**
* @dev The extra data passed to the game for initialization. * @dev The extra data passed to the game for initialization.
*/ */
...@@ -24,10 +21,6 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init { ...@@ -24,10 +21,6 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init {
* @dev The type of the game being tested. * @dev The type of the game being tested.
*/ */
GameType internal constant GAME_TYPE = GameType.wrap(0); GameType internal constant GAME_TYPE = GameType.wrap(0);
/**
* @dev The current version of the `FaultDisputeGame` contract.
*/
string internal constant VERSION = "0.0.1";
/** /**
* @dev The implementation of the game. * @dev The implementation of the game.
...@@ -40,18 +33,33 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init { ...@@ -40,18 +33,33 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init {
event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant); event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant);
function setUp() public override { function init(Claim rootClaim, Claim absolutePrestate) public {
super.setUp(); super.setUp();
// Deploy an implementation of the fault game // Deploy an implementation of the fault game
gameImpl = new FaultDisputeGame(); gameImpl = new FaultDisputeGame(absolutePrestate, 4);
// Register the game implementation with the factory. // Register the game implementation with the factory.
factory.setImplementation(GAME_TYPE, gameImpl); factory.setImplementation(GAME_TYPE, gameImpl);
// Create a new game. // Create a new game.
gameProxy = FaultDisputeGame(address(factory.create(GAME_TYPE, ROOT_CLAIM, EXTRA_DATA))); gameProxy = FaultDisputeGame(address(factory.create(GAME_TYPE, rootClaim, EXTRA_DATA)));
// Label the proxy // Label the proxy
vm.label(address(gameProxy), "FaultDisputeGame_Clone"); vm.label(address(gameProxy), "FaultDisputeGame_Clone");
} }
}
contract FaultDisputeGame_Test is FaultDisputeGame_Init {
/**
* @dev The root claim of the game.
*/
Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32(uint256(10)));
/**
* @dev The absolute prestate of the trace.
*/
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32(uint256(0)));
function setUp() public override {
super.init(ROOT_CLAIM, ABSOLUTE_PRESTATE);
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `IDisputeGame` Implementation Tests // // `IDisputeGame` Implementation Tests //
...@@ -71,13 +79,6 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init { ...@@ -71,13 +79,6 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init {
assertEq(gameProxy.extraData(), EXTRA_DATA); assertEq(gameProxy.extraData(), EXTRA_DATA);
} }
/**
* @dev Tests that the game's version is set correctly.
*/
function test_version_succeeds() public {
assertEq(gameProxy.version(), VERSION);
}
/** /**
* @dev Tests that the game's status is set correctly. * @dev Tests that the game's status is set correctly.
*/ */
...@@ -184,10 +185,12 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init { ...@@ -184,10 +185,12 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init {
function test_gameDepthExceeded_reverts() public { function test_gameDepthExceeded_reverts() public {
Claim claim = Claim.wrap(bytes32(uint256(5))); Claim claim = Claim.wrap(bytes32(uint256(5)));
for (uint256 i = 0; i < 63; i++) { uint256 maxDepth = gameProxy.MAX_GAME_DEPTH();
for (uint256 i = 0; i <= maxDepth; i++) {
// At the max game depth, the `_move` function should revert with // At the max game depth, the `_move` function should revert with
// the `GameDepthExceeded` error. // the `GameDepthExceeded` error.
if (i == 62) { if (i == maxDepth) {
vm.expectRevert(GameDepthExceeded.selector); vm.expectRevert(GameDepthExceeded.selector);
} }
gameProxy.attack(i, claim); gameProxy.attack(i, claim);
...@@ -279,10 +282,20 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init { ...@@ -279,10 +282,20 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init {
} }
/** /**
* @dev Static unit test asserting that resolve reverts when the game is not in progress. * @dev Static unit test asserting that resolve reverts when the game state is
* not in progress.
*/ */
function test_resolve_reverts() public { function test_resolve_notInProgress_reverts() public {
gameProxy.resolve(); uint256 chalWins = uint256(GameStatus.CHALLENGER_WINS);
// Replace the game status in storage. It exists in slot 0 at offset 8.
uint256 slot = uint256(vm.load(address(gameProxy), bytes32(0)));
uint256 offset = (8 << 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); vm.expectRevert(GameNotInProgress.selector);
gameProxy.resolve(); gameProxy.resolve();
} }
...@@ -326,34 +339,387 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init { ...@@ -326,34 +339,387 @@ contract FaultDisputeGame_Test is DisputeGameFactory_Init {
} }
/** /**
* @title BigStepper * @notice A generic game player actor with a configurable trace.
* @notice A mock fault proof processor contract for testing purposes. * @dev This actor always responds rationally with respect to their trace. The
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ * `play` function can be overridden to change this behavior.
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⠶⢅⠒⢄⢔⣶⡦⣤⡤⠄⣀⠀⠀⠀⠀⠀⠀⠀ */
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠨⡏⠀⠀⠈⠢⣙⢯⣄⠀⢨⠯⡺⡘⢄⠀⠀⠀⠀⠀ contract GamePlayer {
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣶⡆⠀⠀⠀⠀⠈⠓⠬⡒⠡⣀⢙⡜⡀⠓⠄⠀⠀⠀ bool public failedToStep;
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡷⠿⣧⣀⡀⠀⠀⠀⠀⠀⠀⠉⠣⣞⠩⠥⠀⠼⢄⠀⠀ FaultDisputeGame public gameProxy;
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠉⢹⣶⠒⠒⠂⠈⠉⠁⠘⡆⠀⣿⣿⠫⡄⠀
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢶⣤⣀⡀⠀⠀⢸⡿⠀⠀⠀⠀⠀⢀⠞⠀⠀⢡⢨⢀⡄⠀ GamePlayer internal counterParty;
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡒⣿⢿⡤⠝⡣⠉⠁⠚⠛⠀⠤⠤⣄⡰⠁⠀⠀⠀⠉⠙⢸⠀⠀ Vm internal vm;
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⢯⡌⡿⡇⠘⡷⠀⠁⠀⠀⢀⣰⠢⠲⠛⣈⣸⠦⠤⠶⠴⢬⣐⣊⡂⠀ bytes internal trace;
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡪⡗⢫⠞⠀⠆⣀⠻⠤⠴⠐⠚⣉⢀⠦⠂⠋⠁⠀⠁⠀⠀⠀⠀⢋⠉⠇⠀ uint256 internal maxDepth;
*⠀⠀⠀⠀⣀⡤⠐⠒⠘⡹⠉⢸⠇⠸⠀⠀⠀⠀⣀⣤⠴⠚⠉⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠼⠀⣾⠀
*⠀⠀⠀⡰⠀⠉⠉⠀⠁⠀⠀⠈⢇⠈⠒⠒⠘⠈⢀⢡⡂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠀⢸⡄ /**
*⠀⠀⠸⣿⣆⠤⢀⡀⠀⠀⠀⠀⢘⡌⠀⠀⣀⣀⣀⡈⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⡇ * @notice Initializes the player
*⠀⠀⢸⣀⠀⠉⠒⠐⠛⠋⠭⠭⠍⠉⠛⠒⠒⠒⠀⠒⠚⠛⠛⠛⠩⠭⠭⠭⠭⠤⠤⠤⠤⠤⠭⠭⠉⠓⡆ */
*⠀⠀⠘⠿⣷⣶⣤⣤⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇ function init(
*⠀⠀⠀⠀⠀⠉⠙⠛⠛⠻⠿⢿⣿⣿⣷⣶⣶⣶⣤⣤⣀⣁⣛⣃⣒⠿⠿⠿⠤⠠⠄⠤⠤⢤⣛⣓⣂⣻⡇ FaultDisputeGame _gameProxy,
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠙⠛⠻⠿⠿⠿⢿⣿⣿⣿⣷⣶⣶⣾⣿⣿⣿⣿⠿⠟⠁ GamePlayer _counterParty,
*⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀ Vm _vm
*/ ) public virtual {
contract BigStepper { gameProxy = _gameProxy;
/** counterParty = _counterParty;
* @notice Steps from the `preState` to the `postState` by adding 1 to the `preState`. vm = _vm;
* @param preState The pre state to start from maxDepth = _gameProxy.MAX_GAME_DEPTH();
* @return postState The state stepped to }
*/
function step(Claim preState) external pure returns (Claim postState) { /**
postState = Claim.wrap(bytes32(uint256(Claim.unwrap(preState)) + 1)); * @notice Perform the next move in the game.
*/
function play(uint256 _parentIndex) public virtual {
// Grab the claim data at the parent index.
(uint32 grandparentIndex, , Claim parentClaim, Position parentPos, ) = gameProxy.claimData(
_parentIndex
);
// The position to move to.
Position movePos;
// May or may not be used.
Position movePos2;
// Signifies whether the move is an attack or not.
bool isAttack;
if (grandparentIndex == type(uint32).max) {
// If the parent claim is the root claim, begin by attacking.
movePos = parentPos.attack();
// Flag the move as an attack.
isAttack = true;
} else {
// If the parent claim is not the root claim, check if we disagree with it and/or its grandparent
// to determine our next move(s).
// Fetch our claim at the parent's position.
Claim ourParentClaim = claimAt(parentPos);
// Fetch our claim at the grandparent's position.
(, , Claim grandparentClaim, Position grandparentPos, ) = gameProxy.claimData(
grandparentIndex
);
Claim ourGrandparentClaim = claimAt(grandparentPos);
if (Claim.unwrap(ourParentClaim) != Claim.unwrap(parentClaim)) {
// Attack parent.
movePos = parentPos.attack();
// If we also disagree with the grandparent, attack it as well.
if (Claim.unwrap(ourGrandparentClaim) != Claim.unwrap(grandparentClaim)) {
movePos2 = grandparentPos.attack();
}
// Flag the move as an attack.
isAttack = true;
} else if (
Claim.unwrap(ourParentClaim) == Claim.unwrap(parentClaim) &&
Claim.unwrap(ourGrandparentClaim) == Claim.unwrap(grandparentClaim)
) {
movePos = parentPos.defend();
}
}
// If we are past the maximum depth, break the recursion and step.
if (movePos.depth() > maxDepth) {
// Perform a step.
uint256 stateIndex;
// 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 index at depth of the
// move position is 0, the prestate is the absolute prestate and we need to
// do nothing.
if (movePos.indexAtDepth() > 0) {
Position leafPos = isAttack
? Position.wrap(Position.unwrap(parentPos) - 1)
: Position.wrap(Position.unwrap(parentPos) + 1);
Position statePos = leafPos;
// Walk up until the valid position that commits to the prestate's
// trace index is found.
while (
Position.unwrap(statePos.parent().rightIndex(maxDepth)) ==
Position.unwrap(leafPos)
) {
statePos = statePos.parent();
}
// Now, search for the index of the claim that commits to the prestate's trace
// index.
uint256 len = claimDataLen();
for (uint256 i = 0; i < len; i++) {
(, , , Position pos, ) = gameProxy.claimData(i);
if (Position.unwrap(pos) == Position.unwrap(statePos)) {
stateIndex = i;
break;
}
}
}
// Perform the step and halt recursion.
try gameProxy.step(stateIndex, _parentIndex, isAttack, hex"", hex"") {
// Do nothing, step succeeded.
} catch {
failedToStep = true;
}
} else {
// Find the trace index that our next claim must commit to.
uint256 traceIndex = movePos.rightIndex(maxDepth).indexAtDepth();
// Grab the claim that we need to make from the helper.
Claim ourClaim = claimAt(traceIndex);
if (isAttack) {
// Attack the parent claim.
gameProxy.attack(_parentIndex, ourClaim);
// Call out to our counter party to respond.
counterParty.play(claimDataLen() - 1);
// If we have a second move position, attack the grandparent.
if (Position.unwrap(movePos2) != 0) {
(, , , Position grandparentPos, ) = gameProxy.claimData(grandparentIndex);
Claim ourGrandparentClaim = claimAt(grandparentPos.attack());
gameProxy.attack(grandparentIndex, ourGrandparentClaim);
counterParty.play(claimDataLen() - 1);
}
} else {
// Defend the parent claim.
gameProxy.defend(_parentIndex, ourClaim);
// Call out to our counter party to respond.
counterParty.play(claimDataLen() - 1);
}
}
}
/**
* @notice Returns the length of the claim data array.
*/
function claimDataLen() internal view returns (uint256 len_) {
return uint256(vm.load(address(gameProxy), bytes32(uint256(1))));
}
/**
* @notice Returns the player's claim that commits to a given gindex.
*/
function claimAt(Position _position) internal view returns (Claim claim_) {
return claimAt(_position.rightIndex(maxDepth).indexAtDepth());
}
/**
* @notice Returns the player's claim that commits to a given trace index.
*/
function claimAt(uint256 _traceIndex) public view returns (Claim claim_) {
return Claim.wrap(bytes32(uint256(bytes32(trace[_traceIndex]) >> 248)));
}
}
contract OneVsOne_Arena is FaultDisputeGame_Init {
/**
* @dev The absolute prestate of the trace.
*/
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32(uint256(15)));
/**
* @dev The honest participant.
*/
GamePlayer internal honest;
/**
* @dev The dishonest participant.
*/
GamePlayer internal dishonest;
function init(
GamePlayer _honest,
GamePlayer _dishonest,
Claim _rootClaim
) public {
super.init(_rootClaim, ABSOLUTE_PRESTATE);
// Deploy a new honest player.
honest = _honest;
// Deploy a new dishonest player.
dishonest = _dishonest;
// Set the counterparties.
honest.init(gameProxy, dishonest, vm);
dishonest.init(gameProxy, honest, vm);
// Label actors for trace.
vm.label(address(honest), "HonestPlayer");
vm.label(address(dishonest), "DishonestPlayer");
}
}
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new FullyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(30))));
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
honest.play(0);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new FullyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31))));
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
dishonest.play(0);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new HalfDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(15))));
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
honest.play(0);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot2 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new HalfDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31))));
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
dishonest.play(0);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new EarlyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(15))));
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
honest.play(0);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot4 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new EarlyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31))));
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
dishonest.play(0);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep());
}
}
////////////////////////////////////////////////////////////////
// ACTORS //
////////////////////////////////////////////////////////////////
contract HonestPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory honestTrace = new bytes(16);
for (uint8 i = 0; i < honestTrace.length; i++) {
honestTrace[i] = bytes1(absolutePrestate + i + 1);
}
trace = honestTrace;
}
}
contract FullyDivergentPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the honest trace by 1.
dishonestTrace[i] = bytes1(absolutePrestate + i);
}
trace = dishonestTrace;
}
}
contract HalfDivergentPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half.
dishonestTrace[i] = i > 7 ? bytes1(i) : bytes1(absolutePrestate + i + 1);
}
trace = dishonestTrace;
}
}
contract EarlyDivergentPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half.
dishonestTrace[i] = i > 2 ? bytes1(i) : bytes1(absolutePrestate + i + 1);
}
trace = dishonestTrace;
} }
} }
...@@ -105,15 +105,14 @@ contract LibPosition_Test is Test { ...@@ -105,15 +105,14 @@ contract LibPosition_Test is Test {
_indexAtDepth = boundIndexAtDepth(_depth, _indexAtDepth); _indexAtDepth = boundIndexAtDepth(_depth, _indexAtDepth);
Position position = LibPosition.wrap(_depth, _indexAtDepth); Position position = LibPosition.wrap(_depth, _indexAtDepth);
uint64 rightIndex = position.rightIndex(_maxDepth); Position rightIndex = position.rightIndex(_maxDepth);
// Find the deepest, rightmost index in Solidity rather than Yul // Find the deepest, rightmost index in Solidity rather than Yul
for (uint256 i = _depth; i < _maxDepth; ++i) { for (uint256 i = _depth; i < _maxDepth; ++i) {
position = position.right(); position = position.right();
} }
uint64 _rightIndex = position.indexAtDepth();
assertEq(rightIndex, _rightIndex); assertEq(Position.unwrap(rightIndex), Position.unwrap(position));
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment