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)
FaucetTest:test_receive_succeeds() (gas: 17401)
FaucetTest:test_withdraw_nonAdmin_reverts() (gas: 13145)
FaucetTest:test_withdraw_succeeds() (gas: 78359)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 499197)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 506057)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 502738)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 505955)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 505224)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 497962)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 504822)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 501503)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 502720)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 501989)
FaultDisputeGame_Test:test_addLocalData_static_succeeds() (gas: 640504)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 660411)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 667293)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 663974)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 667169)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 666460)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot1:test_resolvesCorrectly_succeeds() (gas: 653092)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 658598)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 655943)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 656899)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot5:test_resolvesCorrectly_succeeds() (gas: 656332)
FaultDisputeGame_Test:test_addLocalData_static_succeeds() (gas: 640567)
FaultDisputeGame_Test:test_createdAt_succeeds() (gas: 10342)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 32377)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 32804)
FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8309)
FaultDisputeGame_Test:test_initialize_correctData_succeeds() (gas: 57628)
FaultDisputeGame_Test:test_initialize_firstOutput_reverts() (gas: 210629)
FaultDisputeGame_Test:test_initialize_l1HeadTooOld_reverts() (gas: 228390)
FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 415971)
FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 23197)
FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13344)
FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 102898)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 407913)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 32355)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 32782)
FaultDisputeGame_Test:test_gameType_succeeds() (gas: 8265)
FaultDisputeGame_Test:test_initialize_correctData_succeeds() (gas: 57739)
FaultDisputeGame_Test:test_initialize_firstOutput_reverts() (gas: 210563)
FaultDisputeGame_Test:test_initialize_l1HeadTooOld_reverts() (gas: 228368)
FaultDisputeGame_Test:test_move_clockCorrectness_succeeds() (gas: 594268)
FaultDisputeGame_Test:test_move_clockTimeExceeded_reverts() (gas: 23175)
FaultDisputeGame_Test:test_move_defendRoot_reverts() (gas: 13366)
FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 147389)
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_nonExistentParent_reverts() (gas: 24710)
FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 107384)
FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 224949)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9686)
FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 109879)
FaultDisputeGame_Test:test_resolve_rootUncontestedClockNotExpired_succeeds() (gas: 21421)
FaultDisputeGame_Test:test_resolve_rootUncontested_succeeds() (gas: 27279)
FaultDisputeGame_Test:test_resolve_teamDeathmatch_succeeds() (gas: 395658)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8276)
FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24666)
FaultDisputeGame_Test:test_move_simpleAttack_succeeds() (gas: 151959)
FaultDisputeGame_Test:test_resolve_challengeContested_succeeds() (gas: 269413)
FaultDisputeGame_Test:test_resolve_claimAlreadyResolved_reverts() (gas: 272356)
FaultDisputeGame_Test:test_resolve_claimAtMaxDepthAlreadyResolved_reverts() (gas: 586672)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9732)
FaultDisputeGame_Test:test_resolve_outOfOrderResolution_reverts() (gas: 309037)
FaultDisputeGame_Test:test_resolve_rootContested_succeeds() (gas: 139044)
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)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 354421)
GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2952628)
......
......@@ -17,7 +17,7 @@
"src/L2/L2StandardBridge.sol": "0xfe01bcb1ddc947b9b8a7093d0971854b9fa8d49da5bd933a3dd106167907f882",
"src/L2/L2ToL1MessagePasser.sol": "0xafc710b4d320ef450586d96a61cbd58cac814cb3b0c4fdc280eace3efdcdf321",
"src/L2/SequencerFeeVault.sol": "0xc2f733c1128d06ad60bf1e1d98c8f684a4825b11875ccdf2376ede33f5aad4e6",
"src/dispute/FaultDisputeGame.sol": "0x7b8462c29d003e96a73491c644001e1a9034bcc45c5be2a7bac3caf80d521635",
"src/dispute/FaultDisputeGame.sol": "0x76e7c16431faa32e2074e6abdfe3e86f5ec90b4ac8a6b662edba8c3ce791ad80",
"src/legacy/DeployerWhitelist.sol": "0xf2129ec3da75307ba8e21bc943c332bb04704642e6e263149b5c8ee92dbcb7a8",
"src/legacy/L1BlockNumber.sol": "0x30aae1fc85103476af0226b6e98c71c01feebbdc35d93401390b1ad438a37be6",
"src/legacy/LegacyMessagePasser.sol": "0x5c08b0a663cc49d30e4e38540f6aefab19ef287c3ecd31c8d8c3decd5f5bd497",
......
......@@ -75,6 +75,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @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;
/// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace.
/// @param _maxGameDepth The maximum depth of bisection.
......@@ -232,9 +238,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// Construct the next clock with the new duration and the current 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
// at the same position, however they must have different values.
ClaimHash claimHash = _claim.hashClaimPos(nextPosition);
// 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, the must have different values.
ClaimHash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex);
if (claims[claimHash]) revert ClaimAlreadyExists();
claims[claimHash] = true;
......@@ -252,6 +259,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// 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);
}
......@@ -348,67 +358,64 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
// INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// Search for the left-most dangling non-bottom node
// The most recent claim is always a dangling, non-bottom node so we start with that
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];
// INVARIANT: Resolution cannot occur unless the absolute root subgame has been resolved.
if (!subgameAtRootResolved) revert OutOfOrderResolution();
// Decrement the loop counter; If it underflows, we've reached the root
// claim and can stop searching.
unchecked {
--i;
status_ = claimData[0].countered ? GameStatus.CHALLENGER_WINS : GameStatus.DEFENDER_WINS;
emit Resolved(status = status_);
}
// INVARIANT: A claim can never be considered as the leftMostIndex or leftMostTraceIndex
// if it has been countered.
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;
}
}
}
/// @inheritdoc IFaultDisputeGame
function resolveClaim(uint256 _claimIndex) external payable {
// INVARIANT: Resolution cannot occur unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress();
// Create a reference to the left most uncontested claim and its parent.
ClaimData storage leftMostUncontested = claimData[leftMostIndex];
ClaimData storage parent = claimData[_claimIndex];
// INVARIANT: The game may never be resolved unless the clock of the left-most uncontested
// 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
// creation.
uint256 parentIndex = leftMostUncontested.parentIndex;
Clock opposingClock = parentIndex == type(uint32).max ? leftMostUncontested.clock : claimData[parentIndex].clock;
// INVARIANT: Cannot resolve a subgame unless the clock of its root has expired
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
) {
revert ClockNotExpired();
}
// If the left-most dangling node is at an even depth, the defender wins.
// Otherwise, the challenger wins and the root claim is deemed invalid.
if (
leftMostUncontested
.position
// slither-disable-next-line weak-prng
.depth() % 2 == 0 && leftMostTraceIndex != type(uint128).max
) {
status_ = GameStatus.DEFENDER_WINS;
} else {
status_ = GameStatus.CHALLENGER_WINS;
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;
}
}
// Update the game status
emit Resolved(status = status_);
// 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
if (_claimIndex == 0) {
subgameAtRootResolved = true;
}
}
/// @inheritdoc IDisputeGame
......
......@@ -73,6 +73,14 @@ interface IFaultDisputeGame is IDisputeGame {
/// @param _partOffset The offset of the data to post.
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
/// `BlockOracle` and verified by referencing the timestamp associated with the
/// first L2 Output Proposal in the `L2OutputOracle` that contains the disputed
......
......@@ -9,11 +9,20 @@ library LibHashing {
/// @notice Hashes a claim and a position together.
/// @param _claim A Claim type.
/// @param _position The position of `claim`.
/// @return claimHash_ A hash of abi.encodePacked(claim, position);
function hashClaimPos(Claim _claim, Position _position) internal pure returns (ClaimHash claimHash_) {
/// @param _challengeIndex The index of the claim being moved against.
/// @return claimHash_ A hash of abi.encodePacked(claim, position|challengeIndex);
function hashClaimPos(
Claim _claim,
Position _position,
uint256 _challengeIndex
)
internal
pure
returns (ClaimHash claimHash_)
{
assembly {
mstore(0x00, _claim)
mstore(0x20, _position)
mstore(0x20, or(shl(128, _position), and(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, _challengeIndex)))
claimHash_ := keccak256(0x00, 0x40)
}
}
......
......@@ -66,6 +66,12 @@ error L1HeadTooOld();
/// @notice Thrown when an invalid local identifier is passed to the `addLocalData` function.
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 //
////////////////////////////////////////////////////////////////
......
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