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 #!/bin/sh
. "$(dirname "$0")/_/husky.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 @@ ...@@ -24,14 +24,14 @@
}, },
"private": true, "private": true,
"scripts": { "scripts": {
"clean": "yarn lerna run clean --parallel", "clean": "npx nx run-many --target=clean",
"build": "yarn lerna run build", "build": "npx nx run-many --target=build",
"test": "yarn lerna run test --parallel", "test": "npx nx run-many --target=test",
"test:coverage": "yarn lerna run test:coverage --parallel", "lint": "npx nx run-many --target=lint",
"lint": "yarn lerna run lint", "test:coverage": "npx nx run-many --target=test:coverage",
"lint:ts:check": "yarn lerna run lint:ts:check", "lint:ts:check": "npx nx run-many --target=lint:ts:check",
"lint:check": "yarn lerna run lint:check", "lint:check": "npx nx run-many --target=lint:check",
"lint:fix": "yarn lerna run lint:fix --parallel", "lint:fix": "npx nx run-many --target=lint:fix",
"lint:specs:fix": "yarn run markdownlint-cli2-fix \"./specs/**/*.md\"", "lint:specs:fix": "yarn run markdownlint-cli2-fix \"./specs/**/*.md\"",
"lint:specs:check": "yarn run markdownlint-cli2 \"./specs/**/*.md\"", "lint:specs:check": "yarn run markdownlint-cli2 \"./specs/**/*.md\"",
"lint:specs:toc": "yarn run doctoc '--title=**Table of Contents**' ./specs", "lint:specs:toc": "yarn run doctoc '--title=**Table of Contents**' ./specs",
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.18.2", "@babel/eslint-parser": "^7.18.2",
"@nrwl/nx-cloud": "latest",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/chai-as-promised": "^7.1.4", "@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^8.2.2", "@types/mocha": "^8.2.2",
...@@ -65,12 +66,12 @@ ...@@ -65,12 +66,12 @@
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-unicorn": "^42.0.0", "eslint-plugin-unicorn": "^42.0.0",
"husky": "^6.0.0", "husky": "^6.0.0",
"lerna": "^4.0.0",
"lint-staged": "11.0.0", "lint-staged": "11.0.0",
"markdownlint": "^0.24.0", "markdownlint": "^0.24.0",
"markdownlint-cli2": "0.4.0", "markdownlint-cli2": "0.4.0",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"mocha": "^8.4.0", "mocha": "^8.4.0",
"nx": "15.6.0",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"patch-package": "^6.4.7", "patch-package": "^6.4.7",
"prettier": "^2.8.0", "prettier": "^2.8.0",
......
...@@ -32,12 +32,12 @@ DisputeGameFactory_SetImplementation_Test:test_setImplementation_notOwner_revert ...@@ -32,12 +32,12 @@ DisputeGameFactory_SetImplementation_Test:test_setImplementation_notOwner_revert
DisputeGameFactory_SetImplementation_Test:test_setImplementation_succeeds() (gas: 44243) DisputeGameFactory_SetImplementation_Test:test_setImplementation_succeeds() (gas: 44243)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_notOwner_reverts() (gas: 15950) DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_notOwner_reverts() (gas: 15950)
DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_succeeds() (gas: 18642) DisputeGameFactory_TransferOwnership_Test:test_transferOwnership_succeeds() (gas: 18642)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 497198) FaultDisputeGame_ResolvesCorrectly_CorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 502842)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot4:test_resolvesCorrectly_succeeds() (gas: 499064) FaultDisputeGame_ResolvesCorrectly_CorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 504699)
FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 489092) FaultDisputeGame_ResolvesCorrectly_CorrectRoot:test_resolvesCorrectly_succeeds() (gas: 492706)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 494067) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2:test_resolvesCorrectly_succeeds() (gas: 501717)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 495933) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3:test_resolvesCorrectly_succeeds() (gas: 503574)
FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 485961) FaultDisputeGame_ResolvesCorrectly_IncorrectRoot:test_resolvesCorrectly_succeeds() (gas: 491581)
FaultDisputeGame_Test:test_defendRoot_invalidMove_reverts() (gas: 13250) FaultDisputeGame_Test:test_defendRoot_invalidMove_reverts() (gas: 13250)
FaultDisputeGame_Test:test_extraData_succeeds() (gas: 17409) FaultDisputeGame_Test:test_extraData_succeeds() (gas: 17409)
FaultDisputeGame_Test:test_gameData_succeeds() (gas: 17834) FaultDisputeGame_Test:test_gameData_succeeds() (gas: 17834)
...@@ -49,10 +49,10 @@ FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 103231) ...@@ -49,10 +49,10 @@ FaultDisputeGame_Test:test_move_duplicateClaim_reverts() (gas: 103231)
FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 407967) FaultDisputeGame_Test:test_move_gameDepthExceeded_reverts() (gas: 407967)
FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10923) FaultDisputeGame_Test:test_move_gameNotInProgress_reverts() (gas: 10923)
FaultDisputeGame_Test:test_move_nonExistentParent_reverts() (gas: 24632) 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_notInProgress_reverts() (gas: 9657)
FaultDisputeGame_Test:test_resolve_rootContested() (gas: 106120) 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_resolve_teamDeathmatch() (gas: 391731)
FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8203) FaultDisputeGame_Test:test_rootClaim_succeeds() (gas: 8203)
FaultDisputeGame_Test:test_simpleAttack_succeeds() (gas: 107322) FaultDisputeGame_Test:test_simpleAttack_succeeds() (gas: 107322)
......
...@@ -6,6 +6,7 @@ import { IVersioned } from "./interfaces/IVersioned.sol"; ...@@ -6,6 +6,7 @@ import { IVersioned } from "./interfaces/IVersioned.sol";
import { IFaultDisputeGame } from "./interfaces/IFaultDisputeGame.sol"; import { IFaultDisputeGame } from "./interfaces/IFaultDisputeGame.sol";
import { IInitializable } from "./interfaces/IInitializable.sol"; import { IInitializable } from "./interfaces/IInitializable.sol";
import { IBondManager } from "./interfaces/IBondManager.sol"; import { IBondManager } from "./interfaces/IBondManager.sol";
import { IBigStepper } from "./interfaces/IBigStepper.sol";
import { Clone } from "../libraries/Clone.sol"; import { Clone } from "../libraries/Clone.sol";
import { LibHashing } from "./lib/LibHashing.sol"; import { LibHashing } from "./lib/LibHashing.sol";
...@@ -29,6 +30,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -29,6 +30,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
/// @notice The max depth of the game. /// @notice The max depth of the game.
uint256 public immutable MAX_GAME_DEPTH; 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. /// @notice The duration of the game.
/// @dev TODO: Account for resolution buffer. (?) /// @dev TODO: Account for resolution buffer. (?)
Duration internal constant GAME_DURATION = Duration.wrap(7 days); Duration internal constant GAME_DURATION = Duration.wrap(7 days);
...@@ -55,9 +59,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -55,9 +59,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
mapping(ClaimHash => bool) internal claims; mapping(ClaimHash => bool) internal claims;
/// @param _absolutePrestate The absolute prestate of the instruction trace. /// @param _absolutePrestate The absolute prestate of the instruction trace.
constructor(Claim _absolutePrestate, uint256 _maxGameDepth) { constructor(
Claim _absolutePrestate,
uint256 _maxGameDepth,
IBigStepper _vm
) {
ABSOLUTE_PRESTATE = _absolutePrestate; ABSOLUTE_PRESTATE = _absolutePrestate;
MAX_GAME_DEPTH = _maxGameDepth; MAX_GAME_DEPTH = _maxGameDepth;
VM = _vm;
} }
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -79,8 +88,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -79,8 +88,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
uint256 _stateIndex, uint256 _stateIndex,
uint256 _claimIndex, uint256 _claimIndex,
bool _isAttack, bool _isAttack,
bytes calldata, bytes calldata _stateData,
bytes calldata bytes calldata _proof
) external { ) external {
// Steps cannot be made unless the game is currently in progress. // Steps cannot be made unless the game is currently in progress.
if (status != GameStatus.IN_PROGRESS) { if (status != GameStatus.IN_PROGRESS) {
...@@ -127,19 +136,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -127,19 +136,19 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
postStateClaim = claimData[_stateIndex].claim; 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 ( if (
Position.unwrap(preStatePos.rightIndex(MAX_GAME_DEPTH)) != 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(); revert InvalidPrestate();
} }
} }
// TODO: Call `MIPS.sol#step` to verify the step. // Perform the VM step and check to see if it is valid.
// For now, we just use a simple state transition function that increments the prestate, if (VM.step(_stateData, _proof) == Claim.unwrap(postStateClaim)) {
// `s_p`, by 1.
if (uint256(Claim.unwrap(preStateClaim)) + 1 == uint256(Claim.unwrap(postStateClaim))) {
revert ValidStep(); revert ValidStep();
} }
...@@ -274,7 +283,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -274,7 +283,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
// Search for the left-most dangling non-bottom node // 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 // The most recent claim is always a dangling, non-bottom node so we start with that
uint256 leftMostIndex = claimData.length - 1; 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; ) { for (uint256 i = leftMostIndex; i < type(uint64).max; ) {
// Fetch the claim at the current index. // Fetch the claim at the current index.
ClaimData storage claim = claimData[i]; ClaimData storage claim = claimData[i];
...@@ -295,8 +304,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -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 // 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 // dangling node we've come across so far. If it is, we can update the
// left-most trace index. // left-most trace index.
Position traceIndex = claimPos.rightIndex(MAX_GAME_DEPTH); uint256 traceIndex = claimPos.traceIndex(MAX_GAME_DEPTH);
if (Position.unwrap(traceIndex) < Position.unwrap(leftMostTraceIndex)) { if (traceIndex < leftMostTraceIndex) {
leftMostTraceIndex = traceIndex; leftMostTraceIndex = traceIndex;
unchecked { unchecked {
leftMostIndex = i + 1; leftMostIndex = i + 1;
...@@ -309,7 +318,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone { ...@@ -309,7 +318,7 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone {
if ( if (
// slither-disable-next-line weak-prng // slither-disable-next-line weak-prng
claimData[leftMostIndex].position.depth() % 2 == 0 && claimData[leftMostIndex].position.depth() % 2 == 0 &&
Position.unwrap(leftMostTraceIndex) != type(uint128).max leftMostTraceIndex != type(uint128).max
) { ) {
status_ = GameStatus.DEFENDER_WINS; status_ = GameStatus.DEFENDER_WINS;
} else { } 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 { ...@@ -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: /// @notice Get the move position of `_position`, which is the left child of:
/// 1. `_position + 1` if `_isAttack` is true. /// 1. `_position + 1` if `_isAttack` is true.
/// 1. `_position` if `_isAttack` is false. /// 1. `_position` if `_isAttack` is false.
......
...@@ -11,6 +11,7 @@ import "../libraries/DisputeTypes.sol"; ...@@ -11,6 +11,7 @@ import "../libraries/DisputeTypes.sol";
import "../libraries/DisputeErrors.sol"; import "../libraries/DisputeErrors.sol";
import { LibClock } from "../dispute/lib/LibClock.sol"; import { LibClock } from "../dispute/lib/LibClock.sol";
import { LibPosition } from "../dispute/lib/LibPosition.sol"; import { LibPosition } from "../dispute/lib/LibPosition.sol";
import { IBigStepper } from "../dispute/interfaces/IBigStepper.sol";
contract FaultDisputeGame_Init is DisputeGameFactory_Init { contract FaultDisputeGame_Init is DisputeGameFactory_Init {
/// @dev The extra data passed to the game for initialization. /// @dev The extra data passed to the game for initialization.
...@@ -28,7 +29,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { ...@@ -28,7 +29,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
function init(Claim rootClaim, Claim absolutePrestate) public { function init(Claim rootClaim, Claim absolutePrestate) public {
super.setUp(); super.setUp();
// Deploy an implementation of the fault game // 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. // Register the game implementation with the factory.
factory.setImplementation(GAME_TYPE, gameImpl); factory.setImplementation(GAME_TYPE, gameImpl);
// Create a new game. // Create a new game.
...@@ -296,10 +297,10 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init { ...@@ -296,10 +297,10 @@ contract FaultDisputeGame_Test is FaultDisputeGame_Init {
contract GamePlayer { contract GamePlayer {
bool public failedToStep; bool public failedToStep;
FaultDisputeGame public gameProxy; FaultDisputeGame public gameProxy;
bytes public trace;
GamePlayer internal counterParty; GamePlayer internal counterParty;
Vm internal vm; Vm internal vm;
bytes internal trace;
uint256 internal maxDepth; uint256 internal maxDepth;
/// @notice Initializes the player /// @notice Initializes the player
...@@ -307,7 +308,7 @@ contract GamePlayer { ...@@ -307,7 +308,7 @@ contract GamePlayer {
FaultDisputeGame _gameProxy, FaultDisputeGame _gameProxy,
GamePlayer _counterParty, GamePlayer _counterParty,
Vm _vm Vm _vm
) public virtual { ) public {
gameProxy = _gameProxy; gameProxy = _gameProxy;
counterParty = _counterParty; counterParty = _counterParty;
vm = _vm; vm = _vm;
...@@ -366,8 +367,9 @@ contract GamePlayer { ...@@ -366,8 +367,9 @@ contract GamePlayer {
// If we are past the maximum depth, break the recursion and step. // If we are past the maximum depth, break the recursion and step.
if (movePos.depth() > maxDepth) { if (movePos.depth() > maxDepth) {
// Perform a step.
uint256 stateIndex; uint256 stateIndex;
bytes memory preStateTrace;
// First, we need to find the pre/post state index depending on whether we // 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 // 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 // move position is 0, the prestate is the absolute prestate and we need to
...@@ -397,17 +399,24 @@ contract GamePlayer { ...@@ -397,17 +399,24 @@ contract GamePlayer {
break; 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. // 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. // Do nothing, step succeeded.
} catch { } catch {
failedToStep = true; failedToStep = true;
} }
} else { } else {
// Find the trace index that our next claim must commit to. // 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. // Grab the claim that we need to make from the helper.
Claim ourClaim = claimAt(traceIndex); Claim ourClaim = claimAt(traceIndex);
...@@ -439,151 +448,156 @@ contract GamePlayer { ...@@ -439,151 +448,156 @@ contract GamePlayer {
return uint256(vm.load(address(gameProxy), bytes32(uint256(1)))); return uint256(vm.load(address(gameProxy), bytes32(uint256(1))));
} }
/// @notice Returns the player's claim that commits to a given gindex. /// @notice Returns the state at the trace index within the player's trace.
function claimAt(Position _position) internal view returns (Claim claim_) { function traceAt(Position _position) public view returns (uint256 state_) {
return claimAt(_position.rightIndex(maxDepth).indexAtDepth()); 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. /// @notice Returns the player's claim that commits to a given trace index.
function claimAt(uint256 _traceIndex) public view returns (Claim claim_) { 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 { contract OneVsOne_Arena is FaultDisputeGame_Init {
/// @dev The absolute prestate of the trace. /// @dev The absolute prestate of the trace.
Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32(uint256(15))); Claim internal constant ABSOLUTE_PRESTATE = Claim.wrap(bytes32(uint256(15)));
/// @dev The honest participant. /// @dev The defender.
GamePlayer internal honest; GamePlayer internal defender;
/// @dev The dishonest participant. /// @dev The challenger.
GamePlayer internal dishonest; GamePlayer internal challenger;
function init( function init(GamePlayer _defender, GamePlayer _challenger) public {
GamePlayer _honest, Claim rootClaim = Claim.wrap(keccak256(abi.encode(15, _defender.traceAt(15))));
GamePlayer _dishonest, super.init(rootClaim, ABSOLUTE_PRESTATE);
Claim _rootClaim defender = _defender;
) public { challenger = _challenger;
super.init(_rootClaim, ABSOLUTE_PRESTATE);
// Deploy a new honest player.
honest = _honest;
// Deploy a new dishonest player.
dishonest = _dishonest;
// Set the counterparties. // Set the counterparties.
honest.init(gameProxy, dishonest, vm); defender.init(gameProxy, challenger, vm);
dishonest.init(gameProxy, honest, vm); challenger.init(gameProxy, defender, vm);
// Label actors for trace. // Label actors for trace.
vm.label(address(honest), "HonestPlayer"); vm.label(address(challenger), "Challenger");
vm.label(address(dishonest), "DishonestPlayer"); vm.label(address(defender), "Defender");
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new FullyDivergentPlayer(); GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(30)))); super.init(dishonest, honest);
} }
function test_resolvesCorrectly_succeeds() public { function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced. // 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 // Resolve the game and assert that the honest player challenged the root
// claim successfully. // claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep()); assertFalse(defender.failedToStep());
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new FullyDivergentPlayer(); GamePlayer dishonest = new FullyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31)))); super.init(honest, dishonest);
} }
function test_resolvesCorrectly_succeeds() public { function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced. // 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 // Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully. // claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep()); assertTrue(challenger.failedToStep());
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2 is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot2 is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new HalfDivergentPlayer(); GamePlayer dishonest = new HalfDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(15)))); super.init(dishonest, honest);
} }
function test_resolvesCorrectly_succeeds() public { function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced. // 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 // Resolve the game and assert that the honest player challenged the root
// claim successfully. // claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS));
assertFalse(honest.failedToStep()); assertFalse(defender.failedToStep());
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot2 is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot2 is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new HalfDivergentPlayer(); GamePlayer dishonest = new HalfDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31)))); super.init(honest, dishonest);
} }
function test_resolvesCorrectly_succeeds() public { function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced. // 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 // Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully. // claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS));
assertTrue(dishonest.failedToStep()); assertTrue(challenger.failedToStep());
} }
} }
contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3 is OneVsOne_Arena { contract FaultDisputeGame_ResolvesCorrectly_IncorrectRoot3 is OneVsOne_Arena {
function setUp() public override { function setUp() public override {
GamePlayer honest = new HonestPlayer(); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new EarlyDivergentPlayer(); GamePlayer dishonest = new EarlyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(15)))); super.init(dishonest, honest);
} }
function test_resolvesCorrectly_succeeds() public { function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced. // 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 // Resolve the game and assert that the honest player challenged the root
// claim successfully. // claim successfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.CHALLENGER_WINS)); 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 { function setUp() public override {
GamePlayer honest = new HonestPlayer(); GamePlayer honest = new HonestPlayer(ABSOLUTE_PRESTATE);
GamePlayer dishonest = new EarlyDivergentPlayer(); GamePlayer dishonest = new EarlyDivergentPlayer(ABSOLUTE_PRESTATE);
super.init(honest, dishonest, Claim.wrap(bytes32(uint256(31)))); super.init(honest, dishonest);
} }
function test_resolvesCorrectly_succeeds() public { function test_resolvesCorrectly_succeeds() public {
// Play the game until a step is forced. // 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 // Resolve the game and assert that the dishonest player challenged the root
// claim unsuccessfully. // claim unsuccessfully.
assertEq(uint8(gameProxy.resolve()), uint8(GameStatus.DEFENDER_WINS)); 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 { ...@@ -592,13 +606,8 @@ contract FaultDisputeGame_ResolvesCorrectly_CorrectRoot4 is OneVsOne_Arena {
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
contract HonestPlayer is GamePlayer { contract HonestPlayer is GamePlayer {
function init( constructor(Claim _absolutePrestate) {
FaultDisputeGame _gameProxy, uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory honestTrace = new bytes(16); bytes memory honestTrace = new bytes(16);
for (uint8 i = 0; i < honestTrace.length; i++) { for (uint8 i = 0; i < honestTrace.length; i++) {
honestTrace[i] = bytes1(absolutePrestate + i + 1); honestTrace[i] = bytes1(absolutePrestate + i + 1);
...@@ -608,13 +617,8 @@ contract HonestPlayer is GamePlayer { ...@@ -608,13 +617,8 @@ contract HonestPlayer is GamePlayer {
} }
contract FullyDivergentPlayer is GamePlayer { contract FullyDivergentPlayer is GamePlayer {
function init( constructor(Claim _absolutePrestate) {
FaultDisputeGame _gameProxy, uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory dishonestTrace = new bytes(16); bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) { for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the honest trace by 1. // Offset the honest trace by 1.
...@@ -625,13 +629,8 @@ contract FullyDivergentPlayer is GamePlayer { ...@@ -625,13 +629,8 @@ contract FullyDivergentPlayer is GamePlayer {
} }
contract HalfDivergentPlayer is GamePlayer { contract HalfDivergentPlayer is GamePlayer {
function init( constructor(Claim _absolutePrestate) {
FaultDisputeGame _gameProxy, uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory dishonestTrace = new bytes(16); bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) { for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half. // Offset the trace after the first half.
...@@ -642,13 +641,8 @@ contract HalfDivergentPlayer is GamePlayer { ...@@ -642,13 +641,8 @@ contract HalfDivergentPlayer is GamePlayer {
} }
contract EarlyDivergentPlayer is GamePlayer { contract EarlyDivergentPlayer is GamePlayer {
function init( constructor(Claim _absolutePrestate) {
FaultDisputeGame _gameProxy, uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_absolutePrestate)));
GamePlayer _counterParty,
Vm _vm
) public virtual override {
super.init(_gameProxy, _counterParty, _vm);
uint8 absolutePrestate = uint8(uint256(Claim.unwrap(_gameProxy.ABSOLUTE_PRESTATE())));
bytes memory dishonestTrace = new bytes(16); bytes memory dishonestTrace = new bytes(16);
for (uint8 i = 0; i < dishonestTrace.length; i++) { for (uint8 i = 0; i < dishonestTrace.length; i++) {
// Offset the trace after the first half. // Offset the trace after the first half.
...@@ -657,3 +651,36 @@ contract EarlyDivergentPlayer is GamePlayer { ...@@ -657,3 +651,36 @@ contract EarlyDivergentPlayer is GamePlayer {
trace = dishonestTrace; 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 @@ ...@@ -15,13 +15,15 @@
], ],
"scripts": { "scripts": {
"bindings": "cd ../../op-bindings && make", "bindings": "cd ../../op-bindings && make",
"build:forge": "forge build",
"build:with-metadata": "FOUNDRY_PROFILE=echidna yarn build:forge", "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:differential": "go build -o ./scripts/differential-testing/differential-testing ./scripts/differential-testing",
"build:fuzz": "(cd test-case-generator && go build ./cmd/fuzz.go)", "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:artifacts": "ts-node scripts/generate-artifacts.ts",
"autogen:invariant-docs": "ts-node scripts/invariant-doc-gen.ts", "autogen:invariant-docs": "ts-node scripts/invariant-doc-gen.ts",
"deploy": "hardhat deploy", "deploy": "hardhat deploy",
......
...@@ -17,10 +17,7 @@ yarn build ...@@ -17,10 +17,7 @@ yarn build
## Running the service ## 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 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. a custom op chain, the `OptimismPortal` contract addresses must also be set associated with the op-chain.
- Bedrock: `OptimismPortal`
- Legacy: `StateCommitmentChain`
Once your environment variables or flags have been set, run the service via: Once your environment variables or flags have been set, run the service via:
...@@ -40,13 +37,11 @@ yarn start ...@@ -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. 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). 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` - ***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. 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. 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. 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`: Check the list of available metrics via `yarn start --help`:
...@@ -62,9 +57,7 @@ Options: ...@@ -62,9 +57,7 @@ Options:
--l2rpcprovider Provider for interacting with L2 (env: FAULT_DETECTOR__L2_RPC_PROVIDER) --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) --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) --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 OP Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification (env: FAULT_DETECTOR__OPTIMISM_PORTAL_ADDRESS, default 0x0)
--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)
--port Port for the app server (env: FAULT_DETECTOR__PORT) --port Port for the app server (env: FAULT_DETECTOR__PORT)
--hostname Hostname for the app server (env: FAULT_DETECTOR__HOSTNAME) --hostname Hostname for the app server (env: FAULT_DETECTOR__HOSTNAME)
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/common-ts": "^0.8.2", "@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/core-utils": "^0.12.1",
"@eth-optimism/sdk": "^3.0.0", "@eth-optimism/sdk": "^3.0.0",
"@ethersproject/abstract-provider": "^5.7.0" "@ethersproject/abstract-provider": "^5.7.0"
......
import { Contract, BigNumber } from 'ethers' import { Contract } from 'ethers'
import { Logger } from '@eth-optimism/common-ts' 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 * Partial event interface, meant to reduce the size of the event cache to avoid
* running out of memory. * running out of memory.
...@@ -54,12 +47,12 @@ const getCache = ( ...@@ -54,12 +47,12 @@ const getCache = (
* @param contract Contract to update cache for. * @param contract Contract to update cache for.
* @param filter Event filter to use. * @param filter Event filter to use.
*/ */
export const updateOracleCache = async <TSubmissionEventArgs>( export const updateOracleCache = async (
oracle: OutputOracle<TSubmissionEventArgs>, oracle: Contract,
logger?: Logger logger?: Logger
): Promise<void> => { ): Promise<void> => {
const cache = getCache(oracle.contract.address) const cache = getCache(oracle.address)
const endBlock = await oracle.contract.provider.getBlockNumber() const endBlock = await oracle.provider.getBlockNumber()
logger?.info('visiting uncached oracle events for range', { logger?.info('visiting uncached oracle events for range', {
node: 'l1', node: 'l1',
cachedUntilBlock: cache.highestBlock, cachedUntilBlock: cache.highestBlock,
...@@ -77,17 +70,15 @@ export const updateOracleCache = async <TSubmissionEventArgs>( ...@@ -77,17 +70,15 @@ export const updateOracleCache = async <TSubmissionEventArgs>(
blockRangeSize: step, blockRangeSize: step,
}) })
const events = await oracle.contract.queryFilter( const events = await oracle.queryFilter(
oracle.filter, oracle.filters.OutputProposed(),
currentBlock, currentBlock,
currentBlock + step currentBlock + step
) )
// Throw the events into the cache. // Throw the events into the cache.
for (const event of events) { for (const event of events) {
cache.eventCache[ cache.eventCache[event.args.l2OutputIndex.toNumber()] = {
oracle.getEventIndex(event.args as TSubmissionEventArgs).toNumber()
] = {
blockNumber: event.blockNumber, blockNumber: event.blockNumber,
transactionHash: event.transactionHash, transactionHash: event.transactionHash,
args: event.args, args: event.args,
...@@ -135,12 +126,12 @@ export const updateOracleCache = async <TSubmissionEventArgs>( ...@@ -135,12 +126,12 @@ export const updateOracleCache = async <TSubmissionEventArgs>(
* @param index State batch index to search for. * @param index State batch index to search for.
* @returns Event corresponding to the batch. * @returns Event corresponding to the batch.
*/ */
export const findEventForStateBatch = async <TSubmissionEventArgs>( export const findEventForStateBatch = async (
oracle: OutputOracle<TSubmissionEventArgs>, oracle: Contract,
index: number, index: number,
logger?: Logger logger?: Logger
): Promise<PartialEvent> => { ): Promise<PartialEvent> => {
const cache = getCache(oracle.contract.address) const cache = getCache(oracle.address)
// Try to find the event in cache first. // Try to find the event in cache first.
if (cache.eventCache[index]) { if (cache.eventCache[index]) {
...@@ -166,13 +157,13 @@ export const findEventForStateBatch = async <TSubmissionEventArgs>( ...@@ -166,13 +157,13 @@ export const findEventForStateBatch = async <TSubmissionEventArgs>(
* @param oracle Output oracle contract. * @param oracle Output oracle contract.
* @returns Starting state root batch index. * @returns Starting state root batch index.
*/ */
export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>( export const findFirstUnfinalizedStateBatchIndex = async (
oracle: OutputOracle<TSubmissionEventArgs>, oracle: Contract,
fpw: number, fpw: number,
logger?: Logger logger?: Logger
): Promise<number> => { ): Promise<number> => {
const latestBlock = await oracle.contract.provider.getBlock('latest') const latestBlock = await oracle.provider.getBlock('latest')
const totalBatches = (await oracle.getTotalElements()).toNumber() const totalBatches = (await oracle.nextOutputIndex()).toNumber()
// Perform a binary search to find the next batch that will pass the challenge period. // Perform a binary search to find the next batch that will pass the challenge period.
let lo = 0 let lo = 0
...@@ -180,7 +171,7 @@ export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>( ...@@ -180,7 +171,7 @@ export const findFirstUnfinalizedStateBatchIndex = async <TSubmissionEventArgs>(
while (lo !== hi) { while (lo !== hi) {
const mid = Math.floor((lo + hi) / 2) const mid = Math.floor((lo + hi) / 2)
const event = await findEventForStateBatch(oracle, mid, logger) 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) { if (block.timestamp + fpw < latestBlock.timestamp) {
lo = mid + 1 lo = mid + 1
......
...@@ -16,7 +16,7 @@ import { ...@@ -16,7 +16,7 @@ import {
OEL1ContractsLike, OEL1ContractsLike,
} from '@eth-optimism/sdk' } from '@eth-optimism/sdk'
import { Provider } from '@ethersproject/abstract-provider' import { Provider } from '@ethersproject/abstract-provider'
import { ethers, Transaction } from 'ethers' import { Contract, ethers } from 'ethers'
import dateformat from 'dateformat' import dateformat from 'dateformat'
import { version } from '../package.json' import { version } from '../package.json'
...@@ -24,7 +24,6 @@ import { ...@@ -24,7 +24,6 @@ import {
findFirstUnfinalizedStateBatchIndex, findFirstUnfinalizedStateBatchIndex,
findEventForStateBatch, findEventForStateBatch,
PartialEvent, PartialEvent,
OutputOracle,
updateOracleCache, updateOracleCache,
} from './helpers' } from './helpers'
...@@ -32,9 +31,7 @@ type Options = { ...@@ -32,9 +31,7 @@ type Options = {
l1RpcProvider: Provider l1RpcProvider: Provider
l2RpcProvider: Provider l2RpcProvider: Provider
startBatchIndex: number startBatchIndex: number
bedrock: boolean
optimismPortalAddress?: string optimismPortalAddress?: string
stateCommitmentChainAddress?: string
} }
type Metrics = { type Metrics = {
...@@ -44,8 +41,8 @@ type Metrics = { ...@@ -44,8 +41,8 @@ type Metrics = {
} }
type State = { type State = {
fpw: number faultProofWindow: number
oo: OutputOracle<any> outputOracle: Contract
messenger: CrossChainMessenger messenger: CrossChainMessenger
currentBatchIndex: number currentBatchIndex: number
diverged: boolean diverged: boolean
...@@ -73,25 +70,13 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -73,25 +70,13 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
startBatchIndex: { startBatchIndex: {
validator: validators.num, validator: validators.num,
default: -1, default: -1,
desc: 'Batch index to start checking from. For bedrock chains, this is the L2 height to start from', desc: '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',
public: true, public: true,
}, },
optimismPortalAddress: { optimismPortalAddress: {
validator: validators.str, validator: validators.str,
default: ethers.constants.AddressZero, default: ethers.constants.AddressZero,
desc: '[Custom Bedrock Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for ouput verification ', desc: '[Custom OP 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.',
public: true, public: true,
}, },
}, },
...@@ -119,10 +104,9 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -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. * will fallback to the pre-defined set of addresses from options, otherwise aborting if unset.
* *
* Required Contracts * 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. * 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. * 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 * @param l2ChainId op chain id
* @returns OEL1ContractsLike set of L1 contracts with only the required addresses set * @returns OEL1ContractsLike set of L1 contracts with only the required addresses set
...@@ -130,19 +114,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -130,19 +114,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
async getOEL1Contracts(l2ChainId: number): Promise<OEL1ContractsLike> { async getOEL1Contracts(l2ChainId: number): Promise<OEL1ContractsLike> {
// CrossChainMessenger requires all address to be defined. Default to `AddressZero` to ignore unused contracts // CrossChainMessenger requires all address to be defined. Default to `AddressZero` to ignore unused contracts
let contracts: OEL1ContractsLike = { let contracts: OEL1ContractsLike = {
OptimismPortal: ethers.constants.AddressZero,
L2OutputOracle: ethers.constants.AddressZero,
// Unused contracts
AddressManager: ethers.constants.AddressZero, AddressManager: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
L1CrossDomainMessenger: ethers.constants.AddressZero, L1CrossDomainMessenger: ethers.constants.AddressZero,
L1StandardBridge: ethers.constants.AddressZero, L1StandardBridge: ethers.constants.AddressZero,
StateCommitmentChain: 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 const knownChainId = L2ChainID[l2ChainId] !== undefined
if (knownChainId) { if (knownChainId) {
this.logger.info(`Recognized L2 chain id ${L2ChainID[l2ChainId]}`) this.logger.info(`Recognized L2 chain id ${L2ChainID[l2ChainId]}`)
...@@ -152,42 +134,29 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -152,42 +134,29 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
} }
this.logger.info('checking contract address options...') this.logger.info('checking contract address options...')
if (this.options.bedrock) { const portalAddress = this.options.optimismPortalAddress
const address = this.options.optimismPortalAddress if (!knownChainId && portalAddress === ethers.constants.AddressZero) {
if (!knownChainId && address === ethers.constants.AddressZero) { this.logger.error('OptimismPortal contract unspecified')
this.logger.error('OptimismPortal contract unspecified') throw new Error(
throw new Error( '--optimismportalcontractaddress needs to set for custom op chains'
'--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()
}
// ... for a known chain ids without an override, the L2OutputOracle will already if (portalAddress !== ethers.constants.AddressZero) {
// be set via the hardcoded default this.logger.info('set OptimismPortal contract override')
} else { contracts.OptimismPortal = portalAddress
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 (address !== ethers.constants.AddressZero) { this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
this.logger.info('set StateCommitmentChain contract override') const opts = {
contracts.StateCommitmentChain = address 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 return contracts
} }
...@@ -211,7 +180,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -211,7 +180,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
l2SignerOrProvider: this.options.l2RpcProvider, l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId, l1ChainId,
l2ChainId, l2ChainId,
bedrock: this.options.bedrock, bedrock: true,
contracts: { l1: await this.getOEL1Contracts(l2ChainId) }, contracts: { l1: await this.getOEL1Contracts(l2ChainId) },
}) })
...@@ -219,46 +188,33 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -219,46 +188,33 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
this.state.diverged = false this.state.diverged = false
// We use this a lot, a bit cleaner to pull out to the top level of the state object. // 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.state.faultProofWindow =
this.logger.info(`fault proof window is ${this.state.fpw} seconds`) await this.state.messenger.getChallengePeriodSeconds()
this.logger.info(
if (this.options.bedrock) { `fault proof window is ${this.state.faultProofWindow} seconds`
const oo = this.state.messenger.contracts.l1.L2OutputOracle )
this.state.oo = {
contract: oo, this.state.outputOracle = this.state.messenger.contracts.l1.L2OutputOracle
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,
}
}
// Populate the event cache. // Populate the event cache.
this.logger.info('warming event cache, this might take a while...') 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. // Figure out where to start syncing from.
if (this.options.startBatchIndex === -1) { if (this.options.startBatchIndex === -1) {
this.logger.info('finding appropriate starting unfinalized batch') this.logger.info('finding appropriate starting unfinalized batch')
const firstUnfinalized = await findFirstUnfinalizedStateBatchIndex( const firstUnfinalized = await findFirstUnfinalizedStateBatchIndex(
this.state.oo, this.state.outputOracle,
this.state.fpw, this.state.faultProofWindow,
this.logger this.logger
) )
// We may not have an unfinalized batches in the case where no batches have been submitted // 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, // 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 FPW is very short. // but it happens often on testnets because the FAULTPROOFWINDOW is very short.
if (firstUnfinalized === undefined) { if (firstUnfinalized === undefined) {
this.logger.info('no unfinalized batches found. skipping all batches.') 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 this.state.currentBatchIndex = totalBatches.toNumber() - 1
} else { } else {
this.state.currentBatchIndex = firstUnfinalized this.state.currentBatchIndex = firstUnfinalized
...@@ -288,17 +244,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -288,17 +244,17 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let latestBatchIndex: number let latestBatchIndex: number
try { try {
const totalBatches = await this.state.oo.getTotalElements() const totalBatches = await this.state.outputOracle.nextOutputIndex()
latestBatchIndex = totalBatches.toNumber() - 1 latestBatchIndex = totalBatches.toNumber() - 1
} catch (err) { } catch (err) {
this.logger.error('failed to query total # of batches', { this.logger.error('failed to query total # of batches', {
error: err, error: err,
node: 'l1', node: 'l1',
section: 'getTotalElements', section: 'nextOutputIndex',
}) })
this.metrics.nodeConnectionFailures.inc({ this.metrics.nodeConnectionFailures.inc({
layer: 'l1', layer: 'l1',
section: 'getTotalElements', section: 'nextOutputIndex',
}) })
await sleep(15000) await sleep(15000)
return return
...@@ -322,7 +278,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -322,7 +278,7 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
let event: PartialEvent let event: PartialEvent
try { try {
event = await findEventForStateBatch( event = await findEventForStateBatch(
this.state.oo, this.state.outputOracle,
this.state.currentBatchIndex, this.state.currentBatchIndex,
this.logger this.logger
) )
...@@ -358,179 +314,86 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> { ...@@ -358,179 +314,86 @@ export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
return return
} }
if (this.options.bedrock) { const outputBlockNumber = event.args.l2BlockNumber.toNumber()
const outputBlockNumber = event.args.l2BlockNumber.toNumber() if (latestBlock < outputBlockNumber) {
if (latestBlock < outputBlockNumber) { this.logger.info('L2 node is behind, waiting for sync...', {
this.logger.info('L2 node is behind, waiting for sync...', { l2BlockHeight: latestBlock,
l2BlockHeight: latestBlock, outputBlock: outputBlockNumber,
outputBlock: outputBlockNumber, })
}) return
return }
}
let outputBlock: any let outputBlock: any
try { try {
outputBlock = await ( outputBlock = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [ ).send('eth_getBlockByNumber', [toRpcHexString(outputBlockNumber), false])
toRpcHexString(outputBlockNumber), } catch (err) {
false, this.logger.error('failed to fetch output block', {
]) error: err,
} catch (err) { node: 'l2',
this.logger.error('failed to fetch output block', { section: 'getBlock',
error: err, block: outputBlockNumber,
node: 'l2', })
section: 'getBlock', this.metrics.nodeConnectionFailures.inc({
block: outputBlockNumber, layer: 'l2',
}) section: 'getBlock',
this.metrics.nodeConnectionFailures.inc({ })
layer: 'l2', await sleep(15000)
section: 'getBlock', return
}) }
await sleep(15000)
return
}
let messagePasserProofResponse: any let messagePasserProofResponse: any
try { try {
messagePasserProofResponse = await ( messagePasserProofResponse = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getProof', [ ).send('eth_getProof', [
this.state.messenger.contracts.l2.BedrockMessagePasser.address, this.state.messenger.contracts.l2.BedrockMessagePasser.address,
[], [],
toRpcHexString(outputBlockNumber), toRpcHexString(outputBlockNumber),
]) ])
} catch (err) { } catch (err) {
this.logger.error('failed to fetch message passer proof', { this.logger.error('failed to fetch message passer proof', {
error: err, error: err,
node: 'l2', node: 'l2',
section: 'getProof', section: 'getProof',
block: outputBlockNumber, block: outputBlockNumber,
}) })
this.metrics.nodeConnectionFailures.inc({ this.metrics.nodeConnectionFailures.inc({
layer: 'l2', layer: 'l2',
section: 'getProof', section: 'getProof',
}) })
await sleep(15000) await sleep(15000)
return return
} }
const outputRoot = ethers.utils.solidityKeccak256( const outputRoot = ethers.utils.solidityKeccak256(
['uint256', 'bytes32', 'bytes32', 'bytes32'], ['uint256', 'bytes32', 'bytes32', 'bytes32'],
[ [
0, 0,
outputBlock.stateRoot, outputBlock.stateRoot,
messagePasserProofResponse.storageHash, messagePasserProofResponse.storageHash,
outputBlock.hash, outputBlock.hash,
] ]
) )
if (outputRoot !== event.args.outputRoot) { if (outputRoot !== event.args.outputRoot) {
this.state.diverged = true this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1) this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', { this.logger.error('state root mismatch', {
blockNumber: outputBlock.number, blockNumber: outputBlock.number,
expectedStateRoot: event.args.outputRoot, expectedStateRoot: event.args.outputRoot,
actualStateRoot: outputRoot, actualStateRoot: outputRoot,
finalizationTime: dateformat( finalizationTime: dateformat(
new Date( new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() + (ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.fpw) * this.state.faultProofWindow) *
1000 1000
),
'mmmm dS, yyyy, h:MM:ss TT'
), ),
}) 'mmmm dS, yyyy, h:MM:ss TT'
return ),
} })
} else { return
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
}
}
} }
const elapsedMs = Date.now() - startMs const elapsedMs = Date.now() - startMs
......
import hre from 'hardhat' import hre from 'hardhat'
import { Contract } from 'ethers' import { Contract, utils } from 'ethers'
import { toRpcHexString } from '@eth-optimism/core-utils' import { toRpcHexString } from '@eth-optimism/core-utils'
import { import { getContractFactory } from '@eth-optimism/contracts-bedrock'
getContractFactory,
getContractInterface,
} from '@eth-optimism/contracts'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { smock, FakeContract } from '@defi-wonderland/smock'
import { expect } from './setup' import { expect } from './setup'
import { import {
findEventForStateBatch, findEventForStateBatch,
findFirstUnfinalizedStateBatchIndex, findFirstUnfinalizedStateBatchIndex,
OutputOracle,
} from '../src' } from '../src'
describe('helpers', () => { describe('helpers', () => {
// Can be any non-zero value, 1000 is fine. const deployConfig = {
const challengeWindowSeconds = 1000 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 let signer: SignerWithAddress
before(async () => { before(async () => {
;[signer] = await hre.ethers.getSigners() ;[signer] = await hre.ethers.getSigners()
}) })
let FakeBondManager: FakeContract<Contract> let L2OutputOracle: Contract
let FakeCanonicalTransactionChain: FakeContract<Contract>
let AddressManager: Contract
let ChainStorageContainer: Contract
let StateCommitmentChain: Contract
let oracle: OutputOracle<any>
beforeEach(async () => { beforeEach(async () => {
// Set up fakes L2OutputOracle = await getContractFactory('L2OutputOracle', signer).deploy(
FakeBondManager = await smock.fake(getContractInterface('BondManager')) deployConfig.l2OutputOracleSubmissionInterval,
FakeCanonicalTransactionChain = await smock.fake( deployConfig.l2BlockTime,
getContractInterface('CanonicalTransactionChain') deployConfig.l2OutputOracleStartingBlockNumber,
) deployConfig.l2OutputOracleStartingTimestamp,
deployConfig.l2OutputOracleProposer,
// Set up contracts deployConfig.l2OutputOracleChallenger,
AddressManager = await getContractFactory( deployConfig.finalizationPeriodSeconds
'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
) )
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('findEventForStateBatch', () => {
describe('when the event exists once', () => { describe('when the event exists once', () => {
beforeEach(async () => { beforeEach(async () => {
await StateCommitmentChain.appendStateBatch( const latestBlock = await hre.ethers.provider.getBlock('latest')
[hre.ethers.constants.HashZero], const params = {
0 _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 () => { 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', () => { describe('when the event does not exist', () => {
it('should throw an error', async () => { it('should throw an error', async () => {
await expect( await expect(
findEventForStateBatch(oracle, 0) findEventForStateBatch(L2OutputOracle, 0)
).to.eventually.be.rejectedWith('unable to find event for batch') ).to.eventually.be.rejectedWith('unable to find event for batch')
}) })
}) })
...@@ -106,30 +79,46 @@ describe('helpers', () => { ...@@ -106,30 +79,46 @@ describe('helpers', () => {
describe('findFirstUnfinalizedIndex', () => { describe('findFirstUnfinalizedIndex', () => {
describe('when the chain is more then FPW seconds old', () => { describe('when the chain is more then FPW seconds old', () => {
beforeEach(async () => { beforeEach(async () => {
await StateCommitmentChain.appendStateBatch( const latestBlock = await hre.ethers.provider.getBlock('latest')
[hre.ethers.constants.HashZero], const params = {
0 _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 // Simulate FPW passing
await hre.ethers.provider.send('evm_increaseTime', [ await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(challengeWindowSeconds * 2), toRpcHexString(deployConfig.finalizationPeriodSeconds * 2),
]) ])
await StateCommitmentChain.appendStateBatch( await L2OutputOracle.proposeL2Output(
[hre.ethers.constants.HashZero], params._outputRoot,
1 params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
) )
await StateCommitmentChain.appendStateBatch( await L2OutputOracle.proposeL2Output(
[hre.ethers.constants.HashZero], params._outputRoot,
2 params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
) )
}) })
it('should find the first batch older than the FPW', async () => { it('should find the first batch older than the FPW', async () => {
const first = await findFirstUnfinalizedStateBatchIndex( const first = await findFirstUnfinalizedStateBatchIndex(
oracle, L2OutputOracle,
challengeWindowSeconds deployConfig.finalizationPeriodSeconds
) )
expect(first).to.equal(1) expect(first).to.equal(1)
...@@ -138,24 +127,40 @@ describe('helpers', () => { ...@@ -138,24 +127,40 @@ describe('helpers', () => {
describe('when the chain is less than FPW seconds old', () => { describe('when the chain is less than FPW seconds old', () => {
beforeEach(async () => { beforeEach(async () => {
await StateCommitmentChain.appendStateBatch( const latestBlock = await hre.ethers.provider.getBlock('latest')
[hre.ethers.constants.HashZero], const params = {
0 _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( await L2OutputOracle.proposeL2Output(
[hre.ethers.constants.HashZero], params._outputRoot,
1 params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
) )
await StateCommitmentChain.appendStateBatch( await L2OutputOracle.proposeL2Output(
[hre.ethers.constants.HashZero], params._outputRoot,
2 params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
) )
}) })
it('should return zero', async () => { it('should return zero', async () => {
const first = await findFirstUnfinalizedStateBatchIndex( const first = await findFirstUnfinalizedStateBatchIndex(
oracle, L2OutputOracle,
challengeWindowSeconds deployConfig.finalizationPeriodSeconds
) )
expect(first).to.equal(0) expect(first).to.equal(0)
...@@ -164,22 +169,38 @@ describe('helpers', () => { ...@@ -164,22 +169,38 @@ describe('helpers', () => {
describe('when no batches submitted for the entire FPW', () => { describe('when no batches submitted for the entire FPW', () => {
beforeEach(async () => { beforeEach(async () => {
await StateCommitmentChain.appendStateBatch( const latestBlock = await hre.ethers.provider.getBlock('latest')
[hre.ethers.constants.HashZero], const params = {
0 _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( await L2OutputOracle.proposeL2Output(
[hre.ethers.constants.HashZero], params._outputRoot,
1 params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
) )
await StateCommitmentChain.appendStateBatch( await L2OutputOracle.proposeL2Output(
[hre.ethers.constants.HashZero], params._outputRoot,
2 params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
) )
// Simulate FPW passing and no new batches // Simulate FPW passing and no new batches
await hre.ethers.provider.send('evm_increaseTime', [ await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(challengeWindowSeconds * 2), toRpcHexString(deployConfig.finalizationPeriodSeconds * 2),
]) ])
// Mine a block to force timestamp to update // Mine a block to force timestamp to update
...@@ -188,8 +209,8 @@ describe('helpers', () => { ...@@ -188,8 +209,8 @@ describe('helpers', () => {
it('should return undefined', async () => { it('should return undefined', async () => {
const first = await findFirstUnfinalizedStateBatchIndex( const first = await findFirstUnfinalizedStateBatchIndex(
oracle, L2OutputOracle,
challengeWindowSeconds deployConfig.finalizationPeriodSeconds
) )
expect(first).to.equal(undefined) 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