Commit d3d920f4 authored by Kevin Z Chen's avatar Kevin Z Chen Committed by GitHub

Delete chain-mon in favor of monitorism (#11239)

* Delete chain-mon

* Delete chain-mon from other places.

---------
Co-authored-by: default avatarKevin Kz <k@oplabs.co>
parent 461b02a4
......@@ -1532,12 +1532,6 @@ workflows:
- contracts-bedrock-validate-spaces:
requires:
- pnpm-monorepo
- js-lint-test:
name: chain-mon-tests
package_name: chain-mon
dependencies: "(contracts-bedrock|sdk)"
requires:
- pnpm-monorepo
- semgrep-scan
- go-mod-download
- fuzz-golang:
......@@ -1764,7 +1758,7 @@ workflows:
type: approval
filters:
tags:
only: /^(da-server|chain-mon|ci-builder(-rust)?|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)\/v.*/
only: /^(da-server|ci-builder(-rust)?|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)\/v.*/
branches:
ignore: /.*/
- docker-build:
......@@ -1962,22 +1956,6 @@ workflows:
op_component: op-supervisor
requires:
- op-supervisor-docker-release
- docker-build:
name: chain-mon-docker-release
filters:
tags:
only: /^chain-mon\/v.*/
branches:
ignore: /.*/
docker_name: chain-mon
docker_tags: <<pipeline.git.revision>>,latest
publish: true
release: true
resource_class: xlarge
context:
- oplabs-gcr-release
requires:
- hold
- docker-build:
name: ci-builder-docker-release
filters:
......@@ -2205,21 +2183,11 @@ workflows:
op_component: op-supervisor
requires:
- op-supervisor-docker-publish
- docker-build:
name: chain-mon-docker-publish
docker_name: chain-mon
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
resource_class: xlarge
publish: true
context:
- oplabs-gcr
- slack
- docker-build:
name: contracts-bedrock-docker-publish
docker_name: contracts-bedrock
docker_tags: <<pipeline.git.revision>>,<<pipeline.git.branch>>
resource_class: xlarge
requires: [ 'chain-mon-docker-publish' ] # use the cached base image
publish: true
context:
- oplabs-gcr
......
# Packages
/packages/chain-mon @ethereum-optimism/security-reviewers
/packages/chain-mon/internal/balance-mon @ethereum-optimism/infra-reviewers
/packages/contracts-bedrock @ethereum-optimism/contract-reviewers
/packages/sdk @ethereum-optimism/devxpod
......
......@@ -195,14 +195,6 @@ pull_request_rules:
label:
add:
- A-ops
- name: Add A-pkg-chain-mon label
conditions:
- 'files~=^packages/chain-mon/'
- '#label<5'
actions:
label:
add:
- A-pkg-chain-mon
- name: Add A-pkg-contracts-bedrock label
conditions:
- 'files~=^packages/contracts-bedrock/'
......
......@@ -21,7 +21,6 @@ on:
- ci-builder
- ci-builder-rust
- op-heartbeat
- chain-mon
- op-node
- op-batcher
- op-proposer
......
......@@ -80,18 +80,6 @@ cross-op-node:
op-node
.PHONY: golang-docker
chain-mon-docker:
# We don't use a buildx builder here, and just load directly into regular docker, for convenience.
GIT_COMMIT=$$(git rev-parse HEAD) \
GIT_DATE=$$(git show -s --format='%ct') \
IMAGE_TAGS=$$(git rev-parse HEAD),latest \
docker buildx bake \
--progress plain \
--load \
-f docker-bake.hcl \
chain-mon
.PHONY: chain-mon-docker
contracts-bedrock-docker:
IMAGE_TAGS=$$(git rev-parse HEAD),latest \
docker buildx bake \
......
......@@ -80,7 +80,6 @@ The Optimism Immunefi program offers up to $2,000,042 for in-scope critical vuln
├── <a href="./ops">ops</a>: Various operational packages
├── <a href="./ops-bedrock">ops-bedrock</a>: Bedrock devnet work
├── <a href="./packages">packages</a>
│ ├── <a href="./packages/chain-mon">chain-mon</a>: Chain monitoring services
│ ├── <a href="./packages/contracts-bedrock">contracts-bedrock</a>: OP Stack smart contracts
│ ├── <a href="./packages/devnet-tasks">devnet-tasks</a>: Legacy Hardhat tasks used within devnet CI tests
├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy
......@@ -116,7 +115,6 @@ See the [Node Software Releases](https://docs.optimism.io/builders/node-operator
The full set of components that have releases are:
- `chain-mon`
- `ci-builder`
- `op-batcher`
- `op-contracts`
......
......@@ -38,5 +38,4 @@ flag_management:
target: 100%
- name: bedrock-go-tests
- name: contracts-tests
- name: chain-mon-tests
- name: sdk-tests
......@@ -216,21 +216,6 @@ target "cannon" {
tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/cannon:${tag}"]
}
target "chain-mon" {
dockerfile = "./ops/docker/Dockerfile.packages"
context = "."
args = {
// proxyd dockerfile has no _ in the args
GITCOMMIT = "${GIT_COMMIT}"
GITDATE = "${GIT_DATE}"
GITVERSION = "${GIT_VERSION}"
}
// this is a multi-stage build, where each stage is a possible output target, but wd-mon is all we publish
target = "wd-mon"
platforms = split(",", PLATFORMS)
tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/chain-mon:${tag}"]
}
target "ci-builder" {
dockerfile = "./ops/docker/ci-builder/Dockerfile"
context = "."
......
......@@ -89,39 +89,6 @@ RUN pnpm build
ENTRYPOINT ["pnpm", "run"]
FROM base as chain-mon
WORKDIR /opt/optimism/packages/chain-mon
# TODO keeping the rest of these here for now because they are being used
# but we should really delete them we only need one image
FROM base as balance-mon
WORKDIR /opt/optimism/packages/chain-mon/internal
CMD ["start:balance-mon"]
from base as fault-mon
WORKDIR /opt/optimism/packages/chain-mon/
CMD ["start:fault-mon"]
from base as multisig-mon
WORKDIR /opt/optimism/packages/internal/multisig-mon
CMD ["start:multisig-mon"]
FROM base as replica-mon
WORKDIR /opt/optimism/packages/chain-mon/contrib
CMD ["start:replica-mon"]
FROM base as wallet-mon
WORKDIR /opt/optimism/packages/chain-mon/contrib
CMD ["start:wallet-mon"]
FROM base as wd-mon
WORKDIR /opt/optimism/packages/chain-mon/
CMD ["start:wd-mon"]
FROM base as faultproof-wd-mon
WORKDIR /opt/optimism/packages/chain-mon/
CMD ["start:faultproof-wd-mon"]
FROM base as contracts-bedrock
WORKDIR /opt/optimism/packages/contracts-bedrock
CMD ["deploy"]
......@@ -6,7 +6,7 @@ DOCKER_REPO=$1
GIT_TAG=$2
GIT_SHA=$3
IMAGE_NAME=$(echo "$GIT_TAG" | grep -Eow '^(ci-builder(-rust)?|chain-mon|da-server|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)' || true)
IMAGE_NAME=$(echo "$GIT_TAG" | grep -Eow '^(ci-builder(-rust)?|da-server|ufm-[a-z0-9\-]*|op-[a-z0-9\-]*)' || true)
if [ -z "$IMAGE_NAME" ]; then
echo "image name could not be parsed from git tag '$GIT_TAG'"
exit 1
......
......@@ -11,7 +11,6 @@ import semver
MIN_VERSIONS = {
'ci-builder': '0.6.0',
'ci-builder-rust': '0.1.0',
'chain-mon': '0.2.2',
'da-server': '0.0.4',
'op-node': '0.10.14',
'op-batcher': '0.10.14',
......
......@@ -5,7 +5,6 @@ import semver
SERVICES = [
'ci-builder',
'ci-builder-rust',
'chain-mon',
'op-node',
'op-batcher',
'op-challenger',
......@@ -88,4 +87,3 @@ def main():
if __name__ == "__main__":
main()
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier",
"chai"
]
###############################################################################
# ↓ balance-mon ↓ #
###############################################################################
# RPC pointing to network to monitor balances on
BALANCE_MON__RPC=
# JSON array in the format [{ "address": <address>, "nickname": <nickname> }, ... ]
BALANCE_MON__ACCOUNTS=
###############################################################################
# ↓ multisig-mon ↓ #
###############################################################################
# RPC pointing to network to monitor safe nonces on
MULTISIG_MON__RPC=
# JSON array in the format [{ "address": <address>, "nickname": <nickname> }, ... ]
MULTISIG_MON__ACCOUNTS=
###############################################################################
# ↓ wallet-mon ↓ #
###############################################################################
# RPC pointing to network to monitor
WALLET_MON__RPC=
# The block number to start monitoring from
# Defaults to the first bedrock block if unset.
WALLET_MON__START_BLOCK_NUMBER=
###############################################################################
# ↓ wd-mon ↓ #
###############################################################################
# RPCs pointing to a base chain and Optimism chain
TWO_STEP_MONITOR__L1_RPC_PROVIDER=
TWO_STEP_MONITOR__L2_RPC_PROVIDER=
# The block number to start monitoring from
TWO_STEP_MONITOR__START_BLOCK_NUMBER=
###############################################################################
# ↓ fault-mon ↓ #
###############################################################################
# --l1rpcprovider Provider for interacting with L1 (env: FAULT_DETECTOR__L1_RPC_PROVIDER)
FAULT_DETECTOR__L1_RPC_PROVIDER=
# --l2rpcprovider Provider for interacting with L2 (env: FAULT_DETECTOR__L2_RPC_PROVIDER)
FAULT_DETECTOR__L2_RPC_PROVIDER=
# --bedrock Whether or not the service is running against a Bedrock chain (env: FAULT_DETECTOR__BEDROCK)
BEDROCK=true
###############################################################################
# ↓ initialized-upgraded-mon ↓ #
###############################################################################
# RPC pointing to network to monitor
INITIALIZED_UPGRADED_MON__RPC=
# The block number to start monitoring from
# Defaults to the first bedrock block if unset.
INITIALIZED_UPGRADED_MON__START_BLOCK_NUMBER=
# JSON array in the format [{ "label": <string>, "address": <address> }, ... ]
INITIALIZED_UPGRADED_MON__CONTRACTS=
# Optional Params
# --startbatchindex Batch index to start checking from. For bedrock chains, this is the L2 height to start from (env: FAULT_DETECTOR__START_BATCH_INDEX)
# FAULT_DETECTOR__START_BATCH_INDEX=
# --optimismportaladdress [Custom Bedrock Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for output verification (env: FAULT_DETECTOR__OPTIMISM_PORTAL_ADDRESS)
# FAULT_DETECTOR__OPTIMISM_PORTAL_ADDRESS=
# --statecommitmentchainaddress [Custom Legacy Chains] Deployed StateCommitmentChain contract address. Used to fetch necessary info for output verification. (env: FAULT_DETECTOR__STATE_COMMITMENT_CHAIN_ADDRESS)
# FAULT_DETECTOR__STATE_COMMITMENT_CHAIN_ADDRESS=
# --loopintervalms Loop interval in milliseconds, only applies if service is set to loop (env: FAULT_DETECTOR__LOOP_INTERVAL_MS)
# FAULT_DETECTOR__LOOP_INTERVAL_MS=
# --port Port for the app server (env: FAULT_DETECTOR__PORT)
# FAULT_DETECTOR__PORT=
# --hostname Hostname for the app server (env: FAULT_DETECTOR__HOSTNAME)
# FAULT_DETECTOR__HOSTNAME=
# --loglevel Log level (env: FAULT_DETECTOR__LOG_LEVEL)
# FAULT_DETECTOR__LOG_LEVEL=
module.exports = {
extends: '../../.eslintrc.js',
}
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
This diff is collapsed.
(The MIT License)
Copyright 2020-2021 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @eth-optimism/chain-mon
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=chain-mon-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
`chain-mon` is a collection of chain monitoring services.
## Installation
Clone, install, and build the Optimism monorepo:
```
git clone https://github.com/ethereum-optimism/optimism.git
pnpm install
pnpm build
```
## Running a service
Copy `.env.example` into a new file named `.env`, then set the environment variables listed there depending on the service you want to run.
Once your environment variables have been set, run via:
```
pnpm start:<service name>
```
For example, to run `balance-mon`, execute:
```
pnpm start:balance-mon
```
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import { getChainId, compareAddrs } from '@eth-optimism/core-utils'
import { Provider, TransactionResponse } from '@ethersproject/abstract-provider'
import mainnetConfig from '@eth-optimism/contracts-bedrock/deploy-config/mainnet.json'
import sepoliaConfig from '@eth-optimism/contracts-bedrock/deploy-config/sepolia.json'
import { version } from '../../package.json'
const networks = {
1: {
name: 'mainnet',
l1StartingBlockTag: mainnetConfig.l1StartingBlockTag,
},
10: {
name: 'op-mainnet',
l1StartingBlockTag: null,
},
11155111: {
name: 'sepolia',
l1StartingBlockTag: sepoliaConfig.l1StartingBlockTag,
},
11155420: {
name: 'op-sepolia',
l1StartingBlockTag: null,
},
420: {
name: 'op-goerli',
l1StartingBlockTag: null,
},
}
// keccak256("Initialized(uint8)") = 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498
const topic_initialized =
'0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498'
// keccak256("Upgraded(address)") = 0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b
const topic_upgraded =
'0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b'
type InitializedUpgradedMonOptions = {
rpc: Provider
startBlockNumber: number
contracts: string
}
type InitializedUpgradedMonMetrics = {
initializedCalls: Counter
upgradedCalls: Counter
unexpectedRpcErrors: Counter
}
type InitializedUpgradedMonState = {
chainId: number
highestUncheckedBlockNumber: number
contracts: Array<{ label: string; address: string }>
}
export class InitializedUpgradedMonService extends BaseServiceV2<
InitializedUpgradedMonOptions,
InitializedUpgradedMonMetrics,
InitializedUpgradedMonState
> {
constructor(
options?: Partial<InitializedUpgradedMonOptions & StandardOptions>
) {
super({
version,
name: 'initialized-upgraded-mon',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
contracts: {
validator: validators.str,
desc: 'JSON array of [{ label, address }] to monitor contracts for',
public: true,
},
},
metricsSpec: {
initializedCalls: {
type: Gauge,
desc: 'Successful transactions to tracked contracts emitting initialized event',
labels: ['label', 'address'],
},
upgradedCalls: {
type: Gauge,
desc: 'Successful transactions to tracked contracts emitting upgraded event',
labels: ['label', 'address'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.rpc, {
logger: this.logger,
name: 'L1',
})
this.state.chainId = await getChainId(this.options.rpc)
const l1StartingBlockTag = networks[this.state.chainId].l1StartingBlockTag
if (this.options.startBlockNumber === -1) {
const block_number =
l1StartingBlockTag != null
? (await this.options.rpc.getBlock(l1StartingBlockTag)).number
: 0
this.state.highestUncheckedBlockNumber = block_number
} else {
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
}
try {
this.state.contracts = JSON.parse(this.options.contracts)
} catch (e) {
throw new Error(
'unable to start service because provided options is not valid json'
)
}
}
protected async main(): Promise<void> {
if (
(await this.options.rpc.getBlockNumber()) <
this.state.highestUncheckedBlockNumber
) {
this.logger.info('Waiting for new blocks')
return
}
const block = await this.options.rpc.getBlock(
this.state.highestUncheckedBlockNumber
)
this.logger.info('Checking block', {
number: block.number,
})
const transactions: TransactionResponse[] = []
for (const txHash of block.transactions) {
const t = await this.options.rpc.getTransaction(txHash)
transactions.push(t)
}
for (const transaction of transactions) {
for (const contract of this.state.contracts) {
const to =
transaction.to != null ? transaction.to : transaction['creates']
if (compareAddrs(contract.address, to)) {
try {
const transactionReceipt = await transaction.wait()
for (const log of transactionReceipt.logs) {
if (log.topics.includes(topic_initialized)) {
this.metrics.initializedCalls.inc({
label: contract.label,
address: contract.address,
})
this.logger.info('initialized event', {
label: contract.label,
address: contract.address,
})
} else if (log.topics.includes(topic_upgraded)) {
this.metrics.upgradedCalls.inc({
label: contract.label,
address: contract.address,
})
this.logger.info('upgraded event', {
label: contract.label,
address: contract.address,
})
}
}
} catch (err) {
// If error is due to transaction failing, ignore transaction
if (
err.message.length >= 18 &&
err.message.slice(0, 18) === 'transaction failed'
) {
break
}
// Otherwise, we have an unexpected RPC error
this.logger.info(`got unexpected RPC error`, {
section: 'creations',
name: 'NULL',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'creations',
name: 'NULL',
})
return
}
}
}
}
this.logger.info('Checked block', {
number: this.state.highestUncheckedBlockNumber,
})
this.state.highestUncheckedBlockNumber++
}
}
if (require.main === module) {
const service = new InitializedUpgradedMonService()
service.run()
}
# @eth-optimism/replica-healthcheck
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=replica-healthcheck-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
## What is this?
`replica-healthcheck` is an express server to be run alongside a replica instance, to ensure that the replica is healthy. Currently, it exposes metrics on syncing stats and exits when the replica has a mismatched state root against the sequencer.
## Installation
Clone, install, and build the Optimism monorepo:
```
git clone https://github.com/ethereum-optimism/optimism.git
pnpm install
pnpm build
```
## Running the service (manual)
Copy `.env.example` into a new file named `.env`, then set the environment variables listed there.
You can view a list of all environment variables and descriptions for each via:
```
pnpm start:replica-mon --help
```
Once your environment variables have been set, run the healthcheck service via:
```
pnpm start:replica-mon
```
import { Provider, Block } from '@ethersproject/abstract-provider'
import {
BaseServiceV2,
StandardOptions,
Counter,
Gauge,
validators,
} from '@eth-optimism/common-ts'
import { sleep } from '@eth-optimism/core-utils'
import { version } from '../../package.json'
type HealthcheckOptions = {
referenceRpcProvider: Provider
targetRpcProvider: Provider
onDivergenceWaitMs?: number
}
type HealthcheckMetrics = {
lastMatchingStateRootHeight: Gauge
isCurrentlyDiverged: Gauge
referenceHeight: Gauge
targetHeight: Gauge
heightDifference: Gauge
targetConnectionFailures: Counter
referenceConnectionFailures: Counter
}
type HealthcheckState = {}
export class HealthcheckService extends BaseServiceV2<
HealthcheckOptions,
HealthcheckMetrics,
HealthcheckState
> {
constructor(options?: Partial<HealthcheckOptions & StandardOptions>) {
super({
version,
name: 'healthcheck',
options: {
loopIntervalMs: 5000,
...options,
},
optionsSpec: {
referenceRpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
},
targetRpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
},
onDivergenceWaitMs: {
validator: validators.num,
desc: 'Waiting time in ms per loop when divergence is detected',
default: 60_000,
public: true,
},
},
metricsSpec: {
lastMatchingStateRootHeight: {
type: Gauge,
desc: 'Highest matching state root between target and reference',
},
isCurrentlyDiverged: {
type: Gauge,
desc: 'Whether or not the two nodes are currently diverged',
},
referenceHeight: {
type: Gauge,
desc: 'Block height of the reference client',
},
targetHeight: {
type: Gauge,
desc: 'Block height of the target client',
},
heightDifference: {
type: Gauge,
desc: 'Difference in block heights between the two clients',
},
targetConnectionFailures: {
type: Counter,
desc: 'Number of connection failures to the target client',
},
referenceConnectionFailures: {
type: Counter,
desc: 'Number of connection failures to the reference client',
},
},
})
}
async main() {
// Get the latest block from the target client and check for connection failures.
let targetLatest: Block
try {
targetLatest = await this.options.targetRpcProvider.getBlock('latest')
} catch (err) {
if (err.message.includes('could not detect network')) {
this.logger.error('target client not connected')
this.metrics.targetConnectionFailures.inc()
return
} else {
throw err
}
}
// Get the latest block from the reference client and check for connection failures.
let referenceLatest: Block
try {
referenceLatest = await this.options.referenceRpcProvider.getBlock(
'latest'
)
} catch (err) {
if (err.message.includes('could not detect network')) {
this.logger.error('reference client not connected')
this.metrics.referenceConnectionFailures.inc()
return
} else {
throw err
}
}
// Later logic will depend on the height difference.
const heightDiff = Math.abs(referenceLatest.number - targetLatest.number)
const minBlock = Math.min(targetLatest.number, referenceLatest.number)
// Update these metrics first so they'll refresh no matter what.
this.metrics.targetHeight.set(targetLatest.number)
this.metrics.referenceHeight.set(referenceLatest.number)
this.metrics.heightDifference.set(heightDiff)
this.logger.info(`latest block heights`, {
targetHeight: targetLatest.number,
referenceHeight: referenceLatest.number,
heightDifference: heightDiff,
minBlockNumber: minBlock,
})
const reference = await this.options.referenceRpcProvider.getBlock(minBlock)
if (!reference) {
// This is ok, but we should log it and restart the loop.
this.logger.info(`reference block was not found`, {
blockNumber: reference.number,
})
return
}
const target = await this.options.targetRpcProvider.getBlock(minBlock)
if (!target) {
// This is ok, but we should log it and restart the loop.
this.logger.info(`target block was not found`, {
blockNumber: target.number,
})
return
}
// We used to use state roots here, but block hashes are even more reliable because they will
// catch discrepancies in blocks that may not impact the state. For example, if clients have
// blocks with two different timestamps, the state root will only diverge if the timestamp is
// actually used during the transaction(s) within the block.
if (reference.hash !== target.hash) {
this.logger.error(`reference client has different hash for block`, {
blockNumber: target.number,
referenceHash: reference.hash,
targetHash: target.hash,
})
// The main loop polls for "latest" so aren't checking every block. We need to use a binary
// search to find the first block where a mismatch occurred.
this.logger.info(`beginning binary search to find first mismatched block`)
let start = 0
let end = target.number
while (start !== end) {
const mid = Math.floor((start + end) / 2)
this.logger.info(`checking block`, { blockNumber: mid })
const blockA = await this.options.referenceRpcProvider.getBlock(mid)
const blockB = await this.options.targetRpcProvider.getBlock(mid)
if (blockA.hash === blockB.hash) {
start = mid + 1
} else {
end = mid
}
}
this.logger.info(`found first mismatched block`, { blockNumber: end })
this.metrics.lastMatchingStateRootHeight.set(end)
this.metrics.isCurrentlyDiverged.set(1)
// Old version of the service would exit here, but we want to keep looping just in case the
// the system recovers later. This is better than exiting because it means we don't have to
// restart the entire service. Running these checks once per minute will not trigger too many
// requests, so this should be fine.
await sleep(this.options.onDivergenceWaitMs)
return
}
this.logger.info(`blocks are matching`, {
blockNumber: target.number,
})
// Update latest matching state root height and reset the diverged metric in case it was set.
this.metrics.lastMatchingStateRootHeight.set(target.number)
this.metrics.isCurrentlyDiverged.set(0)
}
}
if (require.main === module) {
const service = new HealthcheckService()
service.run()
}
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import { getChainId, compareAddrs } from '@eth-optimism/core-utils'
import { Provider, TransactionResponse } from '@ethersproject/abstract-provider'
import mainnetConfig from '@eth-optimism/contracts-bedrock/deploy-config/mainnet.json'
import { version } from '../../package.json'
const networks = {
1: {
name: 'mainnet',
l1StartingBlockTag: mainnetConfig.l1StartingBlockTag,
accounts: [
{
label: 'Proposer',
wallet: mainnetConfig.l2OutputOracleProposer,
target: '0xdfe97868233d1aa22e815a266982f2cf17685a27',
},
{
label: 'Batcher',
wallet: mainnetConfig.batchSenderAddress,
target: mainnetConfig.batchInboxAddress,
},
],
},
}
type WalletMonOptions = {
rpc: Provider
startBlockNumber: number
}
type WalletMonMetrics = {
validatedCalls: Counter
unexpectedCalls: Counter
unexpectedRpcErrors: Counter
}
type WalletMonState = {
chainId: number
highestUncheckedBlockNumber: number
}
export class WalletMonService extends BaseServiceV2<
WalletMonOptions,
WalletMonMetrics,
WalletMonState
> {
constructor(options?: Partial<WalletMonOptions & StandardOptions>) {
super({
version,
name: 'wallet-mon',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
},
metricsSpec: {
validatedCalls: {
type: Gauge,
desc: 'Transactions from the account checked',
labels: ['wallet', 'target', 'nickname'],
},
unexpectedCalls: {
type: Counter,
desc: 'Number of unexpected wallets',
labels: ['wallet', 'target', 'nickname', 'transactionHash'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.rpc, {
logger: this.logger,
name: 'L1',
})
this.state.chainId = await getChainId(this.options.rpc)
const l1StartingBlockTag = networks[this.state.chainId].l1StartingBlockTag
if (this.options.startBlockNumber === -1) {
const block = await this.options.rpc.getBlock(l1StartingBlockTag)
this.state.highestUncheckedBlockNumber = block.number
} else {
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
}
}
protected async main(): Promise<void> {
if (
(await this.options.rpc.getBlockNumber()) <
this.state.highestUncheckedBlockNumber
) {
this.logger.info('Waiting for new blocks')
return
}
const network = networks[this.state.chainId]
const accounts = network.accounts
const block = await this.options.rpc.getBlock(
this.state.highestUncheckedBlockNumber
)
this.logger.info('Checking block', {
number: block.number,
})
const transactions: TransactionResponse[] = []
for (const txHash of block.transactions) {
const t = await this.options.rpc.getTransaction(txHash)
transactions.push(t)
}
for (const transaction of transactions) {
for (const account of accounts) {
if (compareAddrs(account.wallet, transaction.from)) {
if (compareAddrs(account.target, transaction.to)) {
this.metrics.validatedCalls.inc({
nickname: account.label,
wallet: account.address,
target: account.target,
})
this.logger.info('validated call', {
nickname: account.label,
wallet: account.address,
target: account.target,
})
} else {
this.metrics.unexpectedCalls.inc({
nickname: account.label,
wallet: account.address,
target: transaction.to,
transactionHash: transaction.hash,
})
this.logger.error('Unexpected call detected', {
nickname: account.label,
address: account.address,
target: transaction.to,
transactionHash: transaction.hash,
})
}
}
}
}
this.logger.info('Checked block', {
number: this.state.highestUncheckedBlockNumber,
})
this.state.highestUncheckedBlockNumber++
}
}
if (require.main === module) {
const service = new WalletMonService()
service.run()
}
import { HardhatUserConfig } from 'hardhat/types'
// Hardhat plugins
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
const config: HardhatUserConfig = {
mocha: {
timeout: 50000,
},
}
export default config
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
} from '@eth-optimism/common-ts'
import { Provider } from '@ethersproject/abstract-provider'
import { version } from '../../package.json'
type BalanceMonOptions = {
rpc: Provider
accounts: string
}
type BalanceMonMetrics = {
balances: Gauge
unexpectedRpcErrors: Counter
}
type BalanceMonState = {
accounts: Array<{ address: string; nickname: string }>
}
export class BalanceMonService extends BaseServiceV2<
BalanceMonOptions,
BalanceMonMetrics,
BalanceMonState
> {
constructor(options?: Partial<BalanceMonOptions & StandardOptions>) {
super({
version,
name: 'balance-mon',
loop: true,
options: {
loopIntervalMs: 60_000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
accounts: {
validator: validators.str,
desc: 'JSON array of [{ address, nickname, safe }] to monitor balances and nonces of',
public: true,
},
},
metricsSpec: {
balances: {
type: Gauge,
desc: 'Balances of addresses',
labels: ['address', 'nickname'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
this.state.accounts = JSON.parse(this.options.accounts)
}
protected async main(): Promise<void> {
for (const account of this.state.accounts) {
try {
const balance = await this.options.rpc.getBalance(account.address)
this.logger.info(`got balance`, {
address: account.address,
nickname: account.nickname,
balance: balance.toString(),
})
// Parse the balance as an integer instead of via toNumber() to avoid ethers throwing an
// an error. We might get rounding errors but we don't need perfect precision here, just a
// generally accurate sense for what the current balance is.
this.metrics.balances.set(
{ address: account.address, nickname: account.nickname },
parseInt(balance.toString(), 10)
)
} catch (err) {
this.logger.info(`got unexpected RPC error`, {
section: 'balances',
name: 'getBalance',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'balances',
name: 'getBalance',
})
}
}
}
}
if (require.main === module) {
const service = new BalanceMonService()
service.run()
}
import { exec } from 'child_process'
import {
BaseServiceV2,
StandardOptions,
Gauge,
Counter,
validators,
} from '@eth-optimism/common-ts'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import Safe from '../../src/abi/IGnosisSafe.0.8.19.json'
import OptimismPortal from '../../src/abi/OptimismPortal.json'
import { version } from '../../package.json'
type MultisigMonOptions = {
rpc: Provider
accounts: string
onePassServiceToken: string
}
type MultisigMonMetrics = {
safeNonce: Gauge
latestPreSignedPauseNonce: Gauge
pausedState: Gauge
unexpectedRpcErrors: Counter
}
type MultisigMonState = {
accounts: Array<{
nickname: string
safeAddress: string
optimismPortalAddress: string
vault: string
}>
}
export class MultisigMonService extends BaseServiceV2<
MultisigMonOptions,
MultisigMonMetrics,
MultisigMonState
> {
constructor(options?: Partial<MultisigMonOptions & StandardOptions>) {
super({
version,
name: 'multisig-mon',
loop: true,
options: {
loopIntervalMs: 60_000,
...options,
},
optionsSpec: {
rpc: {
validator: validators.provider,
desc: 'Provider for network to monitor balances on',
},
accounts: {
validator: validators.str,
desc: 'JSON array of [{ nickname, safeAddress, optimismPortalAddress, vault }] to monitor',
public: true,
},
onePassServiceToken: {
validator: validators.str,
desc: '1Password Service Token',
},
},
metricsSpec: {
safeNonce: {
type: Gauge,
desc: 'Safe nonce',
labels: ['address', 'nickname'],
},
latestPreSignedPauseNonce: {
type: Gauge,
desc: 'Latest pre-signed pause nonce',
labels: ['address', 'nickname'],
},
pausedState: {
type: Gauge,
desc: 'OptimismPortal paused state',
labels: ['address', 'nickname'],
},
unexpectedRpcErrors: {
type: Counter,
desc: 'Number of unexpected RPC errors',
labels: ['section', 'name'],
},
},
})
}
protected async init(): Promise<void> {
this.state.accounts = JSON.parse(this.options.accounts)
}
protected async main(): Promise<void> {
for (const account of this.state.accounts) {
// get the nonce 1pass
if (this.options.onePassServiceToken) {
await this.getOnePassNonce(account)
}
// get the nonce from deployed safe
if (account.safeAddress) {
await this.getSafeNonce(account)
}
// get the paused state of the OptimismPortal
if (account.optimismPortalAddress) {
await this.getPausedState(account)
}
}
}
private async getPausedState(account: {
nickname: string
safeAddress: string
optimismPortalAddress: string
vault: string
}) {
try {
const optimismPortal = new ethers.Contract(
account.optimismPortalAddress,
OptimismPortal.abi,
this.options.rpc
)
const paused = await optimismPortal.paused()
this.logger.info(`got paused state`, {
optimismPortalAddress: account.optimismPortalAddress,
nickname: account.nickname,
paused,
})
this.metrics.pausedState.set(
{ address: account.optimismPortalAddress, nickname: account.nickname },
paused ? 1 : 0
)
} catch (err) {
this.logger.error(`got unexpected RPC error`, {
section: 'pausedState',
name: 'getPausedState',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'pausedState',
name: 'getPausedState',
})
}
}
private async getOnePassNonce(account: {
nickname: string
safeAddress: string
optimismPortalAddress: string
vault: string
}) {
try {
exec(
`OP_SERVICE_ACCOUNT_TOKEN=${this.options.onePassServiceToken} op item list --format json --vault="${account.vault}"`,
(error, stdout, stderr) => {
if (error) {
this.logger.error(`got unexpected error from onepass:`, {
section: 'onePassNonce',
name: 'getOnePassNonce',
})
return
}
if (stderr) {
this.logger.error(
`got unexpected error (from the stderr) from onepass`,
{
section: 'onePassNonce',
name: 'getOnePassNonce',
}
)
return
}
const items = JSON.parse(stdout)
let latestNonce = -1
this.logger.debug(`items in vault '${account.vault}':`)
for (const item of items) {
const title = item['title']
this.logger.debug(`- ${title}`)
if (title.startsWith('ready-') && title.endsWith('.json')) {
const nonce = parseInt(title.substring(6, title.length - 5), 10)
if (nonce > latestNonce) {
latestNonce = nonce
}
}
}
this.metrics.latestPreSignedPauseNonce.set(
{ address: account.safeAddress, nickname: account.nickname },
latestNonce
)
this.logger.debug(`latestNonce: ${latestNonce}`)
}
)
} catch (err) {
this.logger.error(`got unexpected error from onepass`, {
section: 'onePassNonce',
name: 'getOnePassNonce',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'onePassNonce',
name: 'getOnePassNonce',
})
}
}
private async getSafeNonce(account: {
nickname: string
safeAddress: string
optimismPortalAddress: string
vault: string
}) {
try {
const safeContract = new ethers.Contract(
account.safeAddress,
Safe.abi,
this.options.rpc
)
const safeNonce = await safeContract.nonce()
this.logger.info(`got nonce`, {
address: account.safeAddress,
nickname: account.nickname,
nonce: safeNonce.toString(),
})
this.metrics.safeNonce.set(
{ address: account.safeAddress, nickname: account.nickname },
parseInt(safeNonce.toString(), 10)
)
} catch (err) {
this.logger.error(`got unexpected RPC error`, {
section: 'safeNonce',
name: 'getSafeNonce',
err,
})
this.metrics.unexpectedRpcErrors.inc({
section: 'safeNonce',
name: 'getSafeNonce',
})
}
}
}
if (require.main === module) {
const service = new MultisigMonService()
service.run()
}
{
"private": true,
"name": "@eth-optimism/chain-mon",
"version": "0.6.6",
"description": "[Optimism] Chain monitoring services",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*"
],
"scripts": {
"dev:balance-mon": "tsx watch ./internal/balance-mon/service.ts",
"dev:fault-mon": "tsx watch ./src/fault-mon/service.ts",
"dev:multisig-mon": "tsx watch ./internal/multisig-mon/service.ts",
"dev:replica-mon": "tsx watch ./contrib/replica-mon/service.ts",
"dev:wallet-mon": "tsx watch ./contrib/wallet-mon/service.ts",
"dev:wd-mon": "tsx watch ./src/wd-mon/service.ts",
"dev:faultproof-wd-mon": "tsx ./src/faultproof-wd-mon/service.ts",
"dev:initialized-upgraded-mon": "tsx watch ./contrib/initialized-upgraded-mon/service.ts",
"start:balance-mon": "tsx ./internal/balance-mon/service.ts",
"start:fault-mon": "tsx ./src/fault-mon/service.ts",
"start:multisig-mon": "tsx ./internal/multisig-mon/service.ts",
"start:replica-mon": "tsx ./contrib/replica-mon/service.ts",
"start:wallet-mon": "tsx ./contrib/wallet-mon/service.ts",
"start:wd-mon": "tsx ./src/wd-mon/service.ts",
"start:faultproof-wd-mon": "tsx ./src/faultproof-wd-mon/service.ts",
"start:initialized-upgraded-mon": "tsx ./contrib/initialized-upgraded-mon/service.ts",
"test": "hardhat test",
"build": "tsc -p ./tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint": "pnpm lint:fix && pnpm lint:check",
"lint:fix": "pnpm lint:check --fix",
"lint:check": "eslint . --max-warnings=0"
},
"keywords": [
"optimism",
"ethereum",
"monitoring"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/chain-mon#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"dependencies": {
"@eth-optimism/common-ts": "^0.8.9",
"@eth-optimism/contracts-bedrock": "workspace:*",
"@eth-optimism/contracts-periphery": "1.0.8",
"@eth-optimism/core-utils": "^0.13.2",
"@eth-optimism/sdk": "^3.3.2",
"@types/dateformat": "^5.0.0",
"chai-as-promised": "^7.1.1",
"dateformat": "^4.5.1",
"dotenv": "^16.4.5",
"ethers": "^5.7.2"
},
"devDependencies": {
"@ethersproject/abstract-provider": "^5.7.0",
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@nomiclabs/hardhat-waffle": "^2.0.6",
"hardhat": "^2.20.1",
"ts-node": "^10.9.2",
"tsx": "^4.16.2"
}
}
This diff is collapsed.
This diff is collapsed.
# @eth-optimism/fault-mon
[![codecov](https://codecov.io/gh/ethereum-optimism/optimism/branch/develop/graph/badge.svg?token=0VTG7PG7YR&flag=fault-detector-tests)](https://codecov.io/gh/ethereum-optimism/optimism)
The `fault-mon` is a simple service for detecting discrepancies between your local view of the Optimism network and the L2 output proposals published to Ethereum.
## Installation
Clone, install, and build the Optimism monorepo:
```
git clone https://github.com/ethereum-optimism/optimism.git
pnpm install
pnpm build
```
## Running the service
Copy `.env.example` into a new file named `.env`, then set the environment variables listed there. Additional env settings are listed on `--help`. If running the fault detector against
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:
```
pnpm start
```
## Ports
- API is exposed at `$FAULT_DETECTOR__HOSTNAME:$FAULT_DETECTOR__PORT/api`
- Metrics are exposed at `$FAULT_DETECTOR__HOSTNAME:$FAULT_DETECTOR__PORT/metrics`
- `$FAULT_DETECTOR__HOSTNAME` defaults to `0.0.0.0`
- `$FAULT_DETECTOR__PORT` defaults to `7300`
## What this service does
The `fault-mon` 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).
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.
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 export a series of Prometheus metrics that you can use to trigger alerting when issues are detected.
Check the list of available metrics via `pnpm start --help`:
```sh
> pnpm start --help
$ tsx ./src/service.ts --help
Usage: service [options]
Options:
--l1rpcprovider Provider for interacting with L1 (env: FAULT_DETECTOR__L1_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)
--loopintervalms Loop interval in milliseconds (env: FAULT_DETECTOR__LOOP_INTERVAL_MS)
--optimismportaladdress [Custom OP Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for output 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)
-h, --help display help for command
Metrics:
highest_checked_batch_index Highest good batch index (type: Gauge)
highest_known_batch_index Highest known batch index (type: Gauge)
is_currently_mismatched 0 if state is ok, 1 if state is mismatched (type: Gauge)
l1_node_connection_failures Number of times L1 node connection has failed (type: Gauge)
l2_node_connection_failures Number of times L2 node connection has failed (type: Gauge)
metadata Service metadata (type: Gauge)
unhandled_errors Unhandled errors (type: Counter)
```
import { Contract } from 'ethers'
import { Logger } from '@eth-optimism/common-ts'
import { BedrockOutputData } from '@eth-optimism/core-utils'
/**
* Finds the BedrockOutputData that corresponds to a given output index.
*
* @param oracle Output oracle contract
* @param index Output index to search for.
* @returns BedrockOutputData corresponding to the output index.
*/
export const findOutputForIndex = async (
oracle: Contract,
index: number,
logger?: Logger
): Promise<BedrockOutputData> => {
try {
const proposal = await oracle.getL2Output(index)
return {
outputRoot: proposal.outputRoot,
l1Timestamp: proposal.timestamp.toNumber(),
l2BlockNumber: proposal.l2BlockNumber.toNumber(),
l2OutputIndex: index,
}
} catch (err) {
logger?.fatal('error when calling L2OuputOracle.getL2Output', {
errors: err,
})
throw new Error(`unable to find output for index ${index}`)
}
}
/**
* Finds the first L2 output index that has not yet passed the fault proof window.
*
* @param oracle Output oracle contract.
* @returns Starting L2 output index.
*/
export const findFirstUnfinalizedOutputIndex = async (
oracle: Contract,
fpw: number,
logger?: Logger
): Promise<number> => {
const latestBlock = await oracle.provider.getBlock('latest')
const totalOutputs = (await oracle.nextOutputIndex()).toNumber()
// Perform a binary search to find the next batch that will pass the challenge period.
let lo = 0
let hi = totalOutputs
while (lo !== hi) {
const mid = Math.floor((lo + hi) / 2)
const outputData = await findOutputForIndex(oracle, mid, logger)
if (outputData.l1Timestamp + fpw < latestBlock.timestamp) {
lo = mid + 1
} else {
hi = mid
}
}
// Result will be zero if the chain is less than FPW seconds old. Only returns undefined in the
// case that no batches have been submitted for an entire challenge period.
if (lo === totalOutputs) {
return undefined
} else {
return lo
}
}
export * from './service'
export * from './helpers'
This diff is collapsed.
import { L2ChainID } from '@eth-optimism/sdk'
// TODO: Consider moving to `@eth-optimism/constants` and generating from superchain registry.
// @see https://github.com/ethereum-optimism/optimism/pull/9041
/**
* Mapping of L2ChainIDs to the L1 block numbers where the wd-mon service should start looking for
* withdrawals by default. L1 block numbers here are based on the block number in which the
* OptimismPortal proxy contract was deployed to L1.
*/
export const DEFAULT_STARTING_BLOCK_NUMBERS: {
[ChainID in L2ChainID]?: number
} = {
[L2ChainID.OPTIMISM]: 17365802 as const,
[L2ChainID.OPTIMISM_GOERLI]: 8299684 as const,
[L2ChainID.OPTIMISM_SEPOLIA]: 4071248 as const,
[L2ChainID.BASE_MAINNET]: 17482143 as const,
[L2ChainID.BASE_GOERLI]: 8411116 as const,
[L2ChainID.BASE_SEPOLIA]: 4370901 as const,
[L2ChainID.ZORA_MAINNET]: 17473938 as const,
}
This diff is collapsed.
export * from '../internal/balance-mon/service'
export * from './fault-mon/index'
export * from '../internal/multisig-mon/service'
export * from './wd-mon/service'
export * from './faultproof-wd-mon/service'
export * from '../contrib/wallet-mon/service'
export * from '../contrib/initialized-upgraded-mon/service'
import { L2ChainID } from '@eth-optimism/sdk'
// TODO: Consider moving to `@eth-optimism/constants` and generating from superchain registry.
// @see https://github.com/ethereum-optimism/optimism/pull/9041
/**
* Mapping of L2ChainIDs to the L1 block numbers where the wd-mon service should start looking for
* withdrawals by default. L1 block numbers here are based on the block number in which the
* OptimismPortal proxy contract was deployed to L1.
*/
export const DEFAULT_STARTING_BLOCK_NUMBERS: {
[ChainID in L2ChainID]?: number
} = {
[L2ChainID.OPTIMISM]: 17365802 as const,
[L2ChainID.OPTIMISM_GOERLI]: 8299684 as const,
[L2ChainID.OPTIMISM_SEPOLIA]: 4071248 as const,
[L2ChainID.BASE_MAINNET]: 17482143 as const,
[L2ChainID.BASE_GOERLI]: 8411116 as const,
[L2ChainID.BASE_SEPOLIA]: 4370901 as const,
[L2ChainID.ZORA_MAINNET]: 17473938 as const,
}
This diff is collapsed.
import hre from 'hardhat'
import '@nomiclabs/hardhat-ethers'
import { Contract, utils } from 'ethers'
import { toRpcHexString } from '@eth-optimism/core-utils'
import Artifact__L2OutputOracle from '@eth-optimism/contracts-bedrock/forge-artifacts/L2OutputOracle.sol/L2OutputOracle.json'
import Artifact__Proxy from '@eth-optimism/contracts-bedrock/forge-artifacts/Proxy.sol/Proxy.json'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from './setup'
import {
findOutputForIndex,
findFirstUnfinalizedOutputIndex,
} from '../../src/fault-mon'
describe('helpers', () => {
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 L2OutputOracle: Contract
let Proxy: Contract
beforeEach(async () => {
const Factory__Proxy = new hre.ethers.ContractFactory(
Artifact__Proxy.abi,
Artifact__Proxy.bytecode.object,
signer
)
Proxy = await Factory__Proxy.deploy(signer.address)
const Factory__L2OutputOracle = new hre.ethers.ContractFactory(
Artifact__L2OutputOracle.abi,
Artifact__L2OutputOracle.bytecode.object,
signer
)
const L2OutputOracleImplementation = await Factory__L2OutputOracle.deploy()
await Proxy.upgradeToAndCall(
L2OutputOracleImplementation.address,
L2OutputOracleImplementation.interface.encodeFunctionData('initialize', [
deployConfig.l2OutputOracleSubmissionInterval,
deployConfig.l2BlockTime,
deployConfig.l2OutputOracleStartingBlockNumber,
deployConfig.l2OutputOracleStartingTimestamp,
deployConfig.l2OutputOracleProposer,
deployConfig.l2OutputOracleChallenger,
deployConfig.finalizationPeriodSeconds,
])
)
L2OutputOracle = new hre.ethers.Contract(
Proxy.address,
Artifact__L2OutputOracle.abi,
signer
)
})
describe('findOutputForIndex', () => {
describe('when the output exists once', () => {
beforeEach(async () => {
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 output', async () => {
const output = await findOutputForIndex(L2OutputOracle, 0)
expect(output.l2OutputIndex).to.equal(0)
})
})
describe('when the output does not exist', () => {
it('should throw an error', async () => {
await expect(
findOutputForIndex(L2OutputOracle, 0)
).to.eventually.be.rejectedWith('unable to find output for index')
})
})
})
describe('findFirstUnfinalizedIndex', () => {
describe('when the chain is more then FPW seconds old', () => {
beforeEach(async () => {
const latestBlock = await hre.ethers.provider.getBlock('latest')
const params = {
_l2BlockNumber:
deployConfig.l2OutputOracleStartingBlockNumber +
deployConfig.l2OutputOracleSubmissionInterval,
_l1BlockHash: latestBlock.hash,
_l1BlockNumber: latestBlock.number,
}
await L2OutputOracle.proposeL2Output(
utils.formatBytes32String('outputRoot1'),
params._l2BlockNumber,
params._l1BlockHash,
params._l1BlockNumber
)
// Simulate FPW passing
await hre.ethers.provider.send('evm_increaseTime', [
toRpcHexString(deployConfig.finalizationPeriodSeconds * 2),
])
await L2OutputOracle.proposeL2Output(
utils.formatBytes32String('outputRoot2'),
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
)
await L2OutputOracle.proposeL2Output(
utils.formatBytes32String('outputRoot3'),
params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
)
})
it('should find the first batch older than the FPW', async () => {
const first = await findFirstUnfinalizedOutputIndex(
L2OutputOracle,
deployConfig.finalizationPeriodSeconds
)
expect(first).to.equal(1)
})
})
describe('when the chain is less than FPW seconds old', () => {
beforeEach(async () => {
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 L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
)
await L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber +
deployConfig.l2OutputOracleSubmissionInterval * 2,
params._l1BlockHash,
params._l1BlockNumber
)
})
it('should return zero', async () => {
const first = await findFirstUnfinalizedOutputIndex(
L2OutputOracle,
deployConfig.finalizationPeriodSeconds
)
expect(first).to.equal(0)
})
})
describe('when no batches submitted for the entire FPW', () => {
beforeEach(async () => {
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 L2OutputOracle.proposeL2Output(
params._outputRoot,
params._l2BlockNumber + deployConfig.l2OutputOracleSubmissionInterval,
params._l1BlockHash,
params._l1BlockNumber
)
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(deployConfig.finalizationPeriodSeconds * 2),
])
// Mine a block to force timestamp to update
await hre.ethers.provider.send('hardhat_mine', ['0x1'])
})
it('should return undefined', async () => {
const first = await findFirstUnfinalizedOutputIndex(
L2OutputOracle,
deployConfig.finalizationPeriodSeconds
)
expect(first).to.equal(undefined)
})
})
})
})
import chai = require('chai')
import chaiAsPromised from 'chai-as-promised'
// Chai plugins go here.
chai.use(chaiAsPromised)
const should = chai.should()
const expect = chai.expect
export { should, expect }
{
"compilerOptions": {
"outDir": "./dist",
"skipLibCheck": true,
"module": "commonjs",
"target": "es2017",
"sourceMap": true,
"esModuleInterop": true,
"composite": true,
"resolveJsonModule": true,
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"typeRoots": [
"node_modules/@types"
]
},
"exclude": [
"node_modules",
"dist"
],
"include": [
"package.json",
"src/abi/IGnosisSafe.0.8.19.json",
"src/abi/OptimismPortal.json",
"src/**/*",
"contrib/**/*",
"internal/**/*"
]
}
......@@ -84,58 +84,6 @@ importers:
specifier: ^5.5.4
version: 5.5.4
packages/chain-mon:
dependencies:
'@eth-optimism/common-ts':
specifier: ^0.8.9
version: 0.8.9(bufferutil@4.0.8)(utf-8-validate@5.0.7)
'@eth-optimism/contracts-bedrock':
specifier: workspace:*
version: link:../contracts-bedrock
'@eth-optimism/contracts-periphery':
specifier: 1.0.8
version: 1.0.8
'@eth-optimism/core-utils':
specifier: ^0.13.2
version: 0.13.2(bufferutil@4.0.8)(utf-8-validate@5.0.7)
'@eth-optimism/sdk':
specifier: ^3.3.2
version: 3.3.2(bufferutil@4.0.8)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7)
'@types/dateformat':
specifier: ^5.0.0
version: 5.0.0
chai-as-promised:
specifier: ^7.1.1
version: 7.1.1(chai@4.3.10)
dateformat:
specifier: ^4.5.1
version: 4.5.1
dotenv:
specifier: ^16.4.5
version: 16.4.5
ethers:
specifier: ^5.7.2
version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7)
devDependencies:
'@ethersproject/abstract-provider':
specifier: ^5.7.0
version: 5.7.0
'@nomiclabs/hardhat-ethers':
specifier: ^2.2.3
version: 2.2.3(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7))(hardhat@2.20.1(bufferutil@4.0.8)(ts-node@10.9.2(@swc/core@1.4.13)(@types/node@20.14.12)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.7))
'@nomiclabs/hardhat-waffle':
specifier: ^2.0.6
version: 2.0.6(@nomiclabs/hardhat-ethers@2.2.3(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7))(hardhat@2.20.1(bufferutil@4.0.8)(ts-node@10.9.2(@swc/core@1.4.13)(@types/node@20.14.12)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.7)))(@types/sinon-chai@3.2.5)(ethereum-waffle@4.0.10(@ensdomains/ens@0.4.5)(@ensdomains/resolver@0.2.4)(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7))(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7))(typescript@5.5.4))(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.7))(hardhat@2.20.1(bufferutil@4.0.8)(ts-node@10.9.2(@swc/core@1.4.13)(@types/node@20.14.12)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.7))
hardhat:
specifier: ^2.20.1
version: 2.20.1(bufferutil@4.0.8)(ts-node@10.9.2(@swc/core@1.4.13)(@types/node@20.14.12)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.7)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@swc/core@1.4.13)(@types/node@20.14.12)(typescript@5.5.4)
tsx:
specifier: ^4.16.2
version: 4.16.2
packages/contracts-bedrock:
devDependencies:
'@typescript-eslint/eslint-plugin':
......
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