├── .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 | image 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 | image 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 | ![cde1a10c6f3eada39b2c27775faefd093623471db95bde4afcf06b9d43f3211b](https://github.com/user-attachments/assets/a75df38c-df06-4195-831b-08edb036a343) 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 | ![Demo: Running notebooks with make start command](https://github.com/user-attachments/assets/db08e752-983a-4e09-9ca9-5bf9f2b03ffa) 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 | ![Demo: Using notebooks in VS Code](https://github.com/user-attachments/assets/a4a233b7-e244-4edc-92ff-f34888f45cdb) 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 | ![df2b94999628ce8eee98fb60f45667e54be9b13db82add6aa77888f355137329](https://github.com/ethereum-optimism/monitorism/assets/23560242/b8d36a0f-8a17-4e22-be5a-3e9f3586b3ab) 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 | ![ab27497cea05fbd51b7b1c2ecde5bc69307ac0f27349f6bba4f3f21423116071](https://github.com/ethereum-optimism/monitorism/assets/23560242/af7a7e29-fff5-4df3-82f0-94c2f28fde84) 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 | --------------------------------------------------------------------------------