Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ctb): Subgame rez changes #10148

Merged
merged 11 commits into from Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 33 additions & 2 deletions op-bindings/bindings/faultdisputegame.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions op-bindings/bindings/faultdisputegame_more.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/contracts-bedrock/semver-lock.json
Expand Up @@ -112,8 +112,8 @@
"sourceCodeHash": "0x8545910bdb40f5e706a0ae5ed274cabc6d1f17be92d497a490d5732d74ac9c59"
},
"src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0xcd35529eb99878f434b2e899d1937365204fd014333c1448a0eae8b1c765349b",
"sourceCodeHash": "0xc5f90543341e426d0fbc834e997a727c9b71e118491a935afd61fe053c134811"
"initCodeHash": "0x4ea53ec8274b7a25012aab6655cd84a60b4cbcdba95ad199085fd81910731bee",
"sourceCodeHash": "0x778aafed19b2d8dddd61a44d39e94fb3139a89e416a314572534064ab8823ee1"
},
"src/dispute/weth/DelayedWETH.sol": {
"initCodeHash": "0x7b6ec89eaec09e369426e73161a9c6932223bb1f974377190c3f6f552995da35",
Expand Down
19 changes: 19 additions & 0 deletions packages/contracts-bedrock/snapshots/abi/FaultDisputeGame.json
Expand Up @@ -299,6 +299,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_claimIndex",
"type": "uint256"
}
],
"name": "getChallengerDuration",
"outputs": [
{
"internalType": "Duration",
"name": "duration_",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
Expand Down
Expand Up @@ -309,6 +309,25 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_claimIndex",
"type": "uint256"
}
],
"name": "getChallengerDuration",
"outputs": [
{
"internalType": "Duration",
"name": "duration_",
"type": "uint64"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
Expand Down
Expand Up @@ -49,24 +49,24 @@
"type": "mapping(uint256 => uint256[])"
},
{
"bytes": "1",
"label": "subgameAtRootResolved",
"bytes": "32",
"label": "resolvedSubgames",
"offset": 0,
"slot": "5",
"type": "bool"
"type": "mapping(uint256 => bool)"
},
{
"bytes": "1",
"label": "initialized",
"offset": 1,
"slot": "5",
"offset": 0,
"slot": "6",
"type": "bool"
},
{
"bytes": "64",
"label": "startingOutputRoot",
"offset": 0,
"slot": "6",
"slot": "7",
"type": "struct OutputRoot"
}
]
Expand Up @@ -49,24 +49,24 @@
"type": "mapping(uint256 => uint256[])"
},
{
"bytes": "1",
"label": "subgameAtRootResolved",
"bytes": "32",
"label": "resolvedSubgames",
"offset": 0,
"slot": "5",
"type": "bool"
"type": "mapping(uint256 => bool)"
},
{
"bytes": "1",
"label": "initialized",
"offset": 1,
"slot": "5",
"offset": 0,
"slot": "6",
"type": "bool"
},
{
"bytes": "64",
"label": "startingOutputRoot",
"offset": 0,
"slot": "6",
"slot": "7",
"type": "struct OutputRoot"
}
]
107 changes: 55 additions & 52 deletions packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol
Expand Up @@ -57,9 +57,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @notice The global root claim's position is always at gindex 1.
Position internal constant ROOT_POSITION = Position.wrap(1);

/// @notice The flag set in the `bond` field of a `ClaimData` struct to indicate that the bond has been claimed.
uint128 internal constant CLAIMED_BOND_FLAG = type(uint128).max;

/// @notice The starting timestamp of the game
Timestamp public createdAt;

Expand All @@ -81,8 +78,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @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 An interneal mapping of resolved subgames rooted at a claim index.
mapping(uint256 => bool) internal resolvedSubgames;

/// @notice Flag for the `initialize` function to prevent re-initialization.
bool internal initialized;
Expand All @@ -91,8 +88,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
OutputRoot public startingOutputRoot;

/// @notice Semantic version.
/// @custom:semver 0.10.0
string public constant version = "0.10.1";
/// @custom:semver 0.11.0
string public constant version = "0.11.0";

/// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace.
Expand Down Expand Up @@ -259,29 +256,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
// INVARIANT: The `msg.value` must exactly equal the required bond.
if (getRequiredBond(nextPosition) != msg.value) revert IncorrectBondAmount();

// 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.
grandparentClock.duration().raw()
// Second, add the difference between the current block timestamp and the
// parent's clock timestamp.
+ block.timestamp - parent.clock.timestamp().raw()
)
);
Duration nextDuration = getChallengerDuration(_challengeIndex);

// INVARIANT: A move can never be made once its clock has exceeded `GAME_DURATION / 2`
// seconds of time.
if (nextDuration.raw() > GAME_DURATION.raw() >> 1) revert ClockTimeExceeded();
if (nextDuration.raw() == GAME_DURATION.raw() >> 1) revert ClockTimeExceeded();
Inphi marked this conversation as resolved.
Show resolved Hide resolved

// Construct the next clock with the new duration and the current block timestamp.
Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp)));
Expand Down Expand Up @@ -388,7 +370,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();

// INVARIANT: Resolution cannot occur unless the absolute root subgame has been resolved.
if (!subgameAtRootResolved) revert OutOfOrderResolution();
if (!resolvedSubgames[0]) revert OutOfOrderResolution();

// Update the global game status; The dispute has concluded.
status_ = claimData[0].counteredBy == address(0) ? GameStatus.DEFENDER_WINS : GameStatus.CHALLENGER_WINS;
Expand All @@ -406,33 +388,31 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
// INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();

ClaimData storage parent = claimData[_claimIndex];
ClaimData storage subgameRootClaim = claimData[_claimIndex];
Duration challengeClockDuration = getChallengerDuration(_claimIndex);

// INVARIANT: Cannot resolve a subgame unless the clock of its root has expired
uint64 parentClockDuration = parent.clock.duration().raw();
uint64 timeSinceParentMove = uint64(block.timestamp) - parent.clock.timestamp().raw();
if (parentClockDuration + timeSinceParentMove <= GAME_DURATION.raw() >> 1) {
revert ClockNotExpired();
}
// INVARIANT: Cannot resolve a subgame unless the clock of its would-be counter has expired
// INVARIANT: Assuming ordered subgame resolution, challengeClockDuration is always >= GAME_DURATION / 2 if all
// descendant subgames are resolved
if (challengeClockDuration.raw() < GAME_DURATION.raw() >> 1) revert ClockNotExpired();

// INVARIANT: Cannot resolve a subgame twice.
if (resolvedSubgames[_claimIndex]) revert ClaimAlreadyResolved();

uint256[] storage challengeIndices = subgames[_claimIndex];
uint256 challengeIndicesLen = challengeIndices.length;

// INVARIANT: Cannot resolve subgames twice
if (_claimIndex == 0 && subgameAtRootResolved) {
revert ClaimAlreadyResolved();
}

// Uncontested claims are resolved implicitly unless they are the root claim. Pay out the bond to the claimant
// and return early.
if (challengeIndicesLen == 0 && _claimIndex != 0) {
// In the event that the parent claim is at the max depth, there will always be 0 subgames. If the
// `counteredBy` field is set and there are no subgames, this implies that the parent claim was successfully
// stepped against. In this case, we pay out the bond to the party that stepped against the parent claim.
// Otherwise, the parent claim is uncontested, and the bond is returned to the claimant.
address counteredBy = parent.counteredBy;
address recipient = counteredBy == address(0) ? parent.claimant : counteredBy;
_distributeBond(recipient, parent);
address counteredBy = subgameRootClaim.counteredBy;
address recipient = counteredBy == address(0) ? subgameRootClaim.claimant : counteredBy;
_distributeBond(recipient, subgameRootClaim);
resolvedSubgames[_claimIndex] = true;
return;
}

Expand All @@ -443,7 +423,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
uint256 challengeIndex = challengeIndices[i];

// INVARIANT: Cannot resolve a subgame containing an unresolved claim
if (subgames[challengeIndex].length != 0) revert OutOfOrderResolution();
if (!resolvedSubgames[challengeIndex]) revert OutOfOrderResolution();

ClaimData storage claim = claimData[challengeIndex];

Expand All @@ -461,19 +441,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {

// If the parent was not successfully countered, pay out the parent's bond to the claimant.
// If the parent was successfully countered, pay out the parent's bond to the challenger.
_distributeBond(countered == address(0) ? parent.claimant : countered, parent);
_distributeBond(countered == address(0) ? subgameRootClaim.claimant : countered, subgameRootClaim);

// 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.counteredBy = countered;

// Resolved subgames have no entries
delete subgames[_claimIndex];
subgameRootClaim.counteredBy = countered;

// Indicate the game is ready to be resolved globally.
if (_claimIndex == 0) {
subgameAtRootResolved = true;
}
// Mark the subgame as resolved.
resolvedSubgames[_claimIndex] = true;
}

/// @inheritdoc IDisputeGame
Expand Down Expand Up @@ -645,6 +620,36 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
if (!success) revert BondTransferFailed();
}

/// @notice Returns the amount of time elapsed on the potential challenger to `_claimIndex`'s chess clock. Maxes
/// out at `GAME_DURATION / 2`.
/// @param _claimIndex The index of the subgame root claim.
/// @return duration_ The time elapsed on the potential challenger to `_claimIndex`'s chess clock.
function getChallengerDuration(uint256 _claimIndex) public view returns (Duration duration_) {
// INVARIANT: The game must be in progress to query the remaining time to respond to a given claim.
if (status != GameStatus.IN_PROGRESS) {
revert GameNotInProgress();
}

// Fetch the subgame root claim.
ClaimData storage subgameRootClaim = claimData[_claimIndex];

// Fetch the parent of the subgame root's clock, if it exists.
Clock parentClock;
Dismissed Show dismissed Hide dismissed
if (subgameRootClaim.parentIndex != type(uint32).max) {
parentClock = claimData[subgameRootClaim.parentIndex].clock;
}

// Compute the duration elapsed of the potential challenger's clock.
uint64 challengeDuration =
uint64(parentClock.duration().raw() + (block.timestamp - subgameRootClaim.clock.timestamp().raw()));
uint64 maxClockTime = GAME_DURATION.raw() >> 1;
if (challengeDuration > maxClockTime) {
duration_ = Duration.wrap(maxClockTime);
} else {
duration_ = Duration.wrap(challengeDuration);
}
}

////////////////////////////////////////////////////////////////
// IMMUTABLE GETTERS //
////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -694,8 +699,6 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
function _distributeBond(address _recipient, ClaimData storage _bonded) internal {
// Set all bits in the bond value to indicate that the bond has been paid out.
uint256 bond = _bonded.bond;
if (bond == CLAIMED_BOND_FLAG) revert ClaimAlreadyResolved();
_bonded.bond = CLAIMED_BOND_FLAG;

// Increase the recipient's credit.
credit[_recipient] += bond;
Expand Down