├── .circleci
└── config.yml
├── .github
├── CODEOWNERS
└── weekly-digest.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── bootstrap.sh
├── docker-bake.hcl
├── git-hooks
└── pre-push
├── install-hooks.sh
├── op-defender
├── Dockerfile
├── Makefile
├── README.md
├── cmd
│ └── defender
│ │ ├── cli.go
│ │ └── main.go
├── defender.go
├── go.mod
├── go.sum
└── psp_executor
│ ├── README.md
│ ├── api_test.go
│ ├── cli.go
│ ├── defender.go
│ ├── simulator.go
│ └── utils.go
└── op-monitorism
├── Dockerfile
├── Makefile
├── balances
├── README.md
├── cli.go
└── monitor.go
├── cmd
└── monitorism
│ ├── cli.go
│ └── main.go
├── conservation_monitor
├── README.md
├── cli.go
└── monitor.go
├── drippie
├── README.md
├── bindings
│ └── drippie.go
├── cli.go
└── monitor.go
├── fault
├── README.md
├── binding
│ ├── BINDING.md
│ ├── L2OutputOracle.go
│ └── OptimismPortal.go
├── cli.go
└── monitor.go
├── faultproof_withdrawals
├── .env.op.mainnet.example
├── .env.op.sepolia.example
├── .gitignore
├── README.md
├── bindings
│ ├── BINDING.md
│ ├── dispute
│ │ ├── DisputeGameFactory.go
│ │ └── FaultDisputeGame.go
│ └── l1
│ │ └── OptimismPortal2.go
├── cli.go
├── monitor.go
├── monitor_live_mainnet_test.go
├── monitor_live_sepolia_test.go
├── runbooks
│ ├── RUNBOOK.md
│ └── automated
│ │ ├── .env.example
│ │ ├── .gitignore
│ │ ├── Makefile
│ │ ├── README.md
│ │ ├── abi
│ │ ├── FaultDisputeGame.json
│ │ ├── L2ToL1MessagePasser.json
│ │ ├── OptimismPortal.json
│ │ └── OptimismPortal2.json
│ │ ├── lib
│ │ ├── __init__.py
│ │ ├── superchain.py
│ │ └── web3.py
│ │ ├── requirements.txt
│ │ ├── runbooks
│ │ └── lib
│ │ │ └── web3.py
│ │ ├── triage_detection_stalled.ipynb
│ │ └── triage_potential_attack_event.ipynb
├── state.go
└── validator
│ ├── dispute_game_factory_helper.go
│ ├── fault_dispute_game_helper.go
│ ├── l1_proxy.go
│ ├── l2_proxy.go
│ ├── optimism_portal2_helper.go
│ ├── proven_withdrawal_validator.go
│ └── utils.go
├── global_events
├── README.md
├── cli.go
├── monitor.go
├── monitor_test.go
├── rules
│ ├── rules_mainnet_L1
│ │ └── rules_TEMPLATE_COPY_PASTE.yaml
│ └── rules_sepolia_L1
│ │ └── rules_TEMPLATE_COPY_PASTE.yaml
├── types.go
└── types_test.go
├── go.mod
├── go.sum
├── liveness_expiration
├── README.md
├── bindings
│ ├── GnosisSafe.go
│ ├── LivenessGuard.go
│ └── LivenessModul.go
├── cli.go
└── monitor.go
├── monitorism.go
├── multisig
├── README.md
├── bindings
│ ├── BINDING.md
│ ├── L2OutputOracle.go
│ └── OptimismPortal.go
├── cli.go
└── monitor.go
├── processor
└── processor.go
├── secrets
├── README.md
├── bindings
│ ├── checksecrets.go
│ └── drippie.go
├── cli.go
└── monitor.go
├── transaction_monitor
├── README.md
├── bindings
│ └── dispute
│ │ ├── DisputeGame.go
│ │ └── DisputeGameFactory.go
├── checks.go
├── cli.go
├── monitor.go
└── monitor_test.go
└── withdrawals
├── README.md
├── bindings
├── BINDING.md
├── L2ToL1MessagePasser.go
└── OptimismPortal.go
├── cli.go
└── monitor.go
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @ethereum-optimism/security-reviewers @raffaele-oplabs
2 | op-monitorism/drippie @ethereum-optimism/go-reviewers
3 |
--------------------------------------------------------------------------------
/.github/weekly-digest.yml:
--------------------------------------------------------------------------------
1 | # Configuration for weekly-digest - https://github.com/apps/weekly-digest
2 | publishDay: sun
3 | canPublishIssues: true
4 | canPublishPullRequests: true
5 | canPublishContributors: true
6 | canPublishStargazers: true
7 | canPublishCommits: true
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | results
4 | temp
5 | .nyc_output
6 | coverage.json
7 | *.tsbuildinfo
8 | **/lcov.info
9 |
10 | yarn-error.log
11 | .yarn/*
12 | !.yarn/releases
13 | !.yarn/plugins
14 | .pnp.*
15 |
16 | dist
17 | artifacts
18 | cache
19 |
20 | packages/contracts-bedrock/deployments/devnetL1
21 | packages/contracts-bedrock/deployments/anvil
22 |
23 | # vim
24 | *.sw*
25 |
26 | # jetbrains
27 | .idea/
28 |
29 | .secrets
30 | .env
31 | .env*
32 | !.env.example
33 | !.envrc.example
34 | *.log
35 |
36 | .devnet
37 |
38 | # Ignore local fuzzing results
39 | **/testdata/fuzz/
40 |
41 | coverage.out
42 |
43 | # Ignore bedrock go bindings local output files
44 | op-bindings/bin
45 |
46 | __pycache__
47 |
48 | # Ignore echidna artifacts
49 | crytic-export
50 |
51 | .vscode
52 | .vscode/*
53 | !.vscode/settings.json
54 | !.vscode/tasks.json
55 | !.vscode/launch.json
56 | !.vscode/extensions.json
57 | !.vscode/*.code-snippets
58 |
59 | # Local History for Visual Studio Code
60 | .history/
61 |
62 | # Built Visual Studio Code Extensions
63 | *.vsix
64 |
65 | bin
66 | op-defender/.air.toml
67 | op-defender/run.sh
68 | op-defender/tmp
69 |
70 | local
71 |
72 | .venv
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 |
3 | ### Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ### Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ### Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ### Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ### Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at contributing@optimism.io. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ### Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version].
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 🎈 Thanks for your help improving the project! We are so happy to have you!
4 |
5 | 🚨 Before making any non-trivial change, please first open an issue describing the change to solicit feedback and guidance. This will increase the likelihood of the PR getting merged.
6 |
7 | In general, the smaller the diff the easier it will be for us to review quickly.
8 |
9 | Please note that we have a [Code of Conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
10 |
11 | # Pull Request Process
12 |
13 | ## Steps for the PR author
14 |
15 | We recommend using the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format on commit messages.
16 |
17 | Before opening a PR:
18 |
19 | 1. Ensure that tests pass and code is lint free. You can run `yarn test` and `yarn lint` locally to check.
20 | 1. Update the README.md if your changes invalidate or extend its current content.
21 | 1. Include tests for any new functionality.
22 | 1. Ensure each section of the [Pull Request Template](https://github.com/ethereum-optimism/.github/blob/master/PULL_REQUEST_TEMPLATE.md) is filled out. Delete any sections that are not relevant.
23 |
24 | Unless your PR is ready for immediate review and merging, please mark it as 'draft' (or simply do not open a PR yet).
25 |
26 | **Bonus:** Add comments to the diff under the "Files Changed" tab on the PR page to clarify any sections where you think we might have questions about the approach taken.
27 |
28 | ### Response time:
29 |
30 | We aim to provide a meaningful response to all PRs and issues from external contributors within 2 business days.
31 |
32 | ## Steps for PR Reviewers
33 |
34 | ### For all PRs
35 |
36 | Reviewers should submit their review with either `Approve` or `Request changes` options (not `Comment`).
37 |
38 | If the reviewer selects `Request changes`, they should clearly indicate which changes they require in order to approve.
39 |
40 | If the reviewer selects `Approve`, they should either:
41 |
42 | 1. immediately merge it, or
43 | 2. indicate what further review they deem necessary (and from whom).
44 |
45 | We further distinguish between two classes of PR those which modify production code (ie. smart contracts, go-ethereum), and those which do not (ie. dev tooling, test scripts, comments).
46 |
47 | ### For PRs which modify production code
48 |
49 | The reviewer's job is to check that the PR:
50 |
51 | 1. Conforms to the specification (WIP, soon to be published here), or has an issue describing the additional functionality which a code owner has approved.
52 | 2. Is appropriately tested.
53 | 3. Does not introduce security issues.
54 |
55 | #### Merge Criteria
56 |
57 | 1. All CI checks MUST pass.
58 | 1. At least 2 code owners must approve.
59 | 1. In the case of very simple changes, a single code owner may choose to merge at their discretion.
60 |
61 | ### For PRs which modify non-production code
62 |
63 | For PRs which do not modify production code (ie. test, dev tooling),
64 |
65 | The reviewer's job is to check that the PR:
66 |
67 | 1. Is correct.
68 | 1. Is desirable.
69 |
70 | #### Merge Criteria
71 |
72 | 1. All CI checks MUST pass.
73 | 1. At least one code owner must approve.
74 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright 2020-2024 Optimism
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | COMPOSEFLAGS=-d
2 | OP_STACK_GO_BUILDER?=us-docker.pkg.dev/oplabs-tools-artifacts/images/op-stack-go:latest
3 |
4 | build: build-go
5 | .PHONY: build
6 |
7 | build-go: op-monitorism op-defender
8 | .PHONY: build-go
9 |
10 | golang-docker:
11 | # We don't use a buildx builder here, and just load directly into regular docker, for convenience.
12 | GIT_COMMIT=$$(git rev-parse HEAD) \
13 | GIT_DATE=$$(git show -s --format='%ct') \
14 | IMAGE_TAGS=$$(git rev-parse HEAD),latest \
15 | docker buildx bake \
16 | --progress plain \
17 | --load \
18 | -f docker-bake.hcl \
19 | op-monitorism op-defender
20 | .PHONY: golang-docker
21 |
22 | op-monitorism:
23 | make -C ./op-monitorism
24 | .PHONY: op-monitorism
25 |
26 | op-defender:
27 | make -C ./op-defender
28 | .PHONY: op-defender
29 |
30 | op-defender-lint-go: ## Lints Go code with specific linters
31 | cd op-defender && golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./...
32 | .PHONY: op-defender-lint-go
33 |
34 | op-defender-lint-go-fix: ## Lints Go code with specific linters and fixes reported issues
35 | cd op-defender && golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./... --fix
36 | .PHONY: op-defender-lint-go-fix
37 |
38 | op-monitorism-lint-go: ## Lints Go code with specific linters
39 | cd op-monitorism && golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./...
40 | .PHONY: op-monitorism-lint-go
41 |
42 | op-monitorism-lint-go-fix: ## Lints Go code with specific linters and fixes reported issues
43 | cd op-monitorism && golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./... --fix
44 | .PHONY: op-monitorism-lint-go-fix
45 |
46 | tidy:
47 | make -C ./op-monitorism tidy
48 | make -C ./op-defender tidy
49 | .PHONY: tidy
50 |
51 | clean:
52 | rm -rf ./bin
53 | .PHONY: clean
54 |
55 | nuke: clean devnet-clean
56 | git clean -Xdf
57 | .PHONY: nuke
58 |
--------------------------------------------------------------------------------
/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Sets up the development environment, including Git hooks.
3 |
4 | echo "Setting up the development environment..."
5 |
6 | # Install Git hooks
7 | ./install-hooks.sh
8 |
9 | echo "Environment setup complete."
10 |
--------------------------------------------------------------------------------
/docker-bake.hcl:
--------------------------------------------------------------------------------
1 | variable "REGISTRY" {
2 | default = "us-docker.pkg.dev"
3 | }
4 |
5 | variable "REPOSITORY" {
6 | default = "oplabs-tools-artifacts/images"
7 | }
8 |
9 | variable "GIT_COMMIT" {
10 | default = "dev"
11 | }
12 |
13 | variable "GIT_DATE" {
14 | default = "0"
15 | }
16 |
17 | variable "IMAGE_TAGS" {
18 | default = "${GIT_COMMIT}" // split by ","
19 | }
20 |
21 | variable "PLATFORMS" {
22 | // You can override this as "linux/amd64,linux/arm64".
23 | // Only a specify a single platform when `--load` ing into docker.
24 | // Multi-platform is supported when outputting to disk or pushing to a registry.
25 | // Multi-platform builds can be tested locally with: --set="*.output=type=image,push=false"
26 | default = "linux/amd64"
27 | }
28 |
29 | target "op-monitorism" {
30 | dockerfile = "Dockerfile"
31 | context = "./op-monitorism"
32 | args = {
33 | GITCOMMIT = "${GIT_COMMIT}"
34 | GITDATE = "${GIT_DATE}"
35 | }
36 | platforms = split(",", PLATFORMS)
37 | tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/op-monitorism:${tag}"]
38 | }
39 |
40 | target "op-defender" {
41 | dockerfile = "Dockerfile"
42 | context = "./op-defender"
43 | args = {
44 | GITCOMMIT = "${GIT_COMMIT}"
45 | GITDATE = "${GIT_DATE}"
46 | }
47 | platforms = split(",", PLATFORMS)
48 | tags = [for tag in split(",", IMAGE_TAGS) : "${REGISTRY}/${REPOSITORY}/op-defender:${tag}"]
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/git-hooks/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Run go mod tidy
4 | make tidy
5 |
6 | # Check for uncommitted changes in go.mod and go.sum
7 | CHANGED=$(git status --porcelain | grep -E "go.mod|go.sum")
8 | if [ -n "$CHANGED" ]; then
9 | echo "There are changes in go.mod or go.sum after running go mod tidy."
10 | echo "Please commit the changes before pushing."
11 | exit 1
12 | fi
13 |
14 | exit 0
15 |
--------------------------------------------------------------------------------
/install-hooks.sh:
--------------------------------------------------------------------------------
1 | cp git-hooks/* .git/hooks/
2 | chmod +x .git/hooks/*
3 | echo "Git hooks installed."
--------------------------------------------------------------------------------
/op-defender/Dockerfile:
--------------------------------------------------------------------------------
1 | # Define the build stage
2 | FROM golang:1.22.2-alpine3.19 as builder
3 |
4 | # Set the working directory inside the container
5 | WORKDIR /app
6 |
7 | # Install system dependencies including 'make'
8 | RUN apk update && apk add --no-cache make
9 |
10 | # Copy the source code and Makefile into the container
11 | COPY . .
12 |
13 | # Run the Makefile command to build the binary
14 | RUN make
15 |
16 | # Define the final base image
17 | FROM alpine:3.18
18 |
19 | # Copy the built binary from the builder stage
20 | COPY --from=builder /app/bin/defender /usr/local/bin/defender
21 |
22 | # Ensure the binary is executable
23 | RUN chmod +x /usr/local/bin/defender
24 |
25 | # Set the command to run the binary
26 | CMD ["defender"]
27 |
--------------------------------------------------------------------------------
/op-defender/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile
2 |
3 | # Binary name
4 | BINARY=defender
5 |
6 | # Build directory
7 | BUILD_DIR=./bin
8 |
9 | # Source directory
10 | CMD_DIR=./cmd/defender
11 |
12 | # Go commands
13 | GOBUILD=go build
14 | GOCLEAN=go clean
15 | GORUN=go run
16 |
17 | # Default target
18 | .PHONY: all
19 | all: build
20 |
21 | # Build binary
22 | .PHONY: build
23 | build:
24 | @echo "Building..."
25 | $(GOBUILD) -o $(BUILD_DIR)/$(BINARY) $(CMD_DIR)
26 |
27 | # Run program
28 | .PHONY: run
29 | run:
30 | @echo "Running..."
31 | $(GORUN) $(CMD_DIR)/
32 |
33 | # Clean up binaries
34 | .PHONY: clean
35 | clean:
36 | @echo "Cleaning..."
37 | $(GOCLEAN)
38 | rm -f $(BUILD_DIR)/$(BINARY)
39 |
40 | # Run program
41 | .PHONY: tidy
42 | tidy:
43 | @echo "Tidying..."
44 | go mod tidy
45 |
46 | # Help
47 | .PHONY: help
48 | help:
49 | @echo "Makefile commands:"
50 | @echo " make build"
51 | @echo " make run"
52 | @echo " make clean"
53 | @echo " make help"
54 |
--------------------------------------------------------------------------------
/op-defender/README.md:
--------------------------------------------------------------------------------
1 | # Defenders
2 |
3 | _op-defender_ is an active security service allowing to provide automated defense for the OP Stack.
4 |
5 | The following commands are currently available:
6 |
7 | ```bash
8 | NAME:
9 | Defender - OP Stack Automated Defense
10 |
11 | USAGE:
12 | Defender [global options] command [command options]
13 |
14 | VERSION:
15 | 0.1.0-unstable
16 |
17 | DESCRIPTION:
18 | OP Stack Automated Defense
19 |
20 | COMMANDS:
21 | psp_executor Service to execute PSPs through API.
22 | version Show version
23 | help, h Shows a list of commands or help for one command
24 |
25 | GLOBAL OPTIONS:
26 | --help, -h show help
27 | --version, -v print the version
28 | ```
29 |
30 | Each _defender_ has some common configuration, that are configurable both via CLI or environment variables with default values.
31 |
32 | ### PSP Executor Service
33 |
34 |
35 |
36 | The PSP Executor Service is made for executing PSP onchain faster, to increase our readiness and speed in our response.
37 |
38 | | `op-defender/psp_executor` | [README](https://github.com/ethereum-optimism/monitorism/blob/main/op-defender/psp_executor/README.md) |
39 | | -------------------------- | ------------------------------------------------------------------------------------------------------ |
40 |
--------------------------------------------------------------------------------
/op-defender/cmd/defender/cli.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | defender "github.com/ethereum-optimism/monitorism/op-defender"
8 | "github.com/ethereum-optimism/monitorism/op-defender/psp_executor"
9 | "github.com/ethereum-optimism/optimism/op-service/cliapp"
10 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
11 | opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
12 |
13 | executor "github.com/ethereum-optimism/monitorism/op-defender/psp_executor"
14 | "github.com/ethereum/go-ethereum/params"
15 |
16 | "github.com/urfave/cli/v2"
17 | )
18 |
19 | const (
20 | EnvVarPrefix = "DEFENDER"
21 | )
22 |
23 | func newCli(GitCommit string, GitDate string) *cli.App {
24 | defaultFlags := defender.DefaultCLIFlags("DEFENDER")
25 | return &cli.App{
26 | Name: "Defender",
27 | Usage: "OP Stack Automated Defense",
28 | Description: "OP Stack Automated Defense",
29 | EnableBashCompletion: true,
30 | Version: params.VersionWithCommit(GitCommit, GitDate),
31 | Commands: []*cli.Command{
32 | {
33 | Name: "psp_executor",
34 | Usage: "Service to execute PSPs through API.",
35 | Description: "Service to execute PSPs through API.",
36 | Flags: append(psp_executor.CLIFlags("PSPEXECUTOR"), defaultFlags...),
37 | Action: cliapp.LifecycleCmd(PSPExecutorMain),
38 | },
39 | {
40 | Name: "version",
41 | Usage: "Show version",
42 | Description: "Show version",
43 | Action: func(ctx *cli.Context) error {
44 | cli.ShowVersion(ctx)
45 | return nil
46 | },
47 | },
48 | },
49 | }
50 | }
51 |
52 | // PSPExecutorMain() is the entrypoint for the PSPExecutor API HTTP server.
53 | func PSPExecutorMain(ctx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
54 | log := oplog.NewLogger(oplog.AppOut(ctx), oplog.ReadCLIConfig(ctx))
55 | cfg, err := psp_executor.ReadCLIFlags(ctx)
56 | if err != nil {
57 | return nil, fmt.Errorf("Failed to parse psp_executor config from flags: %w", err)
58 | }
59 | if err := cfg.Check(); err != nil {
60 | return nil, err
61 | }
62 |
63 | metricsRegistry := opmetrics.NewRegistry()
64 | executor := &executor.DefenderExecutor{}
65 | defender_thread, err := psp_executor.NewDefender(ctx.Context, log, opmetrics.With(metricsRegistry), cfg, executor)
66 | if err != nil {
67 | return nil, fmt.Errorf("Failed to create psp_executor HTTP API service: %w", err)
68 | }
69 | return defender.NewCliApp(ctx, log, metricsRegistry, defender_thread)
70 | }
71 |
--------------------------------------------------------------------------------
/op-defender/cmd/defender/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/ethereum/go-ethereum/log"
8 |
9 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
10 | "github.com/ethereum-optimism/optimism/op-service/opio"
11 | )
12 |
13 | var (
14 | GitCommit = ""
15 | GitDate = ""
16 | )
17 |
18 | func main() {
19 | oplog.SetupDefaults()
20 | app := newCli(GitCommit, GitDate)
21 |
22 | // sub-commands set up their individual interrupt lifecycles, which can block the given interrupt as needed.
23 | ctx := opio.WithInterruptBlocker(context.Background())
24 | if err := app.RunContext(ctx, os.Args); err != nil {
25 | log.Error("application failed", "err", err)
26 | os.Exit(1)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/op-defender/defender.go:
--------------------------------------------------------------------------------
1 | package defender
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "sync/atomic"
8 |
9 | "github.com/ethereum/go-ethereum/log"
10 |
11 | "github.com/ethereum-optimism/optimism/op-service/cliapp"
12 | "github.com/ethereum-optimism/optimism/op-service/clock"
13 | "github.com/ethereum-optimism/optimism/op-service/httputil"
14 |
15 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
16 | opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
17 |
18 | "github.com/prometheus/client_golang/prometheus"
19 | "github.com/urfave/cli/v2"
20 | )
21 |
22 | type Defender interface {
23 | Run(context.Context)
24 | Close(context.Context) error
25 | }
26 | type cliApp struct {
27 | log log.Logger
28 | stopped atomic.Bool
29 | loopIntervalMs uint64 // loop interval of the main thread.
30 | worker *clock.LoopFn
31 | defender Defender
32 | registry *prometheus.Registry
33 | metricsCfg opmetrics.CLIConfig
34 | metricsSrv *httputil.HTTPServer
35 | }
36 |
37 | func NewCliApp(ctx *cli.Context, log log.Logger, registry *prometheus.Registry, defender Defender) (cliapp.Lifecycle, error) {
38 |
39 | return &cliApp{
40 | log: log,
41 | defender: defender,
42 | registry: registry,
43 | metricsCfg: opmetrics.ReadCLIConfig(ctx),
44 | }, nil
45 | }
46 |
47 | func DefaultCLIFlags(envVarPrefix string) []cli.Flag {
48 | defaultFlags := append(oplog.CLIFlags(envVarPrefix), opmetrics.CLIFlags(envVarPrefix)...)
49 | return defaultFlags
50 | }
51 |
52 | func (app *cliApp) Start(ctx context.Context) error {
53 | if app.worker != nil {
54 | return errors.New("Defender service already running..")
55 | }
56 |
57 | app.log.Info("starting metrics server", "host", app.metricsCfg.ListenAddr, "port", app.metricsCfg.ListenPort)
58 | srv, err := opmetrics.StartServer(app.registry, app.metricsCfg.ListenAddr, app.metricsCfg.ListenPort)
59 | if err != nil {
60 | return fmt.Errorf("failed to start metrics server: %w", err)
61 | }
62 | app.log.Info("Start defender service", "loop_interval_ms", app.loopIntervalMs)
63 |
64 | // Tick to avoid having to wait a full interval on startup
65 | app.defender.Run(ctx)
66 |
67 | app.metricsSrv = srv
68 | return nil
69 | }
70 |
71 | func (app *cliApp) Stop(ctx context.Context) error {
72 | if app.stopped.Load() {
73 | return errors.New("defender already closed")
74 | }
75 |
76 | app.log.Info("closing defender...")
77 | if err := app.worker.Close(); err != nil {
78 | app.log.Error("error stopping worker loop", "err", err)
79 | }
80 | if err := app.defender.Close(ctx); err != nil {
81 | app.log.Error("error closing monitor", "err", err)
82 | }
83 |
84 | if err := app.metricsSrv.Close(); err != nil {
85 | app.log.Error("error closing metrics server", "err", err)
86 | }
87 |
88 | app.stopped.Store(true)
89 | return nil
90 | }
91 |
92 | func (app *cliApp) Stopped() bool {
93 | return app.stopped.Load()
94 | }
95 |
--------------------------------------------------------------------------------
/op-defender/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ethereum-optimism/monitorism/op-defender
2 |
3 | go 1.21.0
4 |
5 | replace github.com/ethereum/go-ethereum v1.13.11 => github.com/ethereum-optimism/op-geth v1.101311.0-rc.1
6 |
7 | require (
8 | github.com/ethereum-optimism/optimism v1.7.3
9 | github.com/ethereum/go-ethereum v1.13.11
10 | github.com/gorilla/mux v1.8.1
11 | github.com/prometheus/client_golang v1.19.0
12 | github.com/urfave/cli/v2 v2.27.1
13 | golang.org/x/crypto v0.21.0
14 | )
15 |
16 | require (
17 | github.com/BurntSushi/toml v1.3.2 // indirect
18 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
19 | github.com/CloudyKit/jet/v6 v6.2.0 // indirect
20 | github.com/DataDog/zstd v1.5.2 // indirect
21 | github.com/Joker/jade v1.1.3 // indirect
22 | github.com/Microsoft/go-winio v0.6.1 // indirect
23 | github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
24 | github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
25 | github.com/andybalholm/brotli v1.0.5 // indirect
26 | github.com/aymerick/douceur v0.2.0 // indirect
27 | github.com/beorn7/perks v1.0.1 // indirect
28 | github.com/bits-and-blooms/bitset v1.10.0 // indirect
29 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
30 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
31 | github.com/cockroachdb/errors v1.11.1 // indirect
32 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
33 | github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4 // indirect
34 | github.com/cockroachdb/redact v1.1.5 // indirect
35 | github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
36 | github.com/consensys/bavard v0.1.13 // indirect
37 | github.com/consensys/gnark-crypto v0.12.1 // indirect
38 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
39 | github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect
40 | github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect
41 | github.com/deckarep/golang-set/v2 v2.1.0 // indirect
42 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
43 | github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240318114348-52d3dbd1605d // indirect
44 | github.com/ethereum/c-kzg-4844 v0.4.0 // indirect
45 | github.com/fatih/structs v1.1.0 // indirect
46 | github.com/flosch/pongo2/v4 v4.0.2 // indirect
47 | github.com/fsnotify/fsnotify v1.7.0 // indirect
48 | github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect
49 | github.com/getsentry/sentry-go v0.18.0 // indirect
50 | github.com/go-ole/go-ole v1.3.0 // indirect
51 | github.com/gofrs/flock v0.8.1 // indirect
52 | github.com/gogo/protobuf v1.3.2 // indirect
53 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
54 | github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
55 | github.com/google/uuid v1.6.0 // indirect
56 | github.com/gorilla/css v1.0.0 // indirect
57 | github.com/gorilla/websocket v1.5.0 // indirect
58 | github.com/holiman/uint256 v1.2.4 // indirect
59 | github.com/iris-contrib/schema v0.0.6 // indirect
60 | github.com/josharian/intern v1.0.0 // indirect
61 | github.com/kataras/blocks v0.0.7 // indirect
62 | github.com/kataras/golog v0.1.9 // indirect
63 | github.com/kataras/iris/v12 v12.2.1 // indirect
64 | github.com/kataras/pio v0.0.12 // indirect
65 | github.com/kataras/sitemap v0.0.6 // indirect
66 | github.com/kataras/tunnel v0.0.4 // indirect
67 | github.com/klauspost/compress v1.17.2 // indirect
68 | github.com/kr/pretty v0.3.1 // indirect
69 | github.com/kr/text v0.2.0 // indirect
70 | github.com/mailgun/raymond/v2 v2.0.48 // indirect
71 | github.com/mailru/easyjson v0.7.7 // indirect
72 | github.com/mattn/go-runewidth v0.0.14 // indirect
73 | github.com/microcosm-cc/bluemonday v1.0.24 // indirect
74 | github.com/mmcloughlin/addchain v0.4.0 // indirect
75 | github.com/olekukonko/tablewriter v0.0.5 // indirect
76 | github.com/pkg/errors v0.9.1 // indirect
77 | github.com/prometheus/client_model v0.5.0 // indirect
78 | github.com/prometheus/common v0.48.0 // indirect
79 | github.com/prometheus/procfs v0.12.0 // indirect
80 | github.com/rivo/uniseg v0.4.3 // indirect
81 | github.com/rogpeppe/go-internal v1.11.0 // indirect
82 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
83 | github.com/schollz/closestmatch v2.1.0+incompatible // indirect
84 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
85 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect
86 | github.com/sirupsen/logrus v1.9.0 // indirect
87 | github.com/supranational/blst v0.3.11 // indirect
88 | github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
89 | github.com/tdewolff/minify/v2 v2.12.7 // indirect
90 | github.com/tdewolff/parse/v2 v2.6.6 // indirect
91 | github.com/tklauser/go-sysconf v0.3.12 // indirect
92 | github.com/tklauser/numcpus v0.6.1 // indirect
93 | github.com/valyala/bytebufferpool v1.0.0 // indirect
94 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
95 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
96 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
97 | github.com/yosssi/ace v0.0.5 // indirect
98 | github.com/yusufpapurcu/wmi v1.2.3 // indirect
99 | golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
100 | golang.org/x/mod v0.14.0 // indirect
101 | golang.org/x/net v0.23.0 // indirect
102 | golang.org/x/sync v0.6.0 // indirect
103 | golang.org/x/sys v0.18.0 // indirect
104 | golang.org/x/term v0.18.0 // indirect
105 | golang.org/x/text v0.14.0 // indirect
106 | golang.org/x/time v0.5.0 // indirect
107 | golang.org/x/tools v0.16.1 // indirect
108 | google.golang.org/protobuf v1.33.0 // indirect
109 | gopkg.in/ini.v1 v1.67.0 // indirect
110 | gopkg.in/yaml.v3 v3.0.1 // indirect
111 | rsc.io/tmplfunc v0.0.3 // indirect
112 | )
113 |
--------------------------------------------------------------------------------
/op-defender/psp_executor/README.md:
--------------------------------------------------------------------------------
1 | ### PSP Executor Service
2 |
3 | The PSP Executor service is a service designed to execute PSP onchain faster to increase our readiness and speed in case of incident response.
4 |
5 | The service is designed to listen on a port and execute a PSP onchain when a request is received.
6 |
7 | ⚠️ The service has to use an authentication method before calling this API ⚠️
8 |
9 | ## 1. Usage
10 |
11 | ### 1. Run HTTP API service
12 |
13 | To start the HTTP API service we can use the following oneliner command:
14 |
15 |
16 | Settings of the HTTP API service:
17 |
18 | | Port | API Path | HTTP Method |
19 | | -------------- | -------------------- | ----------- |
20 | | 8080 (Default) | `/api/psp_execution` | POST |
21 | | 8080 (Default) | `/api/healthcheck` | GET |
22 |
23 | To run the psp_executor service, you can use the following command:
24 |
25 | ```shell
26 | go run ../cmd/defender psp_executor --privatekey 2a[..]c6 --safe.address 0x837DE453AD5F21E89771e3c06239d8236c0EFd5E --path /tmp/psps.json --chainid 11155111 --superchainconfig.address 0xC2Be75506d5724086DEB7245bd260Cc9753911Be --rpc.url http://localhost:8545 --port.api 8080 --tls.ca certs/ca-cert.pem --tls.cert certs/server-cert.pem --tls.key certs/server-key.pem
27 | ```
28 |
29 | Explanation of the options:
30 | | Argument | Value | Explanation |
31 | | ---------------------------- | ------------------------------------------ | ------------------------------------ |
32 | | `--privatekey` | 2a[..]c6 | Private key for transaction signing |
33 | | `--safe.address` | 0x837DE453AD5F21E89771e3c06239d8236c0EFd5E | Address of the Safe contract that presigned the transaction|
34 | | `--path` | /tmp/psps.json | Path to JSON file containing PSPs |
35 | | `--chainid` | 11155111 | Chain ID for the network |
36 | | `--superchainconfig.address` | 0xC2Be75506d5724086DEB7245bd260Cc9753911Be | Address of SuperchainConfig contract |
37 | | `--rpc.url` | http://localhost:8545 | URL of the RPC node |
38 | | `--port.api` | 8080 | Port for the HTTP API server |
39 | | `--tls.ca` | certs/ca-cert.pem | Certificate Authority's certificate |
40 | | `--tls.cert` | certs/server-cert.pem | Server's certificate |
41 | | `--tls.key` | certs/server-key.pem | Server's private key |
42 | **PSPs Format**
43 | The PSPs are stored with a JSON format. The JSON file should contain an array of PSPs. Each PSP should have the following fields:
44 |
45 | ```JSON
46 | [
47 | {
48 | "chain_id": "11155111",
49 | "rpc_url": "https://ethereum-sepolia.publicnode.com",
50 | "created_at": "2024-08-22T20:00:06+02:00",
51 | "safe_addr": "0x837DE453AD5F21E89771e3c06239d8236c0EFd5E",
52 | "safe_nonce": "0",
53 | "target_addr": "0xfd7E6Ef1f6c9e4cC34F54065Bf8496cE41A4e2e8",
54 | "script_name": "PresignPauseFromJson.s.sol",
55 | "data": "[REDACTED]",
56 | "signatures": [
57 | {
58 | "signer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
59 | "signature": "[REDACTED]"
60 | },
61 | {
62 | "signer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
63 | "signature": "[REDACTED]"
64 | }
65 | ],
66 | "calldata": "[REDACTED]"
67 | }
68 | ]
69 |
70 | ```
71 |
72 | The example above is starting by `[` and finishing by `]` as this is an array of PSPs. So here, only 1 PSP is defined.
73 |
74 | ### 2. Request the HTTP API
75 |
76 | 
77 |
78 | To use the HTTP API you can use the following `curl` command with the following fields:
79 |
80 | ```bash
81 | curl -X POST http://localhost:8080/api/psp_execution \-H "Content-Type: application/json" \-d '{"Pause":true,"Timestamp":1596240000,"Operator":"tom"}' --cert client-cert.pem --key client-key.pem
82 | ```
83 |
84 | Explanation of the _mandatory_ fields:
85 | | Field | Description |
86 | | --------- | -------------------------------------------------------------------------------- |
87 | | pause | A boolean value indicating whether to pause (true) or unpause (false) the SuperchainConfig.|
88 | | timestamp | The Unix timestamp when the request is made. |
89 | | operator | The name or identifier of the person initiating the PSP execution. |
90 |
91 | ### 3. Metrics Server
92 |
93 | To monitor the _PSPexecutor service_ the metrics server can be enabled. The metrics server will expose the metrics on the specified address and port.
94 | The metrics are using **Prometheus** and can be set with the following options:
95 | | Option | Description | Default Value | Environment Variable |
96 | | ------------------- | ------------------------- | ------------- | ----------------------------- |
97 | | `--metrics.enabled` | Enable the metrics server | `false` | `DEFENDER_METRICS_ENABLED` |
98 | | `--metrics.addr` | Metrics listening address | `"0.0.0.0"` | `$DEFENDER_METRICS_ADDR` |
99 | | `--metrics.port` | Metrics listening port | `7300` | `$DEFENDER_METRICS_PORT` |
100 |
101 | The prometheus metrics used are the following:
102 |
103 | ```golang
104 | latestValidPspNonce *prometheus.GaugeVec
105 | balanceSenderAddress *prometheus.GaugeVec
106 | latestSafeNonce *prometheus.GaugeVec
107 | pspNonceValid *prometheus.GaugeVec
108 | highestBlockNumber *prometheus.GaugeVec
109 | unexpectedRpcErrors *prometheus.CounterVec
110 | GetNonceAndFetchAndSimulateAtBlockError *prometheus.CounterVec
111 | ```
112 |
113 | ### 4. Options and Configuration
114 |
115 | The current options are the following:
116 |
117 | Using the `--help` flag will show the options available:
118 |
119 | OPTIONS:
120 |
121 | ```shell
122 | --rpc.url value Node URL of a peer (default: "http://127.0.0.1:8545") [$PSPEXECUTOR_NODE_URL]
123 | --privatekey value Privatekey of the account that will issue the pause transaction [$PSPEXECUTOR_PRIVATE_KEY]
124 | --port.api value Port of the API server you want to listen on (e.g. 8080) (default: 8080) [$PSPEXECUTOR_PORT_API]
125 | --superchainconfig.address value SuperChainConfig address to know the current status of the superchainconfig [$PSPEXECUTOR_SUPERCHAINCONFIG_ADDRESS]
126 | --safe.address value Safe address that will execute the PSPs [$PSPEXECUTOR_SAFE_ADDRESS]
127 | --path value Absolute path to the JSON file containing the PSPs [$PSPEXECUTOR_PATH_TO_PSPS]
128 | --blockduration value Block duration of the current chain that op-defender is running on (default: 12) [$PSPEXECUTOR_BLOCK_DURATION]
129 | --chainid value ChainID of the current chain that op-defender is running on (default: 0) [$PSPEXECUTOR_CHAIN_ID]
130 | --tls.ca value tls ca cert path [$PSPEXECUTOR_TLS_CA]
131 | --tls.cert value tls cert path [$PSPEXECUTOR_TLS_CERT]
132 | --tls.key value tls key [$PSPEXECUTOR_TLS_KEY]
133 | --log.level value The lowest log level that will be output (default: INFO) [$DEFENDER_LOG_LEVEL]
134 | --log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$DEFENDER_LOG_FORMAT]
135 | --log.color Color the log output if in terminal mode (default: false) [$DEFENDER_LOG_COLOR]
136 | --metrics.enabled Enable the metrics server (default: false) [$DEFENDER_METRICS_ENABLED]
137 | --metrics.addr value Metrics listening address (default: "0.0.0.0") [$DEFENDER_METRICS_ADDR]
138 | --metrics.port value Metrics listening port (default: 7300) [$DEFENDER_METRICS_PORT]
139 | --help, -h show help
140 | ```
141 |
--------------------------------------------------------------------------------
/op-defender/psp_executor/cli.go:
--------------------------------------------------------------------------------
1 | package psp_executor
2 |
3 | import (
4 | opservice "github.com/ethereum-optimism/optimism/op-service"
5 | optls "github.com/ethereum-optimism/optimism/op-service/tls"
6 | "github.com/ethereum/go-ethereum/common"
7 | "github.com/urfave/cli/v2"
8 | )
9 |
10 | const (
11 | NodeURLFlagName = "rpc.url"
12 | PrivateKeyFlagName = "privatekey"
13 | PortAPIFlagName = "port.api"
14 | SuperChainConfigAddressFlagName = "superchainconfig.address"
15 | SafeAddressFlagName = "safe.address"
16 | PathFlagName = "path"
17 | ChainIDFlagName = "chainid"
18 | BlockDurationFlagName = "blockduration"
19 | )
20 |
21 | type CLIConfig struct {
22 | NodeURL string
23 | PortAPI string
24 | Path string
25 | BlockDuration uint64
26 | privatekeyflag string
27 | SuperChainConfigAddress common.Address
28 | SafeAddress common.Address
29 | ChainID uint64
30 | TLSConfig optls.CLIConfig
31 | }
32 |
33 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
34 | cfg := CLIConfig{
35 | NodeURL: ctx.String(NodeURLFlagName),
36 | PortAPI: ctx.String(PortAPIFlagName),
37 | Path: ctx.String(PathFlagName),
38 | privatekeyflag: ctx.String(PrivateKeyFlagName),
39 | SuperChainConfigAddress: common.HexToAddress(ctx.String(SuperChainConfigAddressFlagName)),
40 | SafeAddress: common.HexToAddress(ctx.String(SafeAddressFlagName)),
41 | ChainID: ctx.Uint64(ChainIDFlagName),
42 | BlockDuration: ctx.Uint64(BlockDurationFlagName),
43 | TLSConfig: optls.ReadCLIConfig(ctx),
44 | }
45 |
46 | return cfg, nil
47 | }
48 |
49 | func CLIFlags(envPrefix string) []cli.Flag {
50 | flags := []cli.Flag{
51 | &cli.StringFlag{
52 | Name: NodeURLFlagName,
53 | Usage: "Node URL of a peer",
54 | Value: "http://127.0.0.1:8545",
55 | EnvVars: opservice.PrefixEnvVar(envPrefix, "NODE_URL"),
56 | },
57 | &cli.StringFlag{
58 | Name: PrivateKeyFlagName,
59 | Usage: "Privatekey of the account that will issue the pause transaction",
60 | EnvVars: opservice.PrefixEnvVar(envPrefix, "PRIVATE_KEY"),
61 | Required: true,
62 | },
63 | &cli.Uint64Flag{
64 | Name: PortAPIFlagName,
65 | Value: 8080,
66 | Usage: "Port of the API server you want to listen on (e.g. 8080)",
67 | EnvVars: opservice.PrefixEnvVar(envPrefix, "PORT_API"),
68 | Required: false,
69 | },
70 | &cli.StringFlag{
71 | Name: SuperChainConfigAddressFlagName,
72 | Usage: "SuperChainConfig address to know the current status of the superchainconfig",
73 | EnvVars: opservice.PrefixEnvVar(envPrefix, "SUPERCHAINCONFIG_ADDRESS"),
74 | Required: true,
75 | },
76 | &cli.StringFlag{
77 | Name: SafeAddressFlagName,
78 | Usage: "Safe address that will execute the PSPs",
79 | EnvVars: opservice.PrefixEnvVar(envPrefix, "SAFE_ADDRESS"),
80 | Required: true,
81 | },
82 | &cli.StringFlag{
83 | Name: PathFlagName,
84 | Usage: "Absolute path to the JSON file containing the PSPs",
85 | EnvVars: opservice.PrefixEnvVar(envPrefix, "PATH_TO_PSPS"),
86 | Required: true,
87 | },
88 | &cli.Uint64Flag{
89 | Name: BlockDurationFlagName,
90 | Usage: "Block duration of the current chain that op-defender is running on",
91 | Value: 12,
92 | EnvVars: opservice.PrefixEnvVar(envPrefix, "BLOCK_DURATION"),
93 | Required: false,
94 | },
95 | &cli.Uint64Flag{
96 | Name: ChainIDFlagName,
97 | Usage: "ChainID of the current chain that op-defender is running on",
98 | EnvVars: opservice.PrefixEnvVar(envPrefix, "CHAIN_ID"),
99 | Required: true,
100 | },
101 | }
102 | // Add mtls flags
103 | flags = append(flags, []cli.Flag{
104 | &cli.StringFlag{
105 | Name: optls.TLSCaCertFlagName,
106 | Usage: "tls ca cert path",
107 | EnvVars: opservice.PrefixEnvVar(envPrefix, "TLS_CA"),
108 | },
109 | &cli.StringFlag{
110 | Name: optls.TLSCertFlagName,
111 | Usage: "tls cert path",
112 | EnvVars: opservice.PrefixEnvVar(envPrefix, "TLS_CERT"),
113 | },
114 | &cli.StringFlag{
115 | Name: optls.TLSKeyFlagName,
116 | Usage: "tls key",
117 | EnvVars: opservice.PrefixEnvVar(envPrefix, "TLS_KEY"),
118 | },
119 | }...)
120 | return flags
121 | }
122 |
123 | func (c CLIConfig) Check() error {
124 |
125 | if err := c.TLSConfig.Check(); err != nil {
126 | return err
127 | }
128 | return nil
129 | }
130 |
--------------------------------------------------------------------------------
/op-defender/psp_executor/simulator.go:
--------------------------------------------------------------------------------
1 | package psp_executor
2 |
3 | import (
4 | "context"
5 | "math/big"
6 | "strconv"
7 |
8 | "github.com/ethereum/go-ethereum"
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/ethclient"
11 | )
12 |
13 | // SimulateTransaction will simulate a transaction onchain if the blockNumber is `nil` it will simulate the transaction on the latest block.
14 | func SimulateTransaction(ctx context.Context, client *ethclient.Client, blockNumber *uint64, fromAddress common.Address, contractAddress common.Address, data []byte) ([]byte, error) {
15 | // Create a call message
16 | var blockNumber_bigint *big.Int
17 |
18 | callMsg := ethereum.CallMsg{
19 | From: fromAddress,
20 | To: &contractAddress,
21 | Data: data,
22 | Value: &big.Int{},
23 | }
24 | // If the blockNumber is not nil, set the blockNumber_bigint to the blockNumber provided.
25 | if blockNumber != nil {
26 | blockNumber_bigint = new(big.Int).SetUint64(*blockNumber)
27 | }
28 | // Simulate the transaction if the blockNumber_bigint is nil it will simulate the transaction on the latest block.
29 | simulation, err := client.CallContract(ctx, callMsg, blockNumber_bigint)
30 | if err != nil {
31 |
32 | return nil, err
33 | }
34 | return simulation, nil
35 | }
36 |
37 | // FetchAndSimulate will fetch the PSP from a file and simulate it this onchain.
38 | func (e *DefenderExecutor) FetchAndSimulateAtBlock(ctx context.Context, d *Defender, blocknumber *uint64, nonce uint64) ([]byte, error) {
39 | operationSafe, data, err := GetPSPbyNonceFromFile(nonce, d.path) // return the PSP that has the correct nonce.
40 | if err != nil {
41 | return nil, err
42 | }
43 | // Check that operationSafe is the same as the config provided.
44 | if operationSafe != d.safeAddress {
45 | return nil, err
46 | }
47 | // Then simulate PSP with the correct nonce onchain with the PSP data through the `SimulateTransaction()` function.
48 | simulation, err := SimulateTransaction(ctx, d.l1Client, blocknumber, d.senderAddress, operationSafe, data)
49 | if err != nil {
50 | return nil, err
51 | }
52 | return simulation, nil
53 | }
54 |
55 | // GetBalance will get the balance of the senderAddress
56 | func (d *Defender) GetBalance(ctx context.Context) error {
57 | balance, err := d.l1Client.BalanceAt(ctx, d.senderAddress, nil)
58 | if err != nil {
59 | return err
60 | }
61 | balanceInEther, err := weiToEther(balance)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | d.balanceSenderAddress.WithLabelValues(d.senderAddress.Hex()).Set(balanceInEther)
67 | return nil
68 | }
69 |
70 | // GetNonceAndFetchAndSimulateAtBlock will get the nonce of the operationSafe onchain and then fetch the PSP from a file and simulate it onchain at the last block.
71 | func (d *Defender) GetNonceAndFetchAndSimulateAtBlock(ctx context.Context) error {
72 | blocknumber, err := d.l1Client.BlockNumber(ctx) // Get the latest block number.
73 | if err != nil {
74 | d.log.Error("[MON] failed to get the block number:", "error", err)
75 | d.unexpectedRpcErrors.WithLabelValues("l1", "blockNumber").Inc()
76 | return err
77 | }
78 | d.highestBlockNumber.WithLabelValues("blockNumber").Set(float64(blocknumber))
79 | nonce, err := d.getNonceSafe(ctx) // Get the the current nonce of the operationSafe.
80 | if err != nil {
81 | d.unexpectedRpcErrors.WithLabelValues("l1", "latestSafeNonce").Inc()
82 | d.log.Error("[MON] failed to get latest nonce onchain ", "error", err, "blocknumber", blocknumber)
83 | return err
84 | }
85 | d.latestSafeNonce.WithLabelValues("nonce").Set(float64(nonce))
86 |
87 | _, err = d.executor.FetchAndSimulateAtBlock(ctx, d, &blocknumber, nonce) // Fetch and simulate the PSP with the current nonce.
88 | if err != nil {
89 | d.log.Error("[MON] failed to fetch and simulate the PSP onchain", "error", err, "blocknumber", blocknumber, "nonce", nonce)
90 | d.pspNonceValid.WithLabelValues(strconv.FormatUint(nonce, 10)).Set(0)
91 | return err
92 | }
93 | d.pspNonceValid.WithLabelValues(strconv.FormatUint(nonce, 10)).Set(1)
94 | d.latestValidPspNonce.WithLabelValues("nonce").Set(float64(nonce))
95 | d.log.Info("[MON] PSP executed onchain successfully ✅", "blocknumber", blocknumber, "nonce", nonce)
96 | return nil
97 | }
98 |
--------------------------------------------------------------------------------
/op-defender/psp_executor/utils.go:
--------------------------------------------------------------------------------
1 | package psp_executor
2 |
3 | import (
4 | "errors"
5 | "math/big"
6 | )
7 |
8 | const ether = 1e18
9 |
10 | // weiToEther converts a wei value to ether return a float64 return an error if the float is too large.
11 | func weiToEther(wei *big.Int) (float64, error) {
12 | var bigInt big.Int
13 | if wei.Cmp(&bigInt) == 0 {
14 | return 0, nil
15 | }
16 | num := new(big.Rat).SetInt(wei)
17 | denom := big.NewRat(ether, 1)
18 | num = num.Quo(num, denom)
19 | f, isTooLarge := num.Float64()
20 | if isTooLarge {
21 | return 0, errors.New("number is too large to convert to float")
22 | }
23 | return f, nil
24 | }
25 |
--------------------------------------------------------------------------------
/op-monitorism/Dockerfile:
--------------------------------------------------------------------------------
1 | # Define the build stage
2 | FROM golang:1.22.2-alpine3.19 as builder
3 |
4 | # Set the working directory inside the container
5 | WORKDIR /app
6 |
7 | # Install system dependencies including 'make'
8 | RUN apk update && apk add --no-cache make
9 |
10 | # Copy the source code and Makefile into the container
11 | COPY . .
12 |
13 | # Run the Makefile command to build the binary
14 | RUN make
15 |
16 | # Define the final base image
17 | FROM alpine:3.18
18 |
19 | # Install ca-certificates so that HTTPS works
20 | RUN apk update && apk add --no-cache ca-certificates
21 |
22 | # Copy the built binary from the builder stage
23 | COPY --from=builder /app/bin/monitorism /usr/local/bin/monitorism
24 |
25 | # Ensure the binary is executable
26 | RUN chmod +x /usr/local/bin/monitorism
27 |
28 | # Set the command to run the binary
29 | CMD ["monitorism"]
30 |
--------------------------------------------------------------------------------
/op-monitorism/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile
2 |
3 | # Binary name
4 | BINARY=monitorism
5 |
6 | # Build directory
7 | BUILD_DIR=./bin
8 |
9 | # Source directory
10 | CMD_DIR=./cmd/monitorism
11 |
12 | # Go commands
13 | GOBUILD=go build
14 | GOCLEAN=go clean
15 | GORUN=go run
16 | GOTEST=go test
17 |
18 | # Default target
19 | .PHONY: all
20 | all: build
21 |
22 | # Build binary
23 | .PHONY: build
24 | build:
25 | @echo "Building..."
26 | $(GOBUILD) -tags all -o $(BUILD_DIR)/$(BINARY) $(CMD_DIR)
27 |
28 | # Run program
29 | .PHONY: run
30 | run:
31 | @echo "Running..."
32 | $(GORUN) $(CMD_DIR) $(ARGS)
33 |
34 | # Clean up binaries
35 | .PHONY: clean
36 | clean:
37 | @echo "Cleaning..."
38 | $(GOCLEAN)
39 | rm -f $(BUILD_DIR)/$(BINARY)
40 |
41 | # Run tests
42 | .PHONY: test
43 | test:
44 | @echo "Running tests..."
45 | $(GOTEST) ./... -v
46 |
47 | #include tests that require live resources
48 | #these resources are meant to be real and not mocked
49 | .PHONY: test-live
50 | test-live:
51 | @echo "Running live_tests..."
52 | $(GOTEST) ./... -v -tags live
53 |
54 | # Run program
55 | .PHONY: tidy
56 | tidy:
57 | @echo "Tidying..."
58 | go mod tidy
59 |
60 | # Help
61 | .PHONY: help
62 | help:
63 | @echo "Makefile commands:"
64 | @echo " make build"
65 | @echo " make run"
66 | @echo " make clean"
67 | @echo " make test"
68 | @echo " make test-live"
69 | @echo " make tidy"
70 | @echo " make help"
71 |
--------------------------------------------------------------------------------
/op-monitorism/balances/README.md:
--------------------------------------------------------------------------------
1 | ### Balances Monitor
2 |
3 | The balances monitor simply emits a metric reporting the balances for the configured accounts.
4 |
5 | ```
6 | OPTIONS:
7 | --node.url value [$BALANCE_MON_NODE_URL] Node URL of a peer (default: "127.0.0.1:8545")
8 | --accounts address:nickname [ --accounts address:nickname ] [$BALANCE_MON_ACCOUNTS] One or accounts formatted via address:nickname
9 | ```
10 |
--------------------------------------------------------------------------------
/op-monitorism/balances/cli.go:
--------------------------------------------------------------------------------
1 | package balances
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | opservice "github.com/ethereum-optimism/optimism/op-service"
8 |
9 | "github.com/ethereum/go-ethereum/common"
10 |
11 | "github.com/urfave/cli/v2"
12 | )
13 |
14 | const (
15 | NodeURLFlagName = "node.url"
16 | AccountsFlagName = "accounts"
17 | )
18 |
19 | type CLIConfig struct {
20 | NodeUrl string
21 | Accounts []Account
22 | }
23 |
24 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
25 | cfg := CLIConfig{NodeUrl: ctx.String(NodeURLFlagName)}
26 | accounts := ctx.StringSlice(AccountsFlagName)
27 | if len(accounts) == 0 {
28 | return cfg, fmt.Errorf("--%s must have at least one account", AccountsFlagName)
29 | }
30 |
31 | for _, account := range accounts {
32 | split := strings.Split(account, ":")
33 | if len(split) != 2 {
34 | return cfg, fmt.Errorf("failed to parse `address:nickname`: %s", account)
35 | }
36 |
37 | addr, nickname := split[0], split[1]
38 | if !common.IsHexAddress(addr) {
39 | return cfg, fmt.Errorf("address is not a hex-encoded address: %s", addr)
40 | }
41 | if len(nickname) == 0 {
42 | return cfg, fmt.Errorf("nickname for %s not set", addr)
43 | }
44 |
45 | cfg.Accounts = append(cfg.Accounts, Account{common.HexToAddress(addr), nickname})
46 | }
47 |
48 | return cfg, nil
49 | }
50 |
51 | func CLIFlags(envPrefix string) []cli.Flag {
52 | return []cli.Flag{
53 | &cli.StringFlag{
54 | Name: NodeURLFlagName,
55 | Usage: "Node URL of a peer",
56 | Value: "127.0.0.1:8545",
57 | EnvVars: opservice.PrefixEnvVar(envPrefix, "NODE_URL"),
58 | },
59 | &cli.StringSliceFlag{
60 | Name: AccountsFlagName,
61 | Usage: "One or multiples accounts formatted via `address:nickname`",
62 | EnvVars: opservice.PrefixEnvVar(envPrefix, "ACCOUNTS"),
63 | Required: true,
64 | },
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/op-monitorism/balances/monitor.go:
--------------------------------------------------------------------------------
1 | package balances
2 |
3 | import (
4 | "context"
5 | "math/big"
6 |
7 | "github.com/ethereum-optimism/optimism/op-service/client"
8 | "github.com/ethereum-optimism/optimism/op-service/metrics"
9 |
10 | "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/common/hexutil"
12 | "github.com/ethereum/go-ethereum/log"
13 | "github.com/ethereum/go-ethereum/params"
14 | "github.com/ethereum/go-ethereum/rpc"
15 |
16 | "github.com/prometheus/client_golang/prometheus"
17 | )
18 |
19 | const (
20 | MetricsNamespace = "balance_mon"
21 | )
22 |
23 | type Account struct {
24 | Address common.Address
25 | Nickname string
26 | }
27 |
28 | type Monitor struct {
29 | log log.Logger
30 |
31 | rpc client.RPC
32 | accounts []Account
33 |
34 | // metrics
35 | balances *prometheus.GaugeVec
36 | unexpectedRpcErrors *prometheus.CounterVec
37 | }
38 |
39 | func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIConfig) (*Monitor, error) {
40 | log.Info("creating balance monitor")
41 | rpc, err := client.NewRPC(ctx, log, cfg.NodeUrl)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | for _, account := range cfg.Accounts {
47 | log.Info("configured account", "address", account.Address, "nickname", account.Nickname)
48 | }
49 |
50 | return &Monitor{
51 | log: log,
52 | rpc: rpc,
53 | accounts: cfg.Accounts,
54 |
55 | balances: m.NewGaugeVec(prometheus.GaugeOpts{
56 | Namespace: MetricsNamespace,
57 | Name: "balances",
58 | Help: "balances held by accounts registered with the monitor",
59 | }, []string{"address", "nickname"}),
60 | unexpectedRpcErrors: m.NewCounterVec(prometheus.CounterOpts{
61 | Namespace: MetricsNamespace,
62 | Name: "unexpectedRpcErrors",
63 | Help: "number of unexpected rpc errors",
64 | }, []string{"section", "name"}),
65 | }, nil
66 | }
67 |
68 | func (m *Monitor) Run(ctx context.Context) {
69 | m.log.Info("querying balances...")
70 | batchElems := make([]rpc.BatchElem, len(m.accounts))
71 | for i := 0; i < len(m.accounts); i++ {
72 | batchElems[i] = rpc.BatchElem{
73 | Method: "eth_getBalance",
74 | Args: []interface{}{m.accounts[i].Address, "latest"},
75 | Result: new(hexutil.Big),
76 | }
77 | }
78 | if err := m.rpc.BatchCallContext(ctx, batchElems); err != nil {
79 | m.log.Error("failed getBalance batch request", "err", err)
80 | m.unexpectedRpcErrors.WithLabelValues("balances", "batched_getBalance").Inc()
81 | return
82 | }
83 |
84 | for i := 0; i < len(m.accounts); i++ {
85 | account := m.accounts[i]
86 | if batchElems[i].Error != nil {
87 | m.log.Error("failed to query account balance", "address", account.Address, "nickname", account.Nickname, "err", batchElems[i].Error)
88 | m.unexpectedRpcErrors.WithLabelValues("balances", "getBalance").Inc()
89 | continue
90 | }
91 |
92 | ethBalance := weiToEther((batchElems[i].Result).(*hexutil.Big).ToInt())
93 | m.balances.WithLabelValues(account.Address.String(), account.Nickname).Set(ethBalance)
94 | m.log.Info("set balance", "address", account.Address, "nickname", account.Nickname, "balance", ethBalance)
95 | }
96 | }
97 |
98 | func (m *Monitor) Close(_ context.Context) error {
99 | m.rpc.Close()
100 | return nil
101 | }
102 |
103 | func weiToEther(wei *big.Int) float64 {
104 | num := new(big.Rat).SetInt(wei)
105 | denom := big.NewRat(params.Ether, 1)
106 | num = num.Quo(num, denom)
107 | f, _ := num.Float64()
108 | return f
109 | }
110 |
--------------------------------------------------------------------------------
/op-monitorism/cmd/monitorism/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/ethereum/go-ethereum/log"
8 |
9 | opio "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt"
10 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
11 | )
12 |
13 | var (
14 | GitCommit = ""
15 | GitDate = ""
16 | )
17 |
18 | func main() {
19 | oplog.SetupDefaults()
20 | app := newCli(GitCommit, GitDate)
21 |
22 | // sub-commands set up their individual interrupt lifecycles, which can block on the given interrupt as needed.
23 | ctx := opio.WithSignalWaiterMain(context.Background())
24 | if err := app.RunContext(ctx, os.Args); err != nil {
25 | log.Error("application failed", "err", err)
26 | os.Exit(1)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/op-monitorism/conservation_monitor/README.md:
--------------------------------------------------------------------------------
1 | # ETH Conservation Monitor
2 |
3 | A service that monitors OP Stack blocks to ensure that no ETH is minted or burned (except for the special cases of
4 | deposits with a non-zero mint value.)
5 |
6 | ## ETH Conservation Invariant
7 |
8 | Fee distribution on the OP Stack is altered from L1, and all fees are sent to [vaults][fee-vault]. There is currently no
9 | in-protocol route to burn ETH on the OP Stack other than `SELFDESTRUCT`. ETH can _only_ be minted on L2 by deposit
10 | transactions.
11 |
12 | The invariant to be checked by this monitoring service is:
13 |
14 | $$
15 | \displaylines{
16 | TB(b) = \text{balance of account all touched accounts in block \`b' at block \`b', including fee vaults}
17 | \\
18 | TDM(b) = \text{amount of ETH minted by deposit transactions in block \`b'}
19 | \\
20 | TB(b-1) \ge TB(b) - TDM(b)
21 | }
22 | $$
23 |
24 | ## Features
25 |
26 | - Monitor chains for the ETH conservation invariant.
27 | - Prometheus metrics for monitoring and alerting
28 |
29 | ## Metrics
30 |
31 | The service exports the following Prometheus metrics:
32 |
33 | - `conservation_mon_invariant_held`: Total number blocks that the invariant has held for.
34 | - `conservation_mon_invariant_violations`: Total number of blocks that the invariant has been broken within.
35 |
36 | ## Usage
37 |
38 | ```bash
39 | monitorism conservation_monitor \
40 | --node.url
41 | ```
42 |
43 | _NOTE_: `l2_el_url` must have `debug_traceBlockByHash` exposed.
44 |
45 | [fee-vault]: https://specs.optimism.io/protocol/exec-engine.html?highlight=vault#fee-vaults
46 |
--------------------------------------------------------------------------------
/op-monitorism/conservation_monitor/cli.go:
--------------------------------------------------------------------------------
1 | package conservation_monitor
2 |
3 | import (
4 | "time"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 | "github.com/urfave/cli/v2"
8 | )
9 |
10 | const (
11 | NodeURLFlagName = "node.url"
12 | StartBlockFlagName = "start.block"
13 | PollingIntervalFlagName = "poll.interval"
14 | )
15 |
16 | type CLIConfig struct {
17 | NodeUrl string `yaml:"node_url"`
18 | StartBlock uint64 `yaml:"start_block"`
19 | PollingInterval time.Duration `yaml:"poll_interval"`
20 | }
21 |
22 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
23 | cfg := CLIConfig{
24 | NodeUrl: ctx.String(NodeURLFlagName),
25 | StartBlock: ctx.Uint64(StartBlockFlagName),
26 | PollingInterval: ctx.Duration(PollingIntervalFlagName),
27 | }
28 | return cfg, nil
29 | }
30 |
31 | func CLIFlags(envPrefix string) []cli.Flag {
32 | return []cli.Flag{
33 | &cli.StringFlag{
34 | Name: NodeURLFlagName,
35 | Usage: "Node URL",
36 | Value: "http://localhost:8545",
37 | EnvVars: opservice.PrefixEnvVar(envPrefix, "NODE_URL"),
38 | },
39 | &cli.Uint64Flag{
40 | Name: StartBlockFlagName,
41 | Usage: "Starting block number (0 for latest)",
42 | Value: 0,
43 | EnvVars: opservice.PrefixEnvVar(envPrefix, "START_BLOCK"),
44 | },
45 | &cli.DurationFlag{
46 | Name: PollingIntervalFlagName,
47 | Usage: "The polling interval (should be less than blocktime for safety) in seconds",
48 | Value: 12 * time.Second,
49 | EnvVars: opservice.PrefixEnvVar(envPrefix, "POLL_INTERVAL"),
50 | },
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/op-monitorism/drippie/README.md:
--------------------------------------------------------------------------------
1 | ### Drippie Monitor
2 |
3 | The drippie monitor tracks the execution and executability of drips within a Drippie contract.
4 |
5 | ```
6 | OPTIONS:
7 | --l1.node.url value Node URL of L1 peer (default: "127.0.0.1:8545") [$DRIPPIE_MON_L1_NODE_URL]
8 | --drippie.address value Address of the Drippie contract [$DRIPPIE_MON_DRIPPIE]
9 | --log.level value The lowest log level that will be output (default: INFO) [$MONITORISM_LOG_LEVEL]
10 | --log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$MONITORISM_LOG_FORMAT]
11 | --log.color Color the log output if in terminal mode (default: false) [$MONITORISM_LOG_COLOR]
12 | --metrics.enabled Enable the metrics server (default: false) [$MONITORISM_METRICS_ENABLED]
13 | --metrics.addr value Metrics listening address (default: "0.0.0.0") [$MONITORISM_METRICS_ADDR]
14 | --metrics.port value Metrics listening port (default: 7300) [$MONITORISM_METRICS_PORT]
15 | --loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
16 | ```
17 |
--------------------------------------------------------------------------------
/op-monitorism/drippie/cli.go:
--------------------------------------------------------------------------------
1 | package drippie
2 |
3 | import (
4 | "fmt"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 |
8 | "github.com/ethereum/go-ethereum/common"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | const (
14 | L1NodeURLFlagName = "l1.node.url"
15 | DrippieAddressFlagName = "drippie.address"
16 | )
17 |
18 | type CLIConfig struct {
19 | L1NodeURL string
20 | DrippieAddress common.Address
21 | }
22 |
23 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
24 | cfg := CLIConfig{
25 | L1NodeURL: ctx.String(L1NodeURLFlagName),
26 | }
27 |
28 | drippieAddress := ctx.String(DrippieAddressFlagName)
29 | if !common.IsHexAddress(drippieAddress) {
30 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", DrippieAddressFlagName)
31 | }
32 | cfg.DrippieAddress = common.HexToAddress(drippieAddress)
33 |
34 | return cfg, nil
35 | }
36 |
37 | func CLIFlags(envVar string) []cli.Flag {
38 | return []cli.Flag{
39 | &cli.StringFlag{
40 | Name: L1NodeURLFlagName,
41 | Usage: "Node URL of L1 peer",
42 | Value: "127.0.0.1:8545",
43 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
44 | },
45 | &cli.StringFlag{
46 | Name: DrippieAddressFlagName,
47 | Usage: "Address of the Drippie contract",
48 | EnvVars: opservice.PrefixEnvVar(envVar, "DRIPPIE"),
49 | Required: true,
50 | },
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/op-monitorism/drippie/monitor.go:
--------------------------------------------------------------------------------
1 | package drippie
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/ethereum-optimism/monitorism/op-monitorism/drippie/bindings"
9 | "github.com/ethereum-optimism/optimism/op-service/metrics"
10 |
11 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
12 | "github.com/ethereum/go-ethereum/common"
13 | "github.com/ethereum/go-ethereum/ethclient"
14 | "github.com/ethereum/go-ethereum/log"
15 |
16 | "github.com/prometheus/client_golang/prometheus"
17 | )
18 |
19 | const (
20 | MetricsNamespace = "drippie_mon"
21 | )
22 |
23 | type Monitor struct {
24 | log log.Logger
25 |
26 | l1Client *ethclient.Client
27 |
28 | drippieAddress common.Address
29 | drippie *bindings.Drippie
30 | created []string
31 |
32 | // Metrics
33 | dripCount *prometheus.GaugeVec
34 | dripLastTimestamp *prometheus.GaugeVec
35 | dripExecutableState *prometheus.GaugeVec
36 | highestBlockNumber *prometheus.GaugeVec
37 | nodeConnectionFailures *prometheus.CounterVec
38 | }
39 |
40 | func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIConfig) (*Monitor, error) {
41 | log.Info("creating drippie monitor...")
42 |
43 | l1Client, err := ethclient.Dial(cfg.L1NodeURL)
44 | if err != nil {
45 | return nil, fmt.Errorf("failed to dial l1: %w", err)
46 | }
47 |
48 | drippie, err := bindings.NewDrippie(cfg.DrippieAddress, l1Client)
49 | if err != nil {
50 | return nil, fmt.Errorf("failed to bind to Drippie: %w", err)
51 | }
52 |
53 | return &Monitor{
54 | log: log,
55 |
56 | l1Client: l1Client,
57 |
58 | drippieAddress: cfg.DrippieAddress,
59 | drippie: drippie,
60 |
61 | // Metrics
62 | dripCount: m.NewGaugeVec(prometheus.GaugeOpts{
63 | Namespace: MetricsNamespace,
64 | Name: "dripCounts",
65 | Help: "number of drips created",
66 | }, []string{"name"}),
67 | dripLastTimestamp: m.NewGaugeVec(prometheus.GaugeOpts{
68 | Namespace: MetricsNamespace,
69 | Name: "dripLatestTimestamp",
70 | Help: "latest timestamp of drips",
71 | }, []string{"name"}),
72 | dripExecutableState: m.NewGaugeVec(prometheus.GaugeOpts{
73 | Namespace: MetricsNamespace,
74 | Name: "dripExecutableState",
75 | Help: "drip executable state",
76 | }, []string{"name"}),
77 | highestBlockNumber: m.NewGaugeVec(prometheus.GaugeOpts{
78 | Namespace: MetricsNamespace,
79 | Name: "highestBlockNumber",
80 | Help: "observed l1 heights (checked and known)",
81 | }, []string{"type"}),
82 | nodeConnectionFailures: m.NewCounterVec(prometheus.CounterOpts{
83 | Namespace: MetricsNamespace,
84 | Name: "nodeConnectionFailures",
85 | Help: "number of times node connection has failed",
86 | }, []string{"layer", "section"}),
87 | }, nil
88 | }
89 |
90 | func (m *Monitor) Run(ctx context.Context) {
91 | // Determine current L1 block height.
92 | latestL1Height, err := m.l1Client.BlockNumber(ctx)
93 | if err != nil {
94 | m.log.Error("failed to query latest block number", "err", err)
95 | m.nodeConnectionFailures.WithLabelValues("l1", "blockNumber").Inc()
96 | return
97 | }
98 |
99 | // Update metrics.
100 | m.highestBlockNumber.WithLabelValues("known").Set(float64(latestL1Height))
101 |
102 | // Set up the call options once.
103 | callOpts := bind.CallOpts{
104 | BlockNumber: big.NewInt(int64(latestL1Height)),
105 | }
106 |
107 | // Grab the number of created drips at the current block height.
108 | numCreated, err := m.drippie.GetDripCount(&callOpts)
109 | if err != nil {
110 | m.log.Error("failed to query Drippie for number of created drips", "err", err)
111 | m.nodeConnectionFailures.WithLabelValues("l1", "dripCount").Inc()
112 | return
113 | }
114 |
115 | // Add new drip names if the number of created drips has increased.
116 | if numCreated.Cmp(big.NewInt(int64(len(m.created)))) >= 0 {
117 | // Iterate through the new drip indices and add their names to the stored list.
118 | // TODO: You can optimize this with a multicall. Current code is good enough for now since we
119 | // don't expect a large number of drips to be created. If this changes, consider multicall to
120 | // batch the requests into a single call.
121 | for i := len(m.created); i < int(numCreated.Int64()); i++ {
122 | // Grab the name of the drip at the current index.
123 | m.log.Info("pulling name for new drip index", "index", i)
124 | name, err := m.drippie.Created(&callOpts, big.NewInt(int64(i)))
125 | if err != nil {
126 | m.log.Error("failed to query Drippie for Drip name", "index", i, "err", err)
127 | m.nodeConnectionFailures.WithLabelValues("l1", "dripName").Inc()
128 | return
129 | }
130 |
131 | // Add the name to the list of created drips.
132 | m.log.Info("got drip name", "index", i, "name", name)
133 | m.created = append(m.created, name)
134 | }
135 | } else {
136 | // Should not happen, log an error and reset the created drips.
137 | m.log.Error("number of created drips decreased", "old", len(m.created), "new", numCreated)
138 | m.created = nil
139 | return
140 | }
141 |
142 | // Iterate through all created drips and update their metrics.
143 | for _, name := range m.created {
144 | // Grab the drip state.
145 | m.log.Info("querying metrics for drip", "name", name)
146 | drip, err := m.drippie.Drips(&callOpts, name)
147 | if err != nil {
148 | m.log.Error("failed to query Drippie for Drip", "name", name, "err", err)
149 | m.nodeConnectionFailures.WithLabelValues("l1", "drips").Inc()
150 | return
151 | }
152 |
153 | // Update metrics.
154 | m.dripCount.WithLabelValues(name).Set(float64(drip.Count.Int64()))
155 | m.dripLastTimestamp.WithLabelValues(name).Set(float64(drip.Last.Int64()))
156 |
157 | // Check if this drip is executable.
158 | executable, err := m.drippie.Executable(&callOpts, name)
159 | if err != nil || !executable {
160 | m.dripExecutableState.WithLabelValues(name).Set(0)
161 | } else {
162 | m.dripExecutableState.WithLabelValues(name).Set(1)
163 | }
164 |
165 | // Log so we know what's happening.
166 | m.log.Info("updated metrics for drip", "name", name, "count", drip.Count, "last", drip.Last, "executable", executable)
167 | }
168 | }
169 |
170 | func (m *Monitor) Close(_ context.Context) error {
171 | m.l1Client.Close()
172 | return nil
173 | }
174 |
--------------------------------------------------------------------------------
/op-monitorism/fault/README.md:
--------------------------------------------------------------------------------
1 | ### Fault Monitor
2 |
3 | The fault monitor checks for changes in output roots posted to the `L2OutputOracle` contract. On change, reconstructing the output root from a trusted L2 source and looking for a match
4 |
5 | NOTE: Fault monitor only working against chains Pre-Faultproof. For chains using Faultproof system please check [dispute-mon service](https://github.com/ethereum-optimism/optimism/blob/develop/op-dispute-mon/README.md)
6 |
7 | ```
8 | OPTIONS:
9 | --l1.node.url value Node URL of L1 peer Geth node [$FAULT_MON_L1_NODE_URL]
10 | --l2.node.url value Node URL of L2 peer Op-Geth node [$FAULT_MON_L2_NODE_URL]
11 | --start.output.index value Output index to start from. -1 to find first unfinalized index (default: -1) [$FAULT_MON_START_OUTPUT_INDEX]
12 | --optimismportal.address value Address of the OptimismPortal contract [$FAULT_MON_OPTIMISM_PORTAL]
13 | ```
14 |
15 | On mismatch the `isCurrentlyMismatched` metrics is set to `1`.
16 |
--------------------------------------------------------------------------------
/op-monitorism/fault/binding/BINDING.md:
--------------------------------------------------------------------------------
1 | ## Info
2 | The bindings in this folder are taken from
3 | github.com/ethereum-optimism/optimism/op-bindings/bindings v1.7.3
4 |
5 | This tool is compatible with these bindings. Future binding will break compatibility for those files.
--------------------------------------------------------------------------------
/op-monitorism/fault/cli.go:
--------------------------------------------------------------------------------
1 | package fault
2 |
3 | import (
4 | "fmt"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 |
8 | "github.com/ethereum/go-ethereum/common"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | const (
14 | L1NodeURLFlagName = "l1.node.url"
15 | L2NodeURLFlagName = "l2.node.url"
16 |
17 | OptimismPortalAddressFlagName = "optimismportal.address"
18 | StartOutputIndexFlagName = "start.output.index"
19 | )
20 |
21 | type CLIConfig struct {
22 | L1NodeURL string
23 | L2NodeURL string
24 |
25 | OptimismPortalAddress common.Address
26 | StartOutputIndex int64
27 | }
28 |
29 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
30 | cfg := CLIConfig{
31 | L1NodeURL: ctx.String(L1NodeURLFlagName),
32 | L2NodeURL: ctx.String(L2NodeURLFlagName),
33 | StartOutputIndex: ctx.Int64(StartOutputIndexFlagName),
34 | }
35 |
36 | portalAddress := ctx.String(OptimismPortalAddressFlagName)
37 | if !common.IsHexAddress(portalAddress) {
38 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", OptimismPortalAddressFlagName)
39 | }
40 | cfg.OptimismPortalAddress = common.HexToAddress(portalAddress)
41 |
42 | return cfg, nil
43 | }
44 |
45 | func CLIFlags(envVar string) []cli.Flag {
46 | return []cli.Flag{
47 | &cli.StringFlag{
48 | Name: L1NodeURLFlagName,
49 | Usage: "Node URL of L1 peer Geth node",
50 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
51 | },
52 | &cli.StringFlag{
53 | Name: L2NodeURLFlagName,
54 | Usage: "Node URL of L2 peer Op-Geth node",
55 | EnvVars: opservice.PrefixEnvVar(envVar, "L2_NODE_URL"),
56 | },
57 | &cli.Int64Flag{
58 | Name: StartOutputIndexFlagName,
59 | Usage: "Output index to start from. -1 to find first unfinalized index",
60 | Value: -1,
61 | EnvVars: opservice.PrefixEnvVar(envVar, "START_OUTPUT_INDEX"),
62 | },
63 | &cli.StringFlag{
64 | Name: OptimismPortalAddressFlagName,
65 | Usage: "Address of the OptimismPortal contract",
66 | EnvVars: opservice.PrefixEnvVar(envVar, "OPTIMISM_PORTAL"),
67 | Required: true,
68 | },
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/.env.op.mainnet.example:
--------------------------------------------------------------------------------
1 | FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL=""
2 | FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL=""
3 | FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL=""
4 | FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL="0xbEb5Fc579115071764c7423A4f12eDde41f106Ed" # This is the address of the Optimism portal contract, this should be for the chain you are monitoring
5 | FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT=20872390 # This is the block height from which the monitoring will start, decide at which block height you want to start monitoring
6 | FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE=1000 # This is the range of blocks to be monitored
7 | MONITORISM_LOOP_INTERVAL_MSEC=100 # This is the interval in milliseconds for the monitoring loop
8 | MONITORISM_METRICS_PORT=7300 # This is the port on which the metrics server will run
9 | MONITORISM_METRICS_ENABLED=true # This is the flag to enable/disable the metrics server
10 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/.env.op.sepolia.example:
--------------------------------------------------------------------------------
1 | FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL=""
2 | FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL=""
3 | FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL=""
4 | FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL="0x16Fc5058F25648194471939df75CF27A2fdC48BC" # This is the address of the Optimism portal contract, this should be for the chain you are monitoring
5 | FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT=5914813 # This is the block height from which the monitoring will start, decide at which block height you want to start monitoring
6 | FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE=1000 # This is the range of blocks to be monitored
7 | MONITORISM_LOOP_INTERVAL_MSEC=100 # This is the interval in milliseconds for the monitoring loop
8 | MONITORISM_METRICS_PORT=7300 # This is the port on which the metrics server will run
9 | MONITORISM_METRICS_ENABLED=true # This is the flag to enable/disable the metrics server
10 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/.gitignore:
--------------------------------------------------------------------------------
1 | utilities
2 | testing
3 | !.env.*.example
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/README.md:
--------------------------------------------------------------------------------
1 | # Purpose of the Service
2 | faultproof_withdrawals has the following purpose:
3 | - Monitor Withdrawals: The service listens for WithdrawalProven events on the OptimismPortal contract on L1.
4 | - Validate Withdrawals: It verifies the validity of these withdrawals by checking the corresponding state on L2.
5 | - Detect Forgeries: The service identifies and reports any invalid withdrawals or potential forgeries.
6 |
7 | ## Enable Metrics
8 | This service will optionally expose a [prometeus metrics](https://prometheus.io/docs/concepts/metric_types/).
9 |
10 | In order to start the metrics service make sure to either export the variables or setup the right cli args
11 |
12 | ```bash
13 | export MONITORISM_METRICS_PORT=7300
14 | export MONITORISM_METRICS_ENABLED=true
15 |
16 | cd ../
17 | go run ./cmd/monitorism faultproof_withdrawals
18 | ```
19 | or
20 |
21 | ```bash
22 | cd ../
23 | go run ./cmd/monitorism faultproof_withdrawals --metrics.enabled --metrics.port 7300
24 | ```
25 |
26 | # Cli options
27 |
28 | ```bash
29 | go run ./cmd/monitorism faultproof_withdrawals --help
30 | NAME:
31 | Monitorism faultproof_withdrawals - Monitors withdrawals on the OptimismPortal in order to detect forgery. Note: Requires chains with Fault Proofs.
32 |
33 | USAGE:
34 | Monitorism faultproof_withdrawals [command options]
35 |
36 | DESCRIPTION:
37 | Monitors withdrawals on the OptimismPortal in order to detect forgery. Note: Requires chains with Fault Proofs.
38 |
39 | OPTIONS:
40 | --l1.geth.url value L1 execution layer node URL [$FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL]
41 | --l2.node.url value [DEPRECATED] L2 rollup node consensus layer (op-node) URL [$FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL]
42 | --l2.geth.url value L2 OP Stack execution layer client(op-geth) URL [$FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL]
43 | --l2.geth.backup.urls value Backup L2 OP Stack execution layer client URLs (format: name=url,name2=url2) [$FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_BACKUP_URLS]
44 | --event.block.range value Max block range when scanning for events (default: 1000) [$FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE]
45 | --start.block.height value Starting height to scan for events. This will take precedence if set. (default: -1) [$FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT]
46 | --start.block.hours.ago value How many hours in the past to start to check for forgery. Default will be 336 (14 days) days if not set. The real block to start from will be found within the hour precision. (default: 0) [$FAULTPROOF_WITHDRAWAL_MON_START_HOURS_IN_THE_PAST]
47 | --optimismportal.address value Address of the OptimismPortal contract [$FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL]
48 | --log.level value The lowest log level that will be output (default: INFO) [$MONITORISM_LOG_LEVEL]
49 | --log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$MONITORISM_LOG_FORMAT]
50 | --log.color Color the log output if in terminal mode (default: false) [$MONITORISM_LOG_COLOR]
51 | --log.pid Show pid in the log (default: false) [$MONITORISM_LOG_PID]
52 | --metrics.enabled Enable the metrics server (default: false) [$MONITORISM_METRICS_ENABLED]
53 | --metrics.addr value Metrics listening address (default: "0.0.0.0") [$MONITORISM_METRICS_ADDR]
54 | --metrics.port value Metrics listening port (default: 7300) [$MONITORISM_METRICS_PORT]
55 | --loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
56 | --help, -h show help
57 | ```
58 |
59 | ## Example run on sepolia op chain
60 |
61 | ```bash
62 | L1_GETH_URL="https://..."
63 | L2_OP_NODE_URL="https://..." # [DEPRECATED] This URL is no longer required
64 | L2_OP_GETH_URL="https://..."
65 | L2_OP_GETH_BACKUP_URLS="backup1=https://...,backup2=https://..."
66 |
67 | export MONITORISM_LOOP_INTERVAL_MSEC=100
68 | export MONITORISM_METRICS_PORT=7300
69 | export MONITORISM_METRICS_ENABLED=true
70 | export FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL="$L1_GETH_URL"
71 | export FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL="$L2_OP_NODE_URL"
72 | export FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL="$L2_OP_GETH_URL"
73 | export FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_BACKUP_URLS="$L2_OP_GETH_BACKUP_URLS"
74 | export FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL="0x16Fc5058F25648194471939df75CF27A2fdC48BC"
75 | export FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT=5914813
76 | export FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE=1000
77 |
78 | go run ./cmd/monitorism faultproof_withdrawals
79 | ```
80 |
81 | Metrics will be available at [http://localhost:7300](http://localhost:7300)
82 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/bindings/BINDING.md:
--------------------------------------------------------------------------------
1 | Repository https://github.com/ethereum-optimism/optimism.git cloned at commit fe7875e881ce50e75bf4e460bebb8b00bb38c315.
2 |
3 | This tool is compatible with these bindings. Future binding may break compatibility for those files.
4 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/cli.go:
--------------------------------------------------------------------------------
1 | package faultproof_withdrawals
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 |
8 | opservice "github.com/ethereum-optimism/optimism/op-service"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | const (
14 | L1GethURLFlagName = "l1.geth.url"
15 | L2NodeURLFlagName = "l2.node.url" // Deprecated
16 | L2GethURLFlagName = "l2.geth.url"
17 | L2GethBackupURLsFlagName = "l2.geth.backup.urls"
18 |
19 | EventBlockRangeFlagName = "event.block.range"
20 | StartingL1BlockHeightFlagName = "start.block.height"
21 | HoursInThePastToStartFromFlagName = "start.block.hours.ago"
22 |
23 | OptimismPortalAddressFlagName = "optimismportal.address"
24 | )
25 |
26 | type CLIConfig struct {
27 | L1GethURL string
28 | L2OpGethURL string
29 | L2OpNodeURL string
30 | L2GethBackupURLs []string
31 |
32 | EventBlockRange uint64
33 | StartingL1BlockHeight int64
34 | HoursInThePastToStartFrom uint64
35 |
36 | OptimismPortalAddress common.Address
37 | }
38 |
39 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
40 | cfg := CLIConfig{
41 | L1GethURL: ctx.String(L1GethURLFlagName),
42 | L2OpGethURL: ctx.String(L2GethURLFlagName),
43 | L2GethBackupURLs: ctx.StringSlice(L2GethBackupURLsFlagName),
44 | L2OpNodeURL: "", // Ignored since deprecated
45 | EventBlockRange: ctx.Uint64(EventBlockRangeFlagName),
46 | StartingL1BlockHeight: ctx.Int64(StartingL1BlockHeightFlagName),
47 | HoursInThePastToStartFrom: ctx.Uint64(HoursInThePastToStartFromFlagName),
48 | }
49 |
50 | portalAddress := ctx.String(OptimismPortalAddressFlagName)
51 | if !common.IsHexAddress(portalAddress) {
52 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", OptimismPortalAddressFlagName)
53 | }
54 | cfg.OptimismPortalAddress = common.HexToAddress(portalAddress)
55 |
56 | return cfg, nil
57 | }
58 |
59 | func CLIFlags(envVar string) []cli.Flag {
60 | return []cli.Flag{
61 | &cli.StringFlag{
62 | Name: L1GethURLFlagName,
63 | Usage: "L1 execution layer node URL",
64 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_GETH_URL"),
65 | Required: true,
66 | },
67 | &cli.StringFlag{
68 | Name: L2NodeURLFlagName,
69 | Usage: "[DEPRECATED] L2 rollup node consensus layer (op-node) URL - this flag is ignored",
70 | EnvVars: opservice.PrefixEnvVar(envVar, "L2_OP_NODE_URL"),
71 | Required: false,
72 | },
73 | &cli.StringFlag{
74 | Name: L2GethURLFlagName,
75 | Usage: "L2 OP Stack execution layer client(op-geth) URL",
76 | EnvVars: opservice.PrefixEnvVar(envVar, "L2_OP_GETH_URL"),
77 | Required: true,
78 | },
79 | &cli.StringSliceFlag{
80 | Name: L2GethBackupURLsFlagName,
81 | Usage: "Backup L2 OP Stack execution layer client URLs (format: name=url,name2=url2)",
82 | EnvVars: opservice.PrefixEnvVar(envVar, "L2_OP_GETH_BACKUP_URLS"),
83 | Required: false,
84 | },
85 | &cli.Uint64Flag{
86 | Name: EventBlockRangeFlagName,
87 | Usage: "Max block range when scanning for events",
88 | Value: 1000,
89 | EnvVars: opservice.PrefixEnvVar(envVar, "EVENT_BLOCK_RANGE"),
90 | Required: false,
91 | },
92 | &cli.Int64Flag{
93 | Name: StartingL1BlockHeightFlagName,
94 | Usage: "Starting height to scan for events. This will take precedence if set.",
95 | EnvVars: opservice.PrefixEnvVar(envVar, "START_BLOCK_HEIGHT"),
96 | Required: false,
97 | Value: -1,
98 | },
99 | &cli.Uint64Flag{
100 | Name: HoursInThePastToStartFromFlagName,
101 | Usage: "How many hours in the past to start to check for forgery. Default will be 336 (14 days) days if not set. The real block to start from will be found within the hour precision.",
102 | EnvVars: opservice.PrefixEnvVar(envVar, "START_HOURS_IN_THE_PAST"),
103 | Required: false,
104 | },
105 | &cli.StringFlag{
106 | Name: OptimismPortalAddressFlagName,
107 | Usage: "Address of the OptimismPortal contract",
108 | EnvVars: opservice.PrefixEnvVar(envVar, "OPTIMISM_PORTAL"),
109 | Required: true,
110 | },
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/monitor_live_mainnet_test.go:
--------------------------------------------------------------------------------
1 | //go:build live
2 | // +build live
3 |
4 | package faultproof_withdrawals
5 |
6 | import (
7 | "context"
8 | "io"
9 | "math/big"
10 | "testing"
11 |
12 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
13 | opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
14 | "github.com/ethereum/go-ethereum/common"
15 | "github.com/joho/godotenv"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | // NewTestMonitorMainnet initializes and returns a new Monitor instance for testing.
20 | // It sets up the necessary environment variables and configurations required for the monitor.
21 | func NewTestMonitorMainnet() *Monitor {
22 | envmap, err := godotenv.Read(".env.op.mainnet")
23 | if err != nil {
24 | panic("error")
25 | }
26 |
27 | ctx := context.Background()
28 | L1GethURL := envmap["FAULTPROOF_WITHDRAWAL_MON_L1_GETH_URL"]
29 | L2OpNodeURL := envmap["FAULTPROOF_WITHDRAWAL_MON_L2_OP_NODE_URL"]
30 | L2OpGethURL := envmap["FAULTPROOF_WITHDRAWAL_MON_L2_OP_GETH_URL"]
31 |
32 | FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL := "0xbEb5Fc579115071764c7423A4f12eDde41f106Ed"
33 | FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE := uint64(1000)
34 | FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT := int64(6789100)
35 |
36 | cfg := CLIConfig{
37 | L1GethURL: L1GethURL,
38 | L2OpGethURL: L2OpGethURL,
39 | L2OpNodeURL: L2OpNodeURL,
40 | EventBlockRange: FAULTPROOF_WITHDRAWAL_MON_EVENT_BLOCK_RANGE,
41 | StartingL1BlockHeight: FAULTPROOF_WITHDRAWAL_MON_START_BLOCK_HEIGHT,
42 | OptimismPortalAddress: common.HexToAddress(FAULTPROOF_WITHDRAWAL_MON_OPTIMISM_PORTAL),
43 | }
44 |
45 | clicfg := oplog.DefaultCLIConfig()
46 | output_writer := io.Discard // discard log output during tests to avoid pollution of the standard output
47 | log := oplog.NewLogger(output_writer, clicfg)
48 |
49 | metricsRegistry := opmetrics.NewRegistry()
50 | monitor, err := NewMonitor(ctx, log, opmetrics.With(metricsRegistry), cfg)
51 | if err != nil {
52 | panic(err)
53 | }
54 | return monitor
55 | }
56 |
57 | // TestSingleRunMainnet tests a single execution of the monitor's Run method.
58 | // It verifies that the state updates correctly after running.
59 | func TestSingleRunMainnet(t *testing.T) {
60 | test_monitor := NewTestMonitorMainnet()
61 |
62 | initialBlock := test_monitor.state.nextL1Height
63 | blockIncrement := test_monitor.maxBlockRange
64 | finalBlock := initialBlock + blockIncrement
65 |
66 | test_monitor.Run(test_monitor.ctx)
67 |
68 | require.Equal(t, finalBlock, test_monitor.state.nextL1Height)
69 | require.Equal(t, uint64(0), test_monitor.state.withdrawalsProcessed)
70 | require.Equal(t, uint64(0), test_monitor.state.eventsProcessed)
71 | require.Equal(t, uint64(0), test_monitor.state.numberOfPotentialAttackOnInProgressGames)
72 | require.Equal(t, uint64(0), test_monitor.state.numberOfPotentialAttacksOnDefenderWinsGames)
73 | require.Equal(t, uint64(0), test_monitor.state.numberOfSuspiciousEventsOnChallengerWinsGames)
74 |
75 | require.Equal(t, test_monitor.state.numberOfPotentialAttackOnInProgressGames, uint64(len(test_monitor.state.potentialAttackOnInProgressGames)))
76 | require.Equal(t, test_monitor.state.numberOfPotentialAttacksOnDefenderWinsGames, uint64(len(test_monitor.state.potentialAttackOnDefenderWinsGames)))
77 | require.Equal(t, test_monitor.state.numberOfSuspiciousEventsOnChallengerWinsGames, uint64(test_monitor.state.suspiciousEventsOnChallengerWinsGames.Len()))
78 |
79 | }
80 |
81 | // TestRun5Cycle1000BlocksMainnet tests multiple executions of the monitor's Run method over several cycles.
82 | // It verifies that the state updates correctly after each cycle.
83 | func TestRun5Cycle1000BlocksMainnet(t *testing.T) {
84 | test_monitor := NewTestMonitorMainnet()
85 |
86 | maxCycle := uint64(5)
87 | initialBlock := test_monitor.state.nextL1Height
88 | blockIncrement := test_monitor.maxBlockRange
89 |
90 | for cycle := uint64(1); cycle <= maxCycle; cycle++ {
91 | test_monitor.Run(test_monitor.ctx)
92 | }
93 |
94 | initialL1HeightGaugeValue, _ := GetGaugeValue(test_monitor.metrics.InitialL1HeightGauge)
95 | nextL1HeightGaugeValue, _ := GetGaugeValue(test_monitor.metrics.NextL1HeightGauge)
96 |
97 | withdrawalsProcessedCounterValue, _ := GetCounterValue(test_monitor.metrics.WithdrawalsProcessedCounter)
98 | eventsProcessedCounterValue, _ := GetCounterValue(test_monitor.metrics.EventsProcessedCounter)
99 |
100 | nodeConnectionFailuresCounterValue, _ := GetCounterValue(test_monitor.metrics.NodeConnectionFailuresCounter)
101 |
102 | expected_end_block := blockIncrement*maxCycle + initialBlock
103 | require.Equal(t, uint64(initialBlock), uint64(initialL1HeightGaugeValue))
104 | require.Equal(t, uint64(expected_end_block), uint64(nextL1HeightGaugeValue))
105 |
106 | require.Equal(t, uint64(0), uint64(eventsProcessedCounterValue))
107 | require.Equal(t, uint64(0), uint64(withdrawalsProcessedCounterValue))
108 | require.Equal(t, uint64(0), uint64(nodeConnectionFailuresCounterValue))
109 |
110 | require.Equal(t, uint64(0), test_monitor.metrics.previousEventsProcessed)
111 | require.Equal(t, uint64(0), test_monitor.metrics.previousWithdrawalsProcessed)
112 |
113 | }
114 |
115 | func TestRunSingleBlocksMainnet(t *testing.T) {
116 | test_monitor := NewTestMonitorMainnet()
117 |
118 | maxCycle := 1
119 | initialBlock := test_monitor.state.nextL1Height
120 | blockIncrement := test_monitor.maxBlockRange
121 | finalBlock := initialBlock + blockIncrement
122 |
123 | for cycle := 1; cycle <= maxCycle; cycle++ {
124 | test_monitor.Run(test_monitor.ctx)
125 | }
126 |
127 | require.Equal(t, test_monitor.state.nextL1Height, finalBlock)
128 | require.Equal(t, uint64(0), test_monitor.state.withdrawalsProcessed)
129 | require.Equal(t, uint64(0), test_monitor.state.eventsProcessed)
130 | require.Equal(t, 0, len(test_monitor.state.potentialAttackOnDefenderWinsGames))
131 | require.Equal(t, 0, len(test_monitor.state.potentialAttackOnInProgressGames))
132 | require.Equal(t, 0, test_monitor.state.suspiciousEventsOnChallengerWinsGames.Len())
133 | }
134 |
135 | func TestInvalidWithdrawalsOnMainnet(t *testing.T) {
136 | test_monitor := NewTestMonitorMainnet()
137 |
138 | // On mainnet for OP OptimismPortal, the block number 20873192 is known to have only 1 event
139 | start := uint64(20873192)
140 | stop := uint64(20873193)
141 | newEvents, err := test_monitor.withdrawalValidator.GetEnrichedWithdrawalsEvents(start, &stop)
142 | require.NoError(t, err)
143 | require.Equal(t, len(newEvents), 1)
144 |
145 | event := newEvents[0]
146 | require.NotNil(t, event)
147 |
148 | // Expected event:
149 | //{WithdrawalHash: 0x45fd4bbcf3386b1fdf75929345b9243c05cd7431a707e84c293b710d40220ebd, ProofSubmitter: 0x394400571C825Da37ca4D6780417DFB514141b1f}
150 | require.Equal(t, event.Event.WithdrawalHash, [32]byte(common.HexToHash("0x45fd4bbcf3386b1fdf75929345b9243c05cd7431a707e84c293b710d40220ebd")))
151 | require.Equal(t, event.Event.ProofSubmitter, common.HexToAddress("0x394400571C825Da37ca4D6780417DFB514141b1f"))
152 |
153 | //Expected DisputeGameData:
154 | // Game address: 0x52cE243d552369b11D6445Cd187F6393d3B42D4a
155 | require.Equal(t, event.DisputeGame.DisputeGameData.ProxyAddress, common.HexToAddress("0x52cE243d552369b11D6445Cd187F6393d3B42D4a"))
156 |
157 | // Expected Game root claim
158 | // 0xbc1c5ba13b936c6c23b7c51d425f25a8c9444771e851b6790f817a6002a14a33
159 | require.Equal(t, event.DisputeGame.DisputeGameData.RootClaim, [32]byte(common.HexToHash("0xbc1c5ba13b936c6c23b7c51d425f25a8c9444771e851b6790f817a6002a14a33")))
160 |
161 | // Expected L2 block number 1276288764
162 | require.Equal(t, event.DisputeGame.DisputeGameData.L2blockNumber, big.NewInt(1276288764))
163 |
164 | isValid, err := test_monitor.withdrawalValidator.IsWithdrawalEventValid(&event)
165 | require.EqualError(t, err, "game not enriched")
166 | require.False(t, isValid)
167 | err = test_monitor.withdrawalValidator.UpdateEnrichedWithdrawalEvent(&event)
168 | require.NoError(t, err)
169 | isValid, err = test_monitor.withdrawalValidator.IsWithdrawalEventValid(&event)
170 | require.NoError(t, err)
171 | require.False(t, isValid)
172 |
173 | }
174 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/.env.example:
--------------------------------------------------------------------------------
1 | # .env
2 | mainnet_geth_url="https://l1-geth.rpc"
3 | mainnet_op_geth_url="https://op-geth.rpc"
4 | mainnet_op_node_url="https://op-node.rpc"
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/.gitignore:
--------------------------------------------------------------------------------
1 | *.bundle.*
2 | node_modules/
3 | *.egg-info/
4 | .ipynb_checkpoints
5 | *.tsbuildinfo
6 |
7 | # Created by https://www.gitignore.io/api/python
8 | # Edit at https://www.gitignore.io/?templates=python
9 |
10 | ### Python ###
11 | # Byte-compiled / optimized / DLL files
12 | __pycache__/
13 | *.py[cod]
14 | *$py.class
15 |
16 | # C extensions
17 | *.so
18 |
19 | # Distribution / packaging
20 | .Python
21 | build/
22 | develop-eggs/
23 | dist/
24 | downloads/
25 | eggs/
26 | .eggs/
27 | lib64/
28 | parts/
29 | sdist/
30 | var/
31 | wheels/
32 | pip-wheel-metadata/
33 | share/python-wheels/
34 | .installed.cfg
35 | *.egg
36 | MANIFEST
37 |
38 | # PyInstaller
39 | # Usually these files are written by a python script from a template
40 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
41 | *.manifest
42 | *.spec
43 |
44 | # Installer logs
45 | pip-log.txt
46 | pip-delete-this-directory.txt
47 |
48 | # Unit test / coverage reports
49 | htmlcov/
50 | .tox/
51 | .nox/
52 | .coverage
53 | .coverage.*
54 | .cache
55 | nosetests.xml
56 | coverage.xml
57 | *.cover
58 | .hypothesis/
59 | .pytest_cache/
60 |
61 | # Translations
62 | *.mo
63 | *.pot
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # pyenv
75 | .python-version
76 |
77 | # celery beat schedule file
78 | celerybeat-schedule
79 |
80 | # SageMath parsed files
81 | *.sage.py
82 |
83 | # Spyder project settings
84 | .spyderproject
85 | .spyproject
86 |
87 | # Rope project settings
88 | .ropeproject
89 |
90 | # Mr Developer
91 | .mr.developer.cfg
92 | .project
93 | .pydevproject
94 |
95 | # mkdocs documentation
96 | /site
97 |
98 | # mypy
99 | .mypy_cache/
100 | .dmypy.json
101 | dmypy.json
102 |
103 | # Pyre type checker
104 | .pyre/
105 |
106 | # OS X stuff
107 | *.DS_Store
108 |
109 | # End of https://www.gitignore.io/api/python
110 |
111 | _temp_extension
112 | junit.xml
113 | [uU]ntitled*
114 | notebook/static/*
115 | !notebook/static/favicons
116 | notebook/labextension
117 | notebook/schemas
118 | docs/source/changelog.md
119 | docs/source/contributing.md
120 |
121 | # playwright
122 | ui-tests/test-results
123 | ui-tests/playwright-report
124 |
125 | # VSCode
126 | .vscode
127 |
128 | # RTC
129 | .jupyter_ystore.db
130 |
131 | # yarn >=2.x local files
132 | .yarn/*
133 | .pnp.*
134 | ui-tests/.yarn/*
135 | ui-tests/.pnp.*
136 |
137 | local_data
138 | local_data/*
139 |
140 | .env
141 | nodes.yaml
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/Makefile:
--------------------------------------------------------------------------------
1 | VENV_DIR = .venv
2 | PYTHON = $(VENV_DIR)/bin/python3
3 | PIP = $(VENV_DIR)/bin/pip
4 | REQUIREMENTS = requirements.txt
5 | ENV_DIR = local_data
6 |
7 | .PHONY: all venv install clean start
8 |
9 | all: venv install
10 |
11 | venv:
12 | @which python3 > /dev/null || { echo "Error: python3 not found! Please install Python 3."; exit 1; }
13 | @if [ ! -d "$(VENV_DIR)" ]; then \
14 | echo "Creating virtual environment..."; \
15 | python3 -m venv $(VENV_DIR); \
16 | echo "To activate the virtual environment, run:"; \
17 | echo "source $(VENV_DIR)/bin/activate"; \
18 | else \
19 | echo "Virtual environment already exists."; \
20 | echo "To activate the virtual environment, run:"; \
21 | echo "source $(VENV_DIR)/bin/activate"; \
22 | fi
23 |
24 | install: venv
25 | @echo "Installing requirements..."
26 | $(PIP) install -r $(REQUIREMENTS)
27 |
28 | clean:
29 | @echo "Cleaning up..."
30 | rm -rf $(VENV_DIR)
31 |
32 | start: install
33 | @echo "Starting Jupyter Notebook..."
34 | $(PYTHON) -m jupyter lab
35 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/README.md:
--------------------------------------------------------------------------------
1 | # Jupyter Playbook for Incident Response
2 |
3 | This repository contains Jupyter notebooks designed to help manage and streamline incident response processes. Jupyter notebooks offer an interactive, visual environment that can assist in documenting and automating various steps during incidents, making them an ideal tool for incident response teams.
4 |
5 | ## Why Use Jupyter Notebooks for Incident Response?
6 |
7 | Jupyter notebooks allow for a flexible and dynamic response to incidents by combining live code, notes, and visualizations in one place. They are particularly helpful in:
8 |
9 | - **Documenting steps**: Keep a real-time log of actions taken during incident resolution.
10 | - **Automation**: Execute code directly within the notebook to gather information, analyze logs, or perform specific tasks.
11 | - **Collaboration**: Share the notebook across teams or incident responders to maintain consistent actions and responses.
12 |
13 | ## How to Use
14 |
15 | To run these notebooks locally:
16 |
17 | 1. Clone the repository.
18 | 2. Run the `make start` command, which will launch the notebooks in your local environment, allowing you to start your incident response process.
19 |
20 | 
21 |
22 | ### Alternative Usage in Visual Studio Code
23 |
24 | You can also run these notebooks directly in Visual Studio Code. The video below demonstrates:
25 | - Opening and configuring notebooks
26 | - Executing code blocks interactively
27 | - Setting required variables
28 |
29 | 
30 |
31 | ## Setting Variables
32 |
33 | Before starting, you will need to configure some local variables for the notebooks to function correctly. These variables can be set in your local environment or directly within the text of the notebook. To avoid setting environment variables repeatedly for multiple runbooks, you can store them in a `.env` file located in the same folder as the notebooks.
34 |
35 | There is an example file available for your convenience (`env.example`) that you can use to create your `.env` file and adjust it as needed. This will help streamline the process of setting up your environment variables for different playbooks.
36 |
37 | ## Improving Productivity
38 |
39 | As you develop new actions or workflows during incidents, you can save them within the notebooks and push the updates to Git. This allows the incident response process to evolve and improve continuously, helping to enhance productivity and ensure all team members have access to the latest procedures.
40 |
41 | ## ⚠️ Warning
42 |
43 | When committing runbooks back to the repository, **make sure not to commit any runs or logs containing sensitive data**. Review the content carefully to ensure no private information is included before pushing to Git.
44 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/abi/L2ToL1MessagePasser.json:
--------------------------------------------------------------------------------
1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes","name":"_message","type":"bytes"}],"name":"passMessageToL1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"sentMessages","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/abi/OptimismPortal.json:
--------------------------------------------------------------------------------
1 | [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"version","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"opaqueData","type":"bytes"}],"name":"TransactionDeposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"withdrawalHash","type":"bytes32"},{"indexed":false,"internalType":"bool","name":"success","type":"bool"}],"name":"WithdrawalFinalized","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"withdrawalHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"}],"name":"WithdrawalProven","type":"event"},{"inputs":[],"name":"GUARDIAN","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"L2_ORACLE","outputs":[{"internalType":"contract L2OutputOracle","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"SYSTEM_CONFIG","outputs":[{"internalType":"contract SystemConfig","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"uint64","name":"_gasLimit","type":"uint64"},{"internalType":"bool","name":"_isCreation","type":"bool"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"depositTransaction","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"donateETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"target","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Types.WithdrawalTransaction","name":"_tx","type":"tuple"}],"name":"finalizeWithdrawalTransaction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"finalizedWithdrawals","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"guardian","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"contract L2OutputOracle","name":"_l2Oracle","type":"address"},{"internalType":"contract SystemConfig","name":"_systemConfig","type":"address"},{"internalType":"contract SuperchainConfig","name":"_superchainConfig","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_l2OutputIndex","type":"uint256"}],"name":"isOutputFinalized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"l2Oracle","outputs":[{"internalType":"contract L2OutputOracle","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"l2Sender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint64","name":"_byteCount","type":"uint64"}],"name":"minimumGasLimit","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"params","outputs":[{"internalType":"uint128","name":"prevBaseFee","type":"uint128"},{"internalType":"uint64","name":"prevBoughtGas","type":"uint64"},{"internalType":"uint64","name":"prevBlockNum","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"paused_","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"target","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Types.WithdrawalTransaction","name":"_tx","type":"tuple"},{"internalType":"uint256","name":"_l2OutputIndex","type":"uint256"},{"components":[{"internalType":"bytes32","name":"version","type":"bytes32"},{"internalType":"bytes32","name":"stateRoot","type":"bytes32"},{"internalType":"bytes32","name":"messagePasserStorageRoot","type":"bytes32"},{"internalType":"bytes32","name":"latestBlockhash","type":"bytes32"}],"internalType":"struct Types.OutputRootProof","name":"_outputRootProof","type":"tuple"},{"internalType":"bytes[]","name":"_withdrawalProof","type":"bytes[]"}],"name":"proveWithdrawalTransaction","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"provenWithdrawals","outputs":[{"internalType":"bytes32","name":"outputRoot","type":"bytes32"},{"internalType":"uint128","name":"timestamp","type":"uint128"},{"internalType":"uint128","name":"l2OutputIndex","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"superchainConfig","outputs":[{"internalType":"contract SuperchainConfig","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"systemConfig","outputs":[{"internalType":"contract SystemConfig","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"stateMutability":"payable","type":"receive"}]
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethereum-optimism/monitorism/4bb786b68453d4d01c500929ca9ef665a3a3a8e3/op-monitorism/faultproof_withdrawals/runbooks/automated/lib/__init__.py
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/lib/superchain.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import toml
3 | from typing import Dict
4 |
5 | def get_superchain_file(l1_name: str, l2_superchain_name: str) -> Dict[str, str]:
6 | """
7 | Fetches the superchain file from the given l1_name and l2_superchain_name.
8 |
9 | Args:
10 | l1_name (str): The name of the L1 chain.
11 | l2_superchain_name (str): The name of the L2 superchain.
12 |
13 | Returns:
14 | dict: The parsed superchain file as a dictionary.
15 |
16 | Raises:
17 | requests.exceptions.RequestException: If there is an error making the HTTP request.
18 | toml.TomlDecodeError: If there is an error parsing the TOML file.
19 | """
20 | superchain_file = f"https://raw.githubusercontent.com/ethereum-optimism/superchain-registry/main/superchain/configs/{l1_name}/{l2_superchain_name}.toml"
21 | try:
22 | response = requests.get(superchain_file)
23 | response.raise_for_status()
24 | superchain = toml.loads(response.text)
25 | return superchain
26 | except requests.exceptions.RequestException as e:
27 | raise e
28 | except toml.TomlDecodeError as e:
29 | raise e
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/requirements.txt:
--------------------------------------------------------------------------------
1 | jupyterlab
2 | python-dotenv
3 | requests
4 | toml
5 | web3
6 | urllib3
7 | ipywidgets
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/runbooks/automated/runbooks/lib/web3.py:
--------------------------------------------------------------------------------
1 | from web3 import Web3
2 | from typing import List, Any
3 | import json
4 | from datetime import datetime, timezone
5 | import urllib3
6 | # Disable warnings for insecure HTTPS requests
7 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
8 |
9 | # Custom request settings to ignore SSL certificate verification
10 | request_kwargs = {
11 | 'verify': False # Disable SSL verification
12 | }
13 |
14 | class Web3Utility:
15 |
16 | def __init__(self, l1_geth_url: str,l2_op_geth_url: str,l2_op_node_url: str,OptimismPortal_abi_path:str, OptimismPortalProxy:str,ignore_certificate: bool=False):
17 | self.OptimismPortal_abi_path=OptimismPortal_abi_path
18 | self.OptimismPortalProxy=OptimismPortalProxy
19 | self.l2_op_geth_url=l2_op_geth_url
20 | self.l2_op_node_url=l2_op_node_url
21 | self.l1_geth_url=l1_geth_url
22 |
23 | if ignore_certificate:
24 | self.web3 = Web3(Web3.HTTPProvider(l1_geth_url,request_kwargs=request_kwargs))
25 | else:
26 | self.web3 = Web3(Web3.HTTPProvider(l1_geth_url))
27 | if not self.web3.is_connected():
28 | print("Failed to connect to Web3 provider.")
29 |
30 | contract_abi = None
31 | with open( self.OptimismPortal_abi_path, 'r') as file:
32 | contract_abi = json.load(file)
33 | self.contract_abi = contract_abi
34 |
35 | self.contract = self.web3.eth.contract(address=self.OptimismPortalProxy, abi=contract_abi)
36 |
37 |
38 | def find_latest_withdrawal_event(self, batch_size: int = 1000) -> List[Any]:
39 | """
40 | Fetches the latest event from the OptimismPortal contract by searching in increments of `batch_size` blocks.
41 |
42 | Args:
43 | abi_path (str): The path to the contract ABI.
44 | contract_address (str): The address of the OptimismPortal contract.
45 | batch_size (int, optional): Number of blocks to search at a time. Defaults to 1000.
46 |
47 | Returns:
48 | Dict: A dictionary containing the latest event log and its timestamp.
49 |
50 | Raises:
51 | Exception: If there is an error fetching the events or if no events are found.
52 | """
53 |
54 | contract=self.contract
55 | latest_block = self.web3.eth.block_number
56 | current_block = latest_block
57 |
58 | # Search in batches of `batch_size` blocks
59 | while current_block > 0:
60 | from_block = max(0, current_block - batch_size)
61 | try:
62 | logs = contract.events.WithdrawalProvenExtension1().get_logs(from_block=from_block, to_block=current_block)
63 | if logs:
64 | # Return the latest event found along with its timestamp
65 | last_log = logs[-1]
66 | block_number = last_log["blockNumber"]
67 | timestamp_formatted = self.get_block_timestamp(block_number)
68 | return {"log": last_log, "timestamp": timestamp_formatted}
69 | except Exception as e:
70 | print(f"Error fetching logs between blocks {from_block} and {current_block}: {str(e)}")
71 |
72 | # Move the search window to the previous `batch_size` block range
73 | current_block = from_block
74 |
75 | raise Exception("No WithdrawalProven event found within the searched block range.")
76 |
77 |
78 | def get_block_timestamp(self, blockNumber: int):
79 | """
80 | Fetches the timestamp of a block.
81 |
82 | Args:
83 | web3 (Web3): An instance of the Web3 class.
84 | blockNumber (int): The block number.
85 |
86 | Returns:
87 | dict: A dictionary containing the block number, timestamp, time since the last withdrawal, and formatted timestamp.
88 | """
89 |
90 | block=self.web3.eth.get_block(blockNumber)
91 | timestamp=block["timestamp"]
92 |
93 | ret = {
94 | "blockNumber": blockNumber,
95 | "timestamp": timestamp,
96 | "time_since_last_withdrawal": f"{datetime.now(timezone.utc) - datetime.fromtimestamp(timestamp, tz=timezone.utc)}",
97 | "formatted_timestamp": f"{datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}",
98 | }
99 | return ret
100 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/validator/dispute_game_factory_helper.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/ethereum-optimism/monitorism/op-monitorism/faultproof_withdrawals/bindings/dispute"
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/ethclient"
11 | )
12 |
13 | // DisputeGameFactoryCoordinates holds the details of a dispute game.
14 | type DisputeGameFactoryCoordinates struct {
15 | GameType uint32 // The type of the dispute game.
16 | GameIndex uint64 // The index of the dispute game.
17 | disputeGameProxyAddress common.Address // The address of the dispute game proxy.
18 | disputeGameProxyTimestamp uint64 // The timestamp of the dispute game proxy.
19 | }
20 |
21 | // DisputeFactoryGameHelper assists in interacting with the dispute game factory.
22 | type DisputeFactoryGameHelper struct {
23 | // objects
24 | l1Client *ethclient.Client // The L1 Ethereum client.
25 | DisputeGameFactoryCaller dispute.DisputeGameFactoryCaller // Caller for the dispute game factory contract.
26 | }
27 |
28 | // DisputeGameFactoryIterator iterates through dispute games.
29 | type DisputeGameFactoryIterator struct {
30 | DisputeGameFactoryCaller *dispute.DisputeGameFactoryCaller // Caller for the dispute game factory contract.
31 | currentIndex uint64 // The current index in the iteration.
32 | gameCount uint64 // Total number of games available.
33 | init bool // Indicates if the iterator has been initialized.
34 | DisputeGameFactoryCoordinates *DisputeGameFactoryCoordinates // Coordinates for the current dispute game.
35 | }
36 |
37 | // NewDisputeGameFactoryHelper initializes a new DisputeFactoryGameHelper.
38 | // It binds to the dispute game factory contract and returns a helper instance.
39 | func NewDisputeGameFactoryHelper(ctx context.Context, l1Client *ethclient.Client, disputeGameFactoryAddress common.Address) (*DisputeFactoryGameHelper, error) {
40 | disputeGameFactory, err := dispute.NewDisputeGameFactory(disputeGameFactoryAddress, l1Client)
41 | if err != nil {
42 | return nil, fmt.Errorf("failed to bind to dispute game factory: %w", err)
43 | }
44 | disputeGameFactoryCaller := disputeGameFactory.DisputeGameFactoryCaller
45 |
46 | return &DisputeFactoryGameHelper{
47 | l1Client: l1Client,
48 | DisputeGameFactoryCaller: disputeGameFactoryCaller,
49 | }, nil
50 | }
51 |
52 | // GetDisputeGameCoordinatesFromGameIndex retrieves the coordinates of a dispute game by its index.
53 | // It returns the coordinates including game type, index, proxy address, and timestamp.
54 | func (df *DisputeFactoryGameHelper) GetDisputeGameCoordinatesFromGameIndex(gameIndex uint64) (*DisputeGameFactoryCoordinates, error) {
55 | gameDetails, err := df.DisputeGameFactoryCaller.GameAtIndex(nil, big.NewInt(int64(gameIndex)))
56 | if err != nil {
57 | return nil, fmt.Errorf("failed to get dispute game details: %w", err)
58 | }
59 |
60 | return &DisputeGameFactoryCoordinates{
61 | GameType: gameDetails.GameType,
62 | GameIndex: gameIndex,
63 | disputeGameProxyAddress: gameDetails.Proxy,
64 | disputeGameProxyTimestamp: gameDetails.Timestamp,
65 | }, nil
66 | }
67 |
68 | // GetDisputeGameCount returns the total count of dispute games available in the factory.
69 | func (df *DisputeFactoryGameHelper) GetDisputeGameCount() (uint64, error) {
70 | gameCountBigInt, err := df.DisputeGameFactoryCaller.GameCount(nil)
71 | if err != nil {
72 | return 0, fmt.Errorf("failed to get num dispute games: %w", err)
73 | }
74 | return gameCountBigInt.Uint64(), nil
75 | }
76 |
77 | // GetDisputeGameIteratorFromDisputeGameFactory creates an iterator for the dispute games in the factory.
78 | // It returns the iterator with the total number of games.
79 | func (df *DisputeFactoryGameHelper) GetDisputeGameIteratorFromDisputeGameFactory() (*DisputeGameFactoryIterator, error) {
80 | gameCountBigInt, err := df.DisputeGameFactoryCaller.GameCount(nil)
81 | if err != nil {
82 | return nil, fmt.Errorf("failed to get num dispute games: %w", err)
83 | }
84 | gameCount := gameCountBigInt.Uint64()
85 |
86 | return &DisputeGameFactoryIterator{
87 | DisputeGameFactoryCaller: &df.DisputeGameFactoryCaller,
88 | currentIndex: 0,
89 | gameCount: gameCount,
90 | DisputeGameFactoryCoordinates: nil,
91 | }, nil
92 | }
93 |
94 | // RefreshElements refreshes the game count for the iterator.
95 | func (dgf *DisputeGameFactoryIterator) RefreshElements() error {
96 | gameCountBigInt, err := dgf.DisputeGameFactoryCaller.GameCount(nil)
97 | if err != nil {
98 | return fmt.Errorf("failed to get num dispute games: %w", err)
99 | }
100 | dgf.gameCount = gameCountBigInt.Uint64()
101 | return nil
102 | }
103 |
104 | // Next moves the iterator to the next dispute game.
105 | // It returns true if there is a next game; otherwise, false.
106 | func (dgf *DisputeGameFactoryIterator) Next() bool {
107 | if dgf.currentIndex >= dgf.gameCount-1 {
108 | return false
109 | }
110 |
111 | var currentIndex uint64 = 0
112 | if dgf.init {
113 | currentIndex = dgf.currentIndex + 1
114 | }
115 |
116 | gameDetails, err := dgf.DisputeGameFactoryCaller.GameAtIndex(nil, big.NewInt(int64(currentIndex)))
117 | if err != nil {
118 | return false
119 | }
120 |
121 | dgf.init = true
122 | dgf.currentIndex = currentIndex
123 |
124 | dgf.DisputeGameFactoryCoordinates = &DisputeGameFactoryCoordinates{
125 | GameType: gameDetails.GameType,
126 | GameIndex: currentIndex,
127 | disputeGameProxyAddress: gameDetails.Proxy,
128 | disputeGameProxyTimestamp: gameDetails.Timestamp,
129 | }
130 |
131 | return true
132 | }
133 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/validator/fault_dispute_game_helper.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/ethereum-optimism/monitorism/op-monitorism/faultproof_withdrawals/bindings/dispute"
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/ethclient"
11 |
12 | lru "github.com/hashicorp/golang-lru"
13 | )
14 |
15 | // FaultDisputeGameProxy represents a proxy for the fault dispute game.
16 | type FaultDisputeGameProxy struct {
17 | FaultDisputeGame *dispute.FaultDisputeGame // The underlying fault dispute game.
18 | DisputeGameData *DisputeGameData // Data related to the dispute game.
19 | }
20 |
21 | // DisputeGameData holds the details of a dispute game.
22 | type DisputeGameData struct {
23 | ProxyAddress common.Address // The address of the dispute game proxy.
24 | RootClaim [32]byte // The root claim associated with the dispute game.
25 | L2blockNumber *big.Int // The L2 block number related to the game.
26 | L2ChainID *big.Int // The L2 chain ID associated with the game.
27 | Status GameStatus // The current status of the game.
28 | CreatedAt uint64 // Timestamp when the game was created.
29 | ResolvedAt uint64 // Timestamp when the game was resolved.
30 | }
31 |
32 | // FaultDisputeGameHelper assists in interacting with fault dispute games.
33 | type FaultDisputeGameHelper struct {
34 | // objects
35 | l1Client *ethclient.Client // The L1 Ethereum client.
36 | ctx context.Context // Context for managing cancellation and timeouts.
37 | gameCache *lru.Cache // Cache for storing game proxies.
38 | }
39 |
40 | // OutputResponse represents the response structure for output-related data.
41 | type OutputResponse struct {
42 | Version string `json:"version"` // The version of the output.
43 | OutputRoot string `json:"outputRoot"` // The output root associated with the response.
44 | }
45 |
46 | // GameStatus represents the status of a dispute game.
47 | type GameStatus uint8
48 |
49 | // Define constants for the GameStatus using iota.
50 | const (
51 | IN_PROGRESS GameStatus = iota // The game is currently in progress and has not been resolved.
52 | CHALLENGER_WINS // The game has concluded, and the root claim was challenged successfully.
53 | DEFENDER_WINS // The game has concluded, and the root claim could not be contested.
54 | )
55 |
56 | // String implements the Stringer interface for pretty printing the GameStatus.
57 | func (gs GameStatus) String() string {
58 | switch gs {
59 | case IN_PROGRESS:
60 | return "IN_PROGRESS"
61 | case CHALLENGER_WINS:
62 | return "CHALLENGER_WINS"
63 | case DEFENDER_WINS:
64 | return "DEFENDER_WINS"
65 | default:
66 | return "UNKNOWN"
67 | }
68 | }
69 |
70 | // String provides a string representation of DisputeGameData.
71 | func (d DisputeGameData) String() string {
72 | return fmt.Sprintf("DisputeGame[ disputeGameProxyAddress: %v rootClaim: %s l2blockNumber: %s l2ChainID: %s status: %v createdAt: %v resolvedAt: %v ]",
73 | d.ProxyAddress,
74 | common.BytesToHash(d.RootClaim[:]),
75 | d.L2blockNumber.String(),
76 | d.L2ChainID.String(),
77 | d.Status,
78 | Timestamp(d.CreatedAt),
79 | Timestamp(d.ResolvedAt),
80 | )
81 | }
82 |
83 | // String provides a string representation of the FaultDisputeGameProxy.
84 | func (p *FaultDisputeGameProxy) String() string {
85 | return fmt.Sprintf("FaultDisputeGameProxy[ DisputeGameData=%v ]", p.DisputeGameData)
86 | }
87 |
88 | const gameCacheSize = 1000
89 |
90 | // NewFaultDisputeGameHelper initializes a new FaultDisputeGameHelper.
91 | // It creates a cache for storing game proxies and returns the helper instance.
92 | func NewFaultDisputeGameHelper(ctx context.Context, l1Client *ethclient.Client) (*FaultDisputeGameHelper, error) {
93 | gameCache, err := lru.New(gameCacheSize)
94 | if err != nil {
95 | return nil, fmt.Errorf("failed to create cache: %w", err)
96 | }
97 |
98 | return &FaultDisputeGameHelper{
99 | l1Client: l1Client,
100 | ctx: ctx,
101 | gameCache: gameCache,
102 | }, nil
103 | }
104 |
105 | // GetDisputeGameProxyFromAddress retrieves the FaultDisputeGameProxy from the specified address.
106 | // It fetches the game details and caches the result for future use.
107 | func (fd *FaultDisputeGameHelper) GetDisputeGameProxyFromAddress(disputeGameProxyAddress common.Address) (FaultDisputeGameProxy, error) {
108 | ret, found := fd.gameCache.Get(disputeGameProxyAddress)
109 | if !found {
110 | faultDisputeGame, err := dispute.NewFaultDisputeGame(disputeGameProxyAddress, fd.l1Client)
111 | if err != nil {
112 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to bind to dispute game: %w", err)
113 | }
114 |
115 | rootClaim, err := faultDisputeGame.RootClaim(nil)
116 | if err != nil {
117 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get root claim for game: %w", err)
118 | }
119 | l2blockNumber, err := faultDisputeGame.L2BlockNumber(nil)
120 | if err != nil {
121 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get l2 block number for game: %w", err)
122 | }
123 |
124 | l2ChainID, err := faultDisputeGame.L2ChainId(nil)
125 | if err != nil {
126 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get l2 chain id for game: %w", err)
127 | }
128 |
129 | gameStatus, err := faultDisputeGame.Status(nil)
130 | if err != nil {
131 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get game status: %w", err)
132 | }
133 |
134 | createdAt, err := faultDisputeGame.CreatedAt(nil)
135 | if err != nil {
136 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get game created at: %w", err)
137 | }
138 |
139 | resolvedAt, err := faultDisputeGame.ResolvedAt(nil)
140 | if err != nil {
141 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get game resolved at: %w", err)
142 | }
143 |
144 | ret = &FaultDisputeGameProxy{
145 | DisputeGameData: &DisputeGameData{
146 | ProxyAddress: disputeGameProxyAddress,
147 | RootClaim: rootClaim,
148 | L2blockNumber: l2blockNumber,
149 | L2ChainID: l2ChainID,
150 | Status: GameStatus(gameStatus),
151 | CreatedAt: createdAt,
152 | ResolvedAt: resolvedAt,
153 | },
154 | FaultDisputeGame: faultDisputeGame,
155 | }
156 |
157 | fd.gameCache.Add(disputeGameProxyAddress, ret)
158 | }
159 |
160 | return *(ret.(*FaultDisputeGameProxy)), nil
161 | }
162 |
163 | // RefreshState updates the state of the FaultDisputeGameProxy.
164 | // It retrieves the current status and resolved timestamp of the game.
165 | func (fd *FaultDisputeGameProxy) RefreshState() error {
166 | if fd.FaultDisputeGame == nil {
167 | return fmt.Errorf("dispute game is nil")
168 | }
169 |
170 | gameStatus, err := fd.FaultDisputeGame.Status(nil)
171 | if err != nil {
172 | return fmt.Errorf("failed to get game status: %w", err)
173 | }
174 |
175 | fd.DisputeGameData.Status = GameStatus(gameStatus)
176 |
177 | resolvedAt, err := fd.FaultDisputeGame.ResolvedAt(nil)
178 | if err != nil {
179 | return fmt.Errorf("failed to get game resolved at: %w", err)
180 | }
181 | fd.DisputeGameData.ResolvedAt = resolvedAt
182 | return nil
183 | }
184 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/validator/l1_proxy.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/ethereum-optimism/monitorism/op-monitorism/faultproof_withdrawals/bindings/l1"
8 | "github.com/ethereum/go-ethereum/common"
9 | "github.com/ethereum/go-ethereum/ethclient"
10 | )
11 |
12 | type L1Proxy struct {
13 | l1GethClient *ethclient.Client // The L1 Ethereum client.
14 | optimismPortal2Helper *OptimismPortal2Helper // Helper for interacting with Optimism Portal 2.
15 | faultDisputeGameHelper *FaultDisputeGameHelper // Helper for dispute game interactions.
16 | ctx context.Context // Context for managing cancellation and timeouts.
17 | Connections uint64
18 | ConnectionErrors uint64
19 | }
20 |
21 | func NewL1Proxy(ctx context.Context, l1GethClientURL string, OptimismPortalAddress common.Address) (*L1Proxy, error) {
22 | l1GethClient, err := ethclient.Dial(l1GethClientURL)
23 | if err != nil {
24 | return nil, fmt.Errorf("failed to dial l1: %w", err)
25 | }
26 |
27 | optimismPortal2Helper, err := NewOptimismPortal2Helper(ctx, l1GethClient, OptimismPortalAddress)
28 | if err != nil {
29 | return nil, fmt.Errorf("failed to bind to the OptimismPortal: %w", err)
30 | }
31 |
32 | faultDisputeGameHelper, err := NewFaultDisputeGameHelper(ctx, l1GethClient)
33 | if err != nil {
34 | return nil, fmt.Errorf("failed to create dispute game helper: %w", err)
35 | }
36 |
37 | return &L1Proxy{
38 | l1GethClient: l1GethClient,
39 | optimismPortal2Helper: optimismPortal2Helper,
40 | faultDisputeGameHelper: faultDisputeGameHelper,
41 | ctx: ctx,
42 | Connections: 0,
43 | ConnectionErrors: 0,
44 | }, nil
45 | }
46 |
47 | func (l1Proxy *L1Proxy) IsGameBlacklisted(disputeGame *FaultDisputeGameProxy) (bool, error) {
48 | l1Proxy.Connections++
49 | blacklisted, err := l1Proxy.optimismPortal2Helper.IsGameBlacklisted(disputeGame)
50 | if err != nil {
51 | l1Proxy.ConnectionErrors++
52 | return false, fmt.Errorf("failed to check if game is blacklisted: %w", err)
53 | }
54 | return blacklisted, nil
55 | }
56 |
57 | func (l1Proxy *L1Proxy) GetSubmittedProofsDataFromWithdrawalhashAndProofSubmitterAddress(withdrawalHash [32]byte, proofSubmitterAddress common.Address) (*SubmittedProofData, error) {
58 | l1Proxy.Connections++
59 | submittedProofData, err := l1Proxy.optimismPortal2Helper.GetSubmittedProofsDataFromWithdrawalhashAndProofSubmitterAddress(withdrawalHash, proofSubmitterAddress)
60 | if err != nil {
61 | l1Proxy.ConnectionErrors++
62 | return nil, fmt.Errorf("failed to get submitted proofs data: %w", err)
63 | }
64 | return submittedProofData, nil
65 | }
66 |
67 | func (l1Proxy *L1Proxy) GetDisputeGameProxyFromAddress(disputeGameProxyAddress common.Address) (FaultDisputeGameProxy, error) {
68 | l1Proxy.Connections++
69 | disputeGameProxy, err := l1Proxy.faultDisputeGameHelper.GetDisputeGameProxyFromAddress(disputeGameProxyAddress)
70 | if err != nil {
71 | l1Proxy.ConnectionErrors++
72 | return FaultDisputeGameProxy{}, fmt.Errorf("failed to get dispute game proxy: %w", err)
73 | }
74 | return disputeGameProxy, nil
75 | }
76 |
77 | func (l1Proxy *L1Proxy) GetProvenWithdrawalsExtension1Events(start uint64, end *uint64) ([]WithdrawalProvenExtension1Event, error) {
78 | l1Proxy.Connections++
79 | events, err := l1Proxy.optimismPortal2Helper.GetProvenWithdrawalsExtension1Events(start, end)
80 | if err != nil {
81 | l1Proxy.ConnectionErrors++
82 | return nil, fmt.Errorf("failed to get proven withdrawals extension1 events: %w", err)
83 | }
84 | return events, nil
85 | }
86 |
87 | func (l1Proxy *L1Proxy) GetProvenWithdrawalsExtension1EventsIterator(start uint64, end *uint64) (*l1.OptimismPortal2WithdrawalProvenExtension1Iterator, error) {
88 | l1Proxy.Connections++
89 | eventsIterator, err := l1Proxy.optimismPortal2Helper.GetProvenWithdrawalsExtension1EventsIterator(start, end)
90 | if err != nil {
91 | l1Proxy.ConnectionErrors++
92 | return nil, fmt.Errorf("failed to get proven withdrawals extension1 events iterator: %w", err)
93 | }
94 | return eventsIterator, nil
95 | }
96 |
97 | func (l1Proxy *L1Proxy) BlockNumber() (uint64, error) {
98 | l1Proxy.Connections++
99 | blockNumber, err := l1Proxy.l1GethClient.BlockNumber(l1Proxy.ctx)
100 | if err != nil {
101 | l1Proxy.ConnectionErrors++
102 | return 0, fmt.Errorf("failed to get block number: %w", err)
103 | }
104 | return blockNumber, nil
105 | }
106 |
107 | func (l1Proxy *L1Proxy) GetTotalConnections() uint64 {
108 | return l1Proxy.Connections
109 | }
110 |
111 | func (l1Proxy *L1Proxy) GetTotalConnectionErrors() uint64 {
112 | return l1Proxy.ConnectionErrors
113 | }
114 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/validator/l2_proxy.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "math/big"
8 |
9 | "github.com/ethereum/go-ethereum/crypto"
10 |
11 | "github.com/ethereum-optimism/optimism/op-service/eth"
12 | "github.com/ethereum-optimism/optimism/op-service/predeploys"
13 | "github.com/ethereum/go-ethereum/common"
14 | "github.com/ethereum/go-ethereum/common/hexutil"
15 | "github.com/ethereum/go-ethereum/ethclient"
16 | )
17 |
18 | type L2Proxy struct {
19 | l2GethClient *ethclient.Client
20 | chainID *big.Int
21 | ctx context.Context
22 | l2OpGethBackupClients map[string]*ethclient.Client
23 | ConnectionError map[string]uint64
24 | Connections map[string]uint64
25 | }
26 |
27 | func NewL2Proxy(ctx context.Context, l2GethClientURL string, l2GethBackupClientsURLs map[string]string) (*L2Proxy, error) {
28 | l2GethClient, err := ethclient.Dial(l2GethClientURL)
29 | if err != nil {
30 | return nil, fmt.Errorf("failed to dial l2: %w", err)
31 | }
32 |
33 | chainID, err := l2GethClient.ChainID(ctx)
34 | if err != nil {
35 | return nil, fmt.Errorf("failed to get l2 chain id: %w", err)
36 | }
37 |
38 | // if backup urls are provided, create a backup client for each
39 | var l2OpGethBackupClients map[string]*ethclient.Client
40 | if len(l2GethBackupClientsURLs) > 0 {
41 | l2OpGethBackupClients, err = GethBackupClientsDictionary(ctx, l2GethBackupClientsURLs, chainID)
42 | if err != nil {
43 | return nil, fmt.Errorf("failed to create backup clients: %w", err)
44 | }
45 |
46 | }
47 |
48 | return &L2Proxy{l2GethClient: l2GethClient, chainID: chainID, ctx: ctx, l2OpGethBackupClients: l2OpGethBackupClients, ConnectionError: make(map[string]uint64), Connections: make(map[string]uint64)}, nil
49 | }
50 |
51 | // get latest known L2 block number
52 | func (l2Proxy *L2Proxy) BlockNumber() (uint64, error) {
53 | blockNumber, err := l2Proxy.l2GethClient.BlockNumber(l2Proxy.ctx)
54 | l2Proxy.Connections["default"]++
55 | if err != nil {
56 | l2Proxy.ConnectionError["default"]++
57 | return 0, fmt.Errorf("failed to get block number: %w", err)
58 | }
59 | return blockNumber, nil
60 | }
61 |
62 | // GetOutputRootFromCalculation retrieves the output root by calculating it from the given block number.
63 | // It returns the calculated output root as a Bytes32 array.
64 | func (l2Proxy *L2Proxy) VerifyWithdrawalHashAndClaim(blockNumber *big.Int, withdrawalHash [32]byte, claim [32]byte) (bool, bool, string, error) {
65 | // We get the block from our trusted op-geth node
66 | block, err := l2Proxy.l2GethClient.BlockByNumber(l2Proxy.ctx, blockNumber)
67 | l2Proxy.Connections["default"]++
68 | if err != nil {
69 | l2Proxy.ConnectionError["default"]++
70 | return false, false, "", fmt.Errorf("failed to get output at block for game blockInt:%v error:%w", blockNumber, err)
71 | }
72 |
73 | // We get proof from our trusted op-geth node if present
74 | accountResult, clientUsed, err := l2Proxy.RetrieveEthProof(blockNumber, withdrawalHash)
75 | l2Proxy.Connections["default"]++
76 | if err != nil {
77 | l2Proxy.ConnectionError["default"]++
78 | return false, false, "", fmt.Errorf("failed to get proof: %w", err)
79 | }
80 | // verify the proof when this comes from untrusted node (merkle trie)
81 | err = accountResult.Verify(block.Root())
82 | if err != nil {
83 | return false, false, "", fmt.Errorf("failed to verify proof: %w", err)
84 | }
85 | outputRoot := eth.OutputRoot(ð.OutputV0{StateRoot: [32]byte(block.Root()), MessagePasserStorageRoot: [32]byte(accountResult.StorageHash), BlockHash: block.Hash()})
86 |
87 | return bytes.Equal(outputRoot[:], claim[:]), false, clientUsed, nil
88 | }
89 |
90 | // VerifyRootClaimAndWithdrawalHash verifies that a given root claim matches the computed output root
91 | // for a block and that the withdrawal hash proof is valid.
92 | //
93 | // Parameters:
94 | // - blockNumber: The L2 block number to verify
95 | // - rootClaim: The claimed output root to verify against
96 | // - withdrawalHash: The withdrawal hash to verify
97 | //
98 | // Returns:
99 | // - bool: True if the computed output root matches the claim
100 | // - bool: True if the withdrawal proof is valid
101 | // - string: The name of the client used to retrieve the proof
102 | // - error: Any error that occurred during verification
103 | func (l2Proxy *L2Proxy) VerifyRootClaimAndWithdrawalHash(blockNumber *big.Int, rootClaim [32]byte, withdrawalHash [32]byte) (bool, bool, string, error) {
104 | // We get the block from our trusted op-geth node
105 | block, err := l2Proxy.l2GethClient.BlockByNumber(l2Proxy.ctx, blockNumber)
106 | l2Proxy.Connections["default"]++
107 | if err != nil {
108 | l2Proxy.ConnectionError["default"]++
109 | return false, false, "", fmt.Errorf("failed to get output at block for game blockInt:%v error:%w", blockNumber, err)
110 | }
111 |
112 | // We get proof from our trusted op-geth node if present
113 | accountResult, clientUsed, err := l2Proxy.RetrieveEthProof(blockNumber, withdrawalHash)
114 | l2Proxy.Connections["default"]++
115 | if err != nil {
116 | l2Proxy.ConnectionError["default"]++
117 | return false, false, "", fmt.Errorf("failed to get proof: %w", err)
118 | }
119 |
120 | // verify the proof when this comes from untrusted node (merkle trie)
121 | err = accountResult.Verify(block.Root())
122 |
123 | outputRoot := eth.OutputRoot(ð.OutputV0{StateRoot: [32]byte(block.Root()), MessagePasserStorageRoot: [32]byte(accountResult.StorageHash), BlockHash: block.Hash()})
124 | return bytes.Equal(outputRoot[:], rootClaim[:]), err == nil, clientUsed, nil
125 | }
126 |
127 | // we retrieve the proof from the truested op-geth node and eventually from backup nodes if present
128 | func (l2Proxy *L2Proxy) RetrieveEthProof(blockNumber *big.Int, withdrawalHash [32]byte) (eth.AccountResult, string, error) {
129 | accountResult := eth.AccountResult{}
130 | encodedBlock := hexutil.EncodeBig(blockNumber)
131 |
132 | // Concatenate withdrawalHash with bytes(0) directly
133 | combined := append(withdrawalHash[:], make([]byte, 32)...) // append 32 zero bytes
134 | hash := crypto.Keccak256Hash(combined)
135 |
136 | storageKeys := []common.Hash{hash}
137 | fmt.Printf("Storage Keys: %v\n", storageKeys)
138 |
139 | l2Proxy.Connections["default"]++
140 | err := l2Proxy.l2GethClient.Client().CallContext(l2Proxy.ctx, &accountResult, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, storageKeys, encodedBlock)
141 | if err != nil {
142 | l2Proxy.ConnectionError["default"]++
143 | for clientName, client := range l2Proxy.l2OpGethBackupClients {
144 | l2Proxy.Connections[clientName]++
145 | client_err := client.Client().CallContext(l2Proxy.ctx, &accountResult, "eth_getProof", predeploys.L2ToL1MessagePasserAddr, storageKeys, encodedBlock)
146 | // if we get a proof, we return it
147 | if client_err == nil {
148 | return accountResult, clientName, nil
149 | }
150 | l2Proxy.ConnectionError[clientName]++
151 | }
152 |
153 | return eth.AccountResult{}, "", fmt.Errorf("failed to get proof from any node")
154 | }
155 | return accountResult, "default", nil
156 | }
157 |
158 | func (l2Proxy *L2Proxy) GetTotalConnections() uint64 {
159 | totalConnections := uint64(0)
160 | for _, connection := range l2Proxy.Connections {
161 | totalConnections += connection
162 | }
163 | return totalConnections
164 | }
165 |
166 | func (l2Proxy *L2Proxy) GetTotalConnectionErrors() uint64 {
167 | totalConnectionErrors := uint64(0)
168 | for _, connectionError := range l2Proxy.ConnectionError {
169 | totalConnectionErrors += connectionError
170 | }
171 | return totalConnectionErrors
172 | }
173 |
--------------------------------------------------------------------------------
/op-monitorism/faultproof_withdrawals/validator/utils.go:
--------------------------------------------------------------------------------
1 | package validator
2 |
3 | import (
4 | "encoding/hex"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | "github.com/ethereum/go-ethereum/common"
10 | )
11 |
12 | // Raw represents raw event data associated with a blockchain transaction.
13 | type Raw struct {
14 | BlockNumber uint64 // The block number in which the transaction is included.
15 | TxHash common.Hash // The hash of the transaction.
16 | }
17 |
18 | // String provides a string representation of Raw.
19 | func (r Raw) String() string {
20 | return fmt.Sprintf("{BlockNumber: %d, TxHash: %s}", r.BlockNumber, r.TxHash.String())
21 | }
22 |
23 | // Timestamp represents a Unix timestamp.
24 | type Timestamp uint64
25 |
26 | // String converts a Timestamp to a formatted string representation.
27 | // It returns the timestamp as a string in the format "2006-01-02 15:04:05 MST".
28 | func (timestamp Timestamp) String() string {
29 | t := time.Unix(int64(timestamp), 0)
30 | return t.Format("2006-01-02 15:04:05 MST")
31 | }
32 |
33 | // StringToBytes32 converts a hexadecimal string to a [32]uint8 array.
34 | // It returns the converted array and any error encountered during the conversion.
35 | func StringToBytes32(input string) ([32]uint8, error) {
36 | // Remove the "0x" prefix if present
37 | if strings.HasPrefix(input, "0x") || strings.HasPrefix(input, "0X") {
38 | input = input[2:]
39 | }
40 |
41 | // Decode the hexadecimal string
42 | bytes, err := hex.DecodeString(input)
43 | if err != nil {
44 | return [32]uint8{}, err
45 | }
46 |
47 | // Convert bytes to [32]uint8
48 | var array [32]uint8
49 | copy(array[:], bytes)
50 | return array, nil
51 | }
52 |
--------------------------------------------------------------------------------
/op-monitorism/global_events/README.md:
--------------------------------------------------------------------------------
1 | # Global Events Monitoring
2 |
3 | This monitoring module is made for taking YAML rules as configuration.
4 | 
5 |
6 | Once the YAML rules are configured correctly, we can listen to an event chosen to send the data through prometheus.
7 |
8 | ## CLI and Docs:
9 |
10 | ### CLI Args
11 |
12 | ```bash
13 | NAME:
14 | Monitorism global_events - Monitors global events with YAML configuration
15 |
16 | USAGE:
17 | Monitorism global_events [command options] [arguments...]
18 |
19 | DESCRIPTION:
20 | Monitors global events with YAML configuration
21 |
22 | OPTIONS:
23 | --l1.node.url value Node URL of L1 peer (default: "http://127.0.0.1:8545") [$GLOBAL_EVENT_MON_L1_NODE_URL]
24 | --nickname value Nickname of the chain being monitored [$GLOBAL_EVENT_MON_NICKNAME]
25 | --PathYamlRules value Path to the directory containing the yaml files with the events to monitor [$GLOBAL_EVENT_MON_PATH_YAML]
26 | --log.level value The lowest log level that will be output (default: INFO) [$MONITORISM_LOG_LEVEL]
27 | --log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$MONITORISM_LOG_FORMAT]
28 | --log.color Color the log output if in terminal mode (default: false) [$MONITORISM_LOG_COLOR]
29 | --metrics.enabled Enable the metrics server (default: false) [$MONITORISM_METRICS_ENABLED]
30 | --metrics.addr value Metrics listening address (default: "0.0.0.0") [$MONITORISM_METRICS_ADDR]
31 | --metrics.port value Metrics listening port (default: 7300) [$MONITORISM_METRICS_PORT]
32 | --loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
33 | --help, -h show help
34 |
35 | ```
36 |
37 | ### Yaml rules
38 |
39 | The rules are located here: `op-monitorism/global_events/rules/`. Then we have multiple folders depending on the networks you want to monitor (`mainnet` or `sepolia`) for now.
40 |
41 | ```yaml
42 | # This is a TEMPLATE file please copy this one
43 | # This watches all contracts for OP, Mode, and Base mainnets for two logs.
44 | version: 1.0
45 | name: Template SafeExecution Events (Success/Failure) L1 # Please put the L1 or L2 at the end of the name.
46 | priority: P5 # This is a test, so it is a P5
47 | #If addresses are empty like below, it will watch all addresses; otherwise, you can address specific addresses.
48 | addresses:
49 | # - 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed # Specific Addresses /!\ We are not supporting EIP 3770 yet, if the address is not starting by 0x, this will panic by safety measure.
50 | events:
51 | - signature: ExecutionFailure(bytes32,uint256) # List of the events to watch for the addresses.
52 | - signature: ExecutionSuccess(bytes32,uint256) # List of the events to watch for the addresses.
53 | ```
54 |
55 | ### Execution
56 |
57 | To run it:
58 |
59 | ```bash
60 |
61 | go run ../cmd/monitorism global_events --nickname MySuperNickName --l1.node.url https://localhost:8545 --PathYamlRules /tmp/Monitorism/op-monitorism/global_events/rules/rules_mainnet_L1 --loop.interval.msec 12000
62 |
63 | ```
64 |
--------------------------------------------------------------------------------
/op-monitorism/global_events/cli.go:
--------------------------------------------------------------------------------
1 | package global_events
2 |
3 | import (
4 | // "fmt"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 |
8 | // "github.com/ethereum/go-ethereum/common"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | // args in CLI have to be standardized and clean.
14 | const (
15 | L1NodeURLFlagName = "l1.node.url"
16 | NicknameFlagName = "nickname"
17 | PathYamlRulesFlagName = "PathYamlRules"
18 | )
19 |
20 | type CLIConfig struct {
21 | L1NodeURL string
22 | Nickname string
23 | PathYamlRules string
24 | // Optional
25 | }
26 |
27 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
28 | cfg := CLIConfig{
29 | L1NodeURL: ctx.String(L1NodeURLFlagName),
30 | Nickname: ctx.String(NicknameFlagName),
31 | PathYamlRules: ctx.String(PathYamlRulesFlagName),
32 | }
33 |
34 | return cfg, nil
35 | }
36 |
37 | func CLIFlags(envVar string) []cli.Flag {
38 | return []cli.Flag{
39 | &cli.StringFlag{
40 | Name: L1NodeURLFlagName,
41 | Usage: "Node URL of L1 peer",
42 | Value: "http://127.0.0.1:8545",
43 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
44 | },
45 | &cli.StringFlag{
46 | Name: NicknameFlagName,
47 | Usage: "Nickname of chain being monitored",
48 | EnvVars: opservice.PrefixEnvVar(envVar, "NICKNAME"), //need to change the name to BLOCKCHAIN_NAME
49 | Required: true,
50 | },
51 | &cli.StringFlag{
52 | Name: PathYamlRulesFlagName,
53 | Usage: "Path to the yaml file containing the events to monitor",
54 | EnvVars: opservice.PrefixEnvVar(envVar, "PATH_YAML"), //need to change the name to BLOCKCHAIN_NAME
55 | Required: true,
56 | },
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/op-monitorism/global_events/monitor_test.go:
--------------------------------------------------------------------------------
1 | package global_events
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 | )
8 |
9 | func TestFormatSignature(t *testing.T) {
10 | tests := []struct {
11 | name string
12 | input string
13 | expectedOutput string
14 | }{
15 | {
16 | name: "Basic Function",
17 | input: "balanceOf(address owner)",
18 | expectedOutput: "balanceOf(address)",
19 | },
20 | {
21 | name: "Function With Multiple Params",
22 | input: "transfer(address to, uint256 amount)",
23 | expectedOutput: "transfer(address,uint256)",
24 | },
25 | {
26 | name: "Function With No Params",
27 | input: "pause()",
28 | expectedOutput: "pause()",
29 | },
30 | {
31 | name: "Function With Extra Spaces",
32 | input: " approve ( address spender , uint256 value ) ",
33 | expectedOutput: "approve(address,uint256)",
34 | },
35 | {
36 | name: "Uniswap swap",
37 | input: "Swap (address sender,address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)",
38 | expectedOutput: "Swap(address,address,int256,int256,uint160,uint128,int24)",
39 | },
40 | {
41 | name: "Invalid Input",
42 | input: "invalidInput",
43 | expectedOutput: "",
44 | },
45 | }
46 |
47 | for _, test := range tests {
48 | t.Run(test.name, func(t *testing.T) {
49 | output := formatSignature(test.input)
50 | if output != test.expectedOutput {
51 | t.Errorf("Failed %s: expected %q but got %q", test.name, test.expectedOutput, output)
52 | }
53 | })
54 | }
55 | }
56 |
57 | func TestFormatAndHash(t *testing.T) {
58 | tests := []struct {
59 | name string
60 | input string
61 | expectedOutput common.Hash
62 | }{
63 | {
64 | name: "Uniswap swap",
65 | input: "Swap (address indexed sender,address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)",
66 | expectedOutput: common.HexToHash("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"),
67 | },
68 | }
69 |
70 | for _, test := range tests {
71 | t.Run(test.name, func(t *testing.T) {
72 | output := FormatAndHash(test.input)
73 | if output != test.expectedOutput {
74 | t.Errorf("Failed %s: expected %q but got %q", test.name, test.expectedOutput, output)
75 | }
76 | })
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/op-monitorism/global_events/rules/rules_mainnet_L1/rules_TEMPLATE_COPY_PASTE.yaml:
--------------------------------------------------------------------------------
1 | # This is a TEMPLATE file please copy this one
2 | # This watches all contacts for OP, Mode, and Base mainnets for two logs.
3 | version: 1.0
4 | name: Template SafeExecution Events (Success/Failure) L1 ETHEREUM # Please put the L1 or L2 at the end of the name.
5 | priority: P5 # This is a test, so it is a P5
6 | #If addresses is empty like below it will watch all addresses otherwise you can address specific addresses.
7 | addresses:
8 | # - 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed # /!\ SPECIFIC ADDRESS -> We are not supporting EIP 3770 yet, if the address is not starting by 0x, this will panic by safety measure.
9 | events:
10 | - signature: ExecutionFailure(bytes32,uint256) # List of the events to watch for the addresses.
11 | - signature: ExecutionSuccess(bytes32,uint256) # List of the events to watch for the addresses.
12 |
--------------------------------------------------------------------------------
/op-monitorism/global_events/rules/rules_sepolia_L1/rules_TEMPLATE_COPY_PASTE.yaml:
--------------------------------------------------------------------------------
1 | # This is a TEMPLATE file please copy this one
2 | # This watches all contacts for OP, Mode, and Base mainnets for two logs.
3 | version: 1.0
4 | name: Template SafeExecution Events (Success/Failure) L1 SEPOLIA # Please put the L1 or L2 at the end of the name.
5 | priority: P5 # This is a test, so it is a P5
6 | #If addresses is empty like below it will watch all addresses otherwise you can address specific addresses.
7 | addresses:
8 | # - 0xbEb5Fc579115071764c7423A4f12eDde41f106Ed # /!\ SPECIFIC ADDRESS -> We are not supporting EIP 3770 yet, if the address is not starting by 0x, this will panic by safety measure.
9 | events:
10 | - signature: ExecutionFailure(bytes32,uint256) # List of the events to watch for the addresses.
11 | - signature: ExecutionSuccess(bytes32,uint256) # List of the events to watch for the addresses.
12 |
--------------------------------------------------------------------------------
/op-monitorism/global_events/types_test.go:
--------------------------------------------------------------------------------
1 | package global_events
2 |
3 | import (
4 | "io"
5 | "testing"
6 |
7 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
8 | "github.com/ethereum/go-ethereum/common"
9 | "gopkg.in/yaml.v3"
10 | )
11 |
12 | const data = `
13 | configuration:
14 | - version: "1.0"
15 | name: "BuildLand"
16 | priority: "P0"
17 | addresses:
18 | - 0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5
19 | events:
20 | - signature: "ExecutionFailure(bytes32,uint256)"
21 | - signature: "ExecutionSuccess(bytes32,uint256)"
22 | - version: "1.0"
23 | name: "NightLand"
24 | priority: "P2"
25 | addresses: # We are not supporting EIP 3770 yet, if the address is not starting by '0x', this will panic by safety measure."
26 | events:
27 | - signature: "ExecutionFailure(bytes32,uint256)"
28 | - signature: "ExecutionSuccess(bytes32,uint256)"
29 | `
30 |
31 | // Print the config to see if it's correct
32 | func TestReturnEventsMonitoredForAnAddress(t *testing.T) {
33 | var config GlobalConfiguration
34 | err := yaml.Unmarshal([]byte(data), &config)
35 | if err != nil {
36 | t.Errorf("error: %v", err)
37 | }
38 | config.ReturnEventsMonitoredForAnAddress(common.HexToAddress("0x41"))
39 | }
40 |
41 | func TestDisplayMonitorAddresses(t *testing.T) {
42 | var config GlobalConfiguration
43 | err := yaml.Unmarshal([]byte(data), &config)
44 | if err != nil {
45 | t.Errorf("error: %v", err)
46 | }
47 | log := oplog.NewLogger(io.Discard, oplog.DefaultCLIConfig())
48 | config.DisplayMonitorAddresses(log)
49 | }
50 |
51 | func TestYamlToConfiguration(t *testing.T) {
52 |
53 | var config GlobalConfiguration
54 | err := yaml.Unmarshal([]byte(data), &config)
55 | if err != nil {
56 | t.Errorf("error: %v", err)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/op-monitorism/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ethereum-optimism/monitorism/op-monitorism
2 |
3 | go 1.22.0
4 |
5 | toolchain go1.22.2
6 |
7 | replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101503.1
8 |
9 | require (
10 | github.com/ethereum-optimism/optimism v1.12.2
11 | github.com/ethereum-optimism/optimism/op-bindings v0.10.14
12 | github.com/ethereum/go-ethereum v1.15.3
13 | github.com/hashicorp/golang-lru v0.5.0
14 | github.com/joho/godotenv v1.5.1
15 | github.com/prometheus/client_golang v1.21.1
16 | github.com/prometheus/client_model v0.6.1
17 | github.com/stretchr/testify v1.10.0
18 | github.com/urfave/cli/v2 v2.27.5
19 | gopkg.in/yaml.v3 v3.0.1
20 | )
21 |
22 | require (
23 | github.com/BurntSushi/toml v1.4.0 // indirect
24 | github.com/Microsoft/go-winio v0.6.2 // indirect
25 | github.com/VictoriaMetrics/fastcache v1.12.2 // indirect
26 | github.com/beorn7/perks v1.0.1 // indirect
27 | github.com/bits-and-blooms/bitset v1.20.0 // indirect
28 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
29 | github.com/consensys/bavard v0.1.27 // indirect
30 | github.com/consensys/gnark-crypto v0.16.0 // indirect
31 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
32 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
33 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
35 | github.com/deckarep/golang-set/v2 v2.6.0 // indirect
36 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
37 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
38 | github.com/ethereum/go-verkle v0.2.2 // indirect
39 | github.com/fsnotify/fsnotify v1.8.0 // indirect
40 | github.com/go-ole/go-ole v1.3.0 // indirect
41 | github.com/gofrs/flock v0.8.1 // indirect
42 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
43 | github.com/google/uuid v1.6.0 // indirect
44 | github.com/gorilla/websocket v1.5.3 // indirect
45 | github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
46 | github.com/holiman/uint256 v1.3.2 // indirect
47 | github.com/huin/goupnp v1.3.0 // indirect
48 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect
49 | github.com/klauspost/compress v1.18.0 // indirect
50 | github.com/kylelemons/godebug v1.1.0 // indirect
51 | github.com/mattn/go-runewidth v0.0.16 // indirect
52 | github.com/mmcloughlin/addchain v0.4.0 // indirect
53 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
54 | github.com/naoina/go-stringutil v0.1.0 // indirect
55 | github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 // indirect
56 | github.com/nxadm/tail v1.4.11 // indirect
57 | github.com/olekukonko/tablewriter v0.0.5 // indirect
58 | github.com/pion/dtls/v2 v2.2.12 // indirect
59 | github.com/pion/logging v0.2.2 // indirect
60 | github.com/pion/stun/v2 v2.0.0 // indirect
61 | github.com/pion/transport/v2 v2.2.10 // indirect
62 | github.com/pion/transport/v3 v3.0.7 // indirect
63 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
64 | github.com/prometheus/common v0.62.0 // indirect
65 | github.com/prometheus/procfs v0.15.1 // indirect
66 | github.com/rivo/uniseg v0.4.7 // indirect
67 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
68 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect
69 | github.com/supranational/blst v0.3.14 // indirect
70 | github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect
71 | github.com/tklauser/go-sysconf v0.3.12 // indirect
72 | github.com/tklauser/numcpus v0.6.1 // indirect
73 | github.com/wlynxg/anet v0.0.4 // indirect
74 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
75 | github.com/yusufpapurcu/wmi v1.2.3 // indirect
76 | golang.org/x/crypto v0.32.0 // indirect
77 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
78 | golang.org/x/net v0.34.0 // indirect
79 | golang.org/x/sync v0.10.0 // indirect
80 | golang.org/x/sys v0.29.0 // indirect
81 | golang.org/x/term v0.28.0 // indirect
82 | golang.org/x/time v0.10.0 // indirect
83 | google.golang.org/protobuf v1.36.3 // indirect
84 | rsc.io/tmplfunc v0.0.3 // indirect
85 | )
86 |
--------------------------------------------------------------------------------
/op-monitorism/liveness_expiration/README.md:
--------------------------------------------------------------------------------
1 | # Liveness Expiration Monitoring
2 |
3 | This Liveness expiration module is a monitoring dedicated to the safes of the Optimism network.
4 | Ensuring that owners that operate the safes and perform any important actions are still active enough.
5 |
6 | 
7 |
8 | ## CLI and Docs:
9 |
10 | ```bash
11 | NAME:
12 | Monitorism liveness_expiration - Monitor the liveness expiration on Gnosis Safe.
13 |
14 | USAGE:
15 | Monitorism liveness_expiration [command options] [arguments...]
16 |
17 | DESCRIPTION:
18 | Monitor the liveness expiration on Gnosis Safe.
19 |
20 | OPTIONS:
21 | --l1.node.url value Node URL of L1 peer (default: "127.0.0.1:8545") [$LIVENESS_EXPIRATION_MON_L1_NODE_URL]
22 | --start.block.height value Starting height to scan for events (still not implemented for now.. The monitoring will start at the last block number) (default: 0) [$LIVENESS_EXPIRATION_MON_START_BLOCK_HEIGHT]
23 | --livenessmodule.address value Address of the LivenessModuleAddress contract [$LIVENESS_EXPIRATION_MON_LIVENESS_MODULE_ADDRESS]
24 | --livenessguard.address value Address of the LivenessGuardAddress contract [$LIVENESS_EXPIRATION_MON_LIVENESS_GUARD_ADDRESS]
25 | --safe.address value Address of the safe contract [$LIVENESS_EXPIRATION_MON_SAFE_ADDRESS]
26 | --log.level value The lowest log level that will be output (default: INFO) [$MONITORISM_LOG_LEVEL]
27 | --log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$MONITORISM_LOG_FORMAT]
28 | --log.color Color the log output if in terminal mode (default: false) [$MONITORISM_LOG_COLOR]
29 | --metrics.enabled Enable the metrics server (default: false) [$MONITORISM_METRICS_ENABLED]
30 | --metrics.addr value Metrics listening address (default: "0.0.0.0") [$MONITORISM_METRICS_ADDR]
31 | --metrics.port value Metrics listening port (default: 7300) [$MONITORISM_METRICS_PORT]
32 | --loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
33 | --help, -h show help
34 | ```
35 |
36 | ### Informations
37 |
38 | This tool allows the monitoring of multiple metrics like:
39 |
40 | `blockTimestamp`: The block Timestamp of the latest block number on L1.
41 | `highestBlockNumber`: The latest block number height on L1.
42 | `lastLiveOfAOwner`: Get the last activities for a given safe owner on L1.
43 | `intervalLiveness`: the interval (in seconds) from the LivenessModule on L1.
44 |
45 | The logic for the rules detection is not inside the binary `liveness_expiration` as this is integrated with prometheus. The rules are located in the Prometheus/Grafana side.
46 |
47 | ### Execution
48 |
49 | To execute with a oneliner:
50 |
51 | ```bash
52 | go run ../cmd/monitorism liveness_expiration --safe.address 0xc2819DC788505Aac350142A7A707BF9D03E3Bd03 --l1.node.url https://MySuperRPC --loop.interval.msec 12000 --livenessmodule.address 0x0454092516c9A4d636d3CAfA1e82161376C8a748 --livenessguard.address 0x24424336F04440b1c28685a38303aC33C9D14a25
53 | ```
54 |
55 | Otherwise create an `.env` file with the environment variables present into the _help section_.
56 | This is useful to run without any CLI arguments.
57 |
58 | _Example_:
59 |
60 | ```bash
61 | LIVENESS_EXPIRATION_MON_SAFE_ADDRESS=0xc2819DC788505Aac350142A7A707BF9D03E3Bd03
62 | LIVENESS_EXPIRATION_MON_LIVENESS_MODULE_ADDRESS=0x0454092516c9A4d636d3CAfA1e82161376C8a748
63 | LIVENESS_EXPIRATION_MON_LIVENESS_GUARD_ADDRESS=0x24424336F04440b1c28685a38303aC33C9D14a25
64 | ```
65 |
--------------------------------------------------------------------------------
/op-monitorism/liveness_expiration/cli.go:
--------------------------------------------------------------------------------
1 | package liveness_expiration
2 |
3 | import (
4 | "github.com/ethereum/go-ethereum/common"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 |
8 | "github.com/urfave/cli/v2"
9 | )
10 |
11 | const (
12 | L1NodeURLFlagName = "l1.node.url"
13 | EventBlockRangeFlagName = "event.block.range"
14 | StartingL1BlockHeightFlagName = "start.block.height"
15 |
16 | SafeAddressFlagName = "safe.address"
17 | LivenessModuleAddressFlagName = "livenessmodule.address"
18 | LivenessGuardAddressFlagName = "livenessguard.address"
19 | )
20 |
21 | type CLIConfig struct {
22 | L1NodeURL string
23 | EventBlockRange uint64
24 | StartingL1BlockHeight uint64
25 |
26 | LivenessModuleAddress common.Address
27 | LivenessGuardAddress common.Address
28 | SafeAddress common.Address
29 | }
30 |
31 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
32 | cfg := CLIConfig{
33 | L1NodeURL: ctx.String(L1NodeURLFlagName),
34 | EventBlockRange: ctx.Uint64(EventBlockRangeFlagName),
35 | StartingL1BlockHeight: ctx.Uint64(StartingL1BlockHeightFlagName),
36 | SafeAddress: common.HexToAddress(ctx.String(SafeAddressFlagName)),
37 | LivenessModuleAddress: common.HexToAddress(ctx.String(LivenessModuleAddressFlagName)),
38 | LivenessGuardAddress: common.HexToAddress(ctx.String(LivenessGuardAddressFlagName)),
39 | }
40 |
41 | return cfg, nil
42 | }
43 |
44 | func CLIFlags(envVar string) []cli.Flag {
45 | return []cli.Flag{
46 | &cli.StringFlag{
47 | Name: L1NodeURLFlagName,
48 | Usage: "Node URL of L1 peer",
49 | Value: "127.0.0.1:8545",
50 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
51 | },
52 | &cli.Uint64Flag{
53 | Name: StartingL1BlockHeightFlagName,
54 | Usage: "Starting height to scan for events (still not implemented for now.. The monitoring will start at the last block number)",
55 | EnvVars: opservice.PrefixEnvVar(envVar, "START_BLOCK_HEIGHT"),
56 | Required: false,
57 | },
58 | &cli.StringFlag{
59 | Name: LivenessModuleAddressFlagName,
60 | Usage: "Address of the LivenessModuleAddress contract",
61 | EnvVars: opservice.PrefixEnvVar(envVar, "LIVENESS_MODULE_ADDRESS"),
62 | Required: true,
63 | },
64 | &cli.StringFlag{
65 | Name: LivenessGuardAddressFlagName,
66 | Usage: "Address of the LivenessGuardAddress contract",
67 | EnvVars: opservice.PrefixEnvVar(envVar, "LIVENESS_GUARD_ADDRESS"),
68 | Required: true,
69 | },
70 | &cli.StringFlag{
71 | Name: SafeAddressFlagName,
72 | Usage: "Address of the safe contract",
73 | EnvVars: opservice.PrefixEnvVar(envVar, "SAFE_ADDRESS"),
74 | Required: true,
75 | },
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/op-monitorism/monitorism.go:
--------------------------------------------------------------------------------
1 | package monitorism
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "sync/atomic"
8 | "time"
9 |
10 | "github.com/ethereum/go-ethereum/log"
11 |
12 | "github.com/ethereum-optimism/optimism/op-service/cliapp"
13 | "github.com/ethereum-optimism/optimism/op-service/clock"
14 | "github.com/ethereum-optimism/optimism/op-service/httputil"
15 |
16 | opservice "github.com/ethereum-optimism/optimism/op-service"
17 | oplog "github.com/ethereum-optimism/optimism/op-service/log"
18 | opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
19 |
20 | "github.com/prometheus/client_golang/prometheus"
21 | "github.com/urfave/cli/v2"
22 | )
23 |
24 | const (
25 | LoopIntervalMsecFlagName = "loop.interval.msec"
26 | )
27 |
28 | type Monitor interface {
29 | Run(context.Context)
30 | Close(context.Context) error
31 | }
32 |
33 | type cliApp struct {
34 | log log.Logger
35 | stopped atomic.Bool
36 |
37 | loopIntervalMs uint64
38 | worker *clock.LoopFn
39 |
40 | monitor Monitor
41 |
42 | registry *prometheus.Registry
43 | metricsCfg opmetrics.CLIConfig
44 | metricsSrv *httputil.HTTPServer
45 | }
46 |
47 | func NewCliApp(ctx *cli.Context, log log.Logger, registry *prometheus.Registry, monitor Monitor) (cliapp.Lifecycle, error) {
48 | loopIntervalMs := ctx.Uint64(LoopIntervalMsecFlagName)
49 | if loopIntervalMs == 0 {
50 | return nil, errors.New("zero loop interval configured")
51 | }
52 |
53 | return &cliApp{
54 | log: log,
55 | loopIntervalMs: loopIntervalMs,
56 | monitor: monitor,
57 | registry: registry,
58 | metricsCfg: opmetrics.ReadCLIConfig(ctx),
59 | }, nil
60 | }
61 |
62 | func DefaultCLIFlags(envVarPrefix string) []cli.Flag {
63 | defaultFlags := append(oplog.CLIFlags(envVarPrefix), opmetrics.CLIFlags(envVarPrefix)...)
64 | return append(defaultFlags, &cli.Uint64Flag{
65 | Name: LoopIntervalMsecFlagName,
66 | Usage: "Loop interval of the monitor in milliseconds",
67 | Value: 60_000,
68 | EnvVars: opservice.PrefixEnvVar(envVarPrefix, "LOOP_INTERVAL_MSEC"),
69 | })
70 | }
71 |
72 | func (app *cliApp) Start(ctx context.Context) error {
73 | if app.worker != nil {
74 | return errors.New("monitor already started")
75 | }
76 |
77 | app.log.Info("starting metrics server", "host", app.metricsCfg.ListenAddr, "port", app.metricsCfg.ListenPort)
78 | srv, err := opmetrics.StartServer(app.registry, app.metricsCfg.ListenAddr, app.metricsCfg.ListenPort)
79 | if err != nil {
80 | return fmt.Errorf("failed to start metrics server: %w", err)
81 | }
82 |
83 | app.log.Info("starting monitor...", "loop_interval_ms", app.loopIntervalMs)
84 |
85 | // Tick to avoid having to wait a full interval on startup
86 | app.monitor.Run(ctx)
87 |
88 | app.worker = clock.NewLoopFn(clock.SystemClock, app.monitor.Run, nil, time.Millisecond*time.Duration(app.loopIntervalMs))
89 | app.metricsSrv = srv
90 | return nil
91 | }
92 |
93 | func (app *cliApp) Stop(ctx context.Context) error {
94 | if app.stopped.Load() {
95 | return errors.New("monitor already closed")
96 | }
97 |
98 | app.log.Info("closing monitor...")
99 | if err := app.worker.Close(); err != nil {
100 | app.log.Error("error stopping worker loop", "err", err)
101 | }
102 | if err := app.monitor.Close(ctx); err != nil {
103 | app.log.Error("error closing monitor", "err", err)
104 | }
105 | if err := app.metricsSrv.Close(); err != nil {
106 | app.log.Error("error closing metrics server", "err", err)
107 | }
108 |
109 | app.stopped.Store(true)
110 | return nil
111 | }
112 |
113 | func (app *cliApp) Stopped() bool {
114 | return app.stopped.Load()
115 | }
116 |
--------------------------------------------------------------------------------
/op-monitorism/multisig/README.md:
--------------------------------------------------------------------------------
1 | ### Multisig Monitor
2 |
3 | The multisig monitor reports the paused status of the `OptimismPortal` contract. If set, the latest nonce of the configured `Safe` address. And also if set, the latest presigned nonce stored in One Password. The latest presigned nonce is identified by looking for items in the configured vault that follow a `ready-.json` name. The highest nonce of this item name format is reported.
4 |
5 | - **NOTE**: In order to read from one password, the `OP_SERVICE_ACCOUNT_TOKEN` environment variable must be set granting the process permission to access the specified vault.
6 |
7 | ```
8 | OPTIONS:
9 | --l1.node.url value [$MULTISIG_MON_L1_NODE_URL] Node URL of L1 peer (default: "127.0.0.1:8545")
10 | --optimismportal.address value [$MULTISIG_MON_OPTIMISM_PORTAL] Address of the OptimismPortal contract
11 | --nickname value [$MULTISIG_MON_NICKNAME] Nickname of chain being monitored
12 | --safe.address value [$MULTISIG_MON_SAFE] Address of the Safe contract
13 | --op.vault value [$MULTISIG_MON_1PASS_VAULT_NAME] 1Pass Vault name storing presigned safe txs following a 'ready-.json' item name format
14 | ```
15 |
--------------------------------------------------------------------------------
/op-monitorism/multisig/bindings/BINDING.md:
--------------------------------------------------------------------------------
1 | ## Info
2 | The bindings in this folder are taken from
3 | github.com/ethereum-optimism/optimism/op-bindings/bindings v1.7.3
4 |
5 | This tool is compatible with these bindings. Future binding will break compatibility for those files.
--------------------------------------------------------------------------------
/op-monitorism/multisig/cli.go:
--------------------------------------------------------------------------------
1 | package multisig
2 |
3 | import (
4 | "fmt"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 |
8 | "github.com/ethereum/go-ethereum/common"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | const (
14 | L1NodeURLFlagName = "l1.node.url"
15 |
16 | NicknameFlagName = "nickname"
17 | OptimismPortalAddressFlagName = "optimismportal.address"
18 | SafeAddressFlagName = "safe.address"
19 | OnePassVaultFlagName = "op.vault"
20 | )
21 |
22 | type CLIConfig struct {
23 | L1NodeURL string
24 | Nickname string
25 | OptimismPortalAddress common.Address
26 |
27 | // Optional
28 | SafeAddress *common.Address
29 | OnePassVault *string
30 | }
31 |
32 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
33 | cfg := CLIConfig{
34 | L1NodeURL: ctx.String(L1NodeURLFlagName),
35 | Nickname: ctx.String(NicknameFlagName),
36 | }
37 |
38 | portalAddress := ctx.String(OptimismPortalAddressFlagName)
39 | if !common.IsHexAddress(portalAddress) {
40 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", OptimismPortalAddressFlagName)
41 | }
42 | cfg.OptimismPortalAddress = common.HexToAddress(portalAddress)
43 |
44 | safeAddress := ctx.String(SafeAddressFlagName)
45 | if len(safeAddress) > 0 {
46 | if !common.IsHexAddress(safeAddress) {
47 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", SafeAddressFlagName)
48 | }
49 | addr := common.HexToAddress(safeAddress)
50 | cfg.SafeAddress = &addr
51 | }
52 |
53 | onePassVault := ctx.String(OnePassVaultFlagName)
54 | if len(onePassVault) > 0 {
55 | cfg.OnePassVault = &onePassVault
56 | }
57 |
58 | return cfg, nil
59 | }
60 |
61 | func CLIFlags(envVar string) []cli.Flag {
62 | return []cli.Flag{
63 | &cli.StringFlag{
64 | Name: L1NodeURLFlagName,
65 | Usage: "Node URL of L1 peer",
66 | Value: "127.0.0.1:8545",
67 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
68 | },
69 | &cli.StringFlag{
70 | Name: OptimismPortalAddressFlagName,
71 | Usage: "Address of the OptimismPortal contract",
72 | EnvVars: opservice.PrefixEnvVar(envVar, "OPTIMISM_PORTAL"),
73 | Required: true,
74 | },
75 | &cli.StringFlag{
76 | Name: NicknameFlagName,
77 | Usage: "Nickname of chain being monitored",
78 | EnvVars: opservice.PrefixEnvVar(envVar, "NICKNAME"),
79 | Required: true,
80 | },
81 | &cli.StringFlag{
82 | Name: SafeAddressFlagName,
83 | Usage: "Address of the Safe contract",
84 | EnvVars: opservice.PrefixEnvVar(envVar, "SAFE"),
85 | },
86 | &cli.StringFlag{
87 | Name: OnePassVaultFlagName,
88 | Usage: "1Pass vault name storing presigned safe txs following a 'ready-.json' item name format",
89 | EnvVars: opservice.PrefixEnvVar(envVar, "1PASS_VAULT_NAME"),
90 | },
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/op-monitorism/multisig/monitor.go:
--------------------------------------------------------------------------------
1 | package multisig
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "math/big"
8 | "os"
9 | "os/exec"
10 | "strconv"
11 | "strings"
12 |
13 | "github.com/ethereum-optimism/monitorism/op-monitorism/multisig/bindings"
14 | "github.com/ethereum-optimism/optimism/op-service/metrics"
15 |
16 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
17 | "github.com/ethereum/go-ethereum/common"
18 | "github.com/ethereum/go-ethereum/common/hexutil"
19 | "github.com/ethereum/go-ethereum/crypto"
20 | "github.com/ethereum/go-ethereum/ethclient"
21 | "github.com/ethereum/go-ethereum/log"
22 |
23 | "github.com/prometheus/client_golang/prometheus"
24 | )
25 |
26 | const (
27 | MetricsNamespace = "multisig_mon"
28 | SafeNonceABI = "nonce()"
29 |
30 | OPTokenEnvName = "OP_SERVICE_ACCOUNT_TOKEN"
31 |
32 | // Item names follow a `ready-.json` format
33 | PresignedNonceTitlePrefix = "ready-"
34 | PresignedNonceTitleSuffix = ".json"
35 | )
36 |
37 | var (
38 | SafeNonceSelector = crypto.Keccak256([]byte(SafeNonceABI))[:4]
39 | )
40 |
41 | type Monitor struct {
42 | log log.Logger
43 |
44 | l1Client *ethclient.Client
45 |
46 | optimismPortalAddress common.Address
47 | optimismPortal *bindings.OptimismPortalCaller
48 | nickname string
49 |
50 | //onePassToken string
51 | onePassVault *string
52 | safeAddress *common.Address
53 |
54 | // metrics
55 | safeNonce *prometheus.GaugeVec
56 | latestPresignedPauseNonce *prometheus.GaugeVec
57 | pausedState *prometheus.GaugeVec
58 | unexpectedRpcErrors *prometheus.CounterVec
59 | }
60 |
61 | func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIConfig) (*Monitor, error) {
62 | l1Client, err := ethclient.Dial(cfg.L1NodeURL)
63 | if err != nil {
64 | return nil, fmt.Errorf("failed to dial l1 rpc: %w", err)
65 | }
66 |
67 | optimismPortal, err := bindings.NewOptimismPortalCaller(cfg.OptimismPortalAddress, l1Client)
68 | if err != nil {
69 | return nil, fmt.Errorf("failed to bind to the OptimismPortal: %w", err)
70 | }
71 |
72 | if cfg.OnePassVault != nil && len(os.Getenv(OPTokenEnvName)) == 0 {
73 | return nil, fmt.Errorf("%s ENV name must be set for 1Pass integration", OPTokenEnvName)
74 | }
75 |
76 | if cfg.OnePassVault == nil {
77 | log.Warn("one pass integration is not configured")
78 | }
79 | if cfg.SafeAddress == nil {
80 | log.Warn("safe integration is not configured")
81 | }
82 |
83 | return &Monitor{
84 | log: log,
85 | l1Client: l1Client,
86 |
87 | optimismPortal: optimismPortal,
88 | optimismPortalAddress: cfg.OptimismPortalAddress,
89 | nickname: cfg.Nickname,
90 |
91 | safeAddress: cfg.SafeAddress,
92 | onePassVault: cfg.OnePassVault,
93 |
94 | safeNonce: m.NewGaugeVec(prometheus.GaugeOpts{
95 | Namespace: MetricsNamespace,
96 | Name: "safeNonce",
97 | Help: "Safe Nonce",
98 | }, []string{"address", "nickname"}),
99 | latestPresignedPauseNonce: m.NewGaugeVec(prometheus.GaugeOpts{
100 | Namespace: MetricsNamespace,
101 | Name: "latestPresignedPauseNonce",
102 | Help: "Latest pre-signed pause nonce",
103 | }, []string{"address", "nickname"}),
104 | pausedState: m.NewGaugeVec(prometheus.GaugeOpts{
105 | Namespace: MetricsNamespace,
106 | Name: "pausedState",
107 | Help: "OptimismPortal paused state",
108 | }, []string{"address", "nickname"}),
109 | unexpectedRpcErrors: m.NewCounterVec(prometheus.CounterOpts{
110 | Namespace: MetricsNamespace,
111 | Name: "unexpectedRpcErrors",
112 | Help: "number of unexpected rpc errors",
113 | }, []string{"section", "name"}),
114 | }, nil
115 | }
116 |
117 | func (m *Monitor) Run(ctx context.Context) {
118 | m.checkOptimismPortal(ctx)
119 | m.checkSafeNonce(ctx)
120 | m.checkPresignedNonce(ctx)
121 | }
122 |
123 | func (m *Monitor) checkOptimismPortal(ctx context.Context) {
124 | paused, err := m.optimismPortal.Paused(&bind.CallOpts{Context: ctx})
125 | if err != nil {
126 | m.log.Error("failed to query OptimismPortal paused status", "err", err)
127 | m.unexpectedRpcErrors.WithLabelValues("optimismportal", "paused").Inc()
128 | return
129 | }
130 |
131 | pausedMetric := 0
132 | if paused {
133 | pausedMetric = 1
134 | }
135 |
136 | m.pausedState.WithLabelValues(m.optimismPortalAddress.String(), m.nickname).Set(float64(pausedMetric))
137 | m.log.Info("OptimismPortal status", "address", m.optimismPortalAddress.String(), "paused", paused)
138 | }
139 |
140 | func (m *Monitor) checkSafeNonce(ctx context.Context) {
141 | if m.safeAddress == nil {
142 | m.log.Warn("safe address is not configured, skipping...")
143 | return
144 | }
145 |
146 | nonceBytes := hexutil.Bytes{}
147 | nonceTx := map[string]interface{}{"to": *m.safeAddress, "data": hexutil.Encode(SafeNonceSelector)}
148 | if err := m.l1Client.Client().CallContext(ctx, &nonceBytes, "eth_call", nonceTx, "latest"); err != nil {
149 | m.log.Error("failed to query safe nonce", "err", err)
150 | m.unexpectedRpcErrors.WithLabelValues("safe", "nonce()").Inc()
151 | return
152 | }
153 |
154 | nonce := new(big.Int).SetBytes(nonceBytes).Uint64()
155 | m.safeNonce.WithLabelValues(m.safeAddress.String(), m.nickname).Set(float64(nonce))
156 | m.log.Info("Safe Nonce", "address", m.safeAddress.String(), "nonce", nonce)
157 | }
158 |
159 | func (m *Monitor) checkPresignedNonce(ctx context.Context) {
160 | if m.onePassVault == nil {
161 | m.log.Warn("one pass integration is not configured, skipping...")
162 | return
163 | }
164 |
165 | cmd := exec.CommandContext(ctx, "op", "item", "list", "--format=json", fmt.Sprintf("--vault=%s", *m.onePassVault))
166 |
167 | output, err := cmd.Output()
168 | if err != nil {
169 | m.log.Error("failed to run op cli")
170 | m.unexpectedRpcErrors.WithLabelValues("1pass", "exec").Inc()
171 | return
172 | }
173 |
174 | vaultItems := []struct{ Title string }{}
175 | if err := json.Unmarshal(output, &vaultItems); err != nil {
176 | m.log.Error("failed to unmarshal op cli stdout", "err", err)
177 | m.unexpectedRpcErrors.WithLabelValues("1pass", "stdout").Inc()
178 | return
179 | }
180 |
181 | latestPresignedNonce := int64(-1)
182 | for _, item := range vaultItems {
183 | if strings.HasPrefix(item.Title, PresignedNonceTitlePrefix) && strings.HasSuffix(item.Title, PresignedNonceTitleSuffix) {
184 | nonceStr := item.Title[len(PresignedNonceTitlePrefix) : len(item.Title)-len(PresignedNonceTitleSuffix)]
185 | nonce, err := strconv.ParseInt(nonceStr, 10, 64)
186 | if err != nil {
187 | m.log.Error("failed to parse nonce from item title", "title", item.Title)
188 | m.unexpectedRpcErrors.WithLabelValues("1pass", "title").Inc()
189 | return
190 | }
191 | if nonce > latestPresignedNonce {
192 | latestPresignedNonce = nonce
193 | }
194 | }
195 | }
196 |
197 | m.latestPresignedPauseNonce.WithLabelValues(m.safeAddress.String(), m.nickname).Set(float64(latestPresignedNonce))
198 | if latestPresignedNonce == -1 {
199 | m.log.Error("no presigned nonce found")
200 | return
201 | }
202 |
203 | m.log.Info("Latest Presigned Nonce", "nonce", latestPresignedNonce)
204 | }
205 |
206 | func (m *Monitor) Close(_ context.Context) error {
207 | m.l1Client.Close()
208 | return nil
209 | }
210 |
--------------------------------------------------------------------------------
/op-monitorism/processor/processor.go:
--------------------------------------------------------------------------------
1 | package processor
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 | "time"
8 |
9 | "github.com/ethereum-optimism/optimism/op-service/metrics"
10 | "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/core/types"
12 | "github.com/ethereum/go-ethereum/ethclient"
13 | "github.com/ethereum/go-ethereum/log"
14 | "github.com/prometheus/client_golang/prometheus"
15 | )
16 |
17 | // TxProcessingFunc is the type for transaction processing functions
18 | type TxProcessingFunc func(block *types.Block, tx *types.Transaction, client *ethclient.Client) error
19 |
20 | // TxProcessingFunc is the type for block processing functions
21 | type BlockProcessingFunc func(block *types.Block, client *ethclient.Client) error
22 |
23 | type Metrics struct {
24 | highestBlockSeen prometheus.Gauge
25 | highestBlockProcessed prometheus.Gauge
26 | processingErrors prometheus.Counter
27 | }
28 |
29 | // BlockProcessor handles the monitoring and processing of Ethereum blocks
30 | type BlockProcessor struct {
31 | client *ethclient.Client
32 | txProcessFunc TxProcessingFunc
33 | blockProcessFunc BlockProcessingFunc
34 | interval time.Duration
35 | lastProcessed *big.Int
36 | log log.Logger
37 | ctx context.Context
38 | cancel context.CancelFunc
39 | metrics Metrics
40 | }
41 |
42 | // Config holds the configuration for the processor
43 | type Config struct {
44 | StartBlock *big.Int // Optional: starting block number
45 | Interval time.Duration // Optional: polling interval
46 | }
47 |
48 | // NewBlockProcessor creates a new processor instance
49 | func NewBlockProcessor(
50 | m metrics.Factory,
51 | log log.Logger,
52 | rpcURL string,
53 | txProcessFunc TxProcessingFunc,
54 | blockProcessFunc BlockProcessingFunc,
55 | config *Config,
56 | ) (*BlockProcessor, error) {
57 | client, err := ethclient.Dial(rpcURL)
58 | if err != nil {
59 | return nil, fmt.Errorf("failed to connect to Ethereum client: %w", err)
60 | }
61 |
62 | // Set defaults if config is nil
63 | if config == nil {
64 | config = &Config{
65 | Interval: 12 * time.Second,
66 | }
67 | }
68 |
69 | // Set default interval if not specified
70 | if config.Interval == 0 {
71 | config.Interval = 12 * time.Second
72 | }
73 |
74 | ctx, cancel := context.WithCancel(context.Background())
75 |
76 | metrics := Metrics{
77 | highestBlockSeen: m.NewGauge(prometheus.GaugeOpts{Name: "highest_block_seen"}),
78 | highestBlockProcessed: m.NewGauge(prometheus.GaugeOpts{Name: "highest_block_processed"}),
79 | processingErrors: m.NewCounter(prometheus.CounterOpts{Name: "processing_errors_total"}),
80 | }
81 |
82 | p := &BlockProcessor{
83 | client: client,
84 | txProcessFunc: txProcessFunc,
85 | blockProcessFunc: blockProcessFunc,
86 | interval: config.Interval,
87 | ctx: ctx,
88 | cancel: cancel,
89 | metrics: metrics,
90 | log: log,
91 | }
92 |
93 | // If starting block is specified, use it; otherwise will start from latest block
94 | p.lastProcessed = config.StartBlock
95 |
96 | return p, nil
97 | }
98 |
99 | // Start begins the processing loop
100 | func (p *BlockProcessor) Start() error {
101 | // If no starting block was specified, get the latest finalized block
102 | if p.lastProcessed.Cmp(big.NewInt(0)) == 0 {
103 | block, err := p.getLatestBlock()
104 | if err != nil {
105 | return err
106 | }
107 | p.lastProcessed = block.Number()
108 | }
109 |
110 | ticker := time.NewTicker(p.interval)
111 | defer ticker.Stop()
112 |
113 | for {
114 | select {
115 | case <-p.ctx.Done():
116 | return p.ctx.Err()
117 | case <-ticker.C:
118 | if err := p.processNewBlocks(); err != nil {
119 | // We don't want to stop the processor if we encounter an error, simply log it and keep
120 | // looping. Since the processor won't increment the lastProcessed block number, it will
121 | // keep trying to process the same block over and over again if necessary.
122 | p.log.Error("error processing blocks", "err", err)
123 | }
124 | }
125 | }
126 | }
127 |
128 | // Stop halts the processing loop
129 | func (p *BlockProcessor) Stop() {
130 | p.cancel()
131 | }
132 |
133 | func (p *BlockProcessor) processNewBlocks() error {
134 | latestBlock, err := p.getLatestBlock()
135 | if err != nil {
136 | return err
137 | }
138 |
139 | // Update highest seen block metric.
140 | p.metrics.highestBlockSeen.Set(float64(latestBlock.Number().Int64()))
141 |
142 | // Process blocks one at a time, updating lastProcessed after each
143 | nextBlock := new(big.Int).Add(p.lastProcessed, common.Big1)
144 | for nextBlock.Cmp(latestBlock.Number()) <= 0 {
145 | p.log.Info("processing block", "block", nextBlock.String())
146 |
147 | block, err := p.client.BlockByNumber(p.ctx, nextBlock)
148 | if err != nil {
149 | return fmt.Errorf("failed to get block %s: %w", nextBlock.String(), err)
150 | }
151 |
152 | // Process each transaction in the block
153 | if p.txProcessFunc != nil {
154 | for _, tx := range block.Transactions() {
155 | if err := p.processTransactionWithRetry(block, tx); err != nil {
156 | return err
157 | }
158 | }
159 | }
160 |
161 | // Process the full block
162 | if p.blockProcessFunc != nil {
163 | if err := p.processBlockWithRetry(block); err != nil {
164 | return err
165 | }
166 | }
167 |
168 | // Update lastProcessed after each successful block
169 | p.lastProcessed = new(big.Int).Set(nextBlock)
170 | nextBlock.Add(nextBlock, common.Big1)
171 |
172 | // Update highest processed block metric.
173 | p.metrics.highestBlockProcessed.Set(float64(p.lastProcessed.Int64()))
174 | }
175 |
176 | return nil
177 | }
178 |
179 | func (p *BlockProcessor) processTransactionWithRetry(block *types.Block, tx *types.Transaction) error {
180 | for {
181 | select {
182 | case <-p.ctx.Done():
183 | return p.ctx.Err()
184 | default:
185 | if err := p.txProcessFunc(block, tx, p.client); err == nil {
186 | return nil
187 | } else {
188 | p.log.Error("error processing transaction", "tx", tx.Hash().String(), "err", err)
189 | p.metrics.processingErrors.Inc()
190 | time.Sleep(1 * time.Second)
191 | }
192 | }
193 | }
194 | }
195 |
196 | func (p *BlockProcessor) processBlockWithRetry(block *types.Block) error {
197 | for {
198 | select {
199 | case <-p.ctx.Done():
200 | return p.ctx.Err()
201 | default:
202 | if err := p.blockProcessFunc(block, p.client); err == nil {
203 | return nil
204 | } else {
205 | p.log.Error("error processing block", "block", block.Hash().String(), "err", err)
206 | p.metrics.processingErrors.Inc()
207 | time.Sleep(1 * time.Second)
208 | }
209 | }
210 | }
211 | }
212 |
213 | func (p *BlockProcessor) getLatestBlock() (*types.Block, error) {
214 | var header *types.Header
215 | err := p.client.Client().CallContext(p.ctx, &header, "eth_getBlockByNumber", "latest", false)
216 | if err != nil {
217 | return nil, fmt.Errorf("failed to get finalized header: %w", err)
218 | }
219 |
220 | block, err := p.client.BlockByNumber(p.ctx, header.Number)
221 | if err != nil {
222 | return nil, fmt.Errorf("failed to get finalized block %s: %w", header.Number.String(), err)
223 | }
224 | if block == nil {
225 | return nil, fmt.Errorf("finalized block not found")
226 | }
227 |
228 | return block, nil
229 | }
230 |
--------------------------------------------------------------------------------
/op-monitorism/secrets/README.md:
--------------------------------------------------------------------------------
1 | ### Secrets Monitor
2 |
3 | The secrets monitor takes a Drippie contract as a parameter and monitors for any drips within that contract that use the CheckSecrets dripcheck contract. CheckSecrets is a dripcheck that allows a drip to begin once a specific secret has been revealed (after a delay period) and cancels the drip if a second secret is revealed. It's important to monitor for these secrets being revealed as this could be a sign that the secret storage platform has been compromised and someone is attempting to exfiltrate the ETH controlled by that drip.
4 |
5 | ```
6 | OPTIONS:
7 | --l1.node.url value Node URL of L1 peer (default: "127.0.0.1:8545") [$SECRETS_MON_L1_NODE_URL]
8 | --drippie.address value Address of the Drippie contract [$SECRETS_MON_DRIPPIE]
9 | --log.level value The lowest log level that will be output (default: INFO) [$MONITORISM_LOG_LEVEL]
10 | --log.format value Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty', (default: text) [$MONITORISM_LOG_FORMAT]
11 | --log.color Color the log output if in terminal mode (default: false) [$MONITORISM_LOG_COLOR]
12 | --metrics.enabled Enable the metrics server (default: false) [$MONITORISM_METRICS_ENABLED]
13 | --metrics.addr value Metrics listening address (default: "0.0.0.0") [$MONITORISM_METRICS_ADDR]
14 | --metrics.port value Metrics listening port (default: 7300) [$MONITORISM_METRICS_PORT]
15 | --loop.interval.msec value Loop interval of the monitor in milliseconds (default: 60000) [$MONITORISM_LOOP_INTERVAL_MSEC]
16 | ```
17 |
--------------------------------------------------------------------------------
/op-monitorism/secrets/cli.go:
--------------------------------------------------------------------------------
1 | package secrets
2 |
3 | import (
4 | "fmt"
5 |
6 | opservice "github.com/ethereum-optimism/optimism/op-service"
7 |
8 | "github.com/ethereum/go-ethereum/common"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | const (
14 | L1NodeURLFlagName = "l1.node.url"
15 | DrippieAddressFlagName = "drippie.address"
16 | )
17 |
18 | type CLIConfig struct {
19 | L1NodeURL string
20 | DrippieAddress common.Address
21 | }
22 |
23 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
24 | cfg := CLIConfig{
25 | L1NodeURL: ctx.String(L1NodeURLFlagName),
26 | }
27 |
28 | drippieAddress := ctx.String(DrippieAddressFlagName)
29 | if !common.IsHexAddress(drippieAddress) {
30 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", DrippieAddressFlagName)
31 | }
32 | cfg.DrippieAddress = common.HexToAddress(drippieAddress)
33 |
34 | return cfg, nil
35 | }
36 |
37 | func CLIFlags(envVar string) []cli.Flag {
38 | return []cli.Flag{
39 | &cli.StringFlag{
40 | Name: L1NodeURLFlagName,
41 | Usage: "Node URL of L1 peer",
42 | Value: "127.0.0.1:8545",
43 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
44 | },
45 | &cli.StringFlag{
46 | Name: DrippieAddressFlagName,
47 | Usage: "Address of the Drippie contract",
48 | EnvVars: opservice.PrefixEnvVar(envVar, "DRIPPIE"),
49 | Required: true,
50 | },
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/op-monitorism/transaction_monitor/README.md:
--------------------------------------------------------------------------------
1 | # Transaction Monitor
2 |
3 | A service that monitors Ethereum transactions for specific addresses and raises alerts based on configurable rules.
4 |
5 | ## Features
6 |
7 | - Monitor transactions to specific addresses
8 | - Configurable address filtering with multiple check types:
9 | - Exact match checking for allowlisted addresses
10 | - Dynamic allowlisting through dispute game factory events
11 | - Threshold monitoring for transaction values
12 | - Prometheus metrics for monitoring and alerting
13 | - Flexible YAML configuration
14 |
15 | ## Configuration
16 |
17 | Configuration is provided via YAML file. Example:
18 |
19 | ```yaml
20 | node_url: "http://localhost:8545"
21 | start_block: 0
22 | watch_configs:
23 | - address: "0xAE0b5DF2dFaaCD6EB6c1c56Cc710f529F31C6C44"
24 | filters:
25 | - type: exact_match
26 | params:
27 | match: "0x1234567890123456789012345678901234567890"
28 | - type: dispute_game
29 | params:
30 | disputeGameFactory: "0x9876543210987654321098765432109876543210"
31 | thresholds:
32 | "0x1234567890123456789012345678901234567890": "1000000000000000000"
33 | ```
34 |
35 | * A `start_block` set to `0` indicates the latest block.
36 |
37 | ## Metrics
38 |
39 | The service exports the following Prometheus metrics:
40 |
41 | - `tx_mon_transactions_total`: Total number of transactions processed
42 | - `tx_mon_unauthorized_transactions_total`: Number of transactions from unauthorized addresses
43 | - `tx_mon_threshold_exceeded_transactions_total`: Number of transactions exceeding allowed threshold
44 | - `tx_mon_eth_spent_total`: Cumulative ETH spent by address
45 | - `tx_mon_unexpected_rpc_errors_total`: Number of unexpected RPC errors
46 |
47 | ## Usage
48 |
49 | ```bash
50 | monitorism \
51 | --node.url=http://localhost:8545 \
52 | --config.file=config.yaml
53 | ```
54 |
55 |
--------------------------------------------------------------------------------
/op-monitorism/transaction_monitor/checks.go:
--------------------------------------------------------------------------------
1 | package transaction_monitor
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/ethereum-optimism/monitorism/op-monitorism/transaction_monitor/bindings/dispute"
8 |
9 | "github.com/ethereum/go-ethereum/accounts/abi/bind"
10 | "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/ethclient"
12 | )
13 |
14 | // CheckFunc is a type for address verification functions
15 | type CheckFunc func(ctx context.Context, client *ethclient.Client, addr common.Address, params map[string]interface{}) (bool, error)
16 |
17 | // AddressChecks maps check types to their implementation functions
18 | var AddressChecks = map[CheckType]CheckFunc{
19 | ExactMatchCheck: CheckExactMatch,
20 | DisputeGameCheck: CheckDisputeGame,
21 | }
22 |
23 | // CheckExactMatch verifies if the address matches exactly with the provided match parameter
24 | func CheckExactMatch(ctx context.Context, client *ethclient.Client, addr common.Address, params map[string]interface{}) (bool, error) {
25 | match, ok := params["match"].(string)
26 | if !ok {
27 | return false, fmt.Errorf("match parameter not found or invalid")
28 | }
29 | return addr == common.HexToAddress(match), nil
30 | }
31 |
32 | // CheckDisputeGame verifies if the address is a valid dispute game created by the factory
33 | func CheckDisputeGame(ctx context.Context, client *ethclient.Client, addr common.Address, params map[string]interface{}) (bool, error) {
34 | code, err := client.CodeAt(ctx, addr, nil)
35 | if err != nil {
36 | return false, fmt.Errorf("failed to get code at address: %w", err)
37 | }
38 | if len(code) == 0 {
39 | return false, nil
40 | }
41 |
42 | factoryAddr, ok := params["disputeGameFactory"].(string)
43 | if !ok {
44 | return false, fmt.Errorf("disputeGameFactory parameter not found or invalid")
45 | }
46 |
47 | game, err := dispute.NewDisputeGame(addr, client)
48 | if err != nil {
49 | return false, fmt.Errorf("failed to create dispute game: %w", err)
50 | }
51 |
52 | factory, err := dispute.NewDisputeGameFactory(common.HexToAddress(factoryAddr), client)
53 | if err != nil {
54 | return false, fmt.Errorf("failed to create dispute game factory: %w", err)
55 | }
56 |
57 | gameType, err := game.GameType(&bind.CallOpts{Context: ctx})
58 | if err != nil {
59 | return false, fmt.Errorf("failed to get game type: %w", err)
60 | }
61 |
62 | rootClaim, err := game.RootClaim(&bind.CallOpts{Context: ctx})
63 | if err != nil {
64 | return false, fmt.Errorf("failed to get root claim: %w", err)
65 | }
66 |
67 | extraData, err := game.ExtraData(&bind.CallOpts{Context: ctx})
68 | if err != nil {
69 | return false, fmt.Errorf("failed to get extra data: %w", err)
70 | }
71 |
72 | factoryResult, err := factory.Games(&bind.CallOpts{Context: ctx}, gameType, rootClaim, extraData)
73 | if err != nil {
74 | return false, fmt.Errorf("failed to verify game with factory: %w", err)
75 | }
76 |
77 | return factoryResult.Proxy == addr && factoryResult.Timestamp > 0, nil
78 | }
79 |
80 | // ParamValidationFunc is a type for parameter validation functions
81 | type ParamValidationFunc func(params map[string]interface{}) error
82 |
83 | // ParamValidations maps check types to their parameter validation functions
84 | var ParamValidations = map[CheckType]ParamValidationFunc{
85 | ExactMatchCheck: ValidateCheckExactMatch,
86 | DisputeGameCheck: ValidateCheckDisputeGame,
87 | }
88 |
89 | // ValidateCheckExactMatch validates the parameters for the exact match check
90 | func ValidateCheckExactMatch(params map[string]interface{}) error {
91 | _, ok := params["match"].(string)
92 | if !ok {
93 | return fmt.Errorf("match parameter not found or invalid")
94 | }
95 | return nil
96 | }
97 |
98 | // ValidateCheckDisputeGame validates the parameters for the dispute game check
99 | func ValidateCheckDisputeGame(params map[string]interface{}) error {
100 | _, ok := params["disputeGameFactory"].(string)
101 | if !ok {
102 | return fmt.Errorf("disputeGameFactory parameter not found or invalid")
103 | }
104 | return nil
105 | }
106 |
--------------------------------------------------------------------------------
/op-monitorism/transaction_monitor/cli.go:
--------------------------------------------------------------------------------
1 | package transaction_monitor
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "time"
7 |
8 | opservice "github.com/ethereum-optimism/optimism/op-service"
9 | "github.com/urfave/cli/v2"
10 | "gopkg.in/yaml.v3"
11 | )
12 |
13 | const (
14 | NodeURLFlagName = "node.url"
15 | ConfigFileFlagName = "config.file"
16 | StartBlockFlagName = "start.block"
17 | PollingIntervalFlagName = "poll.interval"
18 | )
19 |
20 | type CLIConfig struct {
21 | NodeUrl string `yaml:"node_url"`
22 | StartBlock uint64 `yaml:"start_block"`
23 | PollingInterval time.Duration `yaml:"poll_interval"`
24 | WatchConfigs []WatchConfig `yaml:"watch_configs"`
25 | }
26 |
27 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
28 | cfg := CLIConfig{
29 | NodeUrl: ctx.String(NodeURLFlagName),
30 | StartBlock: ctx.Uint64(StartBlockFlagName),
31 | PollingInterval: ctx.Duration(PollingIntervalFlagName),
32 | }
33 |
34 | configFile := ctx.String(ConfigFileFlagName)
35 | if configFile == "" {
36 | return cfg, fmt.Errorf("config file must be specified")
37 | }
38 |
39 | data, err := os.ReadFile(configFile)
40 | if err != nil {
41 | return cfg, fmt.Errorf("failed to read config file: %w", err)
42 | }
43 |
44 | if err := yaml.Unmarshal(data, &cfg); err != nil {
45 | return cfg, fmt.Errorf("failed to parse config file: %w", err)
46 | }
47 |
48 | if len(cfg.WatchConfigs) == 0 {
49 | return cfg, fmt.Errorf("at least one watch config must be specified")
50 | }
51 |
52 | return cfg, nil
53 | }
54 |
55 | func CLIFlags(envPrefix string) []cli.Flag {
56 | return []cli.Flag{
57 | &cli.StringFlag{
58 | Name: NodeURLFlagName,
59 | Usage: "Node URL",
60 | Value: "http://localhost:8545",
61 | EnvVars: opservice.PrefixEnvVar(envPrefix, "NODE_URL"),
62 | },
63 | &cli.StringFlag{
64 | Name: ConfigFileFlagName,
65 | Usage: "Path to YAML config file containing watch addresses and filters",
66 | Required: true,
67 | EnvVars: opservice.PrefixEnvVar(envPrefix, "TX_CONFIG_FILE"),
68 | },
69 | &cli.Uint64Flag{
70 | Name: StartBlockFlagName,
71 | Usage: "Starting block number (0 for latest)",
72 | Value: 0,
73 | EnvVars: opservice.PrefixEnvVar(envPrefix, "START_BLOCK"),
74 | },
75 | &cli.DurationFlag{
76 | Name: PollingIntervalFlagName,
77 | Usage: "The polling interval (should be less than blocktime for safety) in seconds",
78 | Value: 12 * time.Second,
79 | EnvVars: opservice.PrefixEnvVar(envPrefix, "POLL_INTERVAL"),
80 | },
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/op-monitorism/transaction_monitor/monitor.go:
--------------------------------------------------------------------------------
1 | package transaction_monitor
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/ethereum-optimism/optimism/op-service/metrics"
9 | "github.com/ethereum/go-ethereum/common"
10 | "github.com/ethereum/go-ethereum/core/types"
11 | "github.com/ethereum/go-ethereum/ethclient"
12 | "github.com/ethereum/go-ethereum/log"
13 | "github.com/prometheus/client_golang/prometheus"
14 |
15 | "github.com/ethereum-optimism/monitorism/op-monitorism/processor"
16 | )
17 |
18 | const (
19 | MetricsNamespace = "tx_mon"
20 | )
21 |
22 | // CheckType represents the type of check to perform on an address
23 | type CheckType string
24 |
25 | const (
26 | // ExactMatchCheck verifies if an address matches exactly with a provided address
27 | ExactMatchCheck CheckType = "exact_match"
28 | // DisputeGameCheck verifies if an address is a valid dispute game created by the factory
29 | DisputeGameCheck CheckType = "dispute_game"
30 | )
31 |
32 | // CheckConfig represents a single check configuration
33 | type CheckConfig struct {
34 | Type CheckType `yaml:"type"`
35 | Params map[string]interface{} `yaml:"params"`
36 | }
37 |
38 | // WatchConfig represents the configuration for watching a specific address
39 | type WatchConfig struct {
40 | Address common.Address `yaml:"address"`
41 | Filters []CheckConfig `yaml:"filters"`
42 | }
43 |
44 | type Metrics struct {
45 | transactions *prometheus.CounterVec
46 | unauthorizedTx *prometheus.CounterVec
47 | ethSpent *prometheus.CounterVec
48 | }
49 |
50 | type Monitor struct {
51 | log log.Logger
52 | client *ethclient.Client
53 | watchConfigs map[common.Address]WatchConfig
54 | processor *processor.BlockProcessor
55 | metrics Metrics
56 | }
57 |
58 | func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIConfig) (*Monitor, error) {
59 | client, err := ethclient.Dial(cfg.NodeUrl)
60 | if err != nil {
61 | return nil, fmt.Errorf("failed to dial node: %w", err)
62 | }
63 |
64 | mon := &Monitor{
65 | log: log,
66 | client: client,
67 | watchConfigs: make(map[common.Address]WatchConfig),
68 | metrics: Metrics{
69 | transactions: m.NewCounterVec(
70 | prometheus.CounterOpts{
71 | Namespace: MetricsNamespace,
72 | Name: "transactions_total",
73 | Help: "Total number of transactions",
74 | },
75 | []string{"from"},
76 | ),
77 | unauthorizedTx: m.NewCounterVec(
78 | prometheus.CounterOpts{
79 | Namespace: MetricsNamespace,
80 | Name: "unauthorized_transactions_total",
81 | Help: "Number of transactions from unauthorized addresses",
82 | },
83 | []string{"from"},
84 | ),
85 | ethSpent: m.NewCounterVec(
86 | prometheus.CounterOpts{
87 | Namespace: MetricsNamespace,
88 | Name: "eth_spent_total",
89 | Help: "Cumulative ETH spent by address",
90 | },
91 | []string{"address"},
92 | ),
93 | },
94 | }
95 |
96 | // Initialize and validate watchConfigs
97 | for _, config := range cfg.WatchConfigs {
98 | for _, filter := range config.Filters {
99 | err := ParamValidations[filter.Type](filter.Params)
100 | if err != nil {
101 | return nil, fmt.Errorf("invalid parameters for check type %s: %w", filter.Type, err)
102 | }
103 | }
104 | mon.watchConfigs[config.Address] = config
105 | }
106 |
107 | // Create the block processor
108 | proc, err := processor.NewBlockProcessor(
109 | m,
110 | log,
111 | cfg.NodeUrl,
112 | mon.processTx,
113 | nil,
114 | &processor.Config{
115 | StartBlock: big.NewInt(int64(cfg.StartBlock)),
116 | Interval: cfg.PollingInterval,
117 | },
118 | )
119 | if err != nil {
120 | return nil, fmt.Errorf("failed to create block processor: %w", err)
121 | }
122 |
123 | mon.processor = proc
124 | return mon, nil
125 | }
126 |
127 | func (m *Monitor) Run(ctx context.Context) {
128 | go func() {
129 | <-ctx.Done()
130 | m.processor.Stop()
131 | }()
132 |
133 | if err := m.processor.Start(); err != nil {
134 | m.log.Error("processor error", "err", err)
135 | }
136 | }
137 |
138 | func (m *Monitor) processTx(block *types.Block, tx *types.Transaction, client *ethclient.Client) error {
139 | ctx := context.Background()
140 |
141 | // Grab the sender of the transaction.
142 | from, err := types.Sender(types.NewLondonSigner(tx.ChainId()), tx)
143 | if err != nil {
144 | return fmt.Errorf("failed to find tx sender: %w", err)
145 | }
146 |
147 | // Return if we're not watching this address.
148 | if _, exists := m.watchConfigs[from]; !exists {
149 | return nil
150 | }
151 |
152 | // If to is nil, use the created address.
153 | var to common.Address
154 | if tx.To() != nil {
155 | to = *tx.To()
156 | } else {
157 | receipt, err := client.TransactionReceipt(ctx, tx.Hash())
158 | if err != nil {
159 | return fmt.Errorf("failed to get transaction receipt: %w", err)
160 | }
161 | to = receipt.ContractAddress
162 | }
163 |
164 | // Check if the recipient is authorized.
165 | allowed, err := m.isAddressAllowed(ctx, from, to)
166 | if err != nil {
167 | return fmt.Errorf("error checking address: %w", err)
168 | }
169 |
170 | // Track metrics.
171 | weiValue := new(big.Float).SetInt(tx.Value())
172 | ethValue := new(big.Float).Quo(weiValue, big.NewFloat(1e18))
173 | ethFloat, _ := ethValue.Float64()
174 | m.metrics.ethSpent.WithLabelValues(from.String()).Add(ethFloat)
175 | m.metrics.transactions.WithLabelValues(from.String()).Inc()
176 | if !allowed {
177 | m.metrics.unauthorizedTx.WithLabelValues(from.String()).Inc()
178 | }
179 |
180 | return nil
181 | }
182 |
183 | func (m *Monitor) isAddressAllowed(ctx context.Context, from common.Address, addr common.Address) (bool, error) {
184 | // Make sure there's a watch config for this address.
185 | watchConfig, ok := m.watchConfigs[from]
186 | if !ok {
187 | return false, fmt.Errorf("no watch config found for address %s", from.String())
188 | }
189 |
190 | // Check each filter.
191 | for _, filter := range watchConfig.Filters {
192 | checkFn, ok := AddressChecks[filter.Type]
193 | if !ok {
194 | return false, fmt.Errorf("unknown check type: %s", filter.Type)
195 | }
196 |
197 | isValid, err := checkFn(ctx, m.client, addr, filter.Params)
198 | if err != nil {
199 | return false, fmt.Errorf("error running check: %w", err)
200 | }
201 |
202 | if isValid {
203 | return true, nil
204 | }
205 | }
206 |
207 | return false, nil
208 | }
209 |
210 | func (m *Monitor) Close(ctx context.Context) error {
211 | m.processor.Stop()
212 | m.client.Close()
213 | return nil
214 | }
215 |
--------------------------------------------------------------------------------
/op-monitorism/transaction_monitor/monitor_test.go:
--------------------------------------------------------------------------------
1 | package transaction_monitor
2 |
3 | import (
4 | "context"
5 | "crypto/ecdsa"
6 | "math/big"
7 | "testing"
8 | "time"
9 |
10 | "github.com/ethereum/go-ethereum/common"
11 | "github.com/ethereum/go-ethereum/core/types"
12 | "github.com/ethereum/go-ethereum/crypto"
13 | "github.com/ethereum/go-ethereum/ethclient"
14 | "github.com/ethereum/go-ethereum/log"
15 | "github.com/ethereum/go-ethereum/params"
16 |
17 | opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
18 | "github.com/ethereum-optimism/optimism/op-service/testutils/devnet"
19 | "github.com/prometheus/client_golang/prometheus"
20 | "github.com/prometheus/client_golang/prometheus/testutil"
21 | "github.com/stretchr/testify/require"
22 | )
23 |
24 | var (
25 | // Anvil test accounts
26 | watchedAddress = common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
27 | allowedAddress = common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8")
28 | unauthorizedAddr = common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC")
29 | factoryAddress = common.HexToAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906")
30 |
31 | // Private keys
32 | watchedKey, _ = crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
33 | )
34 |
35 | func setupAnvil(t *testing.T) (*devnet.Anvil, *ethclient.Client, string) {
36 | logger := log.New()
37 |
38 | anvilRunner, err := devnet.NewAnvil(logger)
39 | require.NoError(t, err)
40 |
41 | err = anvilRunner.Start()
42 | require.NoError(t, err)
43 | t.Cleanup(func() {
44 | _ = anvilRunner.Stop()
45 | })
46 |
47 | client, err := ethclient.Dial(anvilRunner.RPCUrl())
48 | require.NoError(t, err)
49 |
50 | return anvilRunner, client, anvilRunner.RPCUrl()
51 | }
52 |
53 | func sendTx(t *testing.T, ctx context.Context, client *ethclient.Client, key *ecdsa.PrivateKey, to common.Address, value *big.Int) {
54 | nonce, err := client.PendingNonceAt(ctx, crypto.PubkeyToAddress(key.PublicKey))
55 | require.NoError(t, err)
56 |
57 | gasPrice, err := client.SuggestGasPrice(ctx)
58 | require.NoError(t, err)
59 |
60 | tx := types.NewTransaction(nonce, to, value, 21000, gasPrice, nil)
61 | signedTx, err := types.SignTx(tx, types.NewLondonSigner(big.NewInt(31337)), key)
62 | require.NoError(t, err)
63 |
64 | err = client.SendTransaction(ctx, signedTx)
65 | require.NoError(t, err)
66 |
67 | // Wait for receipt
68 | for i := 0; i < 50; i++ {
69 | time.Sleep(100 * time.Millisecond)
70 | receipt, err := client.TransactionReceipt(ctx, signedTx.Hash())
71 | if err == nil {
72 | require.Equal(t, uint64(1), receipt.Status, "transaction failed")
73 | return
74 | }
75 | }
76 | t.Fatal("timeout waiting for transaction receipt")
77 | }
78 |
79 | func TestTransactionMonitoring(t *testing.T) {
80 | ctx, cancel := context.WithCancel(context.Background())
81 | defer cancel()
82 |
83 | _, client, rpc := setupAnvil(t)
84 |
85 | factory := factoryAddress
86 |
87 | cfg := CLIConfig{
88 | NodeUrl: rpc,
89 | StartBlock: 0,
90 | PollingInterval: 100 * time.Millisecond,
91 | WatchConfigs: []WatchConfig{{
92 | Address: watchedAddress,
93 | Filters: []CheckConfig{
94 | {
95 | Type: ExactMatchCheck,
96 | Params: map[string]interface{}{
97 | "match": allowedAddress.Hex(),
98 | },
99 | },
100 | {
101 | Type: DisputeGameCheck,
102 | Params: map[string]interface{}{
103 | "disputeGameFactory": factory.Hex(),
104 | },
105 | },
106 | },
107 | }},
108 | }
109 |
110 | registry := opmetrics.NewRegistry()
111 | monitor, err := NewMonitor(ctx, log.New(), opmetrics.With(registry), cfg)
112 | require.NoError(t, err)
113 |
114 | // Start monitor in background
115 | go monitor.Run(ctx)
116 | defer monitor.Close(ctx)
117 |
118 | t.Run("allowed address transaction", func(t *testing.T) {
119 | // Send transaction to allowed address
120 | sendTx(t, ctx, client, watchedKey, allowedAddress, big.NewInt(params.Ether))
121 | time.Sleep(2 * time.Second)
122 |
123 | // Check metrics
124 | require.Equal(t, float64(1), getCounterValue(t, monitor.metrics.transactions, watchedAddress.Hex()))
125 | require.Equal(t, float64(1.0), getCounterValue(t, monitor.metrics.ethSpent, watchedAddress.Hex()))
126 | require.Equal(t, float64(0), getCounterValue(t, monitor.metrics.unauthorizedTx, watchedAddress.Hex()))
127 | })
128 |
129 | t.Run("unauthorized address", func(t *testing.T) {
130 | monitor.metrics.unauthorizedTx.Reset()
131 | monitor.metrics.ethSpent.Reset()
132 |
133 | sendTx(t, ctx, client, watchedKey, unauthorizedAddr, big.NewInt(params.Ether/2))
134 | time.Sleep(2 * time.Second)
135 |
136 | require.Equal(t, float64(1), getCounterValue(t, monitor.metrics.unauthorizedTx, watchedAddress.Hex()))
137 | require.Equal(t, float64(0.5), getCounterValue(t, monitor.metrics.ethSpent, watchedAddress.Hex()))
138 | })
139 |
140 | t.Run("multiple unauthorized transactions", func(t *testing.T) {
141 | monitor.metrics.unauthorizedTx.Reset()
142 | monitor.metrics.ethSpent.Reset()
143 |
144 | // Send multiple unauthorized transactions
145 | for i := 0; i < 3; i++ {
146 | sendTx(t, ctx, client, watchedKey, unauthorizedAddr, big.NewInt(params.Ether/4))
147 | time.Sleep(500 * time.Millisecond)
148 | }
149 | time.Sleep(2 * time.Second)
150 |
151 | require.Equal(t, float64(3), getCounterValue(t, monitor.metrics.unauthorizedTx, watchedAddress.Hex()))
152 | require.Equal(t, float64(0.75), getCounterValue(t, monitor.metrics.ethSpent, watchedAddress.Hex()))
153 | })
154 | }
155 |
156 | func TestChecks(t *testing.T) {
157 | ctx := context.Background()
158 | _, client, _ := setupAnvil(t)
159 |
160 | t.Run("ExactMatchCheck", func(t *testing.T) {
161 | params := map[string]interface{}{
162 | "match": allowedAddress.Hex(),
163 | }
164 |
165 | // Test matching address
166 | isValid, err := CheckExactMatch(ctx, client, allowedAddress, params)
167 | require.NoError(t, err)
168 | require.True(t, isValid)
169 |
170 | // Test non-matching address
171 | isValid, err = CheckExactMatch(ctx, client, unauthorizedAddr, params)
172 | require.NoError(t, err)
173 | require.False(t, isValid)
174 | })
175 |
176 | t.Run("DisputeGameCheck", func(t *testing.T) {
177 | params := map[string]interface{}{
178 | "disputeGameFactory": factoryAddress.Hex(),
179 | }
180 |
181 | // Test the check with non-contract address (should return false)
182 | isValid, err := CheckDisputeGame(ctx, client, allowedAddress, params)
183 | require.NoError(t, err)
184 | require.False(t, isValid)
185 |
186 | // Note: Testing with actual dispute game contract would require deploying
187 | // contracts and is beyond the scope of this unit test
188 | })
189 | }
190 |
191 | func getCounterValue(t *testing.T, counter *prometheus.CounterVec, labelValues ...string) float64 {
192 | m, err := counter.GetMetricWithLabelValues(labelValues...)
193 | require.NoError(t, err)
194 | return testutil.ToFloat64(m)
195 | }
196 |
--------------------------------------------------------------------------------
/op-monitorism/withdrawals/README.md:
--------------------------------------------------------------------------------
1 | # Purpose of the Service
2 | `Withdrawals` has the following purpose:
3 | - Monitor Withdrawals: The service listens for WithdrawalProven events on the OptimismPortal contract on L1.
4 | - Validate Withdrawals: It verifies the validity of these withdrawals by checking the corresponding state on L2.
5 | - Detect Forgeries: The service identifies and reports any invalid withdrawals or potential forgeries.
6 |
7 | NOTE: The withdrawal monitor is only working against chains that are pre-Faultproof. For chains using the Faultproof system, please check the [faultproof_withdrawals service](https://github.com/ethereum-optimism/monitorism/blob/main/op-monitorism/faultproof_withdrawals/README.md).
8 |
9 | ```bash
10 | OPTIONS:
11 | --l1.node.url value Node URL of L1 peer Geth node [$WITHDRAWAL_MON_L1_NODE_URL]
12 | --l2.node.url value Node URL of L2 peer Op-Geth node [$WITHDRAWAL_MON_L2_NODE_URL]
13 | --event.block.range value Max block range when scanning for events (default: 1000) [$WITHDRAWAL_MON_EVENT_BLOCK_RANGE]
14 | --start.block.height value Starting height to scan for events (default: 0) [$WITHDRAWAL_MON_START_BLOCK_HEIGHT]
15 | --optimismportal.address value Address of the OptimismPortal contract [$WITHDRAWAL_MON_OPTIMISM_PORTAL]
16 | ```
17 |
--------------------------------------------------------------------------------
/op-monitorism/withdrawals/bindings/BINDING.md:
--------------------------------------------------------------------------------
1 | ## Info
2 | The bindings in this folder are taken from
3 | github.com/ethereum-optimism/optimism/op-bindings/bindings v1.7.3
4 |
5 | This tool is compatible with these bindings. Future binding will break compatibility for those files.
--------------------------------------------------------------------------------
/op-monitorism/withdrawals/cli.go:
--------------------------------------------------------------------------------
1 | package withdrawals
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ethereum/go-ethereum/common"
7 |
8 | opservice "github.com/ethereum-optimism/optimism/op-service"
9 |
10 | "github.com/urfave/cli/v2"
11 | )
12 |
13 | const (
14 | L1NodeURLFlagName = "l1.node.url"
15 | L2NodeURLFlagName = "l2.node.url"
16 |
17 | EventBlockRangeFlagName = "event.block.range"
18 | StartingL1BlockHeightFlagName = "start.block.height"
19 |
20 | OptimismPortalAddressFlagName = "optimismportal.address"
21 | )
22 |
23 | type CLIConfig struct {
24 | L1NodeURL string
25 | L2NodeURL string
26 |
27 | EventBlockRange uint64
28 | StartingL1BlockHeight uint64
29 |
30 | OptimismPortalAddress common.Address
31 | }
32 |
33 | func ReadCLIFlags(ctx *cli.Context) (CLIConfig, error) {
34 | cfg := CLIConfig{
35 | L1NodeURL: ctx.String(L1NodeURLFlagName),
36 | L2NodeURL: ctx.String(L2NodeURLFlagName),
37 | EventBlockRange: ctx.Uint64(EventBlockRangeFlagName),
38 | StartingL1BlockHeight: ctx.Uint64(StartingL1BlockHeightFlagName),
39 | }
40 |
41 | portalAddress := ctx.String(OptimismPortalAddressFlagName)
42 | if !common.IsHexAddress(portalAddress) {
43 | return cfg, fmt.Errorf("--%s is not a hex-encoded address", OptimismPortalAddressFlagName)
44 | }
45 | cfg.OptimismPortalAddress = common.HexToAddress(portalAddress)
46 |
47 | return cfg, nil
48 | }
49 |
50 | func CLIFlags(envVar string) []cli.Flag {
51 | return []cli.Flag{
52 | &cli.StringFlag{
53 | Name: L1NodeURLFlagName,
54 | Usage: "Node URL of L1 peer Geth node",
55 | EnvVars: opservice.PrefixEnvVar(envVar, "L1_NODE_URL"),
56 | },
57 | &cli.StringFlag{
58 | Name: L2NodeURLFlagName,
59 | Usage: "Node URL of L2 peer Op-Geth node",
60 | EnvVars: opservice.PrefixEnvVar(envVar, "L2_NODE_URL"),
61 | },
62 | &cli.Uint64Flag{
63 | Name: EventBlockRangeFlagName,
64 | Usage: "Max block range when scanning for events",
65 | Value: 1000,
66 | EnvVars: opservice.PrefixEnvVar(envVar, "EVENT_BLOCK_RANGE"),
67 | },
68 | &cli.Uint64Flag{
69 | Name: StartingL1BlockHeightFlagName,
70 | Usage: "Starting height to scan for events",
71 | EnvVars: opservice.PrefixEnvVar(envVar, "START_BLOCK_HEIGHT"),
72 | Required: true,
73 | },
74 | &cli.StringFlag{
75 | Name: OptimismPortalAddressFlagName,
76 | Usage: "Address of the OptimismPortal contract",
77 | EnvVars: opservice.PrefixEnvVar(envVar, "OPTIMISM_PORTAL"),
78 | Required: true,
79 | },
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/op-monitorism/withdrawals/monitor.go:
--------------------------------------------------------------------------------
1 | package withdrawals
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "math/big"
7 |
8 | "github.com/ethereum-optimism/monitorism/op-monitorism/withdrawals/bindings"
9 | "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
10 | "github.com/ethereum-optimism/optimism/op-service/metrics"
11 |
12 | "github.com/ethereum/go-ethereum"
13 | "github.com/ethereum/go-ethereum/common"
14 | "github.com/ethereum/go-ethereum/crypto"
15 | "github.com/ethereum/go-ethereum/ethclient"
16 | "github.com/ethereum/go-ethereum/log"
17 |
18 | "github.com/prometheus/client_golang/prometheus"
19 | )
20 |
21 | const (
22 | MetricsNamespace = "two_step_monitor"
23 |
24 | // event WithdrawalProven(bytes32 indexed withdrawalHash, address indexed from, address indexed to);
25 | WithdrawalProvenEventABI = "WithdrawalProven(bytes32,address,address)"
26 | )
27 |
28 | var (
29 | WithdrawalProvenEventABIHash = crypto.Keccak256Hash([]byte(WithdrawalProvenEventABI))
30 | )
31 |
32 | type Monitor struct {
33 | log log.Logger
34 |
35 | l1Client *ethclient.Client
36 | l2Client *ethclient.Client
37 |
38 | optimismPortalAddress common.Address
39 | optimismPortal *bindings.OptimismPortalCaller
40 | l2ToL1MP *bindings.L2ToL1MessagePasserCaller
41 |
42 | maxBlockRange uint64
43 | nextL1Height uint64
44 |
45 | // metrics
46 | highestBlockNumber *prometheus.GaugeVec
47 | isDetectingForgeries prometheus.Gauge
48 | withdrawalsValidated prometheus.Counter
49 | nodeConnectionFailures *prometheus.CounterVec
50 | }
51 |
52 | func NewMonitor(ctx context.Context, log log.Logger, m metrics.Factory, cfg CLIConfig) (*Monitor, error) {
53 | log.Info("creating withdrawals monitor...")
54 |
55 | l1Client, err := ethclient.Dial(cfg.L1NodeURL)
56 | if err != nil {
57 | return nil, fmt.Errorf("failed to dial l1: %w", err)
58 | }
59 | l2Client, err := ethclient.Dial(cfg.L2NodeURL)
60 | if err != nil {
61 | return nil, fmt.Errorf("failed to dial l2: %w", err)
62 | }
63 |
64 | optimismPortal, err := bindings.NewOptimismPortalCaller(cfg.OptimismPortalAddress, l1Client)
65 | if err != nil {
66 | return nil, fmt.Errorf("failed to bind to the OptimismPortal: %w", err)
67 | }
68 | l2ToL1MP, err := bindings.NewL2ToL1MessagePasserCaller(predeploys.L2ToL1MessagePasserAddr, l2Client)
69 | if err != nil {
70 | return nil, fmt.Errorf("failed to bind to the OptimismPortal: %w", err)
71 | }
72 |
73 | return &Monitor{
74 | log: log,
75 |
76 | l1Client: l1Client,
77 | l2Client: l2Client,
78 |
79 | optimismPortalAddress: cfg.OptimismPortalAddress,
80 | optimismPortal: optimismPortal,
81 | l2ToL1MP: l2ToL1MP,
82 |
83 | maxBlockRange: cfg.EventBlockRange,
84 | nextL1Height: cfg.StartingL1BlockHeight,
85 |
86 | /** Metrics **/
87 | isDetectingForgeries: m.NewGauge(prometheus.GaugeOpts{
88 | Namespace: MetricsNamespace,
89 | Name: "isDetectingForgeries",
90 | Help: "0 if state is ok. 1 if forged withdrawals are detected",
91 | }),
92 | withdrawalsValidated: m.NewCounter(prometheus.CounterOpts{
93 | Namespace: MetricsNamespace,
94 | Name: "withdrawalsValidated",
95 | Help: "number of withdrawals successfully validated",
96 | }),
97 | highestBlockNumber: m.NewGaugeVec(prometheus.GaugeOpts{
98 | Namespace: MetricsNamespace,
99 | Name: "highestBlockNumber",
100 | Help: "observed l1 heights (checked and known)",
101 | }, []string{"type"}),
102 | nodeConnectionFailures: m.NewCounterVec(prometheus.CounterOpts{
103 | Namespace: MetricsNamespace,
104 | Name: "nodeConnectionFailures",
105 | Help: "number of times node connection has failed",
106 | }, []string{"layer", "section"}),
107 | }, nil
108 | }
109 |
110 | func (m *Monitor) Run(ctx context.Context) {
111 | latestL1Height, err := m.l1Client.BlockNumber(ctx)
112 | if err != nil {
113 | m.log.Error("failed to query latest block number", "err", err)
114 | m.nodeConnectionFailures.WithLabelValues("l1", "blockNumber").Inc()
115 | return
116 | }
117 |
118 | m.highestBlockNumber.WithLabelValues("known").Set(float64(latestL1Height))
119 |
120 | fromBlockNumber := m.nextL1Height
121 | if fromBlockNumber > latestL1Height {
122 | m.log.Info("no new blocks", "next_height", fromBlockNumber, "latest_height", latestL1Height)
123 | return
124 | }
125 |
126 | toBlockNumber := latestL1Height
127 | if toBlockNumber-fromBlockNumber > m.maxBlockRange {
128 | toBlockNumber = fromBlockNumber + m.maxBlockRange
129 | }
130 |
131 | m.log.Info("querying block range", "from_height", fromBlockNumber, "to_height", toBlockNumber)
132 | filterQuery := ethereum.FilterQuery{
133 | FromBlock: big.NewInt(int64(fromBlockNumber)),
134 | ToBlock: big.NewInt(int64(toBlockNumber)),
135 | Addresses: []common.Address{m.optimismPortalAddress},
136 | Topics: [][]common.Hash{{WithdrawalProvenEventABIHash}},
137 | }
138 | provenWithdrawalLogs, err := m.l1Client.FilterLogs(ctx, filterQuery)
139 | if err != nil {
140 | m.log.Error("failed to query withdrawal proven event logs", "err", err)
141 | m.nodeConnectionFailures.WithLabelValues("l1", "filterLogs").Inc()
142 | return
143 | }
144 |
145 | // Check the withdrawals against the L2toL1MP contract
146 |
147 | if len(provenWithdrawalLogs) == 0 {
148 | m.log.Info("no proven withdrawals found", "from_height", fromBlockNumber, "to_height", toBlockNumber)
149 | } else {
150 | m.log.Info("detected proven withdrawals", "num", len(provenWithdrawalLogs), "from_height", fromBlockNumber, "to_height", toBlockNumber)
151 | }
152 |
153 | for _, provenWithdrawalLog := range provenWithdrawalLogs {
154 | withdrawalHash := provenWithdrawalLog.Topics[1]
155 | m.log.Info("checking withdrawal", "withdrawal_hash", withdrawalHash.String(),
156 | "block_height", provenWithdrawalLog.BlockNumber, "tx_hash", provenWithdrawalLog.TxHash.String())
157 |
158 | seen, err := m.l2ToL1MP.SentMessages(nil, withdrawalHash)
159 | if err != nil {
160 | // Return early and loop back into the same block range
161 | log.Error("failed to query L2ToL1MP sentMessages mapping", "withdrawal_hash", withdrawalHash.String(), "err", err)
162 | m.nodeConnectionFailures.WithLabelValues("l2", "sentMessages").Inc()
163 | return
164 | }
165 |
166 | // If forgery is detected, update alerted metrics and return early to enter
167 | // into a loop at this block range. May want to update this logic such that future
168 | // forgeries can be detected -- the existence of one implies many others likely exist.
169 | if !seen {
170 | m.log.Warn("forgery detected!!!!", "withdrawal_hash", withdrawalHash.String())
171 | m.isDetectingForgeries.Set(1)
172 | return
173 | }
174 |
175 | m.withdrawalsValidated.Inc()
176 | }
177 |
178 | m.log.Info("validated withdrawals", "height", toBlockNumber)
179 |
180 | // Update markers
181 | m.nextL1Height = toBlockNumber + 1
182 | m.isDetectingForgeries.Set(0)
183 | m.highestBlockNumber.WithLabelValues("checked").Set(float64(toBlockNumber))
184 | }
185 |
186 | func (m *Monitor) Close(_ context.Context) error {
187 | m.l1Client.Close()
188 | m.l2Client.Close()
189 | return nil
190 | }
191 |
--------------------------------------------------------------------------------