Commit 802b3a2f authored by inphi's avatar inphi

feat(ctb): Rework FaultDisputeGame resolution

Initial changes to the FDG resolution function to be incentive compatible
with bonds.
parent 2220794e
This diff is collapsed.
...@@ -88,39 +88,45 @@ FaucetTest:test_nonAdmin_drip_fails() (gas: 262520) ...@@ -88,39 +88,45 @@ FaucetTest:test_nonAdmin_drip_fails() (gas: 262520)
FaucetTest:test_receive_succeeds() (gas: 17401) FaucetTest:test_receive_succeeds() (gas: 17401)
FaucetTest:test_withdraw_nonAdmin_reverts() (gas: 13145) FaucetTest:test_withdraw_nonAdmin_reverts() (gas: 13145)
FaucetTest:test_withdraw_succeeds() (gas: 78359) FaucetTest:test_withdraw_succeeds() (gas: 78359)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 499197) FaultDisputeGame_ResolvesCorrectly_CorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 660411)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 506057) FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 667293)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 502738) FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 663974)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 505955) FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 667169)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 505224) FaultDisputeGame_ResolvesCorrectly_CorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 666460)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 497962) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 653092)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 504822) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 658598)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 501503) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 655943)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 502720) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 656899)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 501989) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 656332)
FaultDisputeGame_Test:test_addLocalData_static_succeeds() (gas: 640504) FaultDisputeGame_Test:test_addLocalData_static_succeeds() (gas: 640567)
FaultDisputeGame_Test:test_createdAt_succeeds() (gas: 10342) FaultDisputeGame_Test:test_createdAt_succeeds() (gas: 10342)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 32377) FaultDisputeGame_Test:test_extraData_succeeds() (gas: 32355)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 32804) FaultDisputeGame_Test:test_gameData_succeeds() (gas: 32782)
FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8309) FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8265)
FaultDisputeGame_Test:test_initialize_correctData_succeeds() (gas: 57628) FaultDisputeGame_Test:test_initialize_correctData_succeeds() (gas: 57739)
FaultDisputeGame_Test:test_initialize_firstOutput_reverts() (gas: 210629) FaultDisputeGame_Test:test_initialize_firstOutput_reverts() (gas: 210563)
FaultDisputeGame_Test:test_initialize_l1HeadTooOld_reverts() (gas: 228390) FaultDisputeGame_Test:test_initialize_l1HeadTooOld_reverts() (gas: 228368)
FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 415971) FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 594268)
FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 23197) FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 23175)
FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13344) FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13366)
FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 102898) FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 147389)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 407913) FaultDisputeGame_Test:test_move_duplicateClaimsDifferentSubgames_succeeds() (gas: 556885)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 585897)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 11002) FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 11002)
FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24710) FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24666)
FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 107384) FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 151959)
FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 224949) FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 269413)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9686) FaultDisputeGame_Test:test_resolve_claimAlreadyResolved_reverts() (gas: 272356)
FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 109879) FaultDisputeGame_Test:test_resolve_claimAtMaxDepthAlreadyResolved_reverts() (gas: 586672)
FaultDisputeGame_Test:test_resolve_rootUncontestedClockNotExpired_succeeds() (gas: 21421) FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9732)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 27279) FaultDisputeGame_Test:test_resolve_outOfOrderResolution_reverts() (gas: 309037)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 395658) FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 139044)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8276) FaultDisputeGame_Test:test_resolve_rootUncontestedButUnresolved_reverts() (gas: 15883)
FaultDisputeGame_Test:test_resolve_rootUncontestedClockNotExpired_succeeds() (gas: 18406)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 51409)
FaultDisputeGame_Test:test_resolve_stepReached_succeeds() (gas: 498476)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 443373)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8232)
FeeVault_Test:test_constructor_succeeds() (gas: 18185) FeeVault_Test:test_constructor_succeeds() (gas: 18185)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 354421) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 354421)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2952628) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2952628)
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
"src/L2/L2StandardBridge.sol": "0xfe01bcb1ddc947b9b8a7093d0971854b9fa8d49da5bd933a3dd106167907f882", "src/L2/L2StandardBridge.sol": "0xfe01bcb1ddc947b9b8a7093d0971854b9fa8d49da5bd933a3dd106167907f882",
"src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321", "src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321",
"src/L2/SequencerFeeVault.sol": "0xc2f733c1128d06ad60bf1e1d98c8f684a4825b11875ccdf2376ede33f5aad4e6", "src/L2/SequencerFeeVault.sol": "0xc2f733c1128d06ad60bf1e1d98c8f684a4825b11875ccdf2376ede33f5aad4e6",
"src/dispute/FaultDisputeGame.sol": "0x7b8462c29d003e96a73491c644001e1a9034bcc45c5be2a7bac3caf80d521635", "src/dispute/FaultDisputeGame.sol": "0x76e7c16431faa32e2074e6abdfe3e86f5ec90b4ac8a6b662edba8c3ce791ad80",
"src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8", "src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8",
"src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6", "src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6",
"src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497", "src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497",
......
...@@ -75,6 +75,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -75,6 +75,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @notice An internal mapping to allow for constant-time lookups of existing claims. /// @notice An internal mapping to allow for constant-time lookups of existing claims.
mapping(ClaimHash => bool) internal 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;
/// @param _gameType The type ID of the game. /// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace. /// @param _absolutePrestate The absolute prestate of the instruction trace.
/// @param _maxGameDepth The maximum depth of bisection. /// @param _maxGameDepth The maximum depth of bisection.
...@@ -232,9 +238,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -232,9 +238,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// Construct the next clock with the new duration and the current block timestamp. // Construct the next clock with the new duration and the current block timestamp.
Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp))); Clock nextClock = LibClock.wrap(nextDuration, Timestamp.wrap(uint64(block.timestamp)));
// INVARIANT: A claim may only exist at a given position once. Multiple claims may exist // INVARIANT: There cannot be multiple identical claims with identical moves on the same challengeIndex. Multiple
// at the same position, however they must have different values. // claims
ClaimHash claimHash = _claim.hashClaimPos(nextPosition); // at the same position may dispute the same challengeIndex. However, the must have different values.
ClaimHash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex);
if (claims[claimHash]) revert ClaimAlreadyExists(); if (claims[claimHash]) revert ClaimAlreadyExists();
claims[claimHash] = true; claims[claimHash] = true;
...@@ -252,6 +259,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -252,6 +259,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// Set the parent claim as countered. // Set the parent claim as countered.
claimData[_challengeIndex].countered = true; 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 the appropriate event for the attack or defense.
emit Move(_challengeIndex, _claim, msg.sender); emit Move(_challengeIndex, _claim, msg.sender);
} }
...@@ -348,67 +358,64 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -348,67 +358,64 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// INVARIANT: Resolution cannot occur unless the game is currently in progress. // INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// Search for the left-most dangling non-bottom node // INVARIANT: Resolution cannot occur unless the absolute root subgame has been resolved.
// The most recent claim is always a dangling, non-bottom node so we start with that if (!subgameAtRootResolved) revert OutOfOrderResolution();
uint256 leftMostIndex = claimData.length - 1;
uint256 leftMostTraceIndex = type(uint128).max;
for (uint256 i = leftMostIndex; i < type(uint64).max;) {
// Fetch the claim at the current index.
ClaimData storage claim = claimData[i];
// Decrement the loop counter; If it underflows, we've reached the root
// claim and can stop searching.
unchecked {
--i;
}
// INVARIANT: A claim can never be considered as the leftMostIndex or leftMostTraceIndex status_ = claimData[0].countered ? GameStatus.CHALLENGER_WINS : GameStatus.DEFENDER_WINS;
// if it has been countered. emit Resolved(status = status_);
if (claim.countered) continue; }
// If the claim is a dangling node, we can check if it is the left-most
// dangling node we've come across so far. If it is, we can update the
// left-most trace index.
uint256 traceIndex = claim.position.traceIndex(MAX_GAME_DEPTH);
if (traceIndex < leftMostTraceIndex) {
leftMostTraceIndex = traceIndex;
unchecked {
leftMostIndex = i + 1;
}
}
}
// Create a reference to the left most uncontested claim and its parent. /// @inheritdoc IFaultDisputeGame
ClaimData storage leftMostUncontested = claimData[leftMostIndex]; function resolveClaim(uint256 _claimIndex) external payable {
// INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// INVARIANT: The game may never be resolved unless the clock of the left-most uncontested ClaimData storage parent = claimData[_claimIndex];
// claim's parent has expired. If the left-most uncontested claim is the root
// claim, it is uncountered, and we check if 3.5 days has passed since its // INVARIANT: Cannot resolve a subgame unless the clock of its root has expired
// creation.
uint256 parentIndex = leftMostUncontested.parentIndex;
Clock opposingClock = parentIndex == type(uint32).max ? leftMostUncontested.clock : claimData[parentIndex].clock;
if ( if (
Duration.unwrap(opposingClock.duration()) + (block.timestamp - Timestamp.unwrap(opposingClock.timestamp())) Duration.unwrap(parent.clock.duration()) + (block.timestamp - Timestamp.unwrap(parent.clock.timestamp()))
<= Duration.unwrap(GAME_DURATION) >> 1 <= Duration.unwrap(GAME_DURATION) >> 1
) { ) {
revert ClockNotExpired(); revert ClockNotExpired();
} }
// If the left-most dangling node is at an even depth, the defender wins. uint256[] storage challengeIndices = subgames[_claimIndex];
// Otherwise, the challenger wins and the root claim is deemed invalid.
if ( // INVARIANT: Cannot resolve subgames twice
leftMostUncontested // Uncontested claims are resolved implicitly unless they are the root claim
.position if (_claimIndex == 0 && subgameAtRootResolved) revert ClaimAlreadyResolved();
// slither-disable-next-line weak-prng if (challengeIndices.length == 0 && _claimIndex != 0) revert ClaimAlreadyResolved();
.depth() % 2 == 0 && leftMostTraceIndex != type(uint128).max
) { // Assume parent is honest until proven otherwise
status_ = GameStatus.DEFENDER_WINS; bool countered = false;
} else {
status_ = GameStatus.CHALLENGER_WINS; 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;
}
} }
// Update the game status // Once a subgame is resolved, we percolate the result up the DAG so subsequent calls to
emit Resolved(status = status_); // 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
if (_claimIndex == 0) {
subgameAtRootResolved = true;
}
} }
/// @inheritdoc IDisputeGame /// @inheritdoc IDisputeGame
......
...@@ -73,6 +73,14 @@ interface IFaultDisputeGame is IDisputeGame { ...@@ -73,6 +73,14 @@ interface IFaultDisputeGame is IDisputeGame {
/// @param _partOffset The offset of the data to post. /// @param _partOffset The offset of the data to post.
function addLocalData(uint256 _ident, uint256 _partOffset) external; function addLocalData(uint256 _ident, 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 /// @notice An L1 block hash that contains the disputed output root, fetched from the
/// `BlockOracle` and verified by referencing the timestamp associated with the /// `BlockOracle` and verified by referencing the timestamp associated with the
/// first L2 Output Proposal in the `L2OutputOracle` that contains the disputed /// first L2 Output Proposal in the `L2OutputOracle` that contains the disputed
......
...@@ -9,11 +9,20 @@ library LibHashing { ...@@ -9,11 +9,20 @@ library LibHashing {
/// @notice Hashes a claim and a position together. /// @notice Hashes a claim and a position together.
/// @param _claim A Claim type. /// @param _claim A Claim type.
/// @param _position The position of `claim`. /// @param _position The position of `claim`.
/// @return claimHash_ A hash of abi.encodePacked(claim, position); /// @param _challengeIndex The index of the claim being moved against.
function hashClaimPos(Claim _claim, Position _position) internal pure returns (ClaimHash claimHash_) { /// @return claimHash_ A hash of abi.encodePacked(claim, position|challengeIndex);
function hashClaimPos(
Claim _claim,
Position _position,
uint256 _challengeIndex
)
internal
pure
returns (ClaimHash claimHash_)
{
assembly { assembly {
mstore(0x00, _claim) mstore(0x00, _claim)
mstore(0x20, _position) mstore(0x20, or(shl(128, _position), and(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, _challengeIndex)))
claimHash_ := keccak256(0x00, 0x40) claimHash_ := keccak256(0x00, 0x40)
} }
} }
......
...@@ -66,6 +66,12 @@ error L1HeadTooOld(); ...@@ -66,6 +66,12 @@ error L1HeadTooOld();
/// @notice Thrown when an invalid local identifier is passed to the `addLocalData` function. /// @notice Thrown when an invalid local identifier is passed to the `addLocalData` function.
error InvalidLocalIdent(); error InvalidLocalIdent();
/// @notice Thrown when resolving claims out of order.
error OutOfOrderResolution();
/// @notice Thrown when resolving a claim that has already been resolved.
error ClaimAlreadyResolved();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `AttestationDisputeGame` Errors // // `AttestationDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
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