From c6694796828d3300cae173d971c760361e675866 Mon Sep 17 00:00:00 2001 From: billxu Date: Tue, 2 Apr 2024 10:46:49 +0800 Subject: [PATCH 1/3] modify the dispute game feature to 4ary, add unit tests --- .../src/dispute/FaultDisputeGame.sol | 377 ++++++++++++------ .../src/dispute/lib/LibPosition.sol | 6 + .../test/dispute/FaultDisputeGame.t.sol | 315 +++++++++++++++ 3 files changed, 570 insertions(+), 128 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol index 2f7eda6b73212..1085dda248899 100644 --- a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol @@ -17,6 +17,8 @@ import { LibClock } from "src/dispute/lib/LibUDT.sol"; import "src/libraries/DisputeTypes.sol"; import "src/libraries/DisputeErrors.sol"; +error NotSupported(); + /// @title FaultDisputeGame /// @notice An implementation of the `IFaultDisputeGame` interface. contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { @@ -89,6 +91,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { /// @notice Flag for the `initialize` function to prevent re-initialization. bool internal initialized; + /// @notice Bits of N-ary search + uint256 nBits; + /// @notice Semantic version. /// @custom:semver 0.7.1 string public constant version = "0.7.1"; @@ -226,107 +231,17 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { /// @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 virtual { - // INVARIANT: Moves 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 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 parentPos = parent.position; - Position nextPosition = parentPos.move(_isAttack); - uint256 nextPositionDepth = nextPosition.depth(); - - // INVARIANT: A defense can never be made against the root claim of either the output root game or any - // of the execution trace bisection subgames. 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 || nextPositionDepth == SPLIT_DEPTH + 2) && !_isAttack) { - revert CannotDefendRootClaim(); - } - - // 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 (nextPositionDepth > 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 (nextPositionDepth == SPLIT_DEPTH + 1) { - _verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack); - } - - // INVARIANT: The `msg.value` must be sufficient to cover the required bond. - if (getRequiredBond(nextPosition) > msg.value) revert InsufficientBond(); - - // 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() - ) - ); - - // 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(); - - // 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), - // This is updated during subgame resolution - counteredBy: address(0), - claimant: msg.sender, - bond: uint128(msg.value), - claim: _claim, - position: nextPosition, - clock: nextClock - }) - ); - - // Update the subgame rooted at the parent claim. - subgames[_challengeIndex].push(claimData.length - 1); - - // Deposit the bond. - WETH.deposit{ value: msg.value }(); - - // Emit the appropriate event for the attack or defense. - emit Move(_challengeIndex, _claim, msg.sender); + revert NotSupported(); } /// @inheritdoc IFaultDisputeGame function attack(uint256 _parentIndex, Claim _claim) external payable { - move(_parentIndex, _claim, true); + revert NotSupported(); } /// @inheritdoc IFaultDisputeGame function defend(uint256 _parentIndex, Claim _claim) external payable { - move(_parentIndex, _claim, false); + revert NotSupported(); } /// @inheritdoc IFaultDisputeGame @@ -548,6 +463,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { // Set the game's starting timestamp createdAt = Timestamp.wrap(uint64(block.timestamp)); + nBits = 2; // Set the game as initialized. initialized = true; } @@ -788,46 +704,13 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { // 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 = execRootPos.parent().raw() == outputPos.raw(); - - // 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) { - // If this is an attack on the first output root (the block directly after genesis), the - // starting claim nor position exists in the tree. We leave these as 0, which can be easily - // identified due to 0 being an invalid Gindex. - if (outputPos.indexAtDepth() > 0) { - ClaimData storage starting = _findTraceAncestor(Position.wrap(outputPos.raw() - 1), claimIdx, true); - (startingClaim_, startingPos_) = (starting.claim, starting.position); - } else { - startingClaim_ = Claim.wrap(GENESIS_OUTPUT_ROOT.raw()); - } - (disputedClaim_, disputedPos_) = (claim.claim, claim.position); - } else { - ClaimData storage disputed = _findTraceAncestor(Position.wrap(outputPos.raw() + 1), claimIdx, true); - (startingClaim_, startingPos_) = (claim.claim, claim.position); - (disputedClaim_, disputedPos_) = (disputed.claim, disputed.position); - } + (startingPos_, startingClaim_) = findPreStateClaim(1 << nBits, claim.position, claimIdx); + (disputedPos_, disputedClaim_) = findPostStateClaim(1 << nBits, claim.position, claimIdx); } /// @notice Finds the local context hash for a given claim index that is present in an execution trace subgame. @@ -863,4 +746,242 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { uuid_ = Hash.wrap(keccak256(abi.encode(_starting, _startingPos, _disputed, _disputedPos))); } } + + function attackAt(uint256 _parentIndex, Claim _claim, uint256 _attackBranch) public payable { + moveV2(_parentIndex, _claim, _attackBranch); + } + + // ClaimHash => attackBranch => Claim + mapping(Claim => mapping(uint256 => Claim)) internal claimHashToClaims; + + function setClaimHashClaims(Claim _claimHash, uint256 _attackBranch, Claim _claim) public { + claimHashToClaims[_claimHash][_attackBranch] = _claim; + } + + function getClaimFromClaimHash( + Claim claimsHash, + uint256 claimIndex + ) internal view returns (Claim) { + // TODO: retrieve the claim from the claimsHash + // Either: from EIP-4844 BLOB with point-evaluation proof or calldata with Merkle proof + return claimHashToClaims[claimsHash][claimIndex]; + } + + function findPreStateClaim( + uint256 _nary, + Position _pos, + uint256 _start + ) public view returns (Position pos_, Claim claim_) { + ClaimData storage ancestor_ = claimData[_start]; + uint256 pos = _pos.raw(); + while (pos % _nary == 0 && pos != 1) { + pos = pos / _nary; + if (type(uint32).max != ancestor_.parentIndex) { + ancestor_ = claimData[ancestor_.parentIndex]; + } + } + if (pos == 1) { + // S_0 + claim_ = ABSOLUTE_PRESTATE; + } else { + claim_ = getClaimFromClaimHash(ancestor_.claim, (pos - 1) % _nary); + pos = ancestor_.position.raw(); + } + return (Position.wrap(uint128(pos)), claim_); + } + + function findPostStateClaim( + uint256 _nary, + Position _pos, + uint256 _start + ) public view returns (Position pos_, Claim claim_) { + ClaimData storage ancestor_ = claimData[_start]; + uint256 pos = _pos.raw(); + // pos is _nary's multiple, while condition is false + // actually return the claim of _start + while ((pos + 1) % _nary == 0 && pos != 1) { + pos = pos / _nary; + if (type(uint32).max != ancestor_.parentIndex) { + ancestor_ = claimData[ancestor_.parentIndex]; + } + } + return (Position.wrap(uint128(pos)), getClaimFromClaimHash(ancestor_.claim, pos % _nary)); + } + + function stepV2( + uint256 _claimIndex, + uint256 _attackBranch, + bytes calldata _stateData, + bytes calldata _proof + ) + public + virtual + { + // 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.moveN(nBits, _attackBranch); + + // INVARIANT: A step cannot be made unless the move position is 1 below the `MAX_GAME_DEPTH` + if (stepPos.depth() != MAX_GAME_DEPTH + nBits) revert InvalidParent(); + + // Determine the expected pre & post states of the step. + Claim preStateClaim; + Position preStatePosition; + Claim postStateClaim; + Position postStatePosition; + //ClaimData storage postState; + + // TODO: deal with SPLIT_DEPTH + //(preStatePosition, preStateClaim) = findPreStateClaim(1 << nBits, stepPos, _claimIndex); + (preStatePosition, preStateClaim) = findPreStateClaim(1 << nBits, parentPos, _claimIndex); + //(postStatePosition, postStateClaim) = findPostStateClaim(1 << nBits, stepPos, _claimIndex); + (postStatePosition, postStateClaim) = findPostStateClaim(1 << nBits, parentPos, _claimIndex); + + // 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 != preStateClaim.raw() << 8) revert InvalidPrestate(); + + // Compute the local preimage context for the step. + Hash uuid = _findLocalContext(_claimIndex); + + // 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.raw()) == postStateClaim.raw(); + bool parentPostAgree = (parentPos.depth() - postStatePosition.depth()) % 2 == 0; + if (parentPostAgree == validStep) revert ValidStep(); + + // INVARIANT: A step cannot be made against a claim for a second time. + if (parent.counteredBy != address(0)) revert DuplicateStep(); + + // 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.counteredBy = msg.sender; + } + + function moveV2(uint256 _challengeIndex, Claim _claim, uint256 _attackBranch) public payable { + // For N = 4 (bisec), + // 1. _attackBranch == 0 (attack) + // 2. _attackBranch == 1 (attack) + // 3. _attackBranch == 2 (attack) + // 4. _attackBranch == 3 (attack) + require(_attackBranch < (1 << nBits)); + + // INVARIANT: Moves 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 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 parentPos = parent.position; + Position nextPosition = parentPos.moveN(nBits, _attackBranch); + uint256 nextPositionDepth = nextPosition.depth(); + + // INVARIANT: A defense can never be made against the root claim of either the output root game or any + // of the execution trace bisection subgames. 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 || nextPositionDepth == SPLIT_DEPTH + 2) && _attackBranch != 0) { + //revert CannotDefendRootClaim(); + //} + // todo bill modify, nextPositionDepth == SPLIT_DEPTH + 2 check ?? + // root claim only one branch + if (_challengeIndex == 0 && _attackBranch != 0) { + revert CannotDefendRootClaim(); + } + + // 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 (nextPositionDepth > 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. + // TODO + //if (nextPositionDepth == SPLIT_DEPTH + 1) { + //_verifyExecBisectionRoot(_claim, _challengeIndex, parentPos, _isAttack); + //} + + // INVARIANT: The `msg.value` must be sufficient to cover the required bond. + if (getRequiredBond(nextPosition) > msg.value) revert InsufficientBond(); + + // 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() + ) + ); + + // 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(); + + // 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. + // todo bill modify + ClaimHash claimHash = _claim.hashClaimPos(nextPosition, _challengeIndex); + if (claims[claimHash]) revert ClaimAlreadyExists(); + claims[claimHash] = true; + + // Create the new claim. + claimData.push( + ClaimData({ + parentIndex: uint32(_challengeIndex), + // This is updated during subgame resolution + counteredBy: address(0), + claimant: msg.sender, + bond: uint128(msg.value), + claim: _claim, + position: nextPosition, + clock: nextClock + }) + ); + + // Update the subgame rooted at the parent claim. + subgames[_challengeIndex].push(claimData.length - 1); + + // Deposit the bond. + WETH.deposit{ value: msg.value }(); + + // Emit the appropriate event for the attack or defense. + emit Move(_challengeIndex, _claim, msg.sender); + } } diff --git a/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol b/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol index abf79fb51b062..3c4ca681e354b 100644 --- a/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol +++ b/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol @@ -177,6 +177,12 @@ library LibPosition { } } + function moveN(Position _position, uint256 _bits, uint256 _branch) internal pure returns (Position move_) { + assembly { + move_ := shl(_bits, or(_branch, _position)) + } + } + /// @notice Get the value of a `Position` type in the form of the underlying uint128. /// @param _position The position to get the value of. /// @return raw_ The value of the `position` as a uint128 type. diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index ae54cbbdb2b1e..0cc78be5602b5 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -1004,6 +1004,321 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { } } +contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { + /// @dev The root claim of the game. + Claim internal constant ROOT_CLAIM = Claim.wrap(bytes32((uint256(1) << 248) | uint256(10))); + + /// @dev The preimage of the absolute prestate claim + bytes internal absolutePrestateData; + /// @dev The absolute prestate of the trace. + Claim internal absolutePrestate; + + /// @dev Minimum bond value that covers all possible moves. + uint256 internal constant MIN_BOND = 50 ether; + + function setUp() public override { + absolutePrestateData = abi.encode(0); + absolutePrestate = _changeClaimStatus(Claim.wrap(keccak256(absolutePrestateData)), VMStatuses.UNFINISHED); + + super.setUp(); + super.init({ + rootClaim: ROOT_CLAIM, + absolutePrestate: absolutePrestate, + l2BlockNumber: 0x10, + genesisBlockNumber: 0, + genesisOutputRoot: Hash.wrap(bytes32(0)) + }); + } + + //////////////////////////////////////////////////////////////// + // `IDisputeGame` Implementation Tests // + //////////////////////////////////////////////////////////////// + + /// @dev Tests that the constructor of the `FaultDisputeGame` reverts when the `_splitDepth` + /// parameter is greater than or equal to the `MAX_GAME_DEPTH` + function test_constructor_wrongArgs_reverts(uint256 _splitDepth) public { + AlphabetVM alphabetVM = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0)); + + // Test that the constructor reverts when the `_splitDepth` parameter is greater than or equal + // to the `MAX_GAME_DEPTH` parameter. + _splitDepth = bound(_splitDepth, 2 ** 3, type(uint256).max); + vm.expectRevert(InvalidSplitDepth.selector); + new FaultDisputeGame({ + _gameType: GAME_TYPE, + _absolutePrestate: absolutePrestate, + _genesisBlockNumber: 0, + _genesisOutputRoot: Hash.wrap(bytes32(0)), + _maxGameDepth: 2 ** 3, + _splitDepth: _splitDepth, + _gameDuration: Duration.wrap(7 days), + _vm: alphabetVM, + _weth: DelayedWETH(payable(address(0))), + _l2ChainId: 10 + }); + } + + /// @dev Tests that the game's root claim is set correctly. + function test_rootClaim_succeeds() public { + assertEq(gameProxy.rootClaim().raw(), ROOT_CLAIM.raw()); + } + + /// @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(gameProxy.createdAt().raw(), block.timestamp); + } + + /// @dev Tests that the game's type is set correctly. + function test_gameType_succeeds() public { + assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw()); + } + + /// @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.raw(), GAME_TYPE.raw()); + assertEq(rootClaim.raw(), ROOT_CLAIM.raw()); + assertEq(_extraData, extraData); + } + + //////////////////////////////////////////////////////////////// + // `IFaultDisputeGame` Implementation Tests // + //////////////////////////////////////////////////////////////// + + /// @dev Tests that the game cannot be initialized with an output root that commits to <= the configured genesis + /// block number + function testFuzz_initialize_cannotProposeGenesis_reverts(uint256 _blockNumber) public { + _blockNumber = bound(_blockNumber, 0, gameProxy.genesisBlockNumber()); + + Claim claim = _dummyClaim(); + vm.expectRevert(abi.encodeWithSelector(UnexpectedRootClaim.selector, claim)); + gameProxy = + FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, abi.encode(_blockNumber))))); + } + + /// @dev Tests that the proxy receives ETH from the dispute game factory. + function test_initialize_receivesETH_succeeds(uint128 _value) public { + _value = uint128(bound(_value, gameProxy.getRequiredBond(Position.wrap(1)), type(uint128).max)); + vm.deal(address(this), _value); + + assertEq(address(gameProxy).balance, 0); + gameProxy = FaultDisputeGame( + payable(address(disputeGameFactory.create{ value: _value }(GAME_TYPE, ROOT_CLAIM, abi.encode(1)))) + ); + assertEq(address(gameProxy).balance, 0); + assertEq(delayedWeth.balanceOf(address(gameProxy)), _value); + } + + /// @dev Tests that the game cannot be initialized with extra data > 64 bytes long (root claim + l2 block number + /// concatenated) + function testFuzz_initialize_extraDataTooLong_reverts(uint256 _extraDataLen) public { + // The `DisputeGameFactory` will pack the root claim and the extra data into a single array, which is enforced + // to be at least 64 bytes long. + // We bound the upper end to 23.5KB to ensure that the minimal proxy never surpasses the contract size limit + // in this test, as CWIA proxies store the immutable args in their bytecode. + // [33 bytes, 23.5 KB] + _extraDataLen = bound(_extraDataLen, 33, 23_500); + bytes memory _extraData = new bytes(_extraDataLen); + + // Assign the first 32 bytes in `extraData` to a valid L2 block number passed genesis. + uint256 genesisBlockNumber = gameProxy.genesisBlockNumber(); + assembly { + mstore(add(_extraData, 0x20), add(genesisBlockNumber, 1)) + } + + Claim claim = _dummyClaim(); + vm.expectRevert(abi.encodeWithSelector(ExtraDataTooLong.selector)); + gameProxy = FaultDisputeGame(payable(address(disputeGameFactory.create(GAME_TYPE, claim, _extraData)))); + } + + /// @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, + address counteredBy, + address claimant, + uint128 bond, + Claim claim, + Position position, + Clock clock + ) = gameProxy.claimData(0); + assertEq(parentIndex, type(uint32).max); + assertEq(counteredBy, address(0)); + assertEq(claimant, DEFAULT_SENDER); + assertEq(bond, 0); + assertEq(claim.raw(), ROOT_CLAIM.raw()); + assertEq(position.raw(), 1); + assertEq(clock.raw(), LibClock.wrap(Duration.wrap(0), Timestamp.wrap(uint64(block.timestamp))).raw()); + + // Assert that the `createdAt` timestamp is correct. + assertEq(gameProxy.createdAt().raw(), block.timestamp); + + // Assert that the blockhash provided is correct. + assertEq(gameProxy.l1Head().raw(), blockhash(block.number - 1)); + } + + /// @dev Tests that the game cannot be initialized twice. + function test_initialize_onlyOnce_succeeds() public { + vm.expectRevert(AlreadyInitialized.selector); + gameProxy.initialize(); + } + + /// @dev Tests that the bond during the bisection game depths is correct. + function test_getRequiredBond_succeeds() public { + for (uint64 i = 0; i < uint64(gameProxy.splitDepth()); i++) { + Position pos = LibPosition.wrap(i, 0); + uint256 bond = gameProxy.getRequiredBond(pos); + + // Reasonable approximation for a max depth of 8. + uint256 expected = 0.08 ether; + for (uint64 j = 0; j < i; j++) { + expected = expected * 217456; + expected = expected / 100000; + } + + assertApproxEqAbs(bond, expected, 0.01 ether); + } + } + + /// @dev Tests that the bond at a depth greater than the maximum game depth reverts. + function test_getRequiredBond_outOfBounds_reverts() public { + Position pos = LibPosition.wrap(uint64(gameProxy.maxGameDepth() + 1), 0); + vm.expectRevert(GameDepthExceeded.selector); + gameProxy.getRequiredBond(pos); + } + + function test_stepAttackDummyClaim_lastAttackFalse_succeeds() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(4), 0); + gameProxy.attackAt{ value: MIN_BOND }(1, _dummyClaimHashAndSetClaims(4), 3); + + bytes memory claimData3 = abi.encode(5, 5); + Claim preState_ = Claim.wrap(keccak256(claimData3)); + + Claim hash = _dummyClaim(); + gameProxy.setClaimHashClaims(hash, 0, preState_); + gameProxy.setClaimHashClaims(hash, 1, _dummyClaim()); + gameProxy.setClaimHashClaims(hash, 2, _dummyClaim()); + gameProxy.setClaimHashClaims(hash, 3, _dummyClaim()); + + gameProxy.attackAt{ value: MIN_BOND }(2, hash, 2); + gameProxy.attackAt{ value: MIN_BOND }(3, _dummyClaimHashAndSetClaims(4), 1); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 4, 0); + gameProxy.stepV2(4, 0, claimData3, hex""); + + // Ensure that the step successfully countered the leaf claim. + (, address counteredBy,,,,,) = gameProxy.claimData(4); + assertEq(counteredBy, address(this)); + } + + function test_stepAttackDummyClaim_lastAttackTrue_reverts() public { + // Give the test contract some ether + vm.deal(address(this), 1000 ether); + + gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(4), 0); + gameProxy.attackAt{ value: MIN_BOND }(1, _dummyClaimHashAndSetClaims(4), 3); + + bytes memory claimData3 = abi.encode(5, 5); + Claim preState_ = Claim.wrap(keccak256(claimData3)); + Claim postState_ = Claim.wrap(gameImpl.vm().step(claimData3, hex"", bytes32(0))); + + Claim hash2 = _dummyClaim(); + gameProxy.setClaimHashClaims(hash2, 0, preState_); + gameProxy.setClaimHashClaims(hash2, 1, _dummyClaim()); + gameProxy.setClaimHashClaims(hash2, 2, _dummyClaim()); + gameProxy.setClaimHashClaims(hash2, 3, _dummyClaim()); + gameProxy.attackAt{ value: MIN_BOND }(2, hash2, 2); + + Claim hash3 = _dummyClaim(); + gameProxy.setClaimHashClaims(hash3, 0, postState_); + gameProxy.setClaimHashClaims(hash3, 1, _dummyClaim()); + gameProxy.setClaimHashClaims(hash3, 2, _dummyClaim()); + gameProxy.setClaimHashClaims(hash3, 3, _dummyClaim()); + gameProxy.attackAt{ value: MIN_BOND }(3, hash3, 1); + gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 4, 0); + + vm.expectRevert(ValidStep.selector); + gameProxy.stepV2(4, 0, claimData3, hex""); + } + + /// @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.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(4), 0); + + vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds); + + gameProxy.resolveClaim(0); + assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); + } + + + /// @dev Helper to return a pseudo-random claim + function _dummyClaim() internal view returns (Claim) { + return Claim.wrap(keccak256(abi.encode(gasleft()))); + } + + function _dummyClaimHashAndSetClaims(uint256 nary) internal returns (Claim) { + Claim hash = _dummyClaim(); + for (uint256 i; i < nary; i++) { + gameProxy.setClaimHashClaims(hash, i, _dummyClaim()); + } + return hash; + } + + /// @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)); + } +} + contract FaultDispute_1v1_Actors_Test is FaultDisputeGame_Init { /// @dev The honest actor DisputeActor internal honest; From b3b70113cb4be70bef6c66e29b1937b82a1518d7 Mon Sep 17 00:00:00 2001 From: billxu Date: Tue, 16 Apr 2024 01:07:59 +0800 Subject: [PATCH 2/3] fix findPreStateClaim & findPostStateClaim --- .../src/dispute/FaultDisputeGame.sol | 19 +++++++++---------- .../test/dispute/FaultDisputeGame.t.sol | 15 ++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol index 1085dda248899..cc60d5365f5d8 100644 --- a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol @@ -775,10 +775,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ClaimData storage ancestor_ = claimData[_start]; uint256 pos = _pos.raw(); while (pos % _nary == 0 && pos != 1) { - pos = pos / _nary; - if (type(uint32).max != ancestor_.parentIndex) { + if (pos != _pos.raw()) { ancestor_ = claimData[ancestor_.parentIndex]; } + pos = pos / _nary; } if (pos == 1) { // S_0 @@ -797,13 +797,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { ) public view returns (Position pos_, Claim claim_) { ClaimData storage ancestor_ = claimData[_start]; uint256 pos = _pos.raw(); - // pos is _nary's multiple, while condition is false - // actually return the claim of _start + pos = pos / _nary; while ((pos + 1) % _nary == 0 && pos != 1) { - pos = pos / _nary; - if (type(uint32).max != ancestor_.parentIndex) { + if (pos != _pos.raw() / _nary) { ancestor_ = claimData[ancestor_.parentIndex]; } + pos = pos / _nary; } return (Position.wrap(uint128(pos)), getClaimFromClaimHash(ancestor_.claim, pos % _nary)); } @@ -839,10 +838,10 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { //ClaimData storage postState; // TODO: deal with SPLIT_DEPTH - //(preStatePosition, preStateClaim) = findPreStateClaim(1 << nBits, stepPos, _claimIndex); - (preStatePosition, preStateClaim) = findPreStateClaim(1 << nBits, parentPos, _claimIndex); - //(postStatePosition, postStateClaim) = findPostStateClaim(1 << nBits, stepPos, _claimIndex); - (postStatePosition, postStateClaim) = findPostStateClaim(1 << nBits, parentPos, _claimIndex); + (preStatePosition, preStateClaim) = findPreStateClaim(1 << nBits, stepPos, _claimIndex); + //(preStatePosition, preStateClaim) = findPreStateClaim(1 << nBits, parentPos, _claimIndex); + (postStatePosition, postStateClaim) = findPostStateClaim(1 << nBits, stepPos, _claimIndex); + //(postStatePosition, postStateClaim) = findPostStateClaim(1 << nBits, parentPos, _claimIndex); // INVARIANT: The prestate is always invalid if the passed `_stateData` is not the // preimage of the prestate claim hash. diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index 0cc78be5602b5..4c844025d550b 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -1197,8 +1197,8 @@ contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { // Give the test contract some ether vm.deal(address(this), 1000 ether); - gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(4), 0); - gameProxy.attackAt{ value: MIN_BOND }(1, _dummyClaimHashAndSetClaims(4), 3); + gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(3), 0); + gameProxy.attackAt{ value: MIN_BOND }(1, _dummyClaimHashAndSetClaims(3), 3); bytes memory claimData3 = abi.encode(5, 5); Claim preState_ = Claim.wrap(keccak256(claimData3)); @@ -1207,10 +1207,9 @@ contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { gameProxy.setClaimHashClaims(hash, 0, preState_); gameProxy.setClaimHashClaims(hash, 1, _dummyClaim()); gameProxy.setClaimHashClaims(hash, 2, _dummyClaim()); - gameProxy.setClaimHashClaims(hash, 3, _dummyClaim()); gameProxy.attackAt{ value: MIN_BOND }(2, hash, 2); - gameProxy.attackAt{ value: MIN_BOND }(3, _dummyClaimHashAndSetClaims(4), 1); + gameProxy.attackAt{ value: MIN_BOND }(3, _dummyClaimHashAndSetClaims(3), 1); gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 4, 0); gameProxy.stepV2(4, 0, claimData3, hex""); @@ -1223,8 +1222,8 @@ contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { // Give the test contract some ether vm.deal(address(this), 1000 ether); - gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(4), 0); - gameProxy.attackAt{ value: MIN_BOND }(1, _dummyClaimHashAndSetClaims(4), 3); + gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(3), 0); + gameProxy.attackAt{ value: MIN_BOND }(1, _dummyClaimHashAndSetClaims(3), 3); bytes memory claimData3 = abi.encode(5, 5); Claim preState_ = Claim.wrap(keccak256(claimData3)); @@ -1234,14 +1233,12 @@ contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { gameProxy.setClaimHashClaims(hash2, 0, preState_); gameProxy.setClaimHashClaims(hash2, 1, _dummyClaim()); gameProxy.setClaimHashClaims(hash2, 2, _dummyClaim()); - gameProxy.setClaimHashClaims(hash2, 3, _dummyClaim()); gameProxy.attackAt{ value: MIN_BOND }(2, hash2, 2); Claim hash3 = _dummyClaim(); gameProxy.setClaimHashClaims(hash3, 0, postState_); gameProxy.setClaimHashClaims(hash3, 1, _dummyClaim()); gameProxy.setClaimHashClaims(hash3, 2, _dummyClaim()); - gameProxy.setClaimHashClaims(hash3, 3, _dummyClaim()); gameProxy.attackAt{ value: MIN_BOND }(3, hash3, 1); gameProxy.addLocalData(LocalPreimageKey.DISPUTED_L2_BLOCK_NUMBER, 4, 0); @@ -1290,7 +1287,7 @@ contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { /// @dev Static unit test for the correctness of resolving a single attack game state. function test_resolve_rootContested_succeeds() public { - gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(4), 0); + gameProxy.attackAt{ value: MIN_BOND }(0, _dummyClaimHashAndSetClaims(3), 0); vm.warp(block.timestamp + 3 days + 12 hours + 1 seconds); From 024f562b98b9e6176cdce130930df6343e4db293 Mon Sep 17 00:00:00 2001 From: billxu Date: Wed, 1 May 2024 00:56:58 +0800 Subject: [PATCH 3/3] modify attackBranch, uint256 to uint64 --- .../src/dispute/FaultDisputeGame.sol | 16 ++++++++-------- .../src/dispute/lib/LibPosition.sol | 2 +- .../test/dispute/FaultDisputeGame.t.sol | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol index cc60d5365f5d8..d34fb4a08d0ff 100644 --- a/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol +++ b/packages/contracts-bedrock/src/dispute/FaultDisputeGame.sol @@ -747,20 +747,20 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { } } - function attackAt(uint256 _parentIndex, Claim _claim, uint256 _attackBranch) public payable { + function attackAt(uint256 _parentIndex, Claim _claim, uint64 _attackBranch) public payable { moveV2(_parentIndex, _claim, _attackBranch); } // ClaimHash => attackBranch => Claim - mapping(Claim => mapping(uint256 => Claim)) internal claimHashToClaims; + mapping(Claim => mapping(uint64 => Claim)) internal claimHashToClaims; - function setClaimHashClaims(Claim _claimHash, uint256 _attackBranch, Claim _claim) public { + function setClaimHashClaims(Claim _claimHash, uint64 _attackBranch, Claim _claim) public { claimHashToClaims[_claimHash][_attackBranch] = _claim; } function getClaimFromClaimHash( Claim claimsHash, - uint256 claimIndex + uint64 claimIndex ) internal view returns (Claim) { // TODO: retrieve the claim from the claimsHash // Either: from EIP-4844 BLOB with point-evaluation proof or calldata with Merkle proof @@ -784,7 +784,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { // S_0 claim_ = ABSOLUTE_PRESTATE; } else { - claim_ = getClaimFromClaimHash(ancestor_.claim, (pos - 1) % _nary); + claim_ = getClaimFromClaimHash(ancestor_.claim, uint64((pos - 1) % _nary)); pos = ancestor_.position.raw(); } return (Position.wrap(uint128(pos)), claim_); @@ -804,12 +804,12 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { } pos = pos / _nary; } - return (Position.wrap(uint128(pos)), getClaimFromClaimHash(ancestor_.claim, pos % _nary)); + return (Position.wrap(uint128(pos)), getClaimFromClaimHash(ancestor_.claim, uint64(pos % _nary))); } function stepV2( uint256 _claimIndex, - uint256 _attackBranch, + uint64 _attackBranch, bytes calldata _stateData, bytes calldata _proof ) @@ -876,7 +876,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver { parent.counteredBy = msg.sender; } - function moveV2(uint256 _challengeIndex, Claim _claim, uint256 _attackBranch) public payable { + function moveV2(uint256 _challengeIndex, Claim _claim, uint64 _attackBranch) public payable { // For N = 4 (bisec), // 1. _attackBranch == 0 (attack) // 2. _attackBranch == 1 (attack) diff --git a/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol b/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol index 3c4ca681e354b..70db0fa614bcb 100644 --- a/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol +++ b/packages/contracts-bedrock/src/dispute/lib/LibPosition.sol @@ -177,7 +177,7 @@ library LibPosition { } } - function moveN(Position _position, uint256 _bits, uint256 _branch) internal pure returns (Position move_) { + function moveN(Position _position, uint256 _bits, uint64 _branch) internal pure returns (Position move_) { assembly { move_ := shl(_bits, or(_branch, _position)) } diff --git a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol index 4c844025d550b..915b835fada29 100644 --- a/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol +++ b/packages/contracts-bedrock/test/dispute/FaultDisputeGame.t.sol @@ -1303,7 +1303,7 @@ contract FaultDisputeGame4Ary_Test is FaultDisputeGame_Init { function _dummyClaimHashAndSetClaims(uint256 nary) internal returns (Claim) { Claim hash = _dummyClaim(); - for (uint256 i; i < nary; i++) { + for (uint64 i; i < nary; i++) { gameProxy.setClaimHashClaims(hash, i, _dummyClaim()); } return hash;