Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
N
nebula
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
exchain
nebula
Commits
8ae0c24d
Commit
8ae0c24d
authored
Nov 27, 2023
by
clabby
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add secondary game
parent
3b29035a
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
1098 additions
and
0 deletions
+1098
-0
semver-lock.json
packages/contracts-bedrock/semver-lock.json
+1
-0
OutputBisectionGame.sol
...ges/contracts-bedrock/src/dispute/OutputBisectionGame.sol
+545
-0
IOutputBisectionGame.sol
...s-bedrock/src/dispute/interfaces/IOutputBisectionGame.sol
+72
-0
DisputeErrors.sol
packages/contracts-bedrock/src/libraries/DisputeErrors.sol
+8
-0
OutputBisectionGame.t.sol
packages/contracts-bedrock/test/OutputBisectionGame.t.sol
+472
-0
No files found.
packages/contracts-bedrock/semver-lock.json
View file @
8ae0c24d
...
@@ -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"
,
...
...
packages/contracts-bedrock/src/dispute/OutputBisectionGame.sol
0 → 100644
View file @
8ae0c24d
// 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);
}
}
}
packages/contracts-bedrock/src/dispute/interfaces/IOutputBisectionGame.sol
0 → 100644
View file @
8ae0c24d
// 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_);
}
packages/contracts-bedrock/src/libraries/DisputeErrors.sol
View file @
8ae0c24d
...
@@ -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 //
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
...
...
packages/contracts-bedrock/test/OutputBisectionGame.t.sol
0 → 100644
View file @
8ae0c24d
// 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))
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment