Commit ec087c2a authored by mergify[bot]'s avatar mergify[bot] Committed by GitHub

Merge branch 'develop' into aj/reqresp-metrics

parents c5451a59 2a516855
---
'@eth-optimism/fault-detector': minor
---
Remove pre-bedrock support from fault detector.
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lerna run --concurrency 1 --stream pre-commit --since HEAD --exclude-dependents
yarn nx affected --target=pre-commit
{
"defaultBase": "develop",
"affected": {
"defaultBase": "develop"
},
"implicitDependencies": {
"nx.json": "*"
},
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
"options": {
"cacheableOperations": [
"lint",
"build",
"test",
"build:contracts",
"autogen:artifacts"
]
}
}
},
"namedInputs": {
"configsWorkspace": [
"{workspaceRoot}/package.json",
"{workspaceRoot}/tsconfig.json"
],
"configsProject": [
"{projectRoot}/foundry.toml",
"{projectRoot}/tsconfig.json",
"{projectRoot}/hardhat.config.ts",
"{projectRoot}/package.json"
],
"default": ["{projectRoot}/**/*", "configsWorkspace"],
"srcGenerated": [
"{projectRoot}/src/contract-artifacts.ts",
"{projectRoot}/src/contract-deployed-artifacts.ts"
],
"productionSrc": [
"{projectRoot}/src/**/*",
"!{projectRoot}/src/contract-artifacts.ts",
"!{projectRoot}/src/contract-deployed-artifacts.ts",
"!{projectRoot}/src/**/*.spec.ts"
],
"productionContracts": ["{projectRoot}/contracts/**/*"],
"production": ["productionSrc", "productionContracts"],
"testing": ["configsWorkspace", "default"]
},
"targetDefaults": {
"lint": {
"inputs": ["{workspaceRoot}/.markdownlint.json", "default"]
},
"test": {
"inputs": ["default", "testing", "^production"],
"dependsOn": ["^build"]
},
"build:contracts": {
"inputs": [
"configsProject",
"productionContracts",
"^productionContracts"
],
"dependsOn": ["^build"],
"outputs": ["{projectRoot}/artifacts", "{projectRoot}/forge-artifacts"]
},
"autogen:artifacts": {
"inputs": [
"configsWorkspace",
"configsProject",
"productionContracts",
"^productionContracts"
],
"dependsOn": ["^build", "build:contracts"],
"outputs": ["srcGenerated"]
},
"build": {
"inputs": [
"configsWorkspace",
"configsProject",
"production",
"^production"
],
"dependsOn": ["^build", "autogen:artifacts", "build:contracts"],
"outputs": ["{projectRoot}/dist"]
}
}
}
......@@ -24,14 +24,14 @@
},
"private": true,
"scripts": {
"clean": "yarn lerna run clean --parallel",
"build": "yarn lerna run build",
"test": "yarn lerna run test --parallel",
"test:coverage": "yarn lerna run test:coverage --parallel",
"lint": "yarn lerna run lint",
"lint:ts:check": "yarn lerna run lint:ts:check",
"lint:check": "yarn lerna run lint:check",
"lint:fix": "yarn lerna run lint:fix --parallel",
"clean": "npx nx run-many --target=clean",
"build": "npx nx run-many --target=build",
"test": "npx nx run-many --target=test",
"lint": "npx nx run-many --target=lint",
"test:coverage": "npx nx run-many --target=test:coverage",
"lint:ts:check": "npx nx run-many --target=lint:ts:check",
"lint:check": "npx nx run-many --target=lint:check",
"lint:fix": "npx nx run-many --target=lint:fix",
"lint:specs:fix": "yarn run markdownlint-cli2-fix \"./specs/**/*.md\"",
"lint:specs:check": "yarn run markdownlint-cli2 \"./specs/**/*.md\"",
"lint:specs:toc": "yarn run doctoc '--title=**Table of Contents**' ./specs",
......@@ -43,6 +43,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.18.2",
"@nrwl/nx-cloud": "latest",
"@types/chai": "^4.2.18",
"@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^8.2.2",
......@@ -65,12 +66,12 @@
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-unicorn": "^42.0.0",
"husky": "^6.0.0",
"lerna": "^4.0.0",
"lint-staged": "11.0.0",
"markdownlint": "^0.24.0",
"markdownlint-cli2": "0.4.0",
"mkdirp": "^1.0.4",
"mocha": "^8.4.0",
"nx": "15.6.0",
"nyc": "^15.1.0",
"patch-package": "^6.4.7",
"prettier": "^2.8.0",
......
......@@ -32,12 +32,12 @@ DisputeGameFactory_SetImplementation_Test:test_setImplementation_notOwner_revert
DisputeGameFactory_SetImplementation_Test:test_setImplementation_succeeds() (gas: 44243)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_notOwner_reverts() (gas: 15950)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_succeeds() (gas: 18642)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 497198)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 499064)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 489092)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 494067)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 495933)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 485961)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 502842)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 504699)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 492706)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 501717)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 503574)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 491581)
FaultDisputeGame_Test:test_defendRoot_invalidMove_reverts() (gas: 13250)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 17409)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 17834)
......@@ -49,10 +49,10 @@ FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 103231)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 407967)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10923)
FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24632)
FaultDisputeGame_Test:test_resolve_challengeContested() (gas: 221074)
FaultDisputeGame_Test:test_resolve_challengeContested() (gas: 221068)
FaultDisputeGame_Test:test_resolve_notInProgress_reverts() (gas: 9657)
FaultDisputeGame_Test:test_resolve_rootContested() (gas: 106120)
FaultDisputeGame_Test:test_resolve_rootUncontested() (gas: 23630)
FaultDisputeGame_Test:test_resolve_rootUncontested() (gas: 23624)
FaultDisputeGame_Test:test_resolve_teamDeathmatch() (gas: 391731)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8203)
FaultDisputeGame_Test:test_simpleAttack_succeeds() (gas: 107322)
......
......@@ -6,6 +6,7 @@ import { IVersioned } from "./interfaces/IVersioned.sol";
import { IFaultDisputeGame } from "./interfaces/IFaultDisputeGame.sol";
import { IInitializable } from "./interfaces/IInitializable.sol";
import { IBondManager } from "./interfaces/IBondManager.sol";
import { IBigStepper } from "./interfaces/IBigStepper.sol";
import { Clone } from "../libraries/Clone.sol";
import { LibHashing } from "./lib/LibHashing.sol";
......@@ -29,6 +30,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
/// @notice The max depth of the game.
uint256 public immutable MAX_GAME_DEPTH;
/// @notice A hypervisor that performs single instruction steps on a fault proof program trace.
IBigStepper public immutable VM;
/// @notice The duration of the game.
/// @dev TODO: Account for resolution buffer. (?)
Duration internal constant GAME_DURATION = Duration.wrap(7 days);
......@@ -55,9 +59,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
mapping(ClaimHash => bool) internal claims;
/// @param _absolutePrestate The absolute prestate of the instruction trace.
constructor(Claim _absolutePrestate, uint256 _maxGameDepth) {
constructor(
Claim _absolutePrestate,
uint256 _maxGameDepth,
IBigStepper _vm
) {
ABSOLUTE_PRESTATE = _absolutePrestate;
MAX_GAME_DEPTH = _maxGameDepth;
VM = _vm;
}
////////////////////////////////////////////////////////////////
......@@ -79,8 +88,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
uint256 _stateIndex,
uint256 _claimIndex,
bool _isAttack,
bytes calldata,
bytes calldata
bytes calldata _stateData,
bytes calldata _proof
) external {
// Steps cannot be made unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) {
......@@ -127,19 +136,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
postStateClaim = claimData[_stateIndex].claim;
}
// Assert that the given prestate commits to the instruction at `gindex - 1`.
// Assert that the given prestate commits to the instruction at `gindex - 1` and
// that the `_stateData` is the preimage for the prestate claim digest.
if (
Position.unwrap(preStatePos.rightIndex(MAX_GAME_DEPTH)) !=
Position.unwrap(postStatePos.rightIndex(MAX_GAME_DEPTH)) - 1
Position.unwrap(postStatePos.rightIndex(MAX_GAME_DEPTH)) - 1 ||
keccak256(_stateData) != Claim.unwrap(preStateClaim)
) {
revert InvalidPrestate();
}
}
// TODO: Call `MIPS.sol#step` to verify the step.
// For now, we just use a simple state transition function that increments the prestate,
// `s_p`, by 1.
if (uint256(Claim.unwrap(preStateClaim)) + 1 == uint256(Claim.unwrap(postStateClaim))) {
// Perform the VM step and check to see if it is valid.
if (VM.step(_stateData, _proof) == Claim.unwrap(postStateClaim)) {
revert ValidStep();
}
......@@ -274,7 +283,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
// Search for the left-most dangling non-bottom node
// The most recent claim is always a dangling, non-bottom node so we start with that
uint256 leftMostIndex = claimData.length - 1;
Position leftMostTraceIndex = Position.wrap(type(uint128).max);
uint256 leftMostTraceIndex = type(uint128).max;
for (uint256 i = leftMostIndex; i < type(uint64).max; ) {
// Fetch the claim at the current index.
ClaimData storage claim = claimData[i];
......@@ -295,8 +304,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
// If the claim is a dangling node, we can check if it is the left-most
// dangling node we've come across so far. If it is, we can update the
// left-most trace index.
Position traceIndex = claimPos.rightIndex(MAX_GAME_DEPTH);
if (Position.unwrap(traceIndex) < Position.unwrap(leftMostTraceIndex)) {
uint256 traceIndex = claimPos.traceIndex(MAX_GAME_DEPTH);
if (traceIndex < leftMostTraceIndex) {
leftMostTraceIndex = traceIndex;
unchecked {
leftMostIndex = i + 1;
......@@ -309,7 +318,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
if (
// slither-disable-next-line weak-prng
claimData[leftMostIndex].position.depth() % 2 == 0 &&
Position.unwrap(leftMostTraceIndex) != type(uint128).max
leftMostTraceIndex != type(uint128).max
) {
status_ = GameStatus.DEFENDER_WINS;
} else {
......
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
/// @title IBigStepper
/// @notice An interface for a contract with a state transition function that
/// will accept a pre state and return a post state.
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⠶⢅⠒⢄⢔⣶⡦⣤⡤⠄⣀⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠨⡏⠀⠀⠈⠢⣙⢯⣄⠀⢨⠯⡺⡘⢄⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣶⡆⠀⠀⠀⠀⠈⠓⠬⡒⠡⣀⢙⡜⡀⠓⠄⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡷⠿⣧⣀⡀⠀⠀⠀⠀⠀⠀⠉⠣⣞⠩⠥⠀⠼⢄⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠉⢹⣶⠒⠒⠂⠈⠉⠁⠘⡆⠀⣿⣿⠫⡄⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⢶⣤⣀⡀⠀⠀⢸⡿⠀⠀⠀⠀⠀⢀⠞⠀⠀⢡⢨⢀⡄⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡒⣿⢿⡤⠝⡣⠉⠁⠚⠛⠀⠤⠤⣄⡰⠁⠀⠀⠀⠉⠙⢸⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡤⢯⡌⡿⡇⠘⡷⠀⠁⠀⠀⢀⣰⠢⠲⠛⣈⣸⠦⠤⠶⠴⢬⣐⣊⡂⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⡪⡗⢫⠞⠀⠆⣀⠻⠤⠴⠐⠚⣉⢀⠦⠂⠋⠁⠀⠁⠀⠀⠀⠀⢋⠉⠇⠀
/// ⠀⠀⠀⠀⣀⡤⠐⠒⠘⡹⠉⢸⠇⠸⠀⠀⠀⠀⣀⣤⠴⠚⠉⠈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠼⠀⣾⠀
/// ⠀⠀⠀⡰⠀⠉⠉⠀⠁⠀⠀⠈⢇⠈⠒⠒⠘⠈⢀⢡⡂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⠀⢸⡄
/// ⠀⠀⠸⣿⣆⠤⢀⡀⠀⠀⠀⠀⢘⡌⠀⠀⣀⣀⣀⡈⣤⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⠀⢸⡇
/// ⠀⠀⢸⣀⠀⠉⠒⠐⠛⠋⠭⠭⠍⠉⠛⠒⠒⠒⠀⠒⠚⠛⠛⠛⠩⠭⠭⠭⠭⠤⠤⠤⠤⠤⠭⠭⠉⠓⡆
/// ⠀⠀⠘⠿⣷⣶⣤⣤⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇
/// ⠀⠀⠀⠀⠀⠉⠙⠛⠛⠻⠿⢿⣿⣿⣷⣶⣶⣶⣤⣤⣀⣁⣛⣃⣒⠿⠿⠿⠤⠠⠄⠤⠤⢤⣛⣓⣂⣻⡇
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠉⠙⠛⠻⠿⠿⠿⢿⣿⣿⣿⣷⣶⣶⣾⣿⣿⣿⣿⠿⠟⠁
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠈⠉⠉⠉⠉⠁⠀⠀⠀⠀⠀
interface IBigStepper {
/// @notice Performs a single instruction step from a given prestate and returns the poststate
/// hash.
/// @param _stateData The preimage of the prestate hash.
/// @param _proof A proof for the inclusion of the prestate's memory in the merkle tree.
/// @return postState_ The poststate hash after the instruction step.
function step(bytes calldata _stateData, bytes calldata _proof)
external
returns (bytes32 postState_);
}
......@@ -104,6 +104,26 @@ library LibPosition {
}
}
/// @notice Get the deepest, right most trace index relative to the `position`. This is
/// equivalent to calling `right` on a position until the maximum depth is reached and
/// then finding its index at depth.
/// @param _position The position to get the relative trace index of.
/// @param _maxDepth The maximum depth of the game.
/// @return traceIndex_ The trace index relative to the `position`.
function traceIndex(
Position _position,
uint256 _maxDepth
) internal pure returns (uint256 traceIndex_) {
uint256 msb = depth(_position);
assembly {
let remaining := sub(_maxDepth, msb)
traceIndex_ := sub(
or(shl(remaining, _position), sub(shl(remaining, 1), 1)),
shl(_maxDepth, 1)
)
}
}
/// @notice Get the move position of `_position`, which is the left child of:
/// 1. `_position + 1` if `_isAttack` is true.
/// 1. `_position` if `_isAttack` is false.
......
......@@ -11,6 +11,7 @@ import "../libraries/DisputeTypes.sol";
import "../libraries/DisputeErrors.sol";
import { LibClock } from "../dispute/lib/LibClock.sol";
import { LibPosition } from "../dispute/lib/LibPosition.sol";
import { IBigStepper } from "../dispute/interfaces/IBigStepper.sol";
contract FaultDisputeGame_Init is DisputeGameFactory_Init {
/// @dev The extra data passed to the game for initialization.
......@@ -28,7 +29,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
function init(Claim rootClaim, Claim absolutePrestate) public {
super.setUp();
// Deploy an implementation of the fault game
gameImpl = new FaultDisputeGame(absolutePrestate, 4);
gameImpl = new FaultDisputeGame(absolutePrestate, 4, new AlphabetVM(absolutePrestate));
// Register the game implementation with the factory.
factory.setImplementation(GAME_TYPE, gameImpl);
// Create a new game.
......@@ -296,10 +297,10 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
contract GamePlayer {
bool public failedToStep;
FaultDisputeGame public gameProxy;
bytes public trace;
GamePlayer internal counterParty;
Vm internal vm;
bytes internal trace;
uint256 internal maxDepth;
/// @notice Initializes the player
......@@ -307,7 +308,7 @@ contract GamePlayer {
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual {
) public {
gameProxy = _gameProxy;
counterParty = _counterParty;
vm = _vm;
......@@ -366,8 +367,9 @@ contract GamePlayer {
// If we are past the maximum depth, break the recursion and step.
if (movePos.depth() > maxDepth) {
// Perform a step.
uint256 stateIndex;
bytes memory preStateTrace;
// First, we need to find the pre/post state index depending on whether we
// are making an attack step or a defense step. If the index at depth of the
// move position is 0, the prestate is the absolute prestate and we need to
......@@ -397,17 +399,24 @@ contract GamePlayer {
break;
}
}
// Grab the trace up to the prestate's trace index.
if (isAttack) {
preStateTrace = abi.encode(statePos.traceIndex(maxDepth), traceAt(statePos));
} else {
preStateTrace = abi.encode(parentPos.traceIndex(maxDepth), traceAt(parentPos));
}
}
// Perform the step and halt recursion.
try gameProxy.step(stateIndex, _parentIndex, isAttack, hex"", hex"") {
try gameProxy.step(stateIndex, _parentIndex, isAttack, preStateTrace, hex"") {
// Do nothing, step succeeded.
} catch {
failedToStep = true;
}
} else {
// Find the trace index that our next claim must commit to.
uint256 traceIndex = movePos.rightIndex(maxDepth).indexAtDepth();
uint256 traceIndex = movePos.traceIndex(maxDepth);
// Grab the claim that we need to make from the helper.
Claim ourClaim = claimAt(traceIndex);
......@@ -439,151 +448,156 @@ contract GamePlayer {
return uint256(vm.load(address(gameProxy), bytes32(uint256(1))));
}
/// @notice Returns the player's claim that commits to a given gindex.
function claimAt(Position _position) internal view returns (Claim claim_) {
return claimAt(_position.rightIndex(maxDepth).indexAtDepth());
/// @notice Returns the state at the trace index within the player's trace.
function traceAt(Position _position) public view returns (uint256 state_) {
return traceAt(_position.traceIndex(maxDepth));
}
/// @notice Returns the state at the trace index within the player's trace.
function traceAt(uint256 _traceIndex) public view returns (uint256 state_) {
return uint256(uint8(trace[_traceIndex]));
}
/// @notice Returns the player's claim that commits to a given trace index.
function claimAt(uint256 _traceIndex) public view returns (Claim claim_) {
return Claim.wrap(bytes32(uint256(bytes32(trace[_traceIndex]) >> 248)));
return Claim.wrap(keccak256(abi.encode(_traceIndex, traceAt(_traceIndex))));
}
/// @notice Returns the player's claim that commits to a given trace index.
function claimAt(Position _position) public view returns (Claim claim_) {
return claimAt(_position.traceIndex(maxDepth));
}
}
contract OneVsOne_Arena is FaultDisputeGame_Init {
/// @dev The absolute prestate of the trace.
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32(uint256(15)));
/// @dev The honest participant.
GamePlayer internal honest;
/// @dev The dishonest participant.
GamePlayer internal dishonest;
/// @dev The defender.
GamePlayer internal defender;
/// @dev The challenger.
GamePlayer internal challenger;
function init(
GamePlayer _honest,
GamePlayer _dishonest,
Claim _rootClaim
) public {
super.init(_rootClaim, ABSOLUTE_PRESTATE);
// Deploy a new honest player.
honest = _honest;
// Deploy a new dishonest player.
dishonest = _dishonest;
function init(GamePlayer _defender, GamePlayer _challenger) public {
Claim rootClaim = Claim.wrap(keccak256(abi.encode(15, _defender.traceAt(15))));
super.init(rootClaim, ABSOLUTE_PRESTATE);
defender = _defender;
challenger = _challenger;
// Set the counterparties.
honest.init(gameProxy, dishonest, vm);
dishonest.init(gameProxy, honest, vm);
defender.init(gameProxy, challenger, vm);
challenger.init(gameProxy, defender, vm);
// Label actors for trace.
vm.label(address(honest), "HonestPlayer");
vm.label(address(dishonest), "DishonestPlayer");
vm.label(address(challenger), "Challenger");
vm.label(address(defender), "Defender");
}
}
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new FullyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(30))));
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(dishonest, honest);
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
honest.play(0);
challenger.play(0);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep());
assertFalse(defender.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new FullyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31))));
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest);
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
dishonest.play(0);
challenger.play(0);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep());
assertTrue(challenger.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new HalfDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(15))));
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new HalfDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(dishonest, honest);
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
honest.play(0);
challenger.play(0);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep());
assertFalse(defender.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot2 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new HalfDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31))));
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new HalfDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest);
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
dishonest.play(0);
challenger.play(0);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep());
assertTrue(challenger.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new EarlyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(15))));
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new EarlyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(dishonest, honest);
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
honest.play(0);
challenger.play(0);
// Resolve the game and assert that the honest player challenged the root
// claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep());
assertFalse(defender.failedToStep());
}
}
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot4 is OneVsOne_Arena {
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot3 is OneVsOne_Arena {
function setUp() public override {
GamePlayer honest = new HonestPlayer();
GamePlayer dishonest = new EarlyDivergentPlayer();
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31))));
GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new EarlyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest);
}
function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced.
dishonest.play(0);
challenger.play(0);
// Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep());
assertTrue(challenger.failedToStep());
}
}
......@@ -592,13 +606,8 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot4 is OneVsOne_Arena {
////////////////////////////////////////////////////////////////
contract HonestPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
constructor(Claim _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
bytes memory honestTrace = new bytes(16);
for (uint8 i = 0; i < honestTrace.length; i++) {
honestTrace[i] = bytes1(absolutePrestate + i + 1);
......@@ -608,13 +617,8 @@ contract HonestPlayer is GamePlayer {
}
contract FullyDivergentPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
constructor(Claim _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the honest trace by 1.
......@@ -625,13 +629,8 @@ contract FullyDivergentPlayer is GamePlayer {
}
contract HalfDivergentPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
constructor(Claim _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half.
......@@ -642,13 +641,8 @@ contract HalfDivergentPlayer is GamePlayer {
}
contract EarlyDivergentPlayer is GamePlayer {
function init(
FaultDisputeGame _gameProxy,
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
constructor(Claim _absolutePrestate) {
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half.
......@@ -657,3 +651,36 @@ contract EarlyDivergentPlayer is GamePlayer {
trace = dishonestTrace;
}
}
////////////////////////////////////////////////////////////////
// MOCK VMS //
////////////////////////////////////////////////////////////////
contract AlphabetVM is IBigStepper {
Claim internal immutable ABSOLUTE_PRESTATE;
constructor(Claim _absolutePrestate) {
ABSOLUTE_PRESTATE = _absolutePrestate;
}
/// @inheritdoc IBigStepper
function step(bytes calldata _stateData, bytes calldata)
external
view
returns (bytes32 postState_)
{
uint256 traceIndex;
uint256 claim;
if (_stateData.length == 0) {
// If the state data is empty, then the absolute prestate is the claim.
traceIndex = 0;
claim = uint256(Claim.unwrap(ABSOLUTE_PRESTATE));
} else {
// Otherwise, decode the state data.
(traceIndex, claim) = abi.decode(_stateData, (uint256, uint256));
traceIndex++;
}
// STF: n -> n + 1
postState_ = keccak256(abi.encode(traceIndex, claim + 1));
}
}
......@@ -15,13 +15,15 @@
],
"scripts": {
"bindings": "cd ../../op-bindings && make",
"build:forge": "forge build",
"build:with-metadata": "FOUNDRY_PROFILE=echidna yarn build:forge",
"build": "npx nx build:ts && npx nx typechain",
"prebuild:contracts": "yarn ts-node scripts/verify-foundry-install.ts",
"build:contracts": "yarn build:hardhat",
"build:forge": "forge build",
"build:hardhat": "hardhat compile",
"build:ts": "tsc -p tsconfig.build.json",
"build:differential": "go build -o ./scripts/differential-testing/differential-testing ./scripts/differential-testing",
"build:fuzz": "(cd test-case-generator && go build ./cmd/fuzz.go)",
"prebuild": "yarn ts-node scripts/verify-foundry-install.ts",
"build": "hardhat compile && yarn autogen:artifacts && yarn build:ts && yarn typechain",
"build:ts": "tsc -p tsconfig.build.json",
"autogen:artifacts": "ts-node scripts/generate-artifacts.ts",
"autogen:invariant-docs": "ts-node scripts/invariant-doc-gen.ts",
"deploy": "hardhat deploy",
......
......@@ -17,10 +17,7 @@ yarn build
## Running the service
Copy `.env.example` into a new file named `.env`, then set the environment variables listed there. Additional env setting are listed on `--help`. If running the fault detector against
a custom op chain, the necessary contract addresses must also be set associated with the op-chain.
- Bedrock: `OptimismPortal`
- Legacy: `StateCommitmentChain`
a custom op chain, the `OptimismPortal` contract addresses must also be set associated with the op-chain.
Once your environment variables or flags have been set, run the service via:
......@@ -40,13 +37,11 @@ yarn start
The `fault-detector` detects differences between the transaction results generated by your local Optimism node and the transaction results actually published to Ethereum.
Currently, transaction results take the form of [the root of the Optimism state trie](https://medium.com/@eiki1212/ethereum-state-trie-architecture-explained-a30237009d4e).
- Post bedrock upgrade, the state root of the block is published to the [`L2OutputOracle`](https://github.com/ethereum-optimism/optimism/blob/39b7262cc3ffd78cd314341b8512b2683c1d9af7/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol) contract on Ethereum.
The state root of the block is published to the [`L2OutputOracle`](https://github.com/ethereum-optimism/optimism/blob/39b7262cc3ffd78cd314341b8512b2683c1d9af7/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol) contract on Ethereum.
- ***Note***: The service accepts the `OptimismPortal` as a flag instead of the `L2OutputOracle` for backwards compatibility with early versions of these contracts. The `L2OutputOracle`
is inferred from the portal contract.
- For pre-bedrock chains, the state root of the block is published to the [`StateCommitmentChain`](https://github.com/ethereum-optimism/optimism/blob/39b7262cc3ffd78cd314341b8512b2683c1d9af7/packages/contracts/contracts/L1/rollup/StateCommitmentChain.sol) contract on Ethereum.
We can therefore detect differences by, for each block, checking the state root of the given block as reported by an Optimism node and the state root as published to Ethereum.
In order for the fault detector to differentiate between bedrock and legacy chains, please make sure to specify `--bedrock`.
We export a series of Prometheus metrics that you can use to trigger alerting when issues are detected.
Check the list of available metrics via `yarn start --help`:
......@@ -62,9 +57,7 @@ Options:
--l2rpcprovider Provider for interacting with L2 (env: FAULT_DETECTOR__L2_RPC_PROVIDER)
--startbatchindex Batch index to start checking from. Setting it to -1 will cause the fault detector to find the first state batch index that has not yet passed the fault proof window (env: FAULT_DETECTOR__START_BATCH_INDEX, default value: -1)
--loopintervalms Loop interval in milliseconds (env: FAULT_DETECTOR__LOOP_INTERVAL_MS)
--bedrock Whether or not the service is running against a Bedrock chain (env: FAULT_DETECTOR__BEDROCK, default value: false)
--optimismportaladdress [Custom Bedrock Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification (env: FAULT_DETECTOR__OPTIMISM_PORTAL_ADDRESS, default 0x0)
--statecommitmentchainaddress [Custom Legacy Chains] Deployed StateCommitmentChain contract address. Used to fetch necessary info for output verification. (env: FAULT_DETECTOR__STATE_COMMITMENT_CHAIN_ADDRESS, default 0x0)
--optimismportaladdress [Custom OP Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification (env: FAULT_DETECTOR__OPTIMISM_PORTAL_ADDRESS, default 0x0)
--port Port for the app server (env: FAULT_DETECTOR__PORT)
--hostname Hostname for the app server (env: FAULT_DETECTOR__HOSTNAME)
......
......@@ -51,7 +51,7 @@
},
"dependencies": {
"@eth-optimism/common-ts": "^0.8.2",
"@eth-optimism/contracts": "^0.6.0",
"@eth-optimism/contracts-bedrock": "^0.14.0",
"@eth-optimism/core-utils": "^0.12.1",
"@eth-optimism/sdk": "^3.0.0",
"@ethersproject/abstract-provider": "^5.7.0"
......
import { Contract, BigNumber } from 'ethers'
import { Contract } from 'ethers'
import { Logger } from '@eth-optimism/common-ts'
export interface OutputOracle<TSubmissionEventArgs> {
contract: Contract
filter: any
getTotalElements: () => Promise<BigNumber>
getEventIndex: (args: TSubmissionEventArgs) => BigNumber
}
/**
* Partial event interface, meant to reduce the size of the event cache to avoid
* running out of memory.
......@@ -54,12 +47,12 @@ const getCache = (
* @param contract Contract to update cache for.
* @param filter Event filter to use.
*/
export const updateOracleCache = async <TSubmissionEventArgs>(
oracle: OutputOracle<TSubmissionEventArgs>,
export const updateOracleCache = async (
oracle: Contract,
logger?: Logger
): Promise<void> => {
const cache = getCache(oracle.contract.address)
const endBlock = await oracle.contract.provider.getBlockNumber()
const cache = getCache(oracle.address)
const endBlock = await oracle.provider.getBlockNumber()
logger?.info('visiting uncached oracle events for range', {
node: 'l1',
cachedUntilBlock: cache.highestBlock,
......@@ -77,17 +70,15 @@ export const updateOracleCache = async <TSubmissionEventArgs>(
blockRangeSize: step,
})
const events = await oracle.contract.queryFilter(
oracle.filter,
const events = await oracle.queryFilter(
oracle.filters.OutputProposed(),
currentBlock,
currentBlock + step
)
// Throw the events into the cache.
for (const event of events) {
cache.eventCache[
oracle.getEventIndex(event.args as TSubmissionEventArgs).toNumber()
] = {
cache.eventCache[event.args.l2OutputIndex.toNumber()] = {
blockNumber: event.blockNumber,
transactionHash: event.transactionHash,
args: event.args,
......@@ -135,12 +126,12 @@ export const updateOracleCache = async <TSubmissionEventArgs>(
* @param index State batch index to search for.
* @returns Event corresponding to the batch.
*/
export const findEventForStateBatch = async <TSubmissionEventArgs>(
oracle: OutputOracle<TSubmissionEventArgs>,
export const findEventForStateBatch = async (
oracle: Contract,
index: number,
logger?: Logger
): Promise<PartialEvent> => {
const cache = getCache(oracle.contract.address)
const cache = getCache(oracle.address)
// Try to find the event in cache first.
if (cache.eventCache[index]) {
......@@ -166,13 +157,13 @@ export const findEventForStateBatch = async <TSubmissionEventArgs>(
* @param oracle Output oracle contract.
* @returns Starting state root batch index.
*/
export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>(
oracle: OutputOracle<TSubmissionEventArgs>,
export const findFirstUnfinalizedStateBatchIndex = async (
oracle: Contract,
fpw: number,
logger?: Logger
): Promise<number> => {
const latestBlock = await oracle.contract.provider.getBlock('latest')
const totalBatches = (await oracle.getTotalElements()).toNumber()
const latestBlock = await oracle.provider.getBlock('latest')
const totalBatches = (await oracle.nextOutputIndex()).toNumber()
// Perform a binary search to find the next batch that will pass the challenge period.
let lo = 0
......@@ -180,7 +171,7 @@ export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>(
while (lo !== hi) {
const mid = Math.floor((lo + hi) / 2)
const event = await findEventForStateBatch(oracle, mid, logger)
const block = await oracle.contract.provider.getBlock(event.blockNumber)
const block = await oracle.provider.getBlock(event.blockNumber)
if (block.timestamp + fpw < latestBlock.timestamp) {
lo = mid + 1
......
......@@ -16,7 +16,7 @@ import {
OEL1ContractsLike,
} from '@eth-optimism/sdk'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers, Transaction } from 'ethers'
import { Contract, ethers } from 'ethers'
import dateformat from 'dateformat'
import { version } from '../package.json'
......@@ -24,7 +24,6 @@ import {
findFirstUnfinalizedStateBatchIndex,
findEventForStateBatch,
PartialEvent,
OutputOracle,
updateOracleCache,
} from './helpers'
......@@ -32,9 +31,7 @@ type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
startBatchIndex: number
bedrock: boolean
optimismPortalAddress?: string
stateCommitmentChainAddress?: string
}
type Metrics = {
......@@ -44,8 +41,8 @@ type Metrics = {
}
type State = {
fpw: number
oo: OutputOracle<any>
faultProofWindow: number
outputOracle: Contract
messenger: CrossChainMessenger
currentBatchIndex: number
diverged: boolean
......@@ -73,25 +70,13 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
startBatchIndex: {
validator: validators.num,
default: -1,
desc: 'Batch index to start checking from. For bedrock chains, this is the L2 height to start from',
public: true,
},
bedrock: {
validator: validators.bool,
default: true,
desc: 'Whether or not the service is running against a Bedrock chain',
desc: 'The L2 height to start from',
public: true,
},
optimismPortalAddress: {
validator: validators.str,
default: ethers.constants.AddressZero,
desc: '[Custom Bedrock Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification ',
public: true,
},
stateCommitmentChainAddress: {
validator: validators.str,
default: ethers.constants.AddressZero,
desc: '[Custom Legacy Chains] Deployed StateCommitmentChain contract address. Used to fetch necessary info for output verification.',
desc: '[Custom OP Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification ',
public: true,
},
},
......@@ -119,10 +104,9 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
* will fallback to the pre-defined set of addresses from options, otherwise aborting if unset.
*
* Required Contracts
* - Bedrock: OptimismPortal (used to also fetch L2OutputOracle address variable). This is the preferred address
* - OptimismPortal (used to also fetch L2OutputOracle address variable). This is the preferred address
* since in early versions of bedrock, OptimismPortal holds the FINALIZATION_WINDOW variable instead of L2OutputOracle.
* The retrieved L2OutputOracle address from OptimismPortal is used to query for output roots.
* - Legacy: StateCommitmentChain to query for output roots.
*
* @param l2ChainId op chain id
* @returns OEL1ContractsLike set of L1 contracts with only the required addresses set
......@@ -130,19 +114,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
async getOEL1Contracts(l2ChainId: number): Promise<OEL1ContractsLike> {
// CrossChainMessenger requires all address to be defined. Default to `AddressZero` to ignore unused contracts
let contracts: OEL1ContractsLike = {
OptimismPortal: ethers.constants.AddressZero,
L2OutputOracle: ethers.constants.AddressZero,
// Unused contracts
AddressManager: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
L1CrossDomainMessenger: ethers.constants.AddressZero,
L1StandardBridge: ethers.constants.AddressZero,
StateCommitmentChain: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
OptimismPortal: ethers.constants.AddressZero,
L2OutputOracle: ethers.constants.AddressZero,
}
const chainType = this.options.bedrock ? 'bedrock' : 'legacy'
this.logger.info(`Setting contracts for OP chain type: ${chainType}`)
const knownChainId = L2ChainID[l2ChainId] !== undefined
if (knownChainId) {
this.logger.info(`Recognized L2 chain id ${L2ChainID[l2ChainId]}`)
......@@ -152,42 +134,29 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
}
this.logger.info('checking contract address options...')
if (this.options.bedrock) {
const address = this.options.optimismPortalAddress
if (!knownChainId && address === ethers.constants.AddressZero) {
this.logger.error('OptimismPortal contract unspecified')
throw new Error(
'--optimismportalcontractaddress needs to set for custom bedrock op chains'
)
}
if (address !== ethers.constants.AddressZero) {
this.logger.info('set OptimismPortal contract override')
contracts.OptimismPortal = address
this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
const opts = { address, signerOrProvider: this.options.l1RpcProvider }
const portalContract = getOEContract('OptimismPortal', l2ChainId, opts)
contracts.L2OutputOracle = await portalContract.L2_ORACLE()
}
const portalAddress = this.options.optimismPortalAddress
if (!knownChainId && portalAddress === ethers.constants.AddressZero) {
this.logger.error('OptimismPortal contract unspecified')
throw new Error(
'--optimismportalcontractaddress needs to set for custom op chains'
)
}
// ... for a known chain ids without an override, the L2OutputOracle will already
// be set via the hardcoded default
} else {
const address = this.options.stateCommitmentChainAddress
if (!knownChainId && address === ethers.constants.AddressZero) {
this.logger.error('StateCommitmentChain contract unspecified')
throw new Error(
'--statecommitmentchainaddress needs to set for custom legacy op chains'
)
}
if (portalAddress !== ethers.constants.AddressZero) {
this.logger.info('set OptimismPortal contract override')
contracts.OptimismPortal = portalAddress
if (address !== ethers.constants.AddressZero) {
this.logger.info('set StateCommitmentChain contract override')
contracts.StateCommitmentChain = address
this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
const opts = {
portalAddress,
signerOrProvider: this.options.l1RpcProvider,
}
const portalContract = getOEContract('OptimismPortal', l2ChainId, opts)
contracts.L2OutputOracle = await portalContract.L2_ORACLE()
}
// ... for a known chain ids without an override, the L2OutputOracle will already
// be set via the hardcoded default
return contracts
}
......@@ -211,7 +180,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId,
l2ChainId,
bedrock: this.options.bedrock,
bedrock: true,
contracts: { l1: await this.getOEL1Contracts(l2ChainId) },
})
......@@ -219,46 +188,33 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
this.state.diverged = false
// We use this a lot, a bit cleaner to pull out to the top level of the state object.
this.state.fpw = await this.state.messenger.getChallengePeriodSeconds()
this.logger.info(`fault proof window is ${this.state.fpw} seconds`)
if (this.options.bedrock) {
const oo = this.state.messenger.contracts.l1.L2OutputOracle
this.state.oo = {
contract: oo,
filter: oo.filters.OutputProposed(),
getTotalElements: async () => oo.nextOutputIndex(),
getEventIndex: (args) => args.l2OutputIndex,
}
} else {
const oo = this.state.messenger.contracts.l1.StateCommitmentChain
this.state.oo = {
contract: oo,
filter: oo.filters.StateBatchAppended(),
getTotalElements: async () => oo.getTotalBatches(),
getEventIndex: (args) => args._batchIndex,
}
}
this.state.faultProofWindow =
await this.state.messenger.getChallengePeriodSeconds()
this.logger.info(
`fault proof window is ${this.state.faultProofWindow} seconds`
)
this.state.outputOracle = this.state.messenger.contracts.l1.L2OutputOracle
// Populate the event cache.
this.logger.info('warming event cache, this might take a while...')
await updateOracleCache(this.state.oo, this.logger)
await updateOracleCache(this.state.outputOracle, this.logger)
// Figure out where to start syncing from.
if (this.options.startBatchIndex === -1) {
this.logger.info('finding appropriate starting unfinalized batch')
const firstUnfinalized = await findFirstUnfinalizedStateBatchIndex(
this.state.oo,
this.state.fpw,
this.state.outputOracle,
this.state.faultProofWindow,
this.logger
)
// We may not have an unfinalized batches in the case where no batches have been submitted
// for the entire duration of the FPW. We generally do not expect this to happen on mainnet,
// but it happens often on testnets because the FPW is very short.
// for the entire duration of the FAULTPROOFWINDOW. We generally do not expect this to happen on mainnet,
// but it happens often on testnets because the FAULTPROOFWINDOW is very short.
if (firstUnfinalized === undefined) {
this.logger.info('no unfinalized batches found. skipping all batches.')
const totalBatches = await this.state.oo.getTotalElements()
const totalBatches = await this.state.outputOracle.nextOutputIndex()
this.state.currentBatchIndex = totalBatches.toNumber() - 1
} else {
this.state.currentBatchIndex = firstUnfinalized
......@@ -288,17 +244,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let latestBatchIndex: number
try {
const totalBatches = await this.state.oo.getTotalElements()
const totalBatches = await this.state.outputOracle.nextOutputIndex()
latestBatchIndex = totalBatches.toNumber() - 1
} catch (err) {
this.logger.error('failed to query total # of batches', {
error: err,
node: 'l1',
section: 'getTotalElements',
section: 'nextOutputIndex',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'getTotalElements',
section: 'nextOutputIndex',
})
await sleep(15000)
return
......@@ -322,7 +278,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let event: PartialEvent
try {
event = await findEventForStateBatch(
this.state.oo,
this.state.outputOracle,
this.state.currentBatchIndex,
this.logger
)
......@@ -358,179 +314,86 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
return
}
if (this.options.bedrock) {
const outputBlockNumber = event.args.l2BlockNumber.toNumber()
if (latestBlock < outputBlockNumber) {
this.logger.info('L2 node is behind, waiting for sync...', {
l2BlockHeight: latestBlock,
outputBlock: outputBlockNumber,
})
return
}
const outputBlockNumber = event.args.l2BlockNumber.toNumber()
if (latestBlock < outputBlockNumber) {
this.logger.info('L2 node is behind, waiting for sync...', {
l2BlockHeight: latestBlock,
outputBlock: outputBlockNumber,
})
return
}
let outputBlock: any
try {
outputBlock = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [
toRpcHexString(outputBlockNumber),
false,
])
} catch (err) {
this.logger.error('failed to fetch output block', {
error: err,
node: 'l2',
section: 'getBlock',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlock',
})
await sleep(15000)
return
}
let outputBlock: any
try {
outputBlock = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [toRpcHexString(outputBlockNumber), false])
} catch (err) {
this.logger.error('failed to fetch output block', {
error: err,
node: 'l2',
section: 'getBlock',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlock',
})
await sleep(15000)
return
}
let messagePasserProofResponse: any
try {
messagePasserProofResponse = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getProof', [
this.state.messenger.contracts.l2.BedrockMessagePasser.address,
[],
toRpcHexString(outputBlockNumber),
])
} catch (err) {
this.logger.error('failed to fetch message passer proof', {
error: err,
node: 'l2',
section: 'getProof',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getProof',
})
await sleep(15000)
return
}
let messagePasserProofResponse: any
try {
messagePasserProofResponse = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getProof', [
this.state.messenger.contracts.l2.BedrockMessagePasser.address,
[],
toRpcHexString(outputBlockNumber),
])
} catch (err) {
this.logger.error('failed to fetch message passer proof', {
error: err,
node: 'l2',
section: 'getProof',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getProof',
})
await sleep(15000)
return
}
const outputRoot = ethers.utils.solidityKeccak256(
['uint256', 'bytes32', 'bytes32', 'bytes32'],
[
0,
outputBlock.stateRoot,
messagePasserProofResponse.storageHash,
outputBlock.hash,
]
)
const outputRoot = ethers.utils.solidityKeccak256(
['uint256', 'bytes32', 'bytes32', 'bytes32'],
[
0,
outputBlock.stateRoot,
messagePasserProofResponse.storageHash,
outputBlock.hash,
]
)
if (outputRoot !== event.args.outputRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: outputBlock.number,
expectedStateRoot: event.args.outputRoot,
actualStateRoot: outputRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.fpw) *
1000
),
'mmmm dS, yyyy, h:MM:ss TT'
if (outputRoot !== event.args.outputRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: outputBlock.number,
expectedStateRoot: event.args.outputRoot,
actualStateRoot: outputRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.faultProofWindow) *
1000
),
})
return
}
} else {
let batchTransaction: Transaction
try {
batchTransaction = await this.options.l1RpcProvider.getTransaction(
event.transactionHash
)
} catch (err) {
this.logger.error('failed to acquire batch transaction', {
error: err,
node: 'l1',
section: 'getTransaction',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'getTransaction',
})
await sleep(15000)
return
}
const [stateRoots] = this.state.oo.contract.interface.decodeFunctionData(
'appendStateBatch',
batchTransaction.data
)
const batchStart = event.args._prevTotalElements.toNumber() + 1
const batchSize = event.args._batchSize.toNumber()
const batchEnd = batchStart + batchSize
if (latestBlock < batchEnd) {
this.logger.info('L2 node is behind. waiting for sync...', {
batchBlockStart: batchStart,
batchBlockEnd: batchEnd,
l2BlockHeight: latestBlock,
})
return
}
// `getBlockRange` has a limit of 1000 blocks, so we have to break this request out into
// multiple requests of maximum 1000 blocks in the case that batchSize > 1000.
let blocks: any[] = []
for (let i = 0; i < batchSize; i += 1000) {
let newBlocks: any[]
try {
newBlocks = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockRange', [
toRpcHexString(batchStart + i),
toRpcHexString(batchStart + i + Math.min(batchSize - i, 1000) - 1),
false,
])
} catch (err) {
this.logger.error('failed to query for blocks in batch', {
error: err,
node: 'l2',
section: 'getBlockRange',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlockRange',
})
await sleep(15000)
return
}
blocks = blocks.concat(newBlocks)
}
for (const [i, stateRoot] of stateRoots.entries()) {
if (blocks[i].stateRoot !== stateRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: blocks[i].number,
expectedStateRoot: blocks[i].stateRoot,
actualStateRoot: stateRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(blocks[i].timestamp).toNumber() +
this.state.fpw) *
1000
),
'mmmm dS, yyyy, h:MM:ss TT'
),
})
return
}
}
'mmmm dS, yyyy, h:MM:ss TT'
),
})
return
}
const elapsedMs = Date.now() - startMs
......
import hre from 'hardhat'
import { Contract } from 'ethers'
import { Contract, utils } from 'ethers'
import { toRpcHexString } from '@eth-optimism/core-utils'
import {
getContractFactory,
getContractInterface,
} from '@eth-optimism/contracts'
import { getContractFactory } from '@eth-optimism/contracts-bedrock'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { smock, FakeContract } from '@defi-wonderland/smock'
import { expect } from './setup'
import {
findEventForStateBatch,
findFirstUnfinalizedStateBatchIndex,
OutputOracle,
} from '../src'
describe('helpers', () => {
// Can be any non-zero value, 1000 is fine.
const challengeWindowSeconds = 1000
const deployConfig = {
l2OutputOracleSubmissionInterval: 6,
l2BlockTime: 2,
l2OutputOracleStartingBlockNumber: 0,
l2OutputOracleStartingTimestamp: 0,
l2OutputOracleProposer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
l2OutputOracleChallenger: '0x6925B8704Ff96DEe942623d6FB5e946EF5884b63',
// Can be any non-zero value, 1000 is fine.
finalizationPeriodSeconds: 1000,
}
let signer: SignerWithAddress
before(async () => {
;[signer] = await hre.ethers.getSigners()
})
let FakeBondManager: FakeContract<Contract>
let FakeCanonicalTransactionChain: FakeContract<Contract>
let AddressManager: Contract
let ChainStorageContainer: Contract
let StateCommitmentChain: Contract
let oracle: OutputOracle<any>
let L2OutputOracle: Contract
beforeEach(async () => {
// Set up fakes
FakeBondManager = await smock.fake(getContractInterface('BondManager'))
FakeCanonicalTransactionChain = await smock.fake(
getContractInterface('CanonicalTransactionChain')
)
// Set up contracts
AddressManager = await getContractFactory(
'Lib_AddressManager',
signer
).deploy()
ChainStorageContainer = await getContractFactory(
'ChainStorageContainer',
signer
).deploy(AddressManager.address, 'StateCommitmentChain')
StateCommitmentChain = await getContractFactory(
'StateCommitmentChain',
signer
).deploy(AddressManager.address, challengeWindowSeconds, 10000000)
// Set addresses in manager
await AddressManager.setAddress(
'ChainStorageContainer-SCC-batches',
ChainStorageContainer.address
)
await AddressManager.setAddress(
'StateCommitmentChain',
StateCommitmentChain.address
)
await AddressManager.setAddress(
'CanonicalTransactionChain',
FakeCanonicalTransactionChain.address
L2OutputOracle = await getContractFactory('L2OutputOracle', signer).deploy(
deployConfig.l2OutputOracleSubmissionInterval,
deployConfig.l2BlockTime,
deployConfig.l2OutputOracleStartingBlockNumber,
deployConfig.l2OutputOracleStartingTimestamp,
deployConfig.l2OutputOracleProposer,
deployConfig.l2OutputOracleChallenger,
deployConfig.finalizationPeriodSeconds
)
await AddressManager.setAddress('BondManager', FakeBondManager.address)
// Set up mock returns
FakeCanonicalTransactionChain.getTotalElements.returns(1000000000) // just needs to be large
FakeBondManager.isCollateralized.returns(true)
oracle = {
contract: StateCommitmentChain,
filter: StateCommitmentChain.filters.StateBatchAppended(),
getTotalElements: async () => StateCommitmentChain.getTotalBatches(),
getEventIndex: (args: any) => args._batchIndex,
}
})
describe('findEventForStateBatch', () => {
describe('when the event exists once', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = {
_outputRoot: utils.formatBytes32String('testhash'),
_l2BlockNumber:
deployConfig.l2OutputOracleStartingBlockNumber +
deployConfig.l2OutputOracleSubmissionInterval,
_l1BlockHash: latestBlock.hash,
_l1BlockNumber: latestBlock.number,
}
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber,
params._l1BlockHash,
params._l1BlockNumber
)
})
it('should return the event', async () => {
const event = await findEventForStateBatch(oracle, 0)
const event = await findEventForStateBatch(L2OutputOracle, 0)
expect(event.args._batchIndex).to.equal(0)
expect(event.args.l2OutputIndex).to.equal(0)
})
})
describe('when the event does not exist', () => {
it('should throw an error', async () => {
await expect(
findEventForStateBatch(oracle, 0)
findEventForStateBatch(L2OutputOracle, 0)
).to.eventually.be.rejectedWith('unable to find event for batch')
})
})
......@@ -106,30 +79,46 @@ describe('helpers', () => {
describe('findFirstUnfinalizedIndex', () => {
describe('when the chain is more then FPW seconds old', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = {
_outputRoot: utils.formatBytes32String('testhash'),
_l2BlockNumber:
deployConfig.l2OutputOracleStartingBlockNumber +
deployConfig.l2OutputOracleSubmissionInterval,
_l1BlockHash: latestBlock.hash,
_l1BlockNumber: latestBlock.number,
}
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber,
params._l1BlockHash,
params._l1BlockNumber
)
// Simulate FPW passing
await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(challengeWindowSeconds * 2),
toRpcHexString(deployConfig.finalizationPeriodSeconds * 2),
])
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
1
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
2
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
)
})
it('should find the first batch older than the FPW', async () => {
const first = await findFirstUnfinalizedStateBatchIndex(
oracle,
challengeWindowSeconds
L2OutputOracle,
deployConfig.finalizationPeriodSeconds
)
expect(first).to.equal(1)
......@@ -138,24 +127,40 @@ describe('helpers', () => {
describe('when the chain is less than FPW seconds old', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = {
_outputRoot: utils.formatBytes32String('testhash'),
_l2BlockNumber:
deployConfig.l2OutputOracleStartingBlockNumber +
deployConfig.l2OutputOracleSubmissionInterval,
_l1BlockHash: latestBlock.hash,
_l1BlockNumber: latestBlock.number,
}
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber,
params._l1BlockHash,
params._l1BlockNumber
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
1
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
2
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
)
})
it('should return zero', async () => {
const first = await findFirstUnfinalizedStateBatchIndex(
oracle,
challengeWindowSeconds
L2OutputOracle,
deployConfig.finalizationPeriodSeconds
)
expect(first).to.equal(0)
......@@ -164,22 +169,38 @@ describe('helpers', () => {
describe('when no batches submitted for the entire FPW', () => {
beforeEach(async () => {
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
0
const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = {
_outputRoot: utils.formatBytes32String('testhash'),
_l2BlockNumber:
deployConfig.l2OutputOracleStartingBlockNumber +
deployConfig.l2OutputOracleSubmissionInterval,
_l1BlockHash: latestBlock.hash,
_l1BlockNumber: latestBlock.number,
}
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber,
params._l1BlockHash,
params._l1BlockNumber
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
1
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
)
await StateCommitmentChain.appendStateBatch(
[hre.ethers.constants.HashZero],
2
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
)
// Simulate FPW passing and no new batches
await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(challengeWindowSeconds * 2),
toRpcHexString(deployConfig.finalizationPeriodSeconds * 2),
])
// Mine a block to force timestamp to update
......@@ -188,8 +209,8 @@ describe('helpers', () => {
it('should return undefined', async () => {
const first = await findFirstUnfinalizedStateBatchIndex(
oracle,
challengeWindowSeconds
L2OutputOracle,
deployConfig.finalizationPeriodSeconds
)
expect(first).to.equal(undefined)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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