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

Add `PermissionedDisputeGame` (#9421)

* Add `PermissionedDisputeGame`

* Add clarifying doc
parent c6795982
This diff is collapsed.
......@@ -32,6 +32,7 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { Constants } from "src/libraries/Constants.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol";
import { PreimageOracle } from "src/cannon/PreimageOracle.sol";
import { MIPS } from "src/cannon/MIPS.sol";
import { L1ERC721Bridge } from "src/L1/L1ERC721Bridge.sol";
......@@ -328,6 +329,7 @@ contract Deploy is Deployer {
setAlphabetFaultGameImplementation({ _allowUpgrade: false });
setCannonFaultGameImplementation({ _allowUpgrade: false });
setPermissionedCannonFaultGameImplementation({ _allowUpgrade: false });
transferDisputeGameFactoryOwnership();
}
......@@ -1123,7 +1125,7 @@ contract Deploy is Deployer {
}
}
/// @notice Sets the implementation for the `FAULT` game type in the `DisputeGameFactory`
/// @notice Sets the implementation for the `CANNON` game type in the `DisputeGameFactory`
function setCannonFaultGameImplementation(bool _allowUpgrade) public broadcast {
console.log("Setting Cannon FaultDisputeGame implementation");
DisputeGameFactory factory = DisputeGameFactory(mustGetAddress("DisputeGameFactoryProxy"));
......@@ -1139,6 +1141,22 @@ contract Deploy is Deployer {
});
}
/// @notice Sets the implementation for the `PERMISSIONED_CANNON` game type in the `DisputeGameFactory`
function setPermissionedCannonFaultGameImplementation(bool _allowUpgrade) public broadcast {
console.log("Setting Cannon PermissionedDisputeGame implementation");
DisputeGameFactory factory = DisputeGameFactory(mustGetAddress("DisputeGameFactoryProxy"));
// Set the Cannon FaultDisputeGame implementation in the factory.
_setFaultGameImplementation({
_factory: factory,
_gameType: GameTypes.PERMISSIONED_CANNON,
_absolutePrestate: loadMipsAbsolutePrestate(),
_faultVm: IBigStepper(mustGetAddress("Mips")),
_maxGameDepth: cfg.faultGameMaxDepth(),
_allowUpgrade: _allowUpgrade
});
}
/// @notice Sets the implementation for the `ALPHABET` game type in the `DisputeGameFactory`
function setAlphabetFaultGameImplementation(bool _allowUpgrade) public onlyDevnet broadcast {
console.log("Setting Alphabet FaultDisputeGame implementation");
......@@ -1175,25 +1193,45 @@ contract Deploy is Deployer {
return;
}
_factory.setImplementation(
_gameType,
new FaultDisputeGame({
_gameType: _gameType,
_absolutePrestate: _absolutePrestate,
_genesisBlockNumber: cfg.faultGameGenesisBlock(),
_genesisOutputRoot: Hash.wrap(cfg.faultGameGenesisOutputRoot()),
_maxGameDepth: _maxGameDepth,
_splitDepth: cfg.faultGameSplitDepth(),
_gameDuration: Duration.wrap(uint64(cfg.faultGameMaxDuration())),
_vm: _faultVm
})
);
uint32 rawGameType = GameType.unwrap(_gameType);
if (rawGameType != GameTypes.PERMISSIONED_CANNON.raw()) {
_factory.setImplementation(
_gameType,
new FaultDisputeGame({
_gameType: _gameType,
_absolutePrestate: _absolutePrestate,
_genesisBlockNumber: cfg.faultGameGenesisBlock(),
_genesisOutputRoot: Hash.wrap(cfg.faultGameGenesisOutputRoot()),
_maxGameDepth: _maxGameDepth,
_splitDepth: cfg.faultGameSplitDepth(),
_gameDuration: Duration.wrap(uint64(cfg.faultGameMaxDuration())),
_vm: _faultVm
})
);
} else {
_factory.setImplementation(
_gameType,
new PermissionedDisputeGame({
_gameType: _gameType,
_absolutePrestate: _absolutePrestate,
_genesisBlockNumber: cfg.faultGameGenesisBlock(),
_genesisOutputRoot: Hash.wrap(cfg.faultGameGenesisOutputRoot()),
_maxGameDepth: _maxGameDepth,
_splitDepth: cfg.faultGameSplitDepth(),
_gameDuration: Duration.wrap(uint64(cfg.faultGameMaxDuration())),
_vm: _faultVm,
_proposer: cfg.l2OutputOracleProposer(),
_challenger: cfg.l2OutputOracleChallenger()
})
);
}
string memory gameTypeString;
if (rawGameType == GameType.unwrap(GameTypes.CANNON)) {
if (rawGameType == GameTypes.CANNON.raw()) {
gameTypeString = "Cannon";
} else if (rawGameType == GameType.unwrap(GameTypes.ALPHABET)) {
} else if (rawGameType == GameTypes.PERMISSIONED_CANNON.raw()) {
gameTypeString = "PermissionedCannon";
} else if (rawGameType == GameTypes.ALPHABET.raw()) {
gameTypeString = "Alphabet";
} else {
gameTypeString = "Unknown";
......
......@@ -96,8 +96,8 @@
"sourceCodeHash": "0xe1891e7e6a1928b9a2ddc47d1f010650f1125a0617b8bf32190176a3bb674b4f"
},
"src/dispute/FaultDisputeGame.sol": {
"initCodeHash": "0x04fc1926f2ccd02a2c065e4dd0e562be7c8c498d76d6d860cc394fd74fffca06",
"sourceCodeHash": "0x1e539ba88487b2ec1359577dbd82270fddbabc3b8982dc6dc1a4a5169318e479"
"initCodeHash": "0xba582b158027b434b4e7d4d1edc073c7fce8b499c5ce8da03ea98c173c530f5d",
"sourceCodeHash": "0xd5f0f679c4559b277287f1844fb03ed47c702fcffff3c4aaea0e0ce8fc7d0760"
},
"src/legacy/DeployerWhitelist.sol": {
"initCodeHash": "0x8de80fb23b26dd9d849f6328e56ea7c173cd9e9ce1f05c9beea559d1720deb3d",
......
[
{
"bytes": "8",
"label": "createdAt",
"offset": 0,
"slot": "0",
"type": "Timestamp"
},
{
"bytes": "8",
"label": "resolvedAt",
"offset": 8,
"slot": "0",
"type": "Timestamp"
},
{
"bytes": "1",
"label": "status",
"offset": 16,
"slot": "0",
"type": "enum GameStatus"
},
{
"bytes": "32",
"label": "l1Head",
"offset": 0,
"slot": "1",
"type": "Hash"
},
{
"bytes": "32",
"label": "claimData",
"offset": 0,
"slot": "2",
"type": "struct IFaultDisputeGame.ClaimData[]"
},
{
"bytes": "32",
"label": "credit",
"offset": 0,
"slot": "3",
"type": "mapping(address => uint256)"
},
{
"bytes": "32",
"label": "claims",
"offset": 0,
"slot": "4",
"type": "mapping(ClaimHash => bool)"
},
{
"bytes": "32",
"label": "subgames",
"offset": 0,
"slot": "5",
"type": "mapping(uint256 => uint256[])"
},
{
"bytes": "1",
"label": "subgameAtRootResolved",
"offset": 0,
"slot": "6",
"type": "bool"
},
{
"bytes": "1",
"label": "initialized",
"offset": 1,
"slot": "6",
"type": "bool"
}
]
\ No newline at end of file
......@@ -81,8 +81,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
bool internal initialized;
/// @notice Semantic version.
/// @custom:semver 0.2.0
string public constant version = "0.2.0";
/// @custom:semver 0.2.1
string public constant version = "0.2.1";
/// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace.
......@@ -121,7 +121,15 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
////////////////////////////////////////////////////////////////
/// @inheritdoc IFaultDisputeGame
function step(uint256 _claimIndex, bool _isAttack, bytes calldata _stateData, bytes calldata _proof) external {
function step(
uint256 _claimIndex,
bool _isAttack,
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();
......@@ -194,7 +202,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
/// @param _challengeIndex The index of the claim being moved against.
/// @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 {
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();
......@@ -461,7 +469,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, ISemver {
////////////////////////////////////////////////////////////////
/// @inheritdoc IInitializable
function initialize() external payable {
function initialize() public payable virtual {
// SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and
// prevent the game from being created.
//
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;
import { FaultDisputeGame, IFaultDisputeGame, IBigStepper, IInitializable } from "src/dispute/FaultDisputeGame.sol";
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
/// @title PermissionedDisputeGame
/// @notice PermissionedDisputeGame is a contract that inherits from `FaultDisputeGame`, and contains two roles:
/// - The `challenger` role, which is allowed to challenge a dispute.
/// - The `proposer` role, which is allowed to create proposals and participate in their game.
/// This contract exists as a fallback mechanism in case of the failure of the fault proof system in the stage
/// one release. It will not be the default implementation used, and eventually will be deprecated in favor of
/// a fully permissionless system.
contract PermissionedDisputeGame is FaultDisputeGame {
/// @notice The proposer role is allowed to create proposals and participate in the dispute game.
address internal immutable PROPOSER;
/// @notice The challenger role is allowed to participate in the dispute game.
address internal immutable CHALLENGER;
/// @notice Modifier that gates access to the `challenger` and `proposer` roles.
modifier onlyAuthorized() {
if (!(msg.sender == PROPOSER || msg.sender == CHALLENGER)) {
revert BadAuth();
}
_;
}
/// @param _gameType The type ID of the game.
/// @param _absolutePrestate The absolute prestate of the instruction trace.
/// @param _genesisBlockNumber The block number of the genesis block.
/// @param _genesisOutputRoot The output root of the genesis block.
/// @param _maxGameDepth The maximum depth of bisection.
/// @param _splitDepth The final depth of the output bisection portion of the game.
/// @param _gameDuration The duration of the game.
/// @param _vm An onchain VM that performs single instruction steps on a fault proof program
/// trace.
constructor(
GameType _gameType,
Claim _absolutePrestate,
uint256 _genesisBlockNumber,
Hash _genesisOutputRoot,
uint256 _maxGameDepth,
uint256 _splitDepth,
Duration _gameDuration,
IBigStepper _vm,
address _proposer,
address _challenger
)
FaultDisputeGame(
_gameType,
_absolutePrestate,
_genesisBlockNumber,
_genesisOutputRoot,
_maxGameDepth,
_splitDepth,
_gameDuration,
_vm
)
{
PROPOSER = _proposer;
CHALLENGER = _challenger;
}
/// @inheritdoc IFaultDisputeGame
function step(
uint256 _claimIndex,
bool _isAttack,
bytes calldata _stateData,
bytes calldata _proof
)
public
override
onlyAuthorized
{
super.step(_claimIndex, _isAttack, _stateData, _proof);
}
/// @notice Generic move function, used for both `attack` and `defend` moves.
/// @param _challengeIndex The index of the claim being moved against.
/// @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 override onlyAuthorized {
super.move(_challengeIndex, _claim, _isAttack);
}
/// @inheritdoc IInitializable
function initialize() public payable override {
// The creator of the dispute game must be the proposer EOA.
if (tx.origin != PROPOSER) revert BadAuth();
// Fallthrough initialization.
super.initialize();
}
}
......@@ -87,3 +87,10 @@ error ClaimAboveSplit();
/// @notice Thrown on deployment if the split depth is greater than or equal to the max
/// depth of the game.
error InvalidSplitDepth();
////////////////////////////////////////////////////////////////
// `PermissionedDisputeGame` Errors //
////////////////////////////////////////////////////////////////
/// @notice Thrown when an unauthorized address attempts to interact with the game.
error BadAuth();
......@@ -95,6 +95,9 @@ library GameTypes {
/// @dev A dispute game type the uses the cannon vm.
GameType internal constant CANNON = GameType.wrap(0);
/// @dev A permissioned dispute game type the uses the cannon vm.
GameType internal constant PERMISSIONED_CANNON = GameType.wrap(1);
/// @notice A dispute game type that uses an alphabet vm.
/// Not intended for production use.
GameType internal constant ALPHABET = GameType.wrap(255);
......
......@@ -101,7 +101,7 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_Init {
function testFuzz_create_noImpl_reverts(uint32 gameType, Claim rootClaim, bytes calldata extraData) public {
// Ensure that the `gameType` is within the bounds of the `GameType` enum's possible values. We skip over
// game type = 0, since the deploy script set the implementation for that game type.
GameType gt = GameType.wrap(uint32(bound(gameType, 1, type(uint32).max)));
GameType gt = GameType.wrap(uint32(bound(gameType, 2, type(uint32).max)));
// Ensure the rootClaim has a VMStatus that disagrees with the validity.
rootClaim = changeClaimStatus(rootClaim, VMStatuses.INVALID);
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import { Test } from "forge-std/Test.sol";
import { Vm } from "forge-std/Vm.sol";
import { DisputeGameFactory_Init } from "test/dispute/DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { PermissionedDisputeGame } from "src/dispute/PermissionedDisputeGame.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { PreimageOracle } from "src/cannon/PreimageOracle.sol";
import { PreimageKeyLib } from "src/cannon/PreimageKeyLib.sol";
import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol";
import { Types } from "src/libraries/Types.sol";
import { LibClock } from "src/dispute/lib/LibUDT.sol";
import { LibPosition } from "src/dispute/lib/LibPosition.sol";
import { IBigStepper, IPreimageOracle } from "src/dispute/interfaces/IBigStepper.sol";
import { AlphabetVM } from "test/mocks/AlphabetVM.sol";
import { DisputeActor, HonestDisputeActor } from "test/actors/FaultDisputeActors.sol";
contract PermissionedDisputeGame_Init is DisputeGameFactory_Init {
/// @dev The type of the game being tested.
GameType internal constant GAME_TYPE = GameType.wrap(1);
/// @dev Mock proposer key
address internal constant PROPOSER = address(0xfacade9);
/// @dev Mock challenger key
address internal constant CHALLENGER = address(0xfacadec);
/// @dev The implementation of the game.
PermissionedDisputeGame internal gameImpl;
/// @dev The `Clone` proxy of the game.
PermissionedDisputeGame internal gameProxy;
/// @dev The extra data passed to the game for initialization.
bytes internal extraData;
event Move(uint256 indexed parentIndex, Claim indexed pivot, address indexed claimant);
function init(
Claim rootClaim,
Claim absolutePrestate,
uint256 l2BlockNumber,
uint256 genesisBlockNumber,
Hash genesisOutputRoot
)
public
{
// Set the time to a realistic date.
vm.warp(1690906994);
// Set the extra data for the game creation
extraData = abi.encode(l2BlockNumber);
AlphabetVM _vm = new AlphabetVM(absolutePrestate, new PreimageOracle(0, 0, 0));
// Deploy an implementation of the fault game
gameImpl = new PermissionedDisputeGame({
_gameType: GAME_TYPE,
_absolutePrestate: absolutePrestate,
_genesisBlockNumber: genesisBlockNumber,
_genesisOutputRoot: genesisOutputRoot,
_maxGameDepth: 2 ** 3,
_splitDepth: 2 ** 2,
_gameDuration: Duration.wrap(7 days),
_vm: _vm,
_proposer: PROPOSER,
_challenger: CHALLENGER
});
// Register the game implementation with the factory.
disputeGameFactory.setImplementation(GAME_TYPE, gameImpl);
// Create a new game.
vm.prank(PROPOSER, PROPOSER);
gameProxy = PermissionedDisputeGame(address(disputeGameFactory.create(GAME_TYPE, rootClaim, extraData)));
// Check immutables
assertEq(gameProxy.gameType().raw(), GAME_TYPE.raw());
assertEq(gameProxy.absolutePrestate().raw(), absolutePrestate.raw());
assertEq(gameProxy.genesisBlockNumber(), genesisBlockNumber);
assertEq(gameProxy.genesisOutputRoot().raw(), genesisOutputRoot.raw());
assertEq(gameProxy.maxGameDepth(), 2 ** 3);
assertEq(gameProxy.splitDepth(), 2 ** 2);
assertEq(gameProxy.gameDuration().raw(), 7 days);
assertEq(address(gameProxy.vm()), address(_vm));
// Label the proxy
vm.label(address(gameProxy), "FaultDisputeGame_Clone");
}
fallback() external payable { }
receive() external payable { }
}
contract PermissionedDisputeGame_Test is PermissionedDisputeGame_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;
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))
});
}
/// @dev Tests that the proposer can create a permissioned dispute game.
function test_createGame_proposer_succeeds() public {
vm.prank(PROPOSER, PROPOSER);
disputeGameFactory.create(GAME_TYPE, ROOT_CLAIM, abi.encode(0x420));
}
/// @dev Tests that the permissioned game cannot be created by any address other than the proposer.
function testFuzz_createGame_notProposer_reverts(address _p) public {
vm.assume(_p != PROPOSER);
vm.prank(_p, _p);
vm.expectRevert(BadAuth.selector);
disputeGameFactory.create(GAME_TYPE, ROOT_CLAIM, abi.encode(0x420));
}
/// @dev Tests that the challenger can participate in a permissioned dispute game.
function test_participateInGame_challenger_succeeds() public {
vm.startPrank(CHALLENGER, CHALLENGER);
gameProxy.attack(0, Claim.wrap(0));
gameProxy.defend(1, Claim.wrap(0));
gameProxy.move(2, Claim.wrap(0), true);
vm.stopPrank();
}
/// @dev Tests that the proposer can participate in a permissioned dispute game.
function test_participateInGame_proposer_succeeds() public {
vm.startPrank(PROPOSER, PROPOSER);
gameProxy.attack(0, Claim.wrap(0));
gameProxy.defend(1, Claim.wrap(0));
gameProxy.move(2, Claim.wrap(0), true);
vm.stopPrank();
}
/// @dev Tests that addresses that are not the proposer or challenger cannot participate in a permissioned dispute
/// game.
function test_participateInGame_notAuthorized_reverts(address _p) public {
vm.assume(_p != PROPOSER && _p != CHALLENGER);
vm.startPrank(_p, _p);
vm.expectRevert(BadAuth.selector);
gameProxy.attack(0, Claim.wrap(0));
vm.expectRevert(BadAuth.selector);
gameProxy.defend(1, Claim.wrap(0));
vm.expectRevert(BadAuth.selector);
gameProxy.move(2, Claim.wrap(0), true);
vm.stopPrank();
}
}
/// @dev Helper to change the VM status byte of a claim.
function _changeClaimStatus(Claim _claim, VMStatus _status) pure returns (Claim out_) {
assembly {
out_ := or(and(not(shl(248, 0xFF)), _claim), shl(248, _status))
}
}
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