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
# @eth-optimism/drippie-mon
## 0.6.6
### Patch Changes
- Updated dependencies [[`eb454ac72b26211eb8037e7e777315c8f30d994d`](https://github.com/ethereum-optimism/optimism/commit/eb454ac72b26211eb8037e7e777315c8f30d994d)]:
- @eth-optimism/contracts-bedrock@0.17.3
## 0.6.5
### Patch Changes
- Updated dependencies [[`799bc898bfb207e2ccd4b2027e3fb4db4372292b`](https://github.com/ethereum-optimism/optimism/commit/799bc898bfb207e2ccd4b2027e3fb4db4372292b)]:
- @eth-optimism/sdk@3.3.1
## 0.6.4
### Patch Changes
- [#9964](https://github.com/ethereum-optimism/optimism/pull/9964) [`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3) Thanks [@roninjin10](https://github.com/roninjin10)! - Removed only-allow command from package.json
- Updated dependencies [[`8241220898128e1f61064f22dcb6fdd0a5f043c3`](https://github.com/ethereum-optimism/optimism/commit/8241220898128e1f61064f22dcb6fdd0a5f043c3), [`ac5b061dfce6a9817b928a8703be9252daaeeca7`](https://github.com/ethereum-optimism/optimism/commit/ac5b061dfce6a9817b928a8703be9252daaeeca7), [`87093b0e9144a4709f11c7fbd631828847d891f9`](https://github.com/ethereum-optimism/optimism/commit/87093b0e9144a4709f11c7fbd631828847d891f9), [`372bca2257764be33797d67ddca9b53c3dd3c295`](https://github.com/ethereum-optimism/optimism/commit/372bca2257764be33797d67ddca9b53c3dd3c295)]:
- @eth-optimism/common-ts@0.8.9
- @eth-optimism/contracts-bedrock@0.17.2
- @eth-optimism/core-utils@0.13.2
- @eth-optimism/sdk@3.3.0
## 0.6.3
### Patch Changes
- Updated dependencies [[`5fe797f183e502c1c7e91fc1e74dd3cc664ba22e`](https://github.com/ethereum-optimism/optimism/commit/5fe797f183e502c1c7e91fc1e74dd3cc664ba22e), [`3dc129fade77ddf9d45bb4c2ecd34360d1aa838a`](https://github.com/ethereum-optimism/optimism/commit/3dc129fade77ddf9d45bb4c2ecd34360d1aa838a)]:
- @eth-optimism/sdk@3.2.3
## 0.6.2
### Patch Changes
- Updated dependencies [[`3ccd12fe5c8c4c5a6acbf370d474ffa8db816562`](https://github.com/ethereum-optimism/optimism/commit/3ccd12fe5c8c4c5a6acbf370d474ffa8db816562)]:
- @eth-optimism/sdk@3.2.2
## 0.6.1
### Patch Changes
- Updated dependencies [[`a1329f21f33ecafe409990964d3af7bf05a8a756`](https://github.com/ethereum-optimism/optimism/commit/a1329f21f33ecafe409990964d3af7bf05a8a756)]:
- @eth-optimism/sdk@3.2.1
## 0.6.0
### Minor Changes
- [#9334](https://github.com/ethereum-optimism/optimism/pull/9334) [`1ed50c44a5c4fb7244ede3b4c45ea7bbf144c1e5`](https://github.com/ethereum-optimism/optimism/commit/1ed50c44a5c4fb7244ede3b4c45ea7bbf144c1e5) Thanks [@smartcontracts](https://github.com/smartcontracts)! - Updates wd-mon inside chain-mon to support FPAC.
### Patch Changes
- Updated dependencies [[`1ed50c44a5c4fb7244ede3b4c45ea7bbf144c1e5`](https://github.com/ethereum-optimism/optimism/commit/1ed50c44a5c4fb7244ede3b4c45ea7bbf144c1e5), [`d99d425a4f73fba19ffcf180deb0ef48ff3b9a6a`](https://github.com/ethereum-optimism/optimism/commit/d99d425a4f73fba19ffcf180deb0ef48ff3b9a6a), [`79effc52e8b82d15b5eda43acf540ac6c5f8d5d7`](https://github.com/ethereum-optimism/optimism/commit/79effc52e8b82d15b5eda43acf540ac6c5f8d5d7), [`73a748575e7c3d67c293814a12bf41eee216163c`](https://github.com/ethereum-optimism/optimism/commit/73a748575e7c3d67c293814a12bf41eee216163c), [`44a2d9cec5f3b309b723b3e4dd8d29b5b70f1cc8`](https://github.com/ethereum-optimism/optimism/commit/44a2d9cec5f3b309b723b3e4dd8d29b5b70f1cc8)]:
- @eth-optimism/common-ts@0.8.8
- @eth-optimism/sdk@3.2.0
- @eth-optimism/contracts-bedrock@0.17.1
## 0.5.7
### Patch Changes
- Updated dependencies [[`18becd7e4`](https://github.com/ethereum-optimism/optimism/commit/18becd7e457577c105f6bc03597e069334cb7433)]:
- @eth-optimism/sdk@3.1.8
## 0.5.6
### Patch Changes
- Updated dependencies [[`6ec80fd19`](https://github.com/ethereum-optimism/optimism/commit/6ec80fd19d9155b17a0873672fb095d323f6e8fb)]:
- @eth-optimism/sdk@3.1.7
## 0.5.5
### Patch Changes
- [#8306](https://github.com/ethereum-optimism/optimism/pull/8306) [`dcb252917`](https://github.com/ethereum-optimism/optimism/commit/dcb25291768ec0f2386486619971d5cd66fbb409) Thanks [@protolambda](https://github.com/protolambda)! - Fixed bug with custom chains not being able to set a custom portal address
## 0.5.4
### Patch Changes
- Updated dependencies [[`dd0e46986`](https://github.com/ethereum-optimism/optimism/commit/dd0e46986f19dcceb304fc48f2bd410685ecd179)]:
- @eth-optimism/sdk@3.1.6
## 0.5.3
### Patch Changes
- Updated dependencies [[`2534eabb5`](https://github.com/ethereum-optimism/optimism/commit/2534eabb50afe76f176407f83cc1f1c606e6de69)]:
- @eth-optimism/sdk@3.1.5
## 0.5.2
### Patch Changes
- [#7824](https://github.com/ethereum-optimism/optimism/pull/7824) [`98eb885f5`](https://github.com/ethereum-optimism/optimism/commit/98eb885f5003ee5e6b9bbd532a42bba2ad39cb4b) Thanks [@roninjin10](https://github.com/roninjin10)! - Bump node version to LTS node 20.9.0
## 0.5.1
### Patch Changes
- [#7450](https://github.com/ethereum-optimism/optimism/pull/7450) [`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e) Thanks [@roninjin10](https://github.com/roninjin10)! - Updated dev dependencies related to testing that is causing audit tooling to report failures
- Updated dependencies [[`ac90e16a7`](https://github.com/ethereum-optimism/optimism/commit/ac90e16a7f85c4f73661ae6023135c3d00421c1e)]:
- @eth-optimism/common-ts@0.8.7
- @eth-optimism/contracts-bedrock@0.16.2
- @eth-optimism/core-utils@0.13.1
- @eth-optimism/sdk@3.1.4
## 0.5.0
### Minor Changes
- [#7178](https://github.com/ethereum-optimism/optimism/pull/7178) [`85d1622df`](https://github.com/ethereum-optimism/optimism/commit/85d1622dfdc16f220f7df0be42ba8cbc5dea31c5) Thanks [@tynes](https://github.com/tynes)! - Use node.js v18
### Patch Changes
- Updated dependencies [[`210b2c81d`](https://github.com/ethereum-optimism/optimism/commit/210b2c81dd383bad93480aa876b283d9a0c991c2), [`679207751`](https://github.com/ethereum-optimism/optimism/commit/6792077510fd76553c179d8b8d068262cda18db6), [`2440f5e7a`](https://github.com/ethereum-optimism/optimism/commit/2440f5e7ab6577f2d2e9c8b0c78c014290dde8e7)]:
- @eth-optimism/core-utils@0.13.0
- @eth-optimism/sdk@3.1.3
- @eth-optimism/contracts-bedrock@0.16.1
- @eth-optimism/common-ts@0.8.6
## 0.4.4
### Patch Changes
- Updated dependencies [[`9c3a03855`](https://github.com/ethereum-optimism/optimism/commit/9c3a03855dc982f0b4e1d664e83271883536632b), [`33eb63b10`](https://github.com/ethereum-optimism/optimism/commit/33eb63b10559a2267c814eda8129447c72940839)]:
- @eth-optimism/sdk@3.1.2
- @eth-optimism/common-ts@0.8.5
## 0.4.3
### Patch Changes
- [#6796](https://github.com/ethereum-optimism/optimism/pull/6796) [`a196c63ad`](https://github.com/ethereum-optimism/optimism/commit/a196c63ad67de04c4143e0ccd6fe4dc27fb2833b) Thanks [@roninjin10](https://github.com/roninjin10)! - Upgraded npm dependencies to latest
- Updated dependencies [[`dfa309e34`](https://github.com/ethereum-optimism/optimism/commit/dfa309e3430ebc8790b932554dde120aafc4161e)]:
- @eth-optimism/core-utils@0.12.3
- @eth-optimism/common-ts@0.8.4
- @eth-optimism/sdk@3.1.1
## 0.4.2
### Patch Changes
- [#6469](https://github.com/ethereum-optimism/optimism/pull/6469) [`0c769680e`](https://github.com/ethereum-optimism/optimism/commit/0c769680e44208c086deef2f9c03c37da2b536fe) Thanks [@maurelian](https://github.com/maurelian)! - Update language in fault-mon from batches to outputs
## 0.4.1
### Patch Changes
- [#6206](https://github.com/ethereum-optimism/optimism/pull/6206) [`3969730dc`](https://github.com/ethereum-optimism/optimism/commit/3969730dc938947a7105c27989e53d4b5cf788a9) Thanks [@tynes](https://github.com/tynes)! - Update import path for artifact
- Updated dependencies [[`a666c4f20`](https://github.com/ethereum-optimism/optimism/commit/a666c4f2082253abbb68c0678e5a0a1ed0c00f4b), [`ff577455f`](https://github.com/ethereum-optimism/optimism/commit/ff577455f196b5f5b8a889339b845561ca6c538a), [`89ca741a6`](https://github.com/ethereum-optimism/optimism/commit/89ca741a63c5e07f9d691bb6f7a89f7718fc49ca), [`c11039060`](https://github.com/ethereum-optimism/optimism/commit/c11039060bc037a88916c2cba602687b6d69ad1a), [`72d184854`](https://github.com/ethereum-optimism/optimism/commit/72d184854ebad8b2025641f126ed76573b1f0ac3), [`77da6edc6`](https://github.com/ethereum-optimism/optimism/commit/77da6edc643e0b5e39f7b6bb41c3c7ead418a876), [`3f13fd0bb`](https://github.com/ethereum-optimism/optimism/commit/3f13fd0bbea051a4550f1df6def1a53a616aa6f6), [`639163253`](https://github.com/ethereum-optimism/optimism/commit/639163253a5e2128f1c21c446b68d358d38cbd30)]:
- @eth-optimism/sdk@3.1.0
- @eth-optimism/contracts-bedrock@0.16.0
- @eth-optimism/core-utils@0.12.2
- @eth-optimism/common-ts@0.8.3
## 0.4.0
### Minor Changes
- d6388be4a: Added a new service wallet-mon to identify unexpected transfers from key accounts
### Patch Changes
- 287d317d3: Fixed an issue with logging the wrong timestamp.
- Updated dependencies [8d7dcc70c]
- Updated dependencies [119754c2f]
- Updated dependencies [d6388be4a]
- Updated dependencies [af292562f]
- @eth-optimism/core-utils@0.12.1
- @eth-optimism/sdk@3.0.0
- @eth-optimism/contracts-bedrock@0.15.0
- @eth-optimism/common-ts@0.8.2
## 0.3.1
### Patch Changes
- a26c484af: Fixes a bug in the wd-mon service where a node connection failure event was not handled correctly
- Updated dependencies [2129dafa3]
- Updated dependencies [a1b7ff9e3]
- Updated dependencies [8133872ed]
- Updated dependencies [afc2ab8c9]
- Updated dependencies [188d1e930]
- Updated dependencies [5063a69fb]
- Updated dependencies [aa854bdd8]
- @eth-optimism/contracts-periphery@1.0.8
- @eth-optimism/sdk@2.1.0
## 0.3.0
### Minor Changes
- 1e7897c81: Introduces the balance-mon service to chain-mon.
### Patch Changes
- dbe5eb308: Empty patch release to re-release packages that failed to be released by a bug in the release process.
- Updated dependencies [be3315689]
- @eth-optimism/sdk@2.0.2
## 0.2.1
### Patch Changes
- Updated dependencies [fecd42d67]
- Updated dependencies [66cafc00a]
- @eth-optimism/common-ts@0.8.1
- @eth-optimism/sdk@2.0.1
## 0.2.0
### Minor Changes
- 282bda091: Added a withdrawal monitoring service
### Patch Changes
- Updated dependencies [cb19e2f9c]
- @eth-optimism/sdk@2.0.0
## 0.1.3
### Patch Changes
- @eth-optimism/sdk@1.10.4
## 0.1.2
### Patch Changes
- @eth-optimism/sdk@1.10.3
## 0.1.1
### Patch Changes
- 0515a7841: Added withdrawal monitoring to identify proven withdrawals not included in the L2ToL1MessagePasser's sentMessages mapping
- Updated dependencies [0e179781b]
- Updated dependencies [5372c9f5b]
- Updated dependencies [4ae94b412]
- @eth-optimism/common-ts@0.8.0
- @eth-optimism/sdk@1.10.2
## 0.4.3
### Patch Changes
- Updated dependencies [f04e5db2d]
- Updated dependencies [4c64a5811]
- @eth-optimism/common-ts@0.7.1
- @eth-optimism/contracts-periphery@1.0.7
- @eth-optimism/sdk@1.10.1
## 0.4.2
### Patch Changes
- Updated dependencies [3f4b3c328]
- @eth-optimism/sdk@1.10.0
## 0.4.1
### Patch Changes
- Updated dependencies [0222215f6]
- @eth-optimism/contracts-periphery@1.0.6
- @eth-optimism/sdk@1.9.1
## 0.4.0
### Minor Changes
- 9b2891852: Refactors BaseServiceV2 slightly, merges standard options with regular options
### Patch Changes
- ab8ec365c: Updates BaseServiceV2 so that options are secret by default. Services will have to explicitly mark options as "public" for those options to be logged and included in the metadata metric.
- Updated dependencies [fe8f2afd0]
- Updated dependencies [e23f60f63]
- Updated dependencies [886fec5bb]
- Updated dependencies [596d51852]
- Updated dependencies [ab8ec365c]
- Updated dependencies [c12aeb2f9]
- Updated dependencies [a610b4f3b]
- Updated dependencies [55515ba14]
- Updated dependencies [ba8b94a60]
- Updated dependencies [9b2891852]
- Updated dependencies [d1f9098f9]
- Updated dependencies [c6c9c7dbf]
- Updated dependencies [bf5f9febd]
- Updated dependencies [ffcee1013]
- Updated dependencies [9a996a13c]
- Updated dependencies [09924e8ed]
- Updated dependencies [746ce5545]
- Updated dependencies [eceb0de1d]
- Updated dependencies [0e0546a11]
- @eth-optimism/contracts-periphery@1.0.5
- @eth-optimism/common-ts@0.7.0
- @eth-optimism/sdk@1.9.0
## 0.3.24
### Patch Changes
- 1d3c749a2: Bumps the version of ts-node used
- Updated dependencies [1d3c749a2]
- Updated dependencies [767585b07]
- Updated dependencies [c975c9620]
- Updated dependencies [136ea1785]
- @eth-optimism/contracts-periphery@1.0.4
- @eth-optimism/sdk@1.8.0
- @eth-optimism/core-utils@0.12.0
- @eth-optimism/common-ts@0.6.8
## 0.3.23
### Patch Changes
- Updated dependencies [f49b71d50]
- Updated dependencies [1bfe79f20]
- @eth-optimism/contracts-periphery@1.0.3
- @eth-optimism/sdk@1.7.0
## 0.3.22
### Patch Changes
- 97b5f578c: Fixes how versions are imported for BaseServiceV2 services
- @eth-optimism/sdk@1.6.11
## 0.3.21
### Patch Changes
- Updated dependencies [1e76cdb86]
- @eth-optimism/core-utils@0.11.0
- @eth-optimism/common-ts@0.6.7
- @eth-optimism/sdk@1.6.10
## 0.3.20
### Patch Changes
- @eth-optimism/sdk@1.6.9
## 0.3.19
### Patch Changes
- @eth-optimism/sdk@1.6.8
## 0.3.18
### Patch Changes
- Updated dependencies [b40913b1]
- Updated dependencies [a5e715c3]
- Updated dependencies [e81a6ff5]
- Updated dependencies [a3242d4f]
- Updated dependencies [ffa5297e]
- @eth-optimism/sdk@1.6.7
- @eth-optimism/contracts-periphery@1.0.2
## 0.3.17
### Patch Changes
- Updated dependencies [02c457a5]
- Updated dependencies [ce7da914]
- Updated dependencies [d3fe9b6d]
- Updated dependencies [220ad4ef]
- Updated dependencies [5d86ff0e]
- @eth-optimism/contracts-periphery@1.0.1
- @eth-optimism/common-ts@0.6.6
- @eth-optimism/sdk@1.6.6
## 0.3.16
### Patch Changes
- Updated dependencies [e2faaa8b]
- Updated dependencies [5c3f2b1f]
- Updated dependencies [3883f34b]
- @eth-optimism/sdk@1.6.5
- @eth-optimism/contracts-periphery@1.0.0
## 0.3.15
### Patch Changes
- 7215f4ce: Bump ethers to 5.7.0 globally
- 0ceff8b8: Drippie Spearbit audit fix for issue #25, reorder DripStatus enum for clarity
- Updated dependencies [7215f4ce]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [206f6033]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [d7679ca4]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- Updated dependencies [0ceff8b8]
- @eth-optimism/common-ts@0.6.5
- @eth-optimism/contracts-periphery@0.2.4
- @eth-optimism/core-utils@0.10.1
- @eth-optimism/sdk@1.6.4
## 0.3.14
### Patch Changes
- @eth-optimism/sdk@1.6.3
## 0.3.13
### Patch Changes
- Updated dependencies [cfa81f88]
- @eth-optimism/sdk@1.6.2
## 0.3.12
### Patch Changes
- Updated dependencies [f4bf4f52]
- Updated dependencies [b27d0fa7]
- Updated dependencies [dbfea116]
- @eth-optimism/contracts-periphery@0.2.3
- @eth-optimism/sdk@1.6.1
- @eth-optimism/core-utils@0.10.0
- @eth-optimism/common-ts@0.6.4
## 0.3.11
### Patch Changes
- Updated dependencies [ea371af2]
- Updated dependencies [3df66a9a]
- Updated dependencies [8323407f]
- Updated dependencies [3af9c7a9]
- Updated dependencies [aa2949ef]
- Updated dependencies [a1a73e64]
- Updated dependencies [f53c30b9]
- @eth-optimism/contracts-periphery@0.2.2
- @eth-optimism/sdk@1.6.0
## 0.3.10
### Patch Changes
- Updated dependencies [dcd715a6]
- @eth-optimism/sdk@1.5.0
## 0.3.9
### Patch Changes
- Updated dependencies [0df744f6]
- Updated dependencies [8ae39154]
- Updated dependencies [f05ab6b6]
- Updated dependencies [dac4a9f0]
- @eth-optimism/core-utils@0.9.3
- @eth-optimism/sdk@1.4.0
- @eth-optimism/common-ts@0.6.3
## 0.3.8
### Patch Changes
- Updated dependencies [0bf3b9b4]
- Updated dependencies [93d3bd41]
- Updated dependencies [680714c1]
- Updated dependencies [8d26459b]
- Updated dependencies [4477fe9f]
- Updated dependencies [29830750]
- Updated dependencies [bcfd1edc]
- Updated dependencies [0bf3b9b4]
- @eth-optimism/core-utils@0.9.2
- @eth-optimism/contracts-periphery@0.2.1
- @eth-optimism/sdk@1.3.1
- @eth-optimism/common-ts@0.6.2
## 0.3.7
### Patch Changes
- Updated dependencies [032f7214]
- @eth-optimism/sdk@1.3.0
## 0.3.6
### Patch Changes
- Updated dependencies [95fc3fbf]
- Updated dependencies [019657db]
- Updated dependencies [6ff5c0a3]
- Updated dependencies [119f0e97]
- Updated dependencies [9c8b1f00]
- Updated dependencies [8a335b7b]
- Updated dependencies [f9fee446]
- Updated dependencies [89d01f2e]
- @eth-optimism/contracts-periphery@0.2.0
- @eth-optimism/core-utils@0.9.1
- @eth-optimism/sdk@1.2.1
- @eth-optimism/common-ts@0.6.1
## 0.3.5
### Patch Changes
- Updated dependencies [3799bb6f]
- @eth-optimism/contracts-periphery@0.1.5
## 0.3.4
### Patch Changes
- Updated dependencies [977493bc]
- Updated dependencies [700dcbb0]
- Updated dependencies [3d1cb720]
- @eth-optimism/sdk@1.2.0
- @eth-optimism/core-utils@0.9.0
- @eth-optimism/common-ts@0.6.0
## 0.3.3
### Patch Changes
- 7142175a: Fix release
- Updated dependencies [cb71fcde]
- Updated dependencies [10e41522]
- Updated dependencies [9aa8049c]
- @eth-optimism/common-ts@0.5.0
- @eth-optimism/contracts-periphery@0.1.4
## 0.3.2
### Patch Changes
- 29ff7462: Revert es target back to 2017
- Updated dependencies [c201f3f1]
- Updated dependencies [da1633a3]
- Updated dependencies [61a30273]
- Updated dependencies [a320e744]
- Updated dependencies [29ff7462]
- Updated dependencies [604dd315]
- Updated dependencies [52b26878]
- @eth-optimism/common-ts@0.4.0
- @eth-optimism/contracts-periphery@0.1.3
- @eth-optimism/core-utils@0.8.7
- @eth-optimism/sdk@1.1.9
## 0.3.1
### Patch Changes
- Updated dependencies [9ba869a7]
- Updated dependencies [050859fd]
- @eth-optimism/common-ts@0.3.1
## 0.3.0
### Minor Changes
- 84a8934c: BaseServiceV2 exposes service name and version as standard synthetic metric
### Patch Changes
- Updated dependencies [d9e39931]
- Updated dependencies [84a8934c]
- @eth-optimism/common-ts@0.3.0
## 0.2.0
### Minor Changes
- 982cb980: Release drippie-mon
### Patch Changes
- Updated dependencies [e0b89fcd]
- Updated dependencies [982cb980]
- Updated dependencies [9142adc4]
- Updated dependencies [9ecbf3e5]
- @eth-optimism/contracts-periphery@0.1.2
- @eth-optimism/common-ts@0.2.10
- @eth-optimism/sdk@1.1.8
(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 source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
# @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'
import {
BaseServiceV2,
StandardOptions,
ExpressRouter,
Gauge,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import {
BedrockOutputData,
getChainId,
sleep,
toRpcHexString,
} from '@eth-optimism/core-utils'
import { config } from 'dotenv'
import {
CONTRACT_ADDRESSES,
CrossChainMessenger,
getOEContract,
L2ChainID,
OEL1ContractsLike,
} from '@eth-optimism/sdk'
import { Provider } from '@ethersproject/abstract-provider'
import { Contract, ethers } from 'ethers'
import dateformat from 'dateformat'
import { version } from '../../package.json'
import { findFirstUnfinalizedOutputIndex, findOutputForIndex } from './helpers'
type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
startOutputIndex: number
optimismPortalAddress?: string
}
type Metrics = {
highestOutputIndex: Gauge
isCurrentlyMismatched: Gauge
nodeConnectionFailures: Gauge
}
type State = {
faultProofWindow: number
outputOracle: Contract
messenger: CrossChainMessenger
currentOutputIndex: number
diverged: boolean
}
export class FaultDetector extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options & StandardOptions>) {
super({
version,
name: 'fault-detector',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
},
startOutputIndex: {
validator: validators.num,
default: -1,
desc: 'The L2 height to start from',
public: true,
},
optimismPortalAddress: {
validator: validators.str,
default: ethers.constants.AddressZero,
desc: '[Custom OP Chains] Deployed OptimismPortal contract address. Used to retrieve necessary info for output verification ',
public: true,
},
},
metricsSpec: {
highestOutputIndex: {
type: Gauge,
desc: 'Highest output indices (checked and known)',
labels: ['type'],
},
isCurrentlyMismatched: {
type: Gauge,
desc: '0 if state is ok, 1 if state is mismatched',
},
nodeConnectionFailures: {
type: Gauge,
desc: 'Number of times node connection has failed',
labels: ['layer', 'section'],
},
},
})
}
/**
* Provides the required set of addresses used by the fault detector. For recognized op-chains, this
* will fallback to the pre-defined set of addresses from options, otherwise aborting if unset.
*
* Required Contracts
* - OptimismPortal (used to also fetch L2OutputOracle address variable). This is the preferred address
* since in early versions of bedrock, OptimismPortal holds the FINALIZATION_WINDOW variable instead of L2OutputOracle.
* The retrieved L2OutputOracle address from OptimismPortal is used to query for output roots.
*
* @param l2ChainId op chain id
* @returns OEL1ContractsLike set of L1 contracts with only the required addresses set
*/
async getOEL1Contracts(l2ChainId: number): Promise<OEL1ContractsLike> {
// CrossChainMessenger requires all address to be defined. Default to `AddressZero` to ignore unused contracts
let contracts: OEL1ContractsLike = {
OptimismPortal: ethers.constants.AddressZero,
L2OutputOracle: ethers.constants.AddressZero,
// Unused contracts
AddressManager: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
L1CrossDomainMessenger: ethers.constants.AddressZero,
L1StandardBridge: ethers.constants.AddressZero,
StateCommitmentChain: ethers.constants.AddressZero,
}
const knownChainId = L2ChainID[l2ChainId] !== undefined
if (knownChainId) {
this.logger.info(`Recognized L2 chain id ${L2ChainID[l2ChainId]}`)
// fallback to the predefined defaults for this chain id
contracts = CONTRACT_ADDRESSES[l2ChainId].l1
}
this.logger.info('checking contract address options...')
const portalAddress = this.options.optimismPortalAddress
if (!knownChainId && portalAddress === ethers.constants.AddressZero) {
this.logger.error('OptimismPortal contract unspecified')
throw new Error(
'--optimismportalcontractaddress needs to set for custom op chains'
)
}
if (portalAddress !== ethers.constants.AddressZero) {
this.logger.info('set OptimismPortal contract override')
contracts.OptimismPortal = portalAddress
this.logger.info('fetching L2OutputOracle contract from OptimismPortal')
const portalContract = getOEContract('OptimismPortal', l2ChainId, {
address: portalAddress,
signerOrProvider: this.options.l1RpcProvider,
})
contracts.L2OutputOracle = await portalContract.l2Oracle()
}
// ... for a known chain ids without an override, the L2OutputOracle will already
// be set via the hardcoded default
return contracts
}
async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.l1RpcProvider, {
logger: this.logger,
name: 'L1',
})
// Connect to L2.
await waitForProvider(this.options.l2RpcProvider, {
logger: this.logger,
name: 'L2',
})
const l1ChainId = await getChainId(this.options.l1RpcProvider)
const l2ChainId = await getChainId(this.options.l2RpcProvider)
this.state.messenger = new CrossChainMessenger({
l1SignerOrProvider: this.options.l1RpcProvider,
l2SignerOrProvider: this.options.l2RpcProvider,
l1ChainId,
l2ChainId,
bedrock: true,
contracts: { l1: await this.getOEL1Contracts(l2ChainId) },
})
// Not diverged by default.
this.state.diverged = false
// We use this a lot, a bit cleaner to pull out to the top level of the state object.
this.state.faultProofWindow =
await this.state.messenger.getChallengePeriodSeconds()
this.logger.info(
`fault proof window is ${this.state.faultProofWindow} seconds`
)
this.state.outputOracle = this.state.messenger.contracts.l1.L2OutputOracle
// Figure out where to start syncing from.
if (this.options.startOutputIndex === -1) {
this.logger.info('finding appropriate starting unfinalized output')
const firstUnfinalized = await findFirstUnfinalizedOutputIndex(
this.state.outputOracle,
this.state.faultProofWindow,
this.logger
)
// We may not have an unfinalized outputs in the case where no outputs have been submitted
// for the entire duration of the FAULTPROOFWINDOW. We generally do not expect this to happen on mainnet,
// but it happens often on testnets because the FAULTPROOFWINDOW is very short.
if (firstUnfinalized === undefined) {
this.logger.info(
'no unfinalized outputes found. skipping all outputes.'
)
const totalOutputes = await this.state.outputOracle.nextOutputIndex()
this.state.currentOutputIndex = totalOutputes.toNumber() - 1
} else {
this.state.currentOutputIndex = firstUnfinalized
}
} else {
this.state.currentOutputIndex = this.options.startOutputIndex
}
this.logger.info('starting output', {
outputIndex: this.state.currentOutputIndex,
})
// Set the initial metrics.
this.metrics.isCurrentlyMismatched.set(0)
}
async routes(router: ExpressRouter): Promise<void> {
router.get('/status', async (req, res) => {
return res.status(200).json({
ok: !this.state.diverged,
})
})
}
async main(): Promise<void> {
const startMs = Date.now()
let latestOutputIndex: number
try {
const totalOutputes = await this.state.outputOracle.nextOutputIndex()
latestOutputIndex = totalOutputes.toNumber() - 1
} catch (err) {
this.logger.error('failed to query total # of outputes', {
error: err,
node: 'l1',
section: 'nextOutputIndex',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'nextOutputIndex',
})
await sleep(15000)
return
}
if (this.state.currentOutputIndex > latestOutputIndex) {
this.logger.info('output index is ahead of the oracle. waiting...', {
outputIndex: this.state.currentOutputIndex,
latestOutputIndex,
})
await sleep(15000)
return
}
this.metrics.highestOutputIndex.set({ type: 'known' }, latestOutputIndex)
this.logger.info('checking output', {
outputIndex: this.state.currentOutputIndex,
latestOutputIndex,
})
let outputData: BedrockOutputData
try {
outputData = await findOutputForIndex(
this.state.outputOracle,
this.state.currentOutputIndex,
this.logger
)
} catch (err) {
this.logger.error('failed to fetch output associated with output', {
error: err,
node: 'l1',
section: 'findOutputForIndex',
outputIndex: this.state.currentOutputIndex,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'findOutputForIndex',
})
await sleep(15000)
return
}
let latestBlock: number
try {
latestBlock = await this.options.l2RpcProvider.getBlockNumber()
} catch (err) {
this.logger.error('failed to query L2 block height', {
error: err,
node: 'l2',
section: 'getBlockNumber',
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlockNumber',
})
await sleep(15000)
return
}
const outputBlockNumber = outputData.l2BlockNumber
if (latestBlock < outputBlockNumber) {
this.logger.info('L2 node is behind, waiting for sync...', {
l2BlockHeight: latestBlock,
outputBlock: outputBlockNumber,
})
return
}
let outputBlock: any
try {
outputBlock = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getBlockByNumber', [toRpcHexString(outputBlockNumber), false])
} catch (err) {
this.logger.error('failed to fetch output block', {
error: err,
node: 'l2',
section: 'getBlock',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getBlock',
})
await sleep(15000)
return
}
let messagePasserProofResponse: any
try {
messagePasserProofResponse = await (
this.options.l2RpcProvider as ethers.providers.JsonRpcProvider
).send('eth_getProof', [
this.state.messenger.contracts.l2.BedrockMessagePasser.address,
[],
toRpcHexString(outputBlockNumber),
])
} catch (err) {
this.logger.error('failed to fetch message passer proof', {
error: err,
node: 'l2',
section: 'getProof',
block: outputBlockNumber,
})
this.metrics.nodeConnectionFailures.inc({
layer: 'l2',
section: 'getProof',
})
await sleep(15000)
return
}
const outputRoot = ethers.utils.solidityKeccak256(
['uint256', 'bytes32', 'bytes32', 'bytes32'],
[
0,
outputBlock.stateRoot,
messagePasserProofResponse.storageHash,
outputBlock.hash,
]
)
if (outputRoot !== outputData.outputRoot) {
this.state.diverged = true
this.metrics.isCurrentlyMismatched.set(1)
this.logger.error('state root mismatch', {
blockNumber: outputBlock.number,
expectedStateRoot: outputData.outputRoot,
actualStateRoot: outputRoot,
finalizationTime: dateformat(
new Date(
(ethers.BigNumber.from(outputBlock.timestamp).toNumber() +
this.state.faultProofWindow) *
1000
),
'mmmm dS, yyyy, h:MM:ss TT'
),
})
return
}
const elapsedMs = Date.now() - startMs
// Mark the current output index as checked
this.logger.info('checked output ok', {
outputIndex: this.state.currentOutputIndex,
timeMs: elapsedMs,
})
this.metrics.highestOutputIndex.set(
{ type: 'checked' },
this.state.currentOutputIndex
)
// If we got through the above without throwing an error, we should be
// fine to reset and move onto the next output
this.state.diverged = false
this.state.currentOutputIndex++
this.metrics.isCurrentlyMismatched.set(0)
}
}
if (require.main === module) {
config()
const service = new FaultDetector()
service.run()
}
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,
}
import {
BaseServiceV2,
StandardOptions,
ExpressRouter,
Gauge,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import {
getOEContract,
DEFAULT_L2_CONTRACT_ADDRESSES,
makeStateTrieProof,
toJsonRpcProvider,
} from '@eth-optimism/sdk'
import { getChainId, sleep, toRpcHexString } from '@eth-optimism/core-utils'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import dateformat from 'dateformat'
import { version } from '../../package.json'
import { DEFAULT_STARTING_BLOCK_NUMBERS } from './constants'
type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
optimismPortalAddress: string
l2ToL1MessagePasserAddress: string
startBlockNumber: number
eventBlockRange: number
sleepTimeMs: number
}
type Metrics = {
highestCheckedBlockNumber: Gauge
highestKnownBlockNumber: Gauge
highestCheckedBlockTimestamp: Gauge
highestKnownBlockTimestamp: Gauge
withdrawalsValidated: Gauge
invalidProposalWithdrawals: Gauge
invalidProofWithdrawals: Gauge
isDetectingForgeries: Gauge
nodeConnectionFailures: Gauge
}
type State = {
portal: ethers.Contract
messenger: ethers.Contract
highestUncheckedBlockNumber: number
faultProofWindow: number
forgeryDetected: boolean
invalidProposalWithdrawals: Array<{
withdrawalHash: string
senderAddress: string
disputeGame: ethers.Contract
event: ethers.Event
}>
invalidProofWithdrawals: Array<{
withdrawalHash: string
senderAddress: string
disputeGame: ethers.Contract
event: ethers.Event
}>
}
enum GameStatus {
// The game is currently in progress, and has not been resolved.
IN_PROGRESS,
// The game has concluded, and the `rootClaim` was challenged successfully.
CHALLENGER_WINS,
// The game has concluded, and the `rootClaim` could not be contested.
DEFENDER_WINS,
}
export class FaultProofWithdrawalMonitor extends BaseServiceV2<
Options,
Metrics,
State
> {
/**
* Contract objects attached to their respective providers and addresses.
*/
public l2ChainId: number
constructor(options?: Partial<Options & StandardOptions>) {
super({
version,
name: 'two-step-monitor',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
},
optimismPortalAddress: {
validator: validators.address,
default: null,
desc: 'Address of the OptimismPortal proxy contract on L1',
public: true,
},
l2ToL1MessagePasserAddress: {
validator: validators.address,
default: DEFAULT_L2_CONTRACT_ADDRESSES.BedrockMessagePasser as string,
desc: 'Address of the L2ToL1MessagePasser contract on L2',
public: true,
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
eventBlockRange: {
validator: validators.num,
default: 2000,
desc: 'Number of blocks to query for events over per loop',
public: true,
},
sleepTimeMs: {
validator: validators.num,
default: 15000,
desc: 'Time in ms to sleep when waiting for a node',
public: true,
},
},
metricsSpec: {
highestCheckedBlockNumber: {
type: Gauge,
desc: 'Highest L1 block number that we have searched.',
labels: ['type'],
},
highestCheckedBlockTimestamp: {
type: Gauge,
desc: 'Timestamp of the highest L1 block number that we have searched.',
labels: ['type'],
},
highestKnownBlockNumber: {
type: Gauge,
desc: 'Highest L1 block number that we have seen.',
labels: ['type'],
},
highestKnownBlockTimestamp: {
type: Gauge,
desc: 'Timestamp of the highest L1 block number that we have seen.',
labels: ['type'],
},
invalidProposalWithdrawals: {
type: Gauge,
desc: 'Number of withdrawals against invalid proposals.',
labels: ['type'],
},
invalidProofWithdrawals: {
type: Gauge,
desc: 'Number of withdrawals with invalid proofs.',
labels: ['type'],
},
withdrawalsValidated: {
type: Gauge,
desc: 'Latest L1 Block (checked and known)',
labels: ['type'],
},
isDetectingForgeries: {
type: Gauge,
desc: '0 if state is ok. 1 or more if forged withdrawals are detected.',
},
nodeConnectionFailures: {
type: Gauge,
desc: 'Number of times node connection has failed',
labels: ['layer', 'section'],
},
},
})
}
async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.l1RpcProvider, {
logger: this.logger,
name: 'L1',
})
// Connect to L2.
await waitForProvider(this.options.l2RpcProvider, {
logger: this.logger,
name: 'L2',
})
// Need L2 chain ID to resolve contract addresses.
const l2ChainId = await getChainId(this.options.l2RpcProvider)
this.l2ChainId = l2ChainId
// Create the OptimismPortal contract instance. If the optimismPortal option is not provided
// then the SDK will attempt to resolve the address automatically based on the L2 chain ID. If
// the SDK isn't aware of the L2 chain ID then it will throw an error that makes it clear the
// user needs to provide this value explicitly.
this.state.portal = getOEContract('OptimismPortal2', l2ChainId, {
signerOrProvider: this.options.l1RpcProvider,
address: this.options.optimismPortalAddress,
})
// Create the L2ToL1MessagePasser contract instance. If the l2ToL1MessagePasser option is not
// provided then we'll use the default address which typically should be correct. It's very
// unlikely that any user would change this address so this should work in 99% of cases. If we
// really wanted to be extra safe we could do some sanity checks to make sure the contract has
// the interface we need but doesn't seem important for now.
this.state.messenger = getOEContract('L2ToL1MessagePasser', l2ChainId, {
signerOrProvider: this.options.l2RpcProvider,
address: this.options.l2ToL1MessagePasserAddress,
})
// Previous versions of wd-mon would try to pick the starting block number automatically but
// this had the possibility of missing certain withdrawals if the service was restarted at the
// wrong time. Given the added complexity of finding a starting point automatically after FPAC,
// it's much easier to simply start a fixed block number than trying to do something fancy. Use
// the default configured in this service or use zero if no default is defined.
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
if (this.options.startBlockNumber === -1) {
this.state.highestUncheckedBlockNumber =
DEFAULT_STARTING_BLOCK_NUMBERS[l2ChainId] || 0
}
this.logger.info(
`initialized starting at block number ${this.state.highestUncheckedBlockNumber}`
)
//make sure the highestUncheckedBlockNumber is not higher than the latest block number on chain
const latestL1BlockNumber =
await this.options.l1RpcProvider.getBlockNumber()
this.state.highestUncheckedBlockNumber = Math.min(
this.state.highestUncheckedBlockNumber,
latestL1BlockNumber
)
this.logger.info(
`starting at block number ${this.state.highestUncheckedBlockNumber}`
)
// Default state is that forgeries have not been detected.
this.state.forgeryDetected = false
this.state.invalidProposalWithdrawals = []
this.state.invalidProofWithdrawals = []
}
// K8s healthcheck
async routes(router: ExpressRouter): Promise<void> {
router.get('/healthz', async (req, res) => {
return res.status(200).json({
ok: !this.state.forgeryDetected,
})
})
}
async main(): Promise<void> {
this.metrics.isDetectingForgeries.set(Number(this.state.forgeryDetected))
this.metrics.invalidProposalWithdrawals.set(
this.state.invalidProposalWithdrawals.length
)
this.metrics.invalidProofWithdrawals.set(
this.state.invalidProofWithdrawals.length
)
for (
let i = this.state.invalidProposalWithdrawals.length - 1;
i >= 0;
i--
) {
const disputeGameData = this.state.invalidProposalWithdrawals[i]
const disputeGame = disputeGameData.disputeGame
const disputeGameAddress = disputeGame.address
const isGameBlacklisted =
this.state.portal.disputeGameBlacklist(disputeGameAddress)
const event = disputeGameData.event
const block = await event.getBlock()
const ts =
dateformat(
new Date(block.timestamp * 1000),
'mmmm dS, yyyy, h:MM:ss TT',
true
) + ' UTC'
if (isGameBlacklisted) {
this.state.invalidProposalWithdrawals.splice(i, 1)
} else {
const status = await disputeGame.status()
if (status === GameStatus.CHALLENGER_WINS) {
this.state.invalidProposalWithdrawals.splice(i, 1)
this.logger.info(
`withdrawalHash not seen on L2 - game correctly resolved`,
{
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
disputeGameAddress: disputeGame.address,
blockNumber: block.number,
transaction: event.transactionHash,
}
)
} else if (status === GameStatus.DEFENDER_WINS) {
this.logger.error(
`withdrawalHash not seen on L2 - forgery detected`,
{
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
disputeGameAddress: disputeGame.address,
blockNumber: block.number,
transaction: event.transactionHash,
}
)
this.state.forgeryDetected = true
this.metrics.isDetectingForgeries.set(
Number(this.state.forgeryDetected)
)
} else {
this.logger.warn(
`withdrawalHash not seen on L2 - game still IN_PROGRESS`,
{
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
disputeGameAddress: disputeGame.address,
blockNumber: block.number,
transaction: event.transactionHash,
}
)
}
}
}
let latestL1BlockNumber: number
let latestL1Block: ethers.providers.Block
let highestUncheckedBlock: ethers.providers.Block
try {
// Get the latest L1 block number.
latestL1BlockNumber = await this.options.l1RpcProvider.getBlockNumber()
latestL1Block = await this.options.l1RpcProvider.getBlock(
latestL1BlockNumber
)
highestUncheckedBlock = await this.options.l1RpcProvider.getBlock(
this.state.highestUncheckedBlockNumber
)
} catch (err) {
// Log the issue so we can debug it.
this.logger.error(`got error when connecting to node`, {
error: err,
node: 'l1',
section: 'getBlockNumber',
})
// Increment the metric so we can detect the issue.
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'getBlockNumber',
})
// Sleep for a little to give intermittent errors a chance to recover.
return sleep(this.options.sleepTimeMs)
}
// Update highest block number metrics so we can keep track of how the service is doing.
this.metrics.highestCheckedBlockNumber.set(
{ type: 'L1' },
this.state.highestUncheckedBlockNumber
)
this.metrics.highestKnownBlockNumber.set(
{ type: 'L1' },
latestL1BlockNumber
)
this.metrics.highestCheckedBlockTimestamp.set(
{ type: 'L1' },
highestUncheckedBlock.timestamp
)
this.metrics.highestKnownBlockTimestamp.set(
{ type: 'L1' },
latestL1Block.timestamp
)
// Check if the RPC provider is behind us for some reason. Can happen occasionally,
// particularly if connected to an RPC provider that load balances over multiple nodes that
// might not be perfectly in sync.
if (latestL1BlockNumber <= this.state.highestUncheckedBlockNumber) {
// Sleep for a little to give the RPC a chance to catch up.
return sleep(this.options.sleepTimeMs)
}
// Generally better to use a relatively small block range because it means this service can be
// used alongside many different types of L1 nodes. For instance, Geth will typically only
// support a block range of 2000 blocks out of the box.
const toBlockNumber = Math.min(
this.state.highestUncheckedBlockNumber + this.options.eventBlockRange,
latestL1BlockNumber
)
// Useful to log this stuff just in case we get stuck or something.
this.logger.info(`checking recent blocks`, {
fromBlockNumber: this.state.highestUncheckedBlockNumber,
toBlockNumber,
latestL1BlockNumber,
percentageDone:
Math.floor((toBlockNumber / latestL1BlockNumber) * 100) + '% done',
})
// Query for WithdrawalProven events within the specified block range.
let events: ethers.Event[]
try {
events = await this.state.portal.queryFilter(
this.state.portal.filters.WithdrawalProven(),
this.state.highestUncheckedBlockNumber,
toBlockNumber
)
} catch (err) {
// Log the issue so we can debug it.
this.logger.error(`got error when connecting to node`, {
error: err,
node: 'l1',
section: 'querying for WithdrawalProven events',
})
// Increment the metric so we can detect the issue.
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'querying for WithdrawalProven events',
})
// Sleep for a little to give intermittent errors a chance to recover.
return sleep(this.options.sleepTimeMs)
}
// Go over all the events and check if the withdrawal hash actually exists on L2.
for (const event of events) {
// If this loop throws for whatever reason then the same event may be dropped into
// invalidProposalWithdrawals or invalidProofWithdrawals more than once. This can lead to
// inflated metrics. However, it's worth noting that inflated metrics are preferred over not
// incrementing the metrics at all. Documenting this behavior for future reference.
// Grab and format the timestamp for logging purposes.
const block = await event.getBlock()
const ts = `${dateformat(
new Date(block.timestamp * 1000),
'mmmm dS, yyyy, h:MM:ss TT',
true
)} UTC`
// Could consider using multicall here but this is efficient enough for now.
const hash = event.args.withdrawalHash
const disputeGamesData = await this.getWithdrawalDisputeGames(event)
for (const disputeGameData of disputeGamesData) {
const disputeGame = disputeGameData.disputeGame
const rootClaim = await disputeGame.rootClaim()
const l2BlockNumber = await disputeGame.l2BlockNumber()
const isValidRoot = await this.isValidOutputRoot(
rootClaim,
l2BlockNumber
)
if (isValidRoot) {
// Check if the withdrawal exists on L2.
const exists = await this.state.messenger.sentMessages(hash)
// Hopefully the withdrawal exists!
if (exists) {
// Unlike below we don't grab the timestamp here because it adds an unnecessary request.
this.logger.info(`valid withdrawal`, {
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
disputeGameAddress: disputeGame.address,
blockNumber: block.number,
transaction: event.transactionHash,
})
// Bump the withdrawals metric so we can keep track.
this.metrics.withdrawalsValidated.inc()
} else {
this.state.invalidProofWithdrawals.push(disputeGameData)
// Uh oh!
this.logger.error(
`withdrawalHash not seen on L2 - forgery detected`,
{
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
disputeGameAddress: disputeGame.address,
blockNumber: block.number,
transaction: event.transactionHash,
}
)
// Change to forgery state.
this.state.forgeryDetected = true
this.metrics.isDetectingForgeries.set(
Number(this.state.forgeryDetected)
)
}
} else {
this.state.invalidProposalWithdrawals.push(disputeGameData)
this.logger.warn(`invalid proposal`, {
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
disputeGameAddress: disputeGame.address,
})
}
}
}
// Increment the highest unchecked block number for the next loop.
this.state.highestUncheckedBlockNumber = toBlockNumber
}
/**
* Retrieves the dispute games data associated with a withdrawal hash associated in an event.
*
* @param event The event containing the withdrawal hash.
* @returns An array of objects containing the withdrawal hash, sender address, and dispute game address.
*/
async getWithdrawalDisputeGames(event_in: ethers.Event): Promise<
Array<{
withdrawalHash: string
senderAddress: string
disputeGame: ethers.Contract
event: ethers.Event
}>
> {
const withdrawalHash = event_in.args.withdrawalHash
const disputeGameMap: Array<{
withdrawalHash: string
senderAddress: string
disputeGame: ethers.Contract
event: ethers.Event
}> = []
const numProofSubmitter = await this.state.portal.numProofSubmitters(
withdrawalHash
)
// iterate for numProofSubmitter
const proofSubmitterAddresses = await Promise.all(
Array.from({ length: numProofSubmitter.toNumber() }, (_, i) =>
this.state.portal.proofSubmitters(withdrawalHash, i)
)
)
// Iterate for proofSubmitterAddresses and query provenWithdrawals to get the disputeGameProxy for each proofSubmitter
// Note: In the future, if rate limiting becomes an issue, consider breaking up this loop into smaller chunks.
await Promise.all(
proofSubmitterAddresses.map(async (proofSubmitter) => {
const provenWithdrawals_ = await this.state.portal.provenWithdrawals(
withdrawalHash,
proofSubmitter
)
const disputeGame_ = await this.getDisputeGameFromAddress(
provenWithdrawals_['disputeGameProxy']
)
disputeGameMap.push({
withdrawalHash,
senderAddress: proofSubmitter,
disputeGame: disputeGame_,
event: event_in,
})
})
)
return disputeGameMap
}
/**
* Retrieves the FaultDisputeGame contract instance given the dispute game proxy address.
*
* @param disputeGameProxyAddress The address of the dispute game proxy contract.
* @returns The FaultDisputeGame contract instance.
*/
async getDisputeGameFromAddress(
disputeGameProxyAddress: string
): Promise<ethers.Contract> {
// Create the FaultDisputeGame contract instance using the provided dispute game proxy address.
const FaultDisputeGame = getOEContract('FaultDisputeGame', this.l2ChainId, {
signerOrProvider: this.options.l1RpcProvider,
address: disputeGameProxyAddress,
})
return FaultDisputeGame
}
/**
* A private cache to store the validity of output roots.
* The cache is implemented as a Map, where the key is a combination of the output root and the L2 block number,
* and the value is a boolean indicating if the output root is valid.
*/
private outputRootCache: Map<string, boolean> = new Map<string, boolean>()
/**
* The maximum size of the output root cache.
* Once the cache reaches this size, the oldest entries will be automatically evicted to make room for new entries.
*/
private MAX_CACHE_SIZE = 100
/**
* Checks if the provided output root is valid for the given L2 block number.
* Caches the result to improve performance.
*
* @param outputRoot The output root to validate.
* @param l2BlockNumber The L2 block number.
* @returns A promise that resolves to a boolean indicating if the output root is valid.
*/
public async isValidOutputRoot(
outputRoot: string,
l2BlockNumber: number
): Promise<boolean> {
const cacheKey = `${outputRoot}-${l2BlockNumber}`
const cachedValue = this.outputRootCache.get(cacheKey)
if (cachedValue !== undefined) {
return cachedValue
}
try {
// Make sure this is a JSON RPC provider.
const provider = toJsonRpcProvider(this.options.l2RpcProvider)
// Grab the block and storage proof at the same time.
const [block, proof] = await Promise.all([
provider.send('eth_getBlockByNumber', [
toRpcHexString(l2BlockNumber),
false,
]),
makeStateTrieProof(
provider,
l2BlockNumber,
this.state.messenger.address,
ethers.constants.HashZero
),
])
// Compute the output.
const output = ethers.utils.solidityKeccak256(
['bytes32', 'bytes32', 'bytes32', 'bytes32'],
[
ethers.constants.HashZero,
block.stateRoot,
proof.storageRoot,
block.hash,
]
)
// If the output matches the proposal then we're good.
const valid = output === outputRoot
this.outputRootCache.set(cacheKey, valid)
if (this.outputRootCache.size > this.MAX_CACHE_SIZE) {
const oldestKey = this.outputRootCache.keys().next().value
this.outputRootCache.delete(oldestKey)
}
return valid
} catch (err) {
// Assume the game is invalid but don't add it to the cache just in case we had a temp error.
return false
}
}
}
if (require.main === module) {
const service = new FaultProofWithdrawalMonitor()
service.run()
}
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,
}
import {
BaseServiceV2,
StandardOptions,
ExpressRouter,
Gauge,
validators,
waitForProvider,
} from '@eth-optimism/common-ts'
import { getOEContract, DEFAULT_L2_CONTRACT_ADDRESSES } from '@eth-optimism/sdk'
import { getChainId, sleep } from '@eth-optimism/core-utils'
import { Provider } from '@ethersproject/abstract-provider'
import { ethers } from 'ethers'
import dateformat from 'dateformat'
import { version } from '../../package.json'
import { DEFAULT_STARTING_BLOCK_NUMBERS } from './constants'
type Options = {
l1RpcProvider: Provider
l2RpcProvider: Provider
optimismPortalAddress: string
l2ToL1MessagePasserAddress: string
startBlockNumber: number
eventBlockRange: number
sleepTimeMs: number
}
type Metrics = {
highestBlockNumber: Gauge
withdrawalsValidated: Gauge
isDetectingForgeries: Gauge
nodeConnectionFailures: Gauge
detectedForgeries: Gauge
}
type State = {
portal: ethers.Contract
messenger: ethers.Contract
highestUncheckedBlockNumber: number
faultProofWindow: number
forgeryDetected: boolean
}
export class WithdrawalMonitor extends BaseServiceV2<Options, Metrics, State> {
constructor(options?: Partial<Options & StandardOptions>) {
super({
version,
name: 'two-step-monitor',
loop: true,
options: {
loopIntervalMs: 1000,
...options,
},
optionsSpec: {
l1RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L1',
},
l2RpcProvider: {
validator: validators.provider,
desc: 'Provider for interacting with L2',
},
optimismPortalAddress: {
validator: validators.address,
default: null,
desc: 'Address of the OptimismPortal proxy contract on L1',
public: true,
},
l2ToL1MessagePasserAddress: {
validator: validators.address,
default: DEFAULT_L2_CONTRACT_ADDRESSES.BedrockMessagePasser as string,
desc: 'Address of the L2ToL1MessagePasser contract on L2',
public: true,
},
startBlockNumber: {
validator: validators.num,
default: -1,
desc: 'L1 block number to start checking from',
public: true,
},
eventBlockRange: {
validator: validators.num,
default: 2000,
desc: 'Number of blocks to query for events over per loop',
public: true,
},
sleepTimeMs: {
validator: validators.num,
default: 15000,
desc: 'Time in ms to sleep when waiting for a node',
public: true,
},
},
metricsSpec: {
highestBlockNumber: {
type: Gauge,
desc: 'Highest block number (checked and known)',
labels: ['type'],
},
withdrawalsValidated: {
type: Gauge,
desc: 'Latest L1 Block (checked and known)',
labels: ['type'],
},
isDetectingForgeries: {
type: Gauge,
desc: '0 if state is ok. 1 or more if forged withdrawals are detected.',
},
nodeConnectionFailures: {
type: Gauge,
desc: 'Number of times node connection has failed',
labels: ['layer', 'section'],
},
detectedForgeries: {
type: Gauge,
desc: 'detected forged withdrawals',
labels: ['withdrawalHash', 'provenAt', 'blockNumber', 'transaction'],
},
},
})
}
async init(): Promise<void> {
// Connect to L1.
await waitForProvider(this.options.l1RpcProvider, {
logger: this.logger,
name: 'L1',
})
// Connect to L2.
await waitForProvider(this.options.l2RpcProvider, {
logger: this.logger,
name: 'L2',
})
// Need L2 chain ID to resolve contract addresses.
const l2ChainId = await getChainId(this.options.l2RpcProvider)
// Create the OptimismPortal contract instance. If the optimismPortal option is not provided
// then the SDK will attempt to resolve the address automatically based on the L2 chain ID. If
// the SDK isn't aware of the L2 chain ID then it will throw an error that makes it clear the
// user needs to provide this value explicitly.
this.state.portal = getOEContract('OptimismPortal', l2ChainId, {
signerOrProvider: this.options.l1RpcProvider,
address: this.options.optimismPortalAddress,
})
// Create the L2ToL1MessagePasser contract instance. If the l2ToL1MessagePasser option is not
// provided then we'll use the default address which typically should be correct. It's very
// unlikely that any user would change this address so this should work in 99% of cases. If we
// really wanted to be extra safe we could do some sanity checks to make sure the contract has
// the interface we need but doesn't seem important for now.
this.state.messenger = getOEContract('L2ToL1MessagePasser', l2ChainId, {
signerOrProvider: this.options.l2RpcProvider,
address: this.options.l2ToL1MessagePasserAddress,
})
// Previous versions of wd-mon would try to pick the starting block number automatically but
// this had the possibility of missing certain withdrawals if the service was restarted at the
// wrong time. Given the added complexity of finding a starting point automatically after FPAC,
// it's much easier to simply start a fixed block number than trying to do something fancy. Use
// the default configured in this service or use zero if no default is defined.
this.state.highestUncheckedBlockNumber = this.options.startBlockNumber
if (this.options.startBlockNumber === -1) {
this.state.highestUncheckedBlockNumber =
DEFAULT_STARTING_BLOCK_NUMBERS[l2ChainId] || 0
}
// Default state is that forgeries have not been detected.
this.state.forgeryDetected = false
}
// K8s healthcheck
async routes(router: ExpressRouter): Promise<void> {
router.get('/healthz', async (req, res) => {
return res.status(200).json({
ok: !this.state.forgeryDetected,
})
})
}
async main(): Promise<void> {
// Get the latest L1 block number.
let latestL1BlockNumber: number
try {
latestL1BlockNumber = await this.options.l1RpcProvider.getBlockNumber()
} catch (err) {
// Log the issue so we can debug it.
this.logger.error(`got error when connecting to node`, {
error: err,
node: 'l1',
section: 'getBlockNumber',
})
// Increment the metric so we can detect the issue.
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'getBlockNumber',
})
// Sleep for a little to give intermittent errors a chance to recover.
return sleep(this.options.sleepTimeMs)
}
// Update highest block number metrics so we can keep track of how the service is doing.
this.metrics.highestBlockNumber.set({ type: 'known' }, latestL1BlockNumber)
this.metrics.highestBlockNumber.set(
{ type: 'checked' },
this.state.highestUncheckedBlockNumber
)
// Check if the RPC provider is behind us for some reason. Can happen occasionally,
// particularly if connected to an RPC provider that load balances over multiple nodes that
// might not be perfectly in sync.
if (latestL1BlockNumber <= this.state.highestUncheckedBlockNumber) {
// Sleep for a little to give the RPC a chance to catch up.
return sleep(this.options.sleepTimeMs)
}
// Generally better to use a relatively small block range because it means this service can be
// used alongside many different types of L1 nodes. For instance, Geth will typically only
// support a block range of 2000 blocks out of the box.
const toBlockNumber = Math.min(
this.state.highestUncheckedBlockNumber + this.options.eventBlockRange,
latestL1BlockNumber
)
// Useful to log this stuff just in case we get stuck or something.
this.logger.info(`checking recent blocks`, {
fromBlockNumber: this.state.highestUncheckedBlockNumber,
toBlockNumber,
latestL1BlockNumber,
percentageDone:
Math.floor((toBlockNumber / latestL1BlockNumber) * 100) + '% done',
})
// Query for WithdrawalProven events within the specified block range.
let events: ethers.Event[]
try {
events = await this.state.portal.queryFilter(
this.state.portal.filters.WithdrawalProven(),
this.state.highestUncheckedBlockNumber,
toBlockNumber
)
} catch (err) {
// Log the issue so we can debug it.
this.logger.error(`got error when connecting to node`, {
error: err,
node: 'l1',
section: 'querying for WithdrawalProven events',
})
// Increment the metric so we can detect the issue.
this.metrics.nodeConnectionFailures.inc({
layer: 'l1',
section: 'querying for WithdrawalProven events',
})
// Sleep for a little to give intermittent errors a chance to recover.
return sleep(this.options.sleepTimeMs)
}
// Go over all the events and check if the withdrawal hash actually exists on L2.
for (const event of events) {
// Could consider using multicall here but this is efficient enough for now.
const hash = event.args.withdrawalHash
const exists = await this.state.messenger.sentMessages(hash)
const block = await event.getBlock()
const ts = `${dateformat(
new Date(block.timestamp * 1000),
'mmmm dS, yyyy, h:MM:ss TT',
true
)} UTC`
// Hopefully the withdrawal exists!
if (exists) {
// Unlike below we don't grab the timestamp here because it adds an unnecessary request.
this.logger.info(`valid withdrawal`, {
withdrawalHash: event.args.withdrawalHash,
provenAt: ts,
blockNumber: block.number,
transaction: event.transactionHash,
})
// Bump the withdrawals metric so we can keep track.
this.metrics.withdrawalsValidated.inc()
} else {
// Grab and format the timestamp so it's clear how much time is left.
// Uh oh!
this.logger.error(`withdrawalHash not seen on L2`, {
withdrawalHash: event.args.withdrawalHash,
provenAt:
dateformat(
new Date(block.timestamp * 1000),
'mmmm dS, yyyy, h:MM:ss TT',
true
) + ' UTC',
blockNumber: block.number.toString(),
transaction: event.transactionHash,
})
// Change to forgery state.
this.state.forgeryDetected = true
this.metrics.isDetectingForgeries.set(1)
this.metrics.detectedForgeries.inc({
withdrawalHash: hash,
provenAt: ts,
blockNumber: block.number.toString(),
transaction: event.transactionHash,
})
}
}
// Increment the highest unchecked block number for the next loop.
this.state.highestUncheckedBlockNumber = toBlockNumber
// If we got through the above without throwing an error, we should be fine to reset. Only case
// where this is relevant is if something is detected as a forgery accidentally and the error
// doesn't happen again on the next loop.
this.state.forgeryDetected = false
this.metrics.isDetectingForgeries.set(0)
}
}
if (require.main === module) {
const service = new WithdrawalMonitor()
service.run()
}
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