diff --git a/packages/contracts-bedrock/src/dispute/FaultDisputeGameN.sol b/packages/contracts-bedrock/src/dispute/FaultDisputeGameN.sol index 207b54592b28..352feaa060b5 100644 --- a/packages/contracts-bedrock/src/dispute/FaultDisputeGameN.sol +++ b/packages/contracts-bedrock/src/dispute/FaultDisputeGameN.sol @@ -93,6 +93,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { /// @notice Bits of N-ary search uint256 internal immutable N_BITS; + /// @notice Bits of N-ary search + uint256 internal immutable MAX_ATTACK_BRANCH; + /// @notice Flag for whether or not the L2 block number claim has been invalidated via `challengeRootL2Block`. bool public l2BlockNumberChallenged; @@ -162,6 +165,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { L2_CHAIN_ID = _l2ChainId; // N_BITS ** 2 = N-ary N_BITS = 2; + MAX_ATTACK_BRANCH = (1 << N_BITS) - 1; } /// @inheritdoc IInitializable @@ -255,7 +259,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { public virtual { - require(_attackBranch < (1 << N_BITS)); + require(_attackBranch <= MAX_ATTACK_BRANCH); // INVARIANT: Steps cannot be made unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); @@ -275,7 +279,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { Position preStatePos; Claim postStateClaim; Position postStatePos; - if ((1 << N_BITS) - 1 != _attackBranch) { + if (MAX_ATTACK_BRANCH != _attackBranch) { // 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 @@ -348,7 +352,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { // 2. _attackBranch == 1 (attack) // 3. _attackBranch == 2 (attack) // 4. _attackBranch == 3 (defend) - require(_attackBranch < (1 << N_BITS)); + require(_attackBranch <= MAX_ATTACK_BRANCH); // INVARIANT: Moves cannot be made unless the game is currently in progress. if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); @@ -385,7 +389,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { // 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 (nextPositionDepth == SPLIT_DEPTH + N_BITS) { - _verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _attackBranch); + _verifyExecMultisectionRoot(_claim, _challengeIndex, parentPos, _attackBranch); } // INVARIANT: The `msg.value` must exactly equal the required bond. @@ -894,7 +898,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { /// @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( + function _verifyExecMultisectionRoot( Claim _rootClaim, uint256 _parentIdx, Position _parentPos, @@ -910,12 +914,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { // If the move is a defense, the disputed output could have been made by either party. In this case, we // need to search for the parent output to determine what the expected status byte should be. - Position disputedLeafPos = Position.wrap(_parentPos.raw() + 1); + Position disputedLeafPos = Position.wrap(_parentPos.raw() + _attackBranch); (Claim disputedClaim, Position disputedPos) = _findTraceAncestorV2({ _pos: disputedLeafPos, _start: _parentIdx, _global: true }); uint8 vmStatus = uint8(_rootClaim.raw()[0]); - if ((0 != _attackBranch) || (disputedPos.depth() / N_BITS) % 2 == (SPLIT_DEPTH / N_BITS) % 2) { + if ((MAX_ATTACK_BRANCH != _attackBranch) || (disputedPos.depth() / N_BITS) % 2 == (SPLIT_DEPTH / N_BITS) % 2) { // If the move is an attack, the parent output is always deemed to be disputed. In this case, we only need // to check that the root claim signals that the VM panicked or resulted in an invalid transition. // If the move is a defense, and the disputed output and creator of the execution trace subgame disagree, @@ -1070,7 +1074,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { : _firstValidRightIndex(_pos.traceAncestorBounded(SPLIT_DEPTH), N_BITS); uint256 offset = ancestorPos_.raw() % (1 << N_BITS); - if (1 << N_BITS - 1 == offset) { + if (MAX_ATTACK_BRANCH == offset) { offset = 0; } uint256 traceAncestorPosValue = ancestorPos_.raw() - offset; diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGameN.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGameN.t.sol index 9f06b8361eb1..74ea58d2ed5e 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGameN.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGameN.t.sol @@ -584,29 +584,43 @@ contract FaultDisputeGameN_Test is FaultDisputeGame_Init { /// byte reverts with the `UnexpectedRootClaim` error. function test_move_incorrectStatusExecRoot_reverts() public { Claim disputed; - for (uint256 i; i < 4; i++) { + for (uint256 i; i < 2; i++) { (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: _getRequiredBond(i) }(disputed, i, _dummyClaim()); + gameProxy.attackV2{ value: _getRequiredBondV2(i, 0) }(disputed, i, _dummyClaim(), 0); } - uint256 bond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); + uint256 bond = _getRequiredBondV2(2, 2); + (,,,, disputed,,) = gameProxy.claimData(2); vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, bytes32(0))); - gameProxy.attack{ value: bond }(disputed, 4, Claim.wrap(bytes32(0))); + gameProxy.attackV2{ value: bond }(disputed, 2, Claim.wrap(bytes32(0)), 2); } /// @dev Tests that making a claim at the execution trace bisection root level with a valid status /// byte succeeds. function test_move_correctStatusExecRoot_succeeds() public { Claim disputed; - for (uint256 i; i < 4; i++) { - uint256 bond = _getRequiredBond(i); + for (uint256 i; i < 2; i++) { + uint256 bond = _getRequiredBondV2(i, 0); (,,,, disputed,,) = gameProxy.claimData(i); - gameProxy.attack{ value: bond }(disputed, i, _dummyClaim()); + gameProxy.attackV2{ value: bond }(disputed, i, _dummyClaim(), 0); } - uint256 lastBond = _getRequiredBond(4); - (,,,, disputed,,) = gameProxy.claimData(4); - gameProxy.attack{ value: lastBond }(disputed, 4, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC)); + uint256 lastBond = _getRequiredBondV2(2, 0); + (,,,, disputed,,) = gameProxy.claimData(2); + gameProxy.attackV2{ value: lastBond }(disputed, 2, _changeClaimStatus(_dummyClaim(), VMStatuses.PANIC), 2); + } + + /// @dev Tests that making a claim at the execution trace quadsection root level with a valid status + /// byte succeeds when the attack branch is the last branch. + function test_move_correctStatusExecRootForLastAttackBranch_succeeds() public { + Claim disputed; + for (uint256 i; i < 2; i++) { + uint256 bond = _getRequiredBondV2(i, 0); + (,,,, disputed,,) = gameProxy.claimData(i); + gameProxy.attackV2{ value: bond }(disputed, i, _dummyClaim(), 0); + } + uint256 lastBond = _getRequiredBondV2(2, 0); + (,,,, disputed,,) = gameProxy.claimData(2); + gameProxy.attackV2{ value: lastBond }(disputed, 2, Claim.wrap(bytes32(0)), 3); } /// @dev Static unit test asserting that a move reverts when the bonded amount is incorrect.