Commit fc048466 authored by clabby's avatar clabby Committed by GitHub

Add latest game search to DGF (#9258)

parent 602d5b9a
This diff is collapsed.
...@@ -88,8 +88,8 @@ ...@@ -88,8 +88,8 @@
"sourceCodeHash": "0x1afb1d392e8f6a58ff86ea7f648e0d1756d4ba8d0d964279d58a390deaa53b7e" "sourceCodeHash": "0x1afb1d392e8f6a58ff86ea7f648e0d1756d4ba8d0d964279d58a390deaa53b7e"
}, },
"src/dispute/DisputeGameFactory.sol": { "src/dispute/DisputeGameFactory.sol": {
"initCodeHash": "0x56a9edbde3365f51454f6e0f12e136f4f19c0cdb10e89467be00129bb72f139e", "initCodeHash": "0xbb8c4273acaede381fe88e6974e073fac363c7ec21618af737df2d3118608665",
"sourceCodeHash": "0x01c757935c87dcf2faa7f16bcf29cfb29b90c671a874f983067614670147b11d" "sourceCodeHash": "0x78a030cd808a997f4b82c4fa284d6c394dae9d1fafeccb8783059a3b60bfb0ce"
}, },
"src/dispute/FaultDisputeGame.sol": { "src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0xe410bdcc3f0e8e71ddb609d1a0c21a40e1b06debeb55de6aff1452c8dda4af0b", "initCodeHash": "0xe410bdcc3f0e8e71ddb609d1a0c21a40e1b06debeb55de6aff1452c8dda4af0b",
......
...@@ -1079,6 +1079,30 @@ ...@@ -1079,6 +1079,30 @@
"length": 41, "length": 41,
"filename_relative": "src/universal/CrossDomainMessenger.sol" "filename_relative": "src/universal/CrossDomainMessenger.sol"
}, },
{
"id": "ef5cd67f78205ab8eebc31cd1bf78fa51ac2f8ab4d776a5d08bed46c9f1d13fc",
"impact": "Medium",
"confidence": "High",
"check": "tautology",
"description": "DisputeGameFactory.findLatestGames(GameType,uint256,uint256) (src/dispute/DisputeGameFactory.sol#137-177) contains a tautology or contradiction:\n\t- i >= 0 && i <= _start (src/dispute/DisputeGameFactory.sol#157)\n",
"type": "function",
"name": "findLatestGames",
"start": 5076,
"length": 1595,
"filename_relative": "src/dispute/DisputeGameFactory.sol"
},
{
"id": "ef5cd67f78205ab8eebc31cd1bf78fa51ac2f8ab4d776a5d08bed46c9f1d13fc",
"impact": "Medium",
"confidence": "High",
"check": "tautology",
"description": "DisputeGameFactory.findLatestGames(GameType,uint256,uint256) (src/dispute/DisputeGameFactory.sol#137-177) contains a tautology or contradiction:\n\t- i >= 0 && i <= _start (src/dispute/DisputeGameFactory.sol#157)\n",
"type": "node",
"name": "i >= 0 && i <= _start",
"start": 5923,
"length": 21,
"filename_relative": "src/dispute/DisputeGameFactory.sol"
},
{ {
"id": "0369380fef18a61639eac6a12773df792e9969e7bb20eddde54ccf3a263a0987", "id": "0369380fef18a61639eac6a12773df792e9969e7bb20eddde54ccf3a263a0987",
"impact": "Medium", "impact": "Medium",
......
...@@ -33,6 +33,47 @@ ...@@ -33,6 +33,47 @@
"stateMutability": "payable", "stateMutability": "payable",
"type": "function" "type": "function"
}, },
{
"inputs": [
{
"internalType": "GameType",
"name": "_gameType",
"type": "uint32"
},
{
"internalType": "uint256",
"name": "_start",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_n",
"type": "uint256"
}
],
"name": "findLatestGames",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"internalType": "GameId",
"name": "metadata",
"type": "bytes32"
}
],
"internalType": "struct IDisputeGameFactory.GameSearchResult[]",
"name": "games_",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function"
},
{ {
"inputs": [ "inputs": [
{ {
......
...@@ -24,8 +24,8 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver ...@@ -24,8 +24,8 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver
using ClonesWithImmutableArgs for address; using ClonesWithImmutableArgs for address;
/// @notice Semantic version. /// @notice Semantic version.
/// @custom:semver 0.0.8 /// @custom:semver 0.0.9
string public constant version = "0.0.8"; string public constant version = "0.0.9";
/// @inheritdoc IDisputeGameFactory /// @inheritdoc IDisputeGameFactory
mapping(GameType => IDisputeGame) public gameImpls; mapping(GameType => IDisputeGame) public gameImpls;
...@@ -133,6 +133,49 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver ...@@ -133,6 +133,49 @@ contract DisputeGameFactory is OwnableUpgradeable, IDisputeGameFactory, ISemver
uuid_ = Hash.wrap(keccak256(abi.encode(_gameType, _rootClaim, _extraData))); uuid_ = Hash.wrap(keccak256(abi.encode(_gameType, _rootClaim, _extraData)));
} }
/// @inheritdoc IDisputeGameFactory
function findLatestGames(
GameType _gameType,
uint256 _start,
uint256 _n
)
external
view
returns (GameSearchResult[] memory games_)
{
// If the `_start` index is greater than or equal to the game array length or `_n == 0`, return an empty array.
if (_start >= _disputeGameList.length || _n == 0) return games_;
// Allocate enough memory for the full array, but start the array's length at `0`. We may not use all of the
// memory allocated, but we don't know ahead of time the final size of the array.
assembly {
games_ := mload(0x40)
mstore(0x40, add(games_, add(0x20, shl(0x05, _n))))
}
// Perform a reverse linear search for the `_n` most recent games of type `_gameType`.
for (uint256 i = _start; i >= 0 && i <= _start;) {
GameId id = _disputeGameList[i];
(GameType gameType,,) = id.unpack();
if (gameType.raw() == _gameType.raw()) {
// Increase the size of the `games_` array by 1.
// SAFETY: We can safely lazily allocate memory here because we pre-allocated enough memory for the max
// possible size of the array.
assembly {
mstore(games_, add(mload(games_), 0x01))
}
games_[games_.length - 1] = GameSearchResult({ index: i, metadata: id });
if (games_.length >= _n) break;
}
unchecked {
i--;
}
}
}
/// @inheritdoc IDisputeGameFactory /// @inheritdoc IDisputeGameFactory
function setImplementation(GameType _gameType, IDisputeGame _impl) external onlyOwner { function setImplementation(GameType _gameType, IDisputeGame _impl) external onlyOwner {
gameImpls[_gameType] = _impl; gameImpls[_gameType] = _impl;
......
...@@ -24,6 +24,12 @@ interface IDisputeGameFactory { ...@@ -24,6 +24,12 @@ interface IDisputeGameFactory {
/// @param newBond The new bond (in wei) for initializing the game type. /// @param newBond The new bond (in wei) for initializing the game type.
event InitBondUpdated(GameType indexed gameType, uint256 indexed newBond); event InitBondUpdated(GameType indexed gameType, uint256 indexed newBond);
/// @notice Information about a dispute game found in a `findLatestGames` search.
struct GameSearchResult {
uint256 index;
GameId metadata;
}
/// @notice The total number of dispute games created by this factory. /// @notice The total number of dispute games created by this factory.
/// @return gameCount_ The total number of dispute games created by this factory. /// @return gameCount_ The total number of dispute games created by this factory.
function gameCount() external view returns (uint256 gameCount_); function gameCount() external view returns (uint256 gameCount_);
...@@ -111,4 +117,18 @@ interface IDisputeGameFactory { ...@@ -111,4 +117,18 @@ interface IDisputeGameFactory {
external external
pure pure
returns (Hash uuid_); returns (Hash uuid_);
/// @notice Finds the `_n` most recent `GameId`'s of type `_gameType` starting at `_start`. If there are less than
/// `_n` games of type `_gameType` starting at `_start`, then the returned array will be shorter than `_n`.
/// @param _gameType The type of game to find.
/// @param _start The index to start the reverse search from.
/// @param _n The number of games to find.
function findLatestGames(
GameType _gameType,
uint256 _start,
uint256 _n
)
external
view
returns (GameSearchResult[] memory games_);
} }
...@@ -5,7 +5,7 @@ import "src/libraries/DisputeTypes.sol"; ...@@ -5,7 +5,7 @@ import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol"; import "src/libraries/DisputeErrors.sol";
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { DisputeGameFactory, IDisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol"; import { IDisputeGame } from "src/dispute/interfaces/IDisputeGame.sol";
import { Proxy } from "src/universal/Proxy.sol"; import { Proxy } from "src/universal/Proxy.sol";
import { CommonTest } from "test/setup/CommonTest.sol"; import { CommonTest } from "test/setup/CommonTest.sol";
...@@ -236,6 +236,114 @@ contract DisputeGameFactory_TransferOwnership_Test is DisputeGameFactory_Init { ...@@ -236,6 +236,114 @@ contract DisputeGameFactory_TransferOwnership_Test is DisputeGameFactory_Init {
} }
} }
contract DisputeGameFactory_FindLatestGames_Test is DisputeGameFactory_Init {
function setUp() public override {
super.setUp();
// Set three implementations to the same `FakeClone` contract.
for (uint8 i; i < 3; i++) {
GameType lgt = GameType.wrap(i);
factory.setImplementation(lgt, IDisputeGame(address(fakeClone)));
factory.setInitBond(lgt, 0);
}
}
/// @dev Tests that `findLatestGames` returns an empty array when the passed starting index is greater than or equal
/// to the game count.
function testFuzz_findLatestGames_greaterThanLength_succeeds(uint256 _start) public {
// Create some dispute games of varying game types.
for (uint256 i; i < 1 << 5; i++) {
factory.create(GameType.wrap(uint8(i % 2)), Claim.wrap(bytes32(i)), abi.encode(i));
}
// Bound the starting index to a number greater than the length of the game list.
uint256 gameCount = factory.gameCount();
_start = bound(_start, gameCount, type(uint256).max);
// The array's length should always be 0.
IDisputeGameFactory.GameSearchResult[] memory games = factory.findLatestGames(GameTypes.CANNON, _start, 1);
assertEq(games.length, 0);
}
/// @dev Tests that `findLatestGames` returns the correct games.
function test_findLatestGames_static_succeeds() public {
// Create some dispute games of varying game types.
for (uint256 i; i < 1 << 5; i++) {
factory.create(GameType.wrap(uint8(i % 3)), Claim.wrap(bytes32(i)), abi.encode(i));
}
uint256 gameCount = factory.gameCount();
IDisputeGameFactory.GameSearchResult[] memory games =
factory.findLatestGames(GameType.wrap(0), gameCount - 1, 1);
assertEq(games.length, 1);
assertEq(games[0].index, 30);
(GameType gameType, Timestamp createdAt, IDisputeGame game) = games[0].metadata.unpack();
assertEq(gameType.raw(), 0);
assertEq(createdAt.raw(), block.timestamp);
games = factory.findLatestGames(GameType.wrap(1), gameCount - 1, 1);
assertEq(games.length, 1);
assertEq(games[0].index, 31);
(gameType, createdAt, game) = games[0].metadata.unpack();
assertEq(gameType.raw(), 1);
assertEq(createdAt.raw(), block.timestamp);
games = factory.findLatestGames(GameType.wrap(2), gameCount - 1, 1);
assertEq(games.length, 1);
assertEq(games[0].index, 29);
(gameType, createdAt, game) = games[0].metadata.unpack();
assertEq(gameType.raw(), 2);
assertEq(createdAt.raw(), block.timestamp);
}
/// @dev Tests that `findLatestGames` returns the correct games, if there are less than `_n` games of the given type
/// available.
function test_findLatestGames_lessThanNAvailable_succeeds() public {
// Create some dispute games of varying game types.
factory.create(GameType.wrap(1), Claim.wrap(bytes32(0)), abi.encode(0));
factory.create(GameType.wrap(1), Claim.wrap(bytes32(uint256(1))), abi.encode(1));
for (uint256 i; i < 1 << 3; i++) {
factory.create(GameType.wrap(0), Claim.wrap(bytes32(i)), abi.encode(i));
}
uint256 gameCount = factory.gameCount();
IDisputeGameFactory.GameSearchResult[] memory games =
factory.findLatestGames(GameType.wrap(2), gameCount - 1, 5);
assertEq(games.length, 0);
games = factory.findLatestGames(GameType.wrap(1), gameCount - 1, 5);
assertEq(games.length, 2);
assertEq(games[0].index, 1);
assertEq(games[1].index, 0);
}
/// @dev Tests that the expected number of games are returned when `findLatestGames` is called.
function testFuzz_findLatestGames_correctAmount_succeeds(
uint256 _numGames,
uint256 _numSearchedGames,
uint256 _n
)
public
{
_numGames = bound(_numGames, 0, 1 << 8);
_numSearchedGames = bound(_numSearchedGames, 0, _numGames);
_n = bound(_n, 0, _numSearchedGames);
// Create `_numGames` dispute games, with at least `_numSearchedGames` games.
for (uint256 i; i < _numGames; i++) {
uint8 gameType = i < _numSearchedGames ? 0 : 1;
factory.create(GameType.wrap(gameType), Claim.wrap(bytes32(i)), abi.encode(i));
}
// Ensure that the correct number of games are returned.
uint256 start = _numGames == 0 ? 0 : _numGames - 1;
IDisputeGameFactory.GameSearchResult[] memory games = factory.findLatestGames(GameType.wrap(0), start, _n);
assertEq(games.length, _n);
}
}
/// @dev A fake clone used for testing the `DisputeGameFactory` contract's `create` function. /// @dev A fake clone used for testing the `DisputeGameFactory` contract's `create` function.
contract FakeClone { contract FakeClone {
function initialize() external payable { function initialize() external payable {
......
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