Commit 8ae0c24d authored by clabby's avatar clabby

Add secondary game

parent 3b29035a
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
"src/dispute/BlockOracle.sol": "0x7e724b1ee0116dfd744f556e6237af449c2f40c6426d6f1462ae2a47589283bb", "src/dispute/BlockOracle.sol": "0x7e724b1ee0116dfd744f556e6237af449c2f40c6426d6f1462ae2a47589283bb",
"src/dispute/DisputeGameFactory.sol": "0xfdfa141408d7f8de7e230ff4bef088e30d0e4d569ca743d60d292abdd21ff270", "src/dispute/DisputeGameFactory.sol": "0xfdfa141408d7f8de7e230ff4bef088e30d0e4d569ca743d60d292abdd21ff270",
"src/dispute/FaultDisputeGame.sol": "0x7ac7553a47d96a4481a6b95363458bed5f160112b647829c4defc134fa178d9a", "src/dispute/FaultDisputeGame.sol": "0x7ac7553a47d96a4481a6b95363458bed5f160112b647829c4defc134fa178d9a",
"src/dispute/OutputBisectionGame.sol": "0xc5db24fcae2a668852f2a8a283483b75ca598f1d7169bd50970ab6c8e4a1aeb4",
"src/legacy/DeployerWhitelist.sol": "0x0a6840074734c9d167321d3299be18ef911a415e4c471fa92af7d6cfaa8336d4", "src/legacy/DeployerWhitelist.sol": "0x0a6840074734c9d167321d3299be18ef911a415e4c471fa92af7d6cfaa8336d4",
"src/legacy/L1BlockNumber.sol": "0x20d83a636c5e2067fca8c0ed505b295174e6eddb25960d8705e6b6fea8e77fa6", "src/legacy/L1BlockNumber.sol": "0x20d83a636c5e2067fca8c0ed505b295174e6eddb25960d8705e6b6fea8e77fa6",
"src/legacy/LegacyMessagePasser.sol": "0x80f355c9710af586f58cf6a86d1925e0073d1e504d0b3d814284af1bafe4dece", "src/legacy/LegacyMessagePasser.sol": "0x80f355c9710af586f58cf6a86d1925e0073d1e504d0b3d814284af1bafe4dece",
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { IOutputBisectionGame } from "src/dispute/interfaces/IOutputBisectionGame.sol";
import { IInitializable } from "src/dispute/interfaces/IInitializable.sol";
import { IBondManager } from "src/dispute/interfaces/IBondManager.sol";
import { IBigStepper, IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { BlockOracle } from "src/dispute/BlockOracle.sol";
import { Clone } from "src/libraries/Clone.sol";
import { Types } from "src/libraries/Types.sol";
import { ISemver } from "src/universal/ISemver.sol";
import { LibHashing } from "src/dispute/lib/LibHashing.sol";
import { LibPosition } from "src/dispute/lib/LibPosition.sol";
import { LibClock } from "src/dispute/lib/LibClock.sol";
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
/// @title OutputBisectionGame
/// @notice An implementation of the `IOutputBisectionGame` interface.
contract OutputBisectionGame is IOutputBisectionGame, 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 public immutable ABSOLUTE_PRESTATE;
/// @notice The max depth of the game.
uint256 public 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 public immutable SPLIT_DEPTH;
/// @notice The duration of the game.
Duration public immutable GAME_DURATION;
/// @notice An onchain VM that performs single instruction steps on a fault proof program trace.
IBigStepper public immutable VM;
/// @notice The game type ID
GameType internal immutable GAME_TYPE;
/// @notice The genesis block number
uint256 internal immutable GENESIS_BLOCK_NUMBER;
/// @notice The global root claim's position is always at gindex 1.
Position internal constant ROOT_POSITION = Position.wrap(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;
/// @inheritdoc IDisputeGame
IBondManager public bondManager;
/// @inheritdoc IOutputBisectionGame
Hash public l1Head;
/// @notice An append-only array of all claims made during the dispute game.
ClaimData[] public claimData;
/// @notice An internal mapping to allow for constant-time lookups of existing claims.
mapping(ClaimHash => bool) internal claims;
/// @notice An internal mapping of subgames rooted at a claim index to other claim indices in the subgame.
mapping(uint256 => uint256[]) internal subgames;
/// @notice Indicates whether the subgame rooted at the root claim has been resolved.
bool internal subgameAtRootResolved;
/// @notice Semantic version.
/// @custom:semver 0.0.13
string public constant version = "0.0.13";
/// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace.
/// @param _genesisBlockNumber The block number of the genesis block.
/// @param _maxGameDepth The maximum depth of bisection.
/// @param _splitDepth The final depth of the output bisection portion of the game.
/// @param _gameDuration The duration of the game.
/// @param _vm An onchain VM that performs single instruction steps on a fault proof program
/// trace.
constructor(
GameType _gameType,
Claim _absolutePrestate,
uint256 _genesisBlockNumber,
uint256 _maxGameDepth,
uint256 _splitDepth,
Duration _gameDuration,
IBigStepper _vm
) {
if (_splitDepth >= _maxGameDepth) revert InvalidSplitDepth();
GAME_TYPE = _gameType;
ABSOLUTE_PRESTATE = _absolutePrestate;
GENESIS_BLOCK_NUMBER = _genesisBlockNumber;
MAX_GAME_DEPTH = _maxGameDepth;
SPLIT_DEPTH = _splitDepth;
GAME_DURATION = _gameDuration;
VM = _vm;
}
////////////////////////////////////////////////////////////////
// `IOutputBisectionGame` impl //
////////////////////////////////////////////////////////////////
/// @inheritdoc IOutputBisectionGame
function step(uint256 _claimIndex, bool _isAttack, bytes calldata _stateData, bytes calldata _proof) external {
// 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.
preStateClaim = stepPos.indexAtDepth() == 0
? ABSOLUTE_PRESTATE
: findTraceAncestor(Position.wrap(Position.unwrap(parentPos) - 1), parent.parentIndex).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(Position.unwrap(parentPos) + 1), parent.parentIndex);
}
// 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 != Claim.unwrap(preStateClaim) << 8) revert InvalidPrestate();
// TODO(clabby): Include less context. See Adrian's proposal for the local context salt.
(ClaimData storage starting, ClaimData storage disputed) = findStartingAndDisputedOutputs(_claimIndex);
bytes32 uuid = keccak256(abi.encode(starting.claim, starting.parentIndex, disputed.claim, disputed.parentIndex));
// 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) == Claim.unwrap(postState.claim);
bool parentPostAgree = (parentPos.depth() - postState.position.depth()) % 2 == 0;
if (parentPostAgree == validStep) 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;
}
/// @notice Internal move function, used by both `attack` and `defend`.
/// @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 {
// INVARIANT: Moves cannot be made unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// INVARIANT: A defense can never be made against the root claim. 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 && !_isAttack) revert CannotDefendRootClaim();
// 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 nextPosition = parent.position.move(_isAttack);
// 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 (nextPosition.depth() > 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 (nextPosition.depth() == SPLIT_DEPTH + 1) verifyExecBisectionRoot(_claim, _challengeIndex);
// Fetch the grandparent clock, if it exists.
// The grandparent clock should always exist unless the parent is the root claim.
Clock grandparentClock;
if (parent.parentIndex != type(uint32).max) {
grandparentClock = claimData[parent.parentIndex].clock;
}
// 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 = Duration.wrap(
uint64(
// First, fetch the duration of the grandparent claim.
Duration.unwrap(grandparentClock.duration())
// Second, add the difference between the current block timestamp and the
// parent's clock timestamp.
+ block.timestamp - Timestamp.unwrap(parent.clock.timestamp())
)
);
// INVARIANT: A move can never be made once its clock has exceeded `GAME_DURATION / 2`
// seconds of time.
if (Duration.unwrap(nextDuration) > Duration.unwrap(GAME_DURATION) >> 1) {
revert ClockTimeExceeded();
}
// 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.
ClaimHash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex);
if (claims[claimHash]) revert ClaimAlreadyExists();
claims[claimHash] = true;
// Create the new claim.
claimData.push(
ClaimData({
parentIndex: uint32(_challengeIndex),
claim: _claim,
position: nextPosition,
clock: nextClock,
countered: false
})
);
// Set the parent claim as countered.
claimData[_challengeIndex].countered = true;
// Update the subgame rooted at the parent claim.
subgames[_challengeIndex].push(claimData.length - 1);
// Emit the appropriate event for the attack or defense.
emit Move(_challengeIndex, _claim, msg.sender);
}
/// @inheritdoc IOutputBisectionGame
function attack(uint256 _parentIndex, Claim _claim) external payable {
move(_parentIndex, _claim, true);
}
/// @inheritdoc IOutputBisectionGame
function defend(uint256 _parentIndex, Claim _claim) external payable {
move(_parentIndex, _claim, false);
}
/// @inheritdoc IOutputBisectionGame
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();
// TODO(clabby): Include less context. See Adrian's proposal for the local context salt.
(ClaimData storage starting, ClaimData storage disputed) = findStartingAndDisputedOutputs(_execLeafIdx);
bytes32 uuid = keccak256(abi.encode(starting.claim, starting.parentIndex, disputed.claim, disputed.parentIndex));
IPreimageOracle oracle = VM.oracle();
if (_ident == 1) {
// Load the L1 head hash
oracle.loadLocalData(_ident, uuid, Hash.unwrap(l1Head), 32, _partOffset);
} else if (_ident == 2) {
// Load the starting proposal's output root.
oracle.loadLocalData(_ident, uuid, Claim.unwrap(starting.claim), 32, _partOffset);
} else if (_ident == 3) {
// Load the disputed proposal's output root
oracle.loadLocalData(_ident, uuid, Claim.unwrap(disputed.claim), 32, _partOffset);
} else if (_ident == 4) {
// Load the starting proposal's L2 block number as a big-endian uint64 in the
// high order 8 bytes of the word.
// TODO(clabby): +1?
oracle.loadLocalData(_ident, uuid, bytes32(GENESIS_BLOCK_NUMBER + uint256(starting.position.indexAtDepth()) << 0xC0), 8, _partOffset);
} else if (_ident == 5) {
// Load the chain ID as a big-endian uint64 in the high order 8 bytes of the word.
oracle.loadLocalData(_ident, uuid, bytes32(block.chainid << 0xC0), 8, _partOffset);
} else {
revert InvalidLocalIdent();
}
}
/// @inheritdoc IOutputBisectionGame
function l2BlockNumber() public pure returns (uint256 l2BlockNumber_) {
l2BlockNumber_ = _getArgUint256(0x20);
}
////////////////////////////////////////////////////////////////
// `IDisputeGame` impl //
////////////////////////////////////////////////////////////////
/// @inheritdoc IDisputeGame
function gameType() public view override returns (GameType gameType_) {
gameType_ = GAME_TYPE;
}
/// @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 (!subgameAtRootResolved) revert OutOfOrderResolution();
// Update the global game status; The dispute has concluded.
status_ = claimData[0].countered ? GameStatus.CHALLENGER_WINS : GameStatus.DEFENDER_WINS;
resolvedAt = Timestamp.wrap(uint64(block.timestamp));
emit Resolved(status = status_);
}
/// @inheritdoc IOutputBisectionGame
function resolveClaim(uint256 _claimIndex) external payable {
// INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
ClaimData storage parent = claimData[_claimIndex];
// INVARIANT: Cannot resolve a subgame unless the clock of its root has expired
if (
Duration.unwrap(parent.clock.duration()) + (block.timestamp - Timestamp.unwrap(parent.clock.timestamp()))
<= Duration.unwrap(GAME_DURATION) >> 1
) {
revert ClockNotExpired();
}
uint256[] storage challengeIndices = subgames[_claimIndex];
// INVARIANT: Cannot resolve subgames twice
// Uncontested claims are resolved implicitly unless they are the root claim
if (_claimIndex == 0 && subgameAtRootResolved) revert ClaimAlreadyResolved();
if (challengeIndices.length == 0 && _claimIndex != 0) revert ClaimAlreadyResolved();
// Assume parent is honest until proven otherwise
bool countered = false;
for (uint256 i = 0; i < challengeIndices.length; ++i) {
uint256 challengeIndex = challengeIndices[i];
// INVARIANT: Cannot resolve a subgame containing an unresolved claim
if (subgames[challengeIndex].length != 0) revert OutOfOrderResolution();
ClaimData storage claim = claimData[challengeIndex];
// Ignore false claims
if (!claim.countered) {
countered = true;
break;
}
}
// Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
// resolveClaim will not need to traverse this subgame.
parent.countered = countered;
// Resolved subgames have no entries
delete subgames[_claimIndex];
// Indicate the game is ready to be resolved globally.
if (_claimIndex == 0) {
subgameAtRootResolved = true;
}
}
/// @inheritdoc IDisputeGame
function rootClaim() public pure returns (Claim rootClaim_) {
rootClaim_ = Claim.wrap(_getArgFixedBytes(0x00));
}
/// @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_ = _getArgDynBytes(0x20, 0x20);
}
/// @inheritdoc IDisputeGame
function gameData() external view returns (GameType gameType_, Claim rootClaim_, bytes memory extraData_) {
gameType_ = gameType();
rootClaim_ = rootClaim();
extraData_ = extraData();
}
////////////////////////////////////////////////////////////////
// MISC EXTERNAL //
////////////////////////////////////////////////////////////////
/// @inheritdoc IInitializable
function initialize() external {
// 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`
// Set the game's starting timestamp
createdAt = Timestamp.wrap(uint64(block.timestamp));
// Set the root claim
claimData.push(
ClaimData({
parentIndex: type(uint32).max,
claim: rootClaim(),
position: ROOT_POSITION,
clock: LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))),
countered: false
})
);
// Persist the L1 head hash of the parent block.
// TODO(clabby): There may be a bug here - Do we just allow the dispute game to be invalid? We can
// always just create another, but it is possible to create a game where the data was not
// already available on L1.
l1Head = Hash.wrap(blockhash(block.number - 1));
}
/// @notice Returns the length of the `claimData` array.
function claimDataLen() external view returns (uint256 len_) {
len_ = claimData.length;
}
////////////////////////////////////////////////////////////////
// HELPERS //
////////////////////////////////////////////////////////////////
/// @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 /* _parentIndex */ ) internal pure {
// The VMStatus must indicate 'invalid' (1), to argue that disputed thing is invalid.
// Games that agree with the existing outcome are not allowed.
// TODO(clabby): This assumption will change in Alpha Chad, and also depending on the split depth! Be careful
// about what we go with here.
uint8 vmStatus = uint8(Claim.unwrap(_rootClaim)[0]);
if (!(vmStatus == VMStatus.unwrap(VMStatuses.INVALID) || vmStatus == VMStatus.unwrap(VMStatuses.PANIC))) {
revert UnexpectedRootClaim(_rootClaim);
}
// TODO(clabby): Other verification steps (?)
}
/// @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.
/// @return ancestor_ The ancestor claim that commits to the same trace index as `_pos`.
function findTraceAncestor(Position _pos, uint256 _start) internal view returns (ClaimData storage ancestor_) {
// Grab the trace ancestor's expected position.
Position preStateTraceAncestor = _pos.traceAncestor();
// 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 (Position.unwrap(ancestor_.position) != Position.unwrap(preStateTraceAncestor)) {
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 starting_ The starting, agreed upon output root claim.
/// @return disputed_ The disputed output root claim.
function findStartingAndDisputedOutputs(uint256 _start)
internal
view
returns (ClaimData storage starting_, ClaimData storage disputed_)
{
// 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 = Position.unwrap(execRootPos.parent()) == Position.unwrap(outputPos);
// 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) {
starting_ = findTraceAncestor(Position.wrap(Position.unwrap(outputPos) - 1), claimIdx);
disputed_ = claimData[claimIdx];
} else {
starting_ = claimData[claimIdx];
disputed_ = findTraceAncestor(Position.wrap(Position.unwrap(outputPos) + 1), claimIdx);
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { IDisputeGame } from "./IDisputeGame.sol";
import "src/libraries/DisputeTypes.sol";
/// @title IOutputBisectionGame
/// @notice The interface for a fault proof backed dispute game.
interface IOutputBisectionGame is IDisputeGame {
/// @notice The `ClaimData` struct represents the data associated with a Claim.
/// @dev TODO(clabby): Add bond ID information.
struct ClaimData {
uint32 parentIndex;
bool countered;
Claim claim;
Position position;
Clock clock;
}
/// @notice Emitted when a new claim is added to the DAG by `claimant`
/// @param parentIndex The index within the `claimData` array of the parent claim
/// @param claim The claim being added
/// @param claimant The address of the claimant
event Move(uint256 indexed parentIndex, Claim indexed claim, address indexed claimant);
/// @notice Attack a disagreed upon `Claim`.
/// @param _parentIndex Index of the `Claim` to attack in the `claimData` array.
/// @param _claim The `Claim` at the relative attack position.
function attack(uint256 _parentIndex, Claim _claim) external payable;
/// @notice Defend an agreed upon `Claim`.
/// @param _parentIndex Index of the claim to defend in the `claimData` array.
/// @param _claim The `Claim` at the relative defense position.
function defend(uint256 _parentIndex, Claim _claim) external payable;
/// @notice Perform an instruction step via an on-chain fault proof processor.
/// @dev This function should point to a fault proof processor in order to execute
/// a step in the fault proof program on-chain. The interface of the fault proof
/// processor contract should adhere to the `IBigStepper` interface.
/// @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 at the given
/// prestate, which is at `_stateIndex` if the move is an attack and `_claimIndex` if
/// the move is a defense. If the step is an attack on the first instruction, it is
/// the absolute prestate of the fault proof VM.
/// @param _proof Proof to access memory nodes in the VM's merkle state tree.
function step(uint256 _claimIndex, bool _isAttack, bytes calldata _stateData, bytes calldata _proof) external;
/// @notice Posts the requested local data to the VM's `PreimageOralce`.
/// @param _ident The local identifier of the data to post.
/// @param _execLeafIdx The index of the leaf claim in an execution subgame that requires the local data for a step.
/// @param _partOffset The offset of the data to post.
function addLocalData(uint256 _ident, uint256 _execLeafIdx, uint256 _partOffset) external;
/// @notice Resolves the subgame rooted at the given claim index.
/// @dev This function must be called bottom-up in the DAG
/// A subgame is a tree of claims that has a maximum depth of 1.
/// A subgame root claims is valid if, and only if, all of its child claims are invalid.
/// At the deepest level in the DAG, a claim is invalid if there's a successful step against it.
/// @param _claimIndex The index of the subgame root claim to resolve.
function resolveClaim(uint256 _claimIndex) external payable;
/// @notice An L1 block hash that contains the disputed output root, fetched from the
/// `BlockOracle` and verified by referencing the timestamp associated with the
/// first L2 Output Proposal in the `L2OutputOracle` that contains the disputed
/// L2 block number.
function l1Head() external view returns (Hash l1Head_);
/// @notice The l2BlockNumber of the disputed output root in the `L2OutputOracle`.
function l2BlockNumber() external view returns (uint256 l2BlockNumber_);
}
...@@ -72,6 +72,14 @@ error OutOfOrderResolution(); ...@@ -72,6 +72,14 @@ error OutOfOrderResolution();
/// @notice Thrown when resolving a claim that has already been resolved. /// @notice Thrown when resolving a claim that has already been resolved.
error ClaimAlreadyResolved(); error ClaimAlreadyResolved();
/// @notice Thrown when a parent output root is attempted to be found on a claim that is in
/// the output root portion of the tree.
error ClaimAboveSplit();
/// @notice Thrown on deployment if the split depth is greater than or equal to the max
/// depth of the game.
error InvalidSplitDepth();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `AttestationDisputeGame` Errors // // `AttestationDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
// 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/DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { OutputBisectionGame } from "src/dispute/OutputBisectionGame.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { BlockOracle } from "src/dispute/BlockOracle.sol";
import { PreimageOracle } from "src/cannon/PreimageOracle.sol";
import { PreimageKeyLib } from "src/cannon/PreimageKeyLib.sol";
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
import { Types } from "src/libraries/Types.sol";
import { LibClock } from "src/dispute/lib/LibClock.sol";
import { LibPosition } from "src/dispute/lib/LibPosition.sol";
import { IBigStepper, IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol";
import { AlphabetVM } from "test/mocks/AlphabetVM.sol";
contract OutputBisectionGame_Init is DisputeGameFactory_Init {
/// @dev The type of the game being tested.
GameType internal constant GAME_TYPE = GameType.wrap(0);
/// @dev The L2 Block Number for the game's proposed output (the root claim)
uint256 internal constant L2_BLOCK_NUMBER = 0xFFFF;
uint256 internal constant GENESIS_BLOCK_NUMBER = 0;
/// @dev The implementation of the game.
OutputBisectionGame internal gameImpl;
/// @dev The `Clone` proxy of the game.
OutputBisectionGame 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);
function init(Claim rootClaim, Claim absolutePrestate) public {
// Set the time to a realistic date.
vm.warp(1690906994);
// Set the extra data for the game creation
extraData = abi.encode(L2_BLOCK_NUMBER);
// Deploy an implementation of the fault game
gameImpl = new OutputBisectionGame(
GAME_TYPE,
absolutePrestate,
GENESIS_BLOCK_NUMBER,
4,
2,
Duration.wrap(7 days),
new AlphabetVM(absolutePrestate)
);
// Register the game implementation with the factory.
factory.setImplementation(GAME_TYPE, gameImpl);
// Create a new game.
gameProxy = OutputBisectionGame(address(factory.create(GAME_TYPE, rootClaim, extraData)));
// Label the proxy
vm.label(address(gameProxy), "OutputBisectionGame_Clone");
}
}
contract OutputBisectionGame_Test is OutputBisectionGame_Init {
/// @dev The root claim of the game.
Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32((uint256(1) << 248) | uint256(10)));
/// @dev The absolute prestate of the trace.
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32((uint256(3) << 248) | uint256(0)));
function setUp() public override {
super.setUp();
super.init(ROOT_CLAIM, ABSOLUTE_PRESTATE);
}
////////////////////////////////////////////////////////////////
// `IDisputeGame` Implementation Tests //
////////////////////////////////////////////////////////////////
/// @dev Tests that the game's root claim is set correctly.
function test_rootClaim_succeeds() public {
assertEq(Claim.unwrap(gameProxy.rootClaim()), Claim.unwrap(ROOT_CLAIM));
}
/// @dev Tests that the game's extra data is set correctly.
function test_extraData_succeeds() public {
assertEq(gameProxy.extraData(), extraData);
}
/// @dev Tests that the game's starting timestamp is set correctly.
function test_createdAt_succeeds() public {
assertEq(Timestamp.unwrap(gameProxy.createdAt()), block.timestamp);
}
/// @dev Tests that the game's type is set correctly.
function test_gameType_succeeds() public {
assertEq(GameType.unwrap(gameProxy.gameType()), GameType.unwrap(GAME_TYPE));
}
/// @dev Tests that the game's data is set correctly.
function test_gameData_succeeds() public {
(GameType gameType, Claim rootClaim, bytes memory _extraData) = gameProxy.gameData();
assertEq(GameType.unwrap(gameType), GameType.unwrap(GAME_TYPE));
assertEq(Claim.unwrap(rootClaim), Claim.unwrap(ROOT_CLAIM));
assertEq(_extraData, extraData);
}
////////////////////////////////////////////////////////////////
// `IOutputBisectionGame` Implementation Tests //
////////////////////////////////////////////////////////////////
/// @dev Tests that the game is initialized with the correct data.
function test_initialize_correctData_succeeds() public {
// Assert that the root claim is initialized correctly.
(uint32 parentIndex, bool countered, Claim claim, Position position, Clock clock) = gameProxy.claimData(0);
assertEq(parentIndex, type(uint32).max);
assertEq(countered, false);
assertEq(Claim.unwrap(claim), Claim.unwrap(ROOT_CLAIM));
assertEq(Position.unwrap(position), 1);
assertEq(
Clock.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))))
);
// Assert that the `createdAt` timestamp is correct.
assertEq(Timestamp.unwrap(gameProxy.createdAt()), block.timestamp);
// Assert that the blockhash provided is correct.
assertEq(Hash.unwrap(gameProxy.l1Head()), blockhash(block.number - 1));
}
/// @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(uint256(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, Claim.wrap(bytes32(uint256(5))));
}
/// @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 = Claim.wrap(bytes32(uint256(5)));
// 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 an attack
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 = Claim.wrap(bytes32(uint256(5)));
uint256 maxDepth = gameProxy.MAX_GAME_DEPTH();
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);
} else if (i == 2) {
claim = changeClaimStatus(claim, VMStatuses.PANIC);
}
gameProxy.attack(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);
vm.expectRevert(ClockTimeExceeded.selector);
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
}
/// @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.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))))
);
Claim claim = Claim.wrap(bytes32(uint256(5)));
vm.warp(block.timestamp + 15);
gameProxy.attack(0, claim);
(,,,, clock) = gameProxy.claimData(1);
assertEq(
Clock.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(15), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(1, claim);
(,,,, clock) = gameProxy.claimData(2);
assertEq(
Clock.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(10), Timestamp.wrap(uint64(block.timestamp))))
);
// 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);
gameProxy.attack(2, claim);
(,,,, clock) = gameProxy.claimData(3);
assertEq(
Clock.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(25), Timestamp.wrap(uint64(block.timestamp))))
);
vm.warp(block.timestamp + 10);
gameProxy.attack(3, claim);
(,,,, clock) = gameProxy.claimData(4);
assertEq(
Clock.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(20), Timestamp.wrap(uint64(block.timestamp))))
);
}
/// @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 = Claim.wrap(bytes32(uint256(5)));
// Make the first move. This should succeed.
gameProxy.attack(0, claim);
// Attempt to make the same move again.
vm.expectRevert(ClaimAlreadyExists.selector);
gameProxy.attack(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 = Claim.wrap(bytes32(uint256(5)));
Claim claimB = Claim.wrap(bytes32(uint256(6)));
// Make the first move. This should succeed.
gameProxy.attack(0, claimA);
gameProxy.attack(0, claimB);
gameProxy.attack(1, claimB);
gameProxy.attack(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 = Claim.wrap(bytes32(uint256(5)));
// Perform the attack.
vm.expectEmit(true, true, true, false);
emit Move(0, counter, address(this));
gameProxy.attack(0, counter);
// Grab the claim data of the attack.
(uint32 parentIndex, bool countered, Claim claim, Position position, Clock clock) = gameProxy.claimData(1);
// Assert correctness of the attack claim's data.
assertEq(parentIndex, 0);
assertEq(countered, false);
assertEq(Claim.unwrap(claim), Claim.unwrap(counter));
assertEq(Position.unwrap(position), Position.unwrap(Position.wrap(1).move(true)));
assertEq(
Clock.unwrap(clock), Clock.unwrap(LibClock.wrap(Duration.wrap(5), Timestamp.wrap(uint64(block.timestamp))))
);
// Grab the claim data of the parent.
(parentIndex, countered, claim, position, clock) = gameProxy.claimData(0);
// Assert correctness of the parent claim's data.
assertEq(parentIndex, type(uint32).max);
assertEq(countered, true);
assertEq(Claim.unwrap(claim), Claim.unwrap(ROOT_CLAIM));
assertEq(Position.unwrap(position), 1);
assertEq(
Clock.unwrap(clock),
Clock.unwrap(LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp - 5))))
);
}
/// @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 + 1 seconds);
gameProxy.resolveClaim(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);
vm.expectRevert(ClockNotExpired.selector);
gameProxy.resolveClaim(0);
}
/// @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 + 1 seconds);
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);
}
/// @dev Static unit test for the correctness of resolving a single attack game state.
function test_resolve_rootContested_succeeds() public {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
gameProxy.resolveClaim(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(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(6))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
gameProxy.resolveClaim(1);
gameProxy.resolveClaim(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(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.attack(0, Claim.wrap(bytes32(uint256(4))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(6))));
gameProxy.defend(1, Claim.wrap(bytes32(uint256(7))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
gameProxy.resolveClaim(1);
gameProxy.resolveClaim(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 dummyClaim = Claim.wrap(bytes32(uint256(5)));
gameProxy.attack(0, dummyClaim);
gameProxy.attack(1, dummyClaim);
dummyClaim = changeClaimStatus(dummyClaim, VMStatuses.PANIC);
gameProxy.attack(2, dummyClaim);
gameProxy.attack(3, dummyClaim);
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
// resolving claim at 4 isn't necessary
gameProxy.resolveClaim(3);
gameProxy.resolveClaim(2);
gameProxy.resolveClaim(1);
gameProxy.resolveClaim(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 {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.attack(1, Claim.wrap(bytes32(uint256(5))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
gameProxy.resolveClaim(1);
vm.expectRevert(ClaimAlreadyResolved.selector);
gameProxy.resolveClaim(1);
}
/// @dev Static unit test asserting that resolve reverts when attempting to resolve a subgame at max depth
function test_resolve_claimAtMaxDepthAlreadyResolved_reverts() public {
Claim dummyClaim = Claim.wrap(bytes32(uint256(5)));
gameProxy.attack(0, dummyClaim);
gameProxy.attack(1, dummyClaim);
dummyClaim = changeClaimStatus(dummyClaim, VMStatuses.PANIC);
gameProxy.attack(2, dummyClaim);
gameProxy.attack(3, dummyClaim);
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
vm.expectRevert(ClaimAlreadyResolved.selector);
gameProxy.resolveClaim(4);
}
/// @dev Static unit test asserting that resolve reverts when attempting to resolve subgames out of order
function test_resolve_outOfOrderResolution_reverts() public {
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.attack(1, Claim.wrap(bytes32(uint256(5))));
vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds);
vm.expectRevert(OutOfOrderResolution.selector);
gameProxy.resolveClaim(0);
}
/// @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.
gameProxy.attack(0, Claim.wrap(bytes32(uint256(5))));
gameProxy.attack(1, Claim.wrap(bytes32(uint256(5))));
gameProxy.attack(2, ROOT_CLAIM);
// [1, 5] are valid local data identifiers.
if (_ident <= 5) _ident = 0;
vm.expectRevert(InvalidLocalIdent.selector);
gameProxy.addLocalData(_ident, 3, 0);
}
/// @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));
}
/// @dev Helper to change the VM status byte of a claim.
function changeClaimStatus(Claim _claim, VMStatus _status) public pure returns (Claim out_) {
assembly {
out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status))
}
}
}
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