Skip to content
Open
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ If you are interested in building a solution for another chain, please take a lo
* eg. if `APE`, serves L2 → L3 instead of L1 → L2 → L3
* Include `--depth=#` to adjust commit depth
* Include `--step=#` to adjust commit step
* Include `--gameTypes=1,2...` to set allowed game types for `OPFaultRollup`
* Use [`PROVIDER_ORDER`](./test/providers.ts#L479) to customize global RPC provider priority.
* Use `PROVIDER_ORDER_{CHAIN_NAME}` to customize per-chain RPC provider priority.
* Use `PROVIDER_{CHAIN_NAME}` to customize per-chain RPC provider override.
Expand Down
115 changes: 64 additions & 51 deletions contracts/op/OPFaultGameFinder.sol
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import { IDisputeGameFactory, IDisputeGame, IFaultDisputeGame } from './OPInterfaces.sol';
import { OPFaultParams, FinalizationParams } from './OPStructs.sol';
import {
IOptimismPortal,
IDisputeGameFactory,
IDisputeGame,
IFaultDisputeGame
} from './OPInterfaces.sol';
import {OPFaultParams} from './OPStructs.sol';

// https://github.com/ethereum-optimism/optimism/issues/11269

// https://github.com/ethereum-optimism/optimism/blob/v1.13.7/packages/contracts-bedrock/src/dispute/lib/Types.sol
uint256 constant CHALLENGER_WINS = 1;
uint256 constant DEFENDER_WINS = 2;

// https://github.com/ethereum-optimism/optimism/blob/v1.13.7/packages/contracts-bedrock/src/dispute/lib/Types.sol
uint32 constant GAME_TYPE_CANNON = 0;
uint32 constant GAME_TYPE_PERMISSIONED_CANNON = 1;

error GameNotFound();

struct Config {
uint256 finalityDelay;
uint256 respectedGameType;
}

contract OPFaultGameFinder {
function findGameIndex(
OPFaultParams memory params,
uint256 gameCount
) external view virtual returns (uint256) {
FinalizationParams memory finalizationParams = FinalizationParams({
finalityDelay: params.portal.disputeGameFinalityDelaySeconds(),
gameTypeUpdatedAt: params.portal.respectedGameTypeUpdatedAt()
});
Config memory config = _config(params.portal);
IDisputeGameFactory factory = params.portal.disputeGameFactory();
if (gameCount == 0) gameCount = factory.gameCount();
while (gameCount > 0) {
Expand All @@ -33,15 +36,7 @@ contract OPFaultGameFinder {
uint256 created,
IDisputeGame gameProxy
) = factory.gameAtIndex(--gameCount);
if (
_isGameUsable(
gameProxy,
gameType,
created,
params,
finalizationParams
)
) {
if (_isGameUsable(gameProxy, gameType, created, params, config)) {
return gameCount;
}
}
Expand All @@ -62,10 +57,6 @@ contract OPFaultGameFinder {
bytes32 rootClaim
)
{
FinalizationParams memory finalizationParams = FinalizationParams({
finalityDelay: params.portal.disputeGameFinalityDelaySeconds(),
gameTypeUpdatedAt: params.portal.respectedGameTypeUpdatedAt()
});
IDisputeGameFactory factory = params.portal.disputeGameFactory();
(gameType, created, gameProxy) = factory.gameAtIndex(gameIndex);
if (
Expand All @@ -74,7 +65,7 @@ contract OPFaultGameFinder {
gameType,
created,
params,
finalizationParams
_config(params.portal)
)
) {
l2BlockNumber = gameProxy.l2BlockNumber();
Expand All @@ -87,48 +78,70 @@ contract OPFaultGameFinder {
uint256 gameType,
uint256 created,
OPFaultParams memory params,
FinalizationParams memory finalizationParams
Config memory config
) internal view returns (bool) {
if (!_isAllowedGameType(gameType, params.allowedGameTypes)) return false;
if (!_isAllowedProposer(gameProxy.gameCreator(), params.allowedProposers)) return false;
// if allowed gameTypes is empty, accept a respected game OR a previously respected game
if (
params.allowedGameTypes.length == 0
? (gameType != config.respectedGameType &&
!gameProxy.wasRespectedGameTypeWhenCreated())
: !_isAllowedGameType(gameType, params.allowedGameTypes)
) {
return false;
}
if (
!_isAllowedProposer(
gameProxy.gameCreator(),
params.allowedProposers
)
) return false;
// https://specs.optimism.io/fault-proof/stage-one/bridge-integration.html#blacklisting-disputegames
if (params.portal.disputeGameBlacklist(gameProxy)) return false;
if (!gameProxy.wasRespectedGameTypeWhenCreated()) return false;
if (params.minAgeSec > 0) {
if (created > block.timestamp - params.minAgeSec) return false;
if (
gameType == GAME_TYPE_CANNON ||
gameType == GAME_TYPE_PERMISSIONED_CANNON
) {
return IFaultDisputeGame(address(gameProxy))
.l2BlockNumberChallenged() ? false : true;
(bool ok, bytes memory v) = address(gameProxy).staticcall(
abi.encodeCall(IFaultDisputeGame.l2BlockNumberChallenged, ())
);
if (ok && bytes32(v) != bytes32(0)) {
return false; // block number was challenged
}
if (gameProxy.status() != CHALLENGER_WINS) {
return true; // not successfully challenged
}
// Testing for an unchallenged game falls back to finalized mode if unknown game type
}

if (
created > finalizationParams.gameTypeUpdatedAt &&
gameProxy.status() == DEFENDER_WINS
) {
return ((block.timestamp - gameProxy.resolvedAt()) >
finalizationParams.finalityDelay);
}
return false;
// require resolved + sufficiently aged
return
gameProxy.status() == DEFENDER_WINS &&
(block.timestamp - gameProxy.resolvedAt()) >= config.finalityDelay;
}

function _isAllowedGameType(uint256 gameType, uint256[] memory allowedGameTypes) pure internal returns (bool) {
for (uint i = 0; i < allowedGameTypes.length; i++) {
function _isAllowedGameType(
uint256 gameType,
uint256[] memory allowedGameTypes
) internal pure returns (bool) {
for (uint256 i; i < allowedGameTypes.length; ++i) {
if (allowedGameTypes[i] == gameType) return true;
}
return false;
}

function _isAllowedProposer(address proposer, address[] memory allowedProposers) pure internal returns (bool) {
if (allowedProposers.length == 0) return true;

for (uint i = 0; i < allowedProposers.length; i++) {
function _isAllowedProposer(
address proposer,
address[] memory allowedProposers
) internal pure returns (bool) {
for (uint256 i; i < allowedProposers.length; ++i) {
if (allowedProposers[i] == proposer) return true;
}
return false;
return allowedProposers.length == 0;
}

function _config(
IOptimismPortal portal
) internal view returns (Config memory) {
return
Config({
finalityDelay: portal.disputeGameFinalityDelaySeconds(),
respectedGameType: portal.respectedGameType()
});
}
}
54 changes: 34 additions & 20 deletions contracts/op/OPFaultVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,50 @@ pragma solidity ^0.8.0;

import {AbstractVerifier, IVerifierHooks} from '../AbstractVerifier.sol';
import {GatewayRequest, GatewayVM, ProofSequence} from '../GatewayVM.sol';
import {Hashing, Types} from '../../lib/optimism/packages/contracts-bedrock/src/libraries/Hashing.sol';
import { IOptimismPortal, IOPFaultGameFinder, IDisputeGame, OPFaultParams } from './OPInterfaces.sol';


import {
Hashing,
Types
} from '../../lib/optimism/packages/contracts-bedrock/src/libraries/Hashing.sol';
import {
IOptimismPortal,
IOPFaultGameFinder,
IDisputeGame,
OPFaultParams
} from './OPInterfaces.sol';

contract OPFaultVerifier is AbstractVerifier {
IOptimismPortal immutable _portal;
IOPFaultGameFinder immutable _gameFinder;
IOPFaultGameFinder public immutable gameFinder;
OPFaultParams private _params;

constructor(
string[] memory urls,
uint256 window,
IVerifierHooks hooks,
IOPFaultGameFinder gameFinder,
IOPFaultGameFinder gameFinder_,
OPFaultParams memory params
) AbstractVerifier(urls, window, hooks) {
_portal = params.portal;
_gameFinder = gameFinder;
gameFinder = gameFinder_;
_params = params;
}

function portal() external view returns (IOptimismPortal) {
return _params.portal;
}

function minAgeSec() external view returns (uint256) {
return _params.minAgeSec;
}

function gameTypes() external view returns (uint256[] memory) {
return _params.allowedGameTypes;
}

function allowedProposers() external view returns (address[] memory) {
return _params.allowedProposers;
}

function getLatestContext() external view virtual returns (bytes memory) {
return
abi.encode(
_gameFinder.findGameIndex(
_params,
0
)
);
return abi.encode(gameFinder.findGameIndex(_params, 0));
}

struct GatewayProof {
Expand All @@ -49,11 +63,12 @@ contract OPFaultVerifier is AbstractVerifier {
) external view returns (bytes[] memory, uint8 exitCode) {
uint256 gameIndex1 = abi.decode(context, (uint256));
GatewayProof memory p = abi.decode(proof, (GatewayProof));
(, , IDisputeGame gameProxy, uint256 blockNumber,) = _gameFinder
(, , IDisputeGame gameProxy, uint256 blockNumber, ) = gameFinder
.gameAtIndex(_params, p.gameIndex);
require(blockNumber != 0, 'OPFault: invalid game');
if (p.gameIndex != gameIndex1) {
(, , IDisputeGame gameProxy1) = _portal
(, , IDisputeGame gameProxy1) = _params
.portal
.disputeGameFactory()
.gameAtIndex(gameIndex1);
_checkWindow(_getGameTime(gameProxy1), _getGameTime(gameProxy));
Expand All @@ -77,7 +92,6 @@ contract OPFaultVerifier is AbstractVerifier {
}

function _getGameTime(IDisputeGame g) internal view returns (uint256) {
return
_params.minAgeSec == 0 ? g.resolvedAt() : g.createdAt();
return _params.minAgeSec == 0 ? g.resolvedAt() : g.createdAt();
}
}
5 changes: 0 additions & 5 deletions contracts/op/OPStructs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,3 @@ struct OPFaultParams {
uint256[] allowedGameTypes;
address[] allowedProposers;
}

struct FinalizationParams {
uint256 finalityDelay;
uint64 gameTypeUpdatedAt;
}
2 changes: 1 addition & 1 deletion scripts/deploy-gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ await foundry.deploy({
});
await foundry.deploy({
file: 'OPFaultVerifier',
args: [U, 1, A, [A, A, 0, 0]],
args: [U, 1, A, [A, A, [], []]],
libs: { GatewayVM },
});
await foundry.deploy({
Expand Down
8 changes: 2 additions & 6 deletions scripts/l2-primary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,8 @@ async function determineGateway(foundry: Foundry, setup: Setup) {
[],
rollup.defaultWindow,
EthVerifierHooks,
[
rollup.OptimismPortal,
rollup.GameFinder,
rollup.gameTypeBitMask,
rollup.minAgeSec,
],
rollup.GameFinder,
rollup.paramTuple,
],
libs: { GatewayVM },
});
Expand Down
55 changes: 31 additions & 24 deletions scripts/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ let prefetch = false;
let latestBlockTag = '';
let commitDepth: number | undefined = undefined;
let commitStep: number | undefined = undefined;
let gameTypes: bigint[] = [];
let disableFast = false;
let disableCache = false;
let disableDouble = false;
Expand All @@ -82,6 +83,8 @@ const args = process.argv.slice(2).filter((x) => {
commitDepth = parseInt(match[1]);
} else if ((match = x.match(/^--step=(\d+)$/))) {
commitStep = parseInt(match[1]);
} else if ((match = x.match(/^--gameTypes=(\d+(?:,\d+)*)$/))) {
gameTypes = match[1].split(',').map((x) => BigInt(x));
} else if (x === '--dump') {
dumpAndExit = true;
} else if (x === '--debug') {
Expand Down Expand Up @@ -375,9 +378,13 @@ async function createGateway(name: string) {
(x) => x.chain2 === chain
);
if (config) {
return new Gateway(
new OPFaultRollup(createProviderPair(config), config, unfinalized)
const rollup = new OPFaultRollup(
createProviderPair(config),
config,
unfinalized
);
rollup.gameTypes = gameTypes;
return new Gateway(rollup);
}
}
{
Expand Down Expand Up @@ -534,32 +541,32 @@ function proverDetails(prover: AbstractProver) {
};
}

function toJSONValue(x: any): any {
switch (typeof x) {
case 'bigint':
return bigintToJSON(x);
case 'string':
return concealKeys(x);
case 'boolean':
case 'number':
return x;
}
if (Array.isArray(x)) {
return x.map(toJSONValue);
} else if (x && x.constructor === Object) {
return toJSON(x);
}
}

function toJSON(x: object) {
const info: Record<string, any> = {};
const json: Record<string, any> = {};
for (const [k, v] of Object.entries(x)) {
switch (typeof v) {
case 'bigint': {
info[k] = bigintToJSON(v);
break;
}
case 'string': {
info[k] = concealKeys(v);
break;
}
case 'boolean':
case 'number':
info[k] = v;
break;
case 'object':
if (Array.isArray(v)) {
info[k] = v.map(toJSON);
} else if (v && v.constructor === Object) {
info[k] = toJSON(v);
}
break;
const value = toJSONValue(v);
if (value !== undefined) {
json[k] = value;
}
}
return info;
return json;
}

// use number when it fits
Expand Down
Loading
Loading