├── .env.example ├── .github ├── pull_request_template.md └── workflows │ └── checks.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── analysis ├── block.go ├── reorg.go ├── tree.go ├── treeanalysis.go └── treenode.go ├── cmd ├── db-utils │ ├── add-missing-uncles │ │ └── main.go │ ├── simulate-missing │ │ └── main.go │ └── test │ │ └── main.go ├── reorg-monitor-test │ └── main.go └── reorg-monitor │ └── main.go ├── database ├── dbservice.go └── types.go ├── deploy └── k8s │ ├── README.md │ ├── base │ ├── default.env │ ├── kustomization.yaml │ ├── postgres-service.yaml │ ├── postgres-statefulset.yaml │ ├── reorg-monitor-deployment.yaml │ ├── reorg-monitor-secrets.yaml │ └── reorg-monitor-service.yaml │ └── dev │ ├── kustomization.yaml │ └── reorg-monitor-service-patch.yaml ├── docker-compose.yml ├── go.mod ├── go.sum ├── monitor ├── config.go ├── gethconnection.go ├── monitor.go └── webserver.go ├── reorgutils └── reorgutils.go └── testutils ├── testcases.go └── testutils.go /.env.example: -------------------------------------------------------------------------------- 1 | export ETHEREUM_JSONRPC_URIS="ws://your_geth_node:8546" # eth nodes, comma separated 2 | export MEV_GETH_URI="http://your_mevgeth_node:8545" # optional - only needed for simulating blocks to get coinbase diff 3 | export SIMULATE_BLOCKS="1" 4 | 5 | # DB credentials for postgres in local docker-compose 6 | export POSTGRES_DSN="postgres://user1:password@localhost/reorg?timezone=utc" 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📝 Summary 2 | 3 | 4 | 5 | ## ⛱ Motivation and Context 6 | 7 | 8 | 9 | ## 📚 References 10 | 11 | 12 | 13 | --- 14 | 15 | ## ✅ I have run these commands 16 | 17 | * [ ] `make lint` 18 | * [ ] `make test` 19 | * [ ] `go mod tidy` 20 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ^1.20 18 | id: go 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | 23 | - name: Run unit tests and generate the coverage report 24 | run: make test 25 | 26 | lint: 27 | name: Lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Set up Go 31 | uses: actions/setup-go@v2 32 | with: 33 | go-version: ^1.20 34 | id: go 35 | 36 | - name: Check out code into the Go module directory 37 | uses: actions/checkout@v2 38 | 39 | - name: Install gofumpt 40 | run: go install mvdan.cc/gofumpt@v0.4.0 41 | 42 | - name: Install staticcheck 43 | run: go install honnef.co/go/tools/cmd/staticcheck@v0.4.3 44 | 45 | - name: Install golangci-lint 46 | run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2 47 | 48 | - name: Lint 49 | run: make lint 50 | 51 | - name: Ensure go mod tidy runs without changes 52 | run: | 53 | go mod tidy 54 | git diff-index HEAD 55 | git diff-index --quiet HEAD 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | /cmd/test 3 | /tmp 4 | /out.txt 5 | /out*.txt 6 | /*.txt 7 | /output/ 8 | /reorg-monitor 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.20.3 as builder 3 | WORKDIR /build 4 | ADD . /build/ 5 | RUN --mount=type=cache,target=/root/.cache/go-build make build-for-docker 6 | 7 | FROM scratch 8 | WORKDIR /app 9 | COPY --from=builder /build/reorg-monitor /app/reorg-monitor 10 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 11 | CMD ["/app/reorg-monitor"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2023 Flashbots 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build test clean lint cover cover-html build-for-docker docker-image docker-push vendor verify-image-registry 2 | 3 | GOPATH := $(if $(GOPATH),$(GOPATH),~/go) 4 | GIT_VER := $(shell git describe --tags --always --dirty="-dev") 5 | 6 | PACKAGES := $(shell go list -mod=readonly ./...) 7 | DOCKER_TAG ?= flashbots/reorg-monitor:latest 8 | IMAGE_REGISTRY_URI ?= 9 | 10 | all: clean build 11 | 12 | build: 13 | go build -ldflags "-X main.version=${GIT_VER}" -v -o reorg-monitor cmd/reorg-monitor/main.go 14 | 15 | clean: 16 | rm -rf reorg-monitor build/ 17 | 18 | test: 19 | go test ./... 20 | 21 | lint: 22 | gofmt -d -s . 23 | gofumpt -d -extra . 24 | go vet ./... 25 | staticcheck ./... 26 | # golangci-lint run 27 | 28 | lt: lint test 29 | 30 | gofumpt: 31 | gofumpt -l -w -extra . 32 | 33 | fmt: 34 | gofmt -s -w . 35 | gofumpt -extra -w . 36 | gci write . 37 | go mod tidy 38 | 39 | vendor: 40 | go mod tidy 41 | go mod vendor -v 42 | 43 | cover: 44 | go test -coverprofile=/tmp/go-bid-receiver.cover.tmp ./... 45 | go tool cover -func /tmp/go-bid-receiver.cover.tmp 46 | unlink /tmp/go-bid-receiver.cover.tmp 47 | 48 | cover-html: 49 | go test -coverprofile=/tmp/go-bid-receiver.cover.tmp ./... 50 | go tool cover -html=/tmp/go-bid-receiver.cover.tmp 51 | unlink /tmp/go-bid-receiver.cover.tmp 52 | 53 | build-for-docker: 54 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.version=${GIT_VER}" -v -o reorg-monitor cmd/reorg-monitor/main.go 55 | 56 | docker-image: 57 | DOCKER_BUILDKIT=1 docker build . -t ${DOCKER_TAG} 58 | 59 | docker-push: verify-image-registry 60 | docker tag ${DOCKER_TAG} ${IMAGE_REGISTRY_URI}:${GIT_VER} 61 | docker tag ${DOCKER_TAG} ${IMAGE_REGISTRY_URI}:latest 62 | docker push ${IMAGE_REGISTRY_URI}:${GIT_VER} 63 | docker push ${IMAGE_REGISTRY_URI}:latest 64 | 65 | k8s-deploy: verify-image-registry 66 | @echo "Checking if Docker image ${IMAGE_REGISTRY_URI}:${GIT_VER} exists..." 67 | @docker manifest inspect ${IMAGE_REGISTRY_URI}:${GIT_VER} > /dev/null || (echo "Docker image not found" && exit 1) 68 | kubectl set image deploy/deployment-reorg-monitor app-reorg-monitor=${IMAGE_REGISTRY_URI}:${GIT_VER} 69 | kubectl rollout status deploy/deployment-reorg-monitor 70 | 71 | verify-image-registry: 72 | ifndef IMAGE_REGISTRY_URI 73 | $(error IMAGE_REGISTRY_URI is not set) 74 | endif 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ethereum Reorg Monitor 2 | 3 | [![Goreport status](https://goreportcard.com/badge/github.com/flashbots/reorg-monitor)](https://goreportcard.com/report/github.com/flashbots/reorg-monitor) 4 | [![Test status](https://github.com/flashbots/reorg-monitor/workflows/Checks/badge.svg)](https://github.com/flashbots/reorg-monitor/actions?query=workflow%3A%22Checks%22) 5 | 6 | Watch and document Ethereum reorgs, including miner values of blocks. 7 | 8 | * Subscribe to multiple Ethereum nodes for new blocks (via WebSocket or IPC connection) 9 | * Capture block value (gas fees and smart contract payments) by simulating blocks with [mev-geth](https://github.com/flashbots/mev-geth/) 10 | * Collect data in a Postgres database (summary and individual block info) 11 | * Webserver that shows status information and recent reorgs 12 | 13 | This project is work in progress and there may be bugs, although it works pretty stable now. 14 | Please open issues if you have ideas, questions or want to contribute :) 15 | 16 | --- 17 | 18 | 19 | * [Ethereum Reorg Monitor](#ethereum-reorg-monitor) 20 | * [Getting started](#getting-started) 21 | * [Codebase Overview & Architecture](#codebase-overview--architecture) 22 | * [Notes & References](#notes--references) 23 | * [Maintainers](#maintainers) 24 | * [Security](#security) 25 | 26 | 27 | ## Getting started 28 | 29 | * Clone this repository 30 | * For database testing, you can use `docker-compose up` to start a local Postgres database and adminer 31 | * See [`.env.example`](https://github.com/flashbots/reorg-monitor/blob/master/.env.example) for environment variables you can use (eg create `.env.local` and use them with `source .env.local`). 32 | * Start the monitor: 33 | 34 | 35 | ```bash 36 | # Normal run, print only 37 | $ go run cmd/reorg-monitor/main.go --ethereum-jsonrpc-uris ws://geth_node:8546 38 | 39 | # Simulate blocks in a reorg - note: this requires passing in one or more RPC endpoints that support eth_callBundle API 40 | # The Flashbots RPC endpoints can be used to simulate blocks, for additional details see: https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint#bundle-relay-urls 41 | $ go run cmd/reorg-monitor/main.go --simulate-blocks --mev-geth-uri https://relay.flashbots.net --ethereum-jsonrpc-uris ws://geth_node:8546 42 | 43 | # Save to database 44 | $ go run cmd/reorg-monitor/main.go --simulate-blocks --mev-geth-uri https://relay.flashbots.net --postgres-dsn ${POSTGRES_DSN_HERE} --ethereum-jsonrpc-uris ws://geth_node:8546 45 | 46 | # Get status from webserver 47 | $ curl localhost:9094 48 | ``` 49 | 50 | You can also install the reorg monitor with `go install`: 51 | 52 | ```bash 53 | $ go install github.com/flashbots/reorg-monitor/cmd/reorg-monitor@latest 54 | $ reorg-monitor -h 55 | ``` 56 | 57 | --- 58 | 59 | ## Codebase Overview & Architecture 60 | 61 | See also: [Story of an Ethereum Reorg](https://docs.google.com/presentation/d/1ZHJp2HFOFeZxQAyPETRvcXW0oSOkZHAUhm7G-MoYyoQ/edit?usp=sharing) 62 | 63 | Code layout: 64 | 65 | * [`cmd/reorg-monitor`](https://github.com/flashbots/reorg-monitor/blob/master/cmd/reorg-monitor/main.go) is the main command-line entrypoint 66 | * [`cmd/reorg-monitor-test`](https://github.com/flashbots/reorg-monitor/blob/master/cmd/reorg-monitor-test/main.go) is used for local testing and development 67 | * [`monitor` module](https://github.com/flashbots/reorg-monitor/tree/master/monitor) - block collection: subscription to geth nodes, building a history of as many blocks as possible 68 | * [`analysis` module](https://github.com/flashbots/reorg-monitor/tree/master/analysis) - detect reorgs by building a tree data structure of all known blocks (blocks with >1 child start a reorg) 69 | 70 | --- 71 | 72 | ## Notes & References 73 | 74 | * [https://beaconscan.com/slots-forked](beaconscan.com/slots-forked) 75 | * [etherscan.io/blocks_forked](https://etherscan.io/blocks_forked) 76 | * [etherscan.io/chart/uncles](https://etherscan.io/chart/uncles) 77 | * [Story of an Ethereum Reorg](https://docs.google.com/presentation/d/1ZHJp2HFOFeZxQAyPETRvcXW0oSOkZHAUhm7G-MoYyoQ/edit?usp=sharing) 78 | * [go-ethereum `WriteBlock` function](https://github.com/ethereum/go-ethereum/blob/525116dbff916825463931361f75e75e955c12e2/core/blockchain.go#L860), which calls the `reorg` method if a block is seen of which the parent is not the current block 79 | * [Ethereum Whitepaper: Modified GHOST Implementation](https://ethereum.org/en/whitepaper/#modified-ghost-implementation) 80 | * [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) 81 | * [Ghost whitepaper](https://eprint.iacr.org/2013/881.pdf) 82 | * For Ethereum 2.0: [Combining Ghost and Casper](https://arxiv.org/abs/2003.03052) 83 | 84 | See also: 85 | 86 | * [An Empirical Analysis of Chain Reorganizations and Double-Spend Attacks on Proof-of-Work Cryptocurrencies](https://static1.squarespace.com/static/59aae5e9a803bb10bedeb03e/t/5f08d13a1cd5592cb330a0d0/1594413374526/LovejoyJamesP-meng-eecs-2020.pdf) (pdf) 87 | 88 | Tools: 89 | 90 | * https://composer.alchemyapi.io - to find out more about non-mainchain blocks ([`eth_getBlockByHash`](https://composer.alchemyapi.io/?composer_state=%7B%22chain%22%3A0%2C%22network%22%3A0%2C%22methodName%22%3A%22eth_getBlockByHash%22%2C%22paramValues%22%3A%5B%22YOUR_BLOCK_HASH_HERE%22%2Ctrue%5D%7D)) 91 | * https://mermaid-js.github.io/mermaid-live-editor 92 | 93 | ## Maintainers 94 | 95 | This project is currently maintained by: 96 | 97 | * [@metachris](https://twitter.com/metachris) 98 | * [@wazzymandias](https://twitter.com/wazzymandias) 99 | 100 | ## Security 101 | 102 | If you find a security vulnerability on this project or any other initiative related to Flashbots, please let us know sending an email to security@flashbots.net. 103 | -------------------------------------------------------------------------------- /analysis/block.go: -------------------------------------------------------------------------------- 1 | // Ethereum Block with some information about where it came from. 2 | package analysis 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/ethereum/go-ethereum/core/types" 10 | ) 11 | 12 | type BlockOrigin string 13 | 14 | const ( 15 | OriginSubscription BlockOrigin = "Subscription" 16 | OriginGetParent BlockOrigin = "GetParent" 17 | OriginUncle BlockOrigin = "Uncle" 18 | ) 19 | 20 | // Block is an geth Block and information about where it came from 21 | type Block struct { 22 | Block *types.Block 23 | Origin BlockOrigin 24 | NodeUri string 25 | ObservedUnixTimestamp int64 26 | 27 | // some helpers 28 | Number uint64 29 | Hash common.Hash 30 | ParentHash common.Hash 31 | } 32 | 33 | func NewBlock(block *types.Block, origin BlockOrigin, nodeUri string, observedUnix int64) *Block { 34 | return &Block{ 35 | Block: block, 36 | Origin: origin, 37 | NodeUri: nodeUri, 38 | ObservedUnixTimestamp: observedUnix, 39 | 40 | Number: block.NumberU64(), 41 | Hash: block.Hash(), 42 | ParentHash: block.ParentHash(), 43 | } 44 | } 45 | 46 | func (block *Block) String() string { 47 | t := time.Unix(int64(block.Block.Time()), 0).UTC() 48 | return fmt.Sprintf("Block %d %s / %s / tx: %4d, uncles: %d", block.Number, block.Hash, t, len(block.Block.Transactions()), len(block.Block.Uncles())) 49 | } 50 | -------------------------------------------------------------------------------- /analysis/reorg.go: -------------------------------------------------------------------------------- 1 | // Package analysis Summary of a specific reorg 2 | package analysis 3 | 4 | import ( 5 | "fmt" 6 | "reflect" 7 | "sort" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | ) 11 | 12 | type Reorg struct { 13 | IsFinished bool 14 | SeenLive bool 15 | 16 | StartBlockHeight uint64 // first block in a reorg (block number after common parent) 17 | EndBlockHeight uint64 // last block in a reorg 18 | 19 | Chains map[common.Hash][]*Block 20 | 21 | Depth int 22 | 23 | BlocksInvolved map[common.Hash]*Block 24 | MainChainHash common.Hash 25 | MainChainBlocks map[common.Hash]*Block 26 | NumReplacedBlocks int 27 | EthNodesInvolved map[string]bool 28 | 29 | CommonParent *Block 30 | FirstBlockAfterReorg *Block 31 | } 32 | 33 | func NewReorg(parentNode *TreeNode) (*Reorg, error) { 34 | if len(parentNode.Children) < 2 { 35 | return nil, fmt.Errorf("cannot create reorg because parent node with < 2 children") 36 | } 37 | 38 | reorg := Reorg{ 39 | CommonParent: parentNode.Block, 40 | StartBlockHeight: parentNode.Block.Number + 1, 41 | 42 | Chains: make(map[common.Hash][]*Block), 43 | BlocksInvolved: make(map[common.Hash]*Block), 44 | EthNodesInvolved: make(map[string]bool), 45 | MainChainBlocks: make(map[common.Hash]*Block), 46 | 47 | SeenLive: true, // will be set to false if any of the added blocks was received via uncle-info 48 | } 49 | 50 | // Build the individual chains until the enc, by iterating over children recursively 51 | for _, chainRootNode := range parentNode.Children { 52 | chain := make([]*Block, 0) 53 | 54 | var addChildToChainRecursive func(node *TreeNode) 55 | addChildToChainRecursive = func(node *TreeNode) { 56 | chain = append(chain, node.Block) 57 | 58 | for _, childNode := range node.Children { 59 | addChildToChainRecursive(childNode) 60 | } 61 | } 62 | addChildToChainRecursive(chainRootNode) 63 | reorg.Chains[chainRootNode.Block.Hash] = chain 64 | } 65 | 66 | // Find depth of chains 67 | chainLengths := []int{} 68 | for _, chain := range reorg.Chains { 69 | chainLengths = append(chainLengths, len(chain)) 70 | } 71 | sort.Sort(sort.Reverse(sort.IntSlice(chainLengths))) 72 | 73 | // Depth is number of blocks in second chain 74 | reorg.Depth = chainLengths[1] 75 | 76 | // Truncate the longest chain to the second, which is when the reorg actually stopped 77 | for key, chain := range reorg.Chains { 78 | if len(chain) > reorg.Depth { 79 | reorg.FirstBlockAfterReorg = chain[reorg.Depth] // first block that will be truncated 80 | reorg.Chains[key] = chain[:reorg.Depth] 81 | reorg.MainChainHash = key 82 | } 83 | } 84 | 85 | // If two chains with same height, then the reorg isn't yet finalized 86 | if chainLengths[0] == chainLengths[1] { 87 | reorg.IsFinished = false 88 | } else { 89 | reorg.IsFinished = true 90 | } 91 | 92 | // Build final list of involved blocks, and get end blockheight 93 | for chainHash, chain := range reorg.Chains { 94 | for _, block := range chain { 95 | reorg.BlocksInvolved[block.Hash] = block 96 | reorg.EthNodesInvolved[block.NodeUri] = true 97 | 98 | if block.Origin != OriginSubscription && block.Origin != OriginGetParent { 99 | reorg.SeenLive = false 100 | } 101 | 102 | if chainHash == reorg.MainChainHash { 103 | reorg.MainChainBlocks[block.Hash] = block 104 | reorg.EndBlockHeight = block.Number 105 | } 106 | } 107 | } 108 | 109 | reorg.NumReplacedBlocks = len(reorg.BlocksInvolved) - reorg.Depth 110 | 111 | return &reorg, nil 112 | } 113 | 114 | func (r *Reorg) Id() string { 115 | id := fmt.Sprintf("%d_%d_d%d_b%d", r.StartBlockHeight, r.EndBlockHeight, r.Depth, len(r.BlocksInvolved)) 116 | if r.SeenLive { 117 | id += "_l" 118 | } 119 | return id 120 | } 121 | 122 | func (r *Reorg) String() string { 123 | nodeUris := reflect.ValueOf(r.EthNodesInvolved).MapKeys() 124 | return fmt.Sprintf("Reorg %s: live=%-5v chains=%d, depth=%d, replaced=%d, nodes: %v", r.Id(), r.SeenLive, len(r.Chains), r.Depth, r.NumReplacedBlocks, nodeUris) 125 | } 126 | 127 | func (r *Reorg) MermaidSyntax() string { 128 | ret := "stateDiagram-v2\n" 129 | 130 | for _, block := range r.BlocksInvolved { 131 | ret += fmt.Sprintf(" %s --> %s\n", block.ParentHash, block.Hash) 132 | } 133 | 134 | // Add first block after reorg 135 | ret += fmt.Sprintf(" %s --> %s", r.FirstBlockAfterReorg.ParentHash, r.FirstBlockAfterReorg.Hash) 136 | return ret 137 | } 138 | -------------------------------------------------------------------------------- /analysis/tree.go: -------------------------------------------------------------------------------- 1 | // Tree of blocks, used to traverse up (from children to parents) and down (from parents to children). 2 | // Reorgs start on each node with more than one child. 3 | package analysis 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | type BlockTree struct { 12 | FirstNode *TreeNode 13 | LatestNodes []*TreeNode // Nodes at latest blockheight (can be more than 1) 14 | NodeByHash map[common.Hash]*TreeNode 15 | MainChainNodeByHash map[common.Hash]*TreeNode 16 | } 17 | 18 | func NewBlockTree() *BlockTree { 19 | return &BlockTree{ 20 | LatestNodes: []*TreeNode{}, 21 | NodeByHash: make(map[common.Hash]*TreeNode), 22 | MainChainNodeByHash: make(map[common.Hash]*TreeNode), 23 | } 24 | } 25 | 26 | func (t *BlockTree) AddBlock(block *Block) error { 27 | // First block is a special case 28 | if t.FirstNode == nil { 29 | node := NewTreeNode(block, nil) 30 | t.FirstNode = node 31 | t.LatestNodes = []*TreeNode{node} 32 | t.NodeByHash[block.Hash] = node 33 | return nil 34 | } 35 | 36 | // All other blocks are inserted as child of it's parent parent 37 | parent, parentFound := t.NodeByHash[block.ParentHash] 38 | if !parentFound { 39 | err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Number, block.Hash, block.ParentHash) 40 | return err 41 | } 42 | 43 | node := NewTreeNode(block, parent) 44 | t.NodeByHash[block.Hash] = node 45 | parent.AddChild(node) 46 | 47 | // Remember nodes at latest block height 48 | if len(t.LatestNodes) == 0 { 49 | t.LatestNodes = []*TreeNode{node} 50 | } else { 51 | if block.Number == t.LatestNodes[0].Block.Number { // add to list of latest nodes! 52 | t.LatestNodes = append(t.LatestNodes, node) 53 | } else if block.Number > t.LatestNodes[0].Block.Number { // replace 54 | t.LatestNodes = []*TreeNode{node} 55 | } 56 | } 57 | 58 | // Mark main-chain nodes as such. Step 1: reset all nodes to non-main-chain 59 | t.MainChainNodeByHash = make(map[common.Hash]*TreeNode) 60 | for _, n := range t.NodeByHash { 61 | n.IsMainChain = false 62 | } 63 | 64 | // Step 2: Traverse backwards and mark main chain. If there's more than 1 nodes at latest height, then we don't yet know which chain will be the main-chain 65 | if len(t.LatestNodes) == 1 { 66 | var TraverseMainChainFromLatestToEarliest func(node *TreeNode) 67 | TraverseMainChainFromLatestToEarliest = func(node *TreeNode) { 68 | if node == nil { 69 | return 70 | } 71 | node.IsMainChain = true 72 | t.MainChainNodeByHash[node.Block.Hash] = node 73 | TraverseMainChainFromLatestToEarliest(node.Parent) 74 | } 75 | TraverseMainChainFromLatestToEarliest(t.LatestNodes[0]) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func PrintNodeAndChildren(node *TreeNode, depth int) { 82 | indent := "-" 83 | fmt.Printf("%s %s\n", indent, node.String()) 84 | for _, childNode := range node.Children { 85 | PrintNodeAndChildren(childNode, depth+1) 86 | } 87 | } 88 | 89 | func (t *BlockTree) Print() { 90 | fmt.Printf("BlockTree: nodes=%d\n", len(t.NodeByHash)) 91 | 92 | if t.FirstNode == nil { 93 | return 94 | } 95 | 96 | // Print tree by traversing from parent to all children 97 | PrintNodeAndChildren(t.FirstNode, 1) 98 | 99 | // Print latest nodes 100 | fmt.Printf("Latest nodes:\n") 101 | for _, n := range t.LatestNodes { 102 | fmt.Println("-", n.String()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /analysis/treeanalysis.go: -------------------------------------------------------------------------------- 1 | // TreeAnalysis takes in a BlockTree and collects information about reorgs 2 | package analysis 3 | 4 | import ( 5 | "fmt" 6 | ) 7 | 8 | type TreeAnalysis struct { 9 | Tree *BlockTree 10 | 11 | StartBlockHeight uint64 // first block number with siblings 12 | EndBlockHeight uint64 13 | IsSplitOngoing bool 14 | 15 | NumBlocks int 16 | NumBlocksMainChain int 17 | 18 | Reorgs map[string]*Reorg 19 | } 20 | 21 | func NewTreeAnalysis(t *BlockTree) (*TreeAnalysis, error) { 22 | analysis := TreeAnalysis{ 23 | Tree: t, 24 | Reorgs: make(map[string]*Reorg), 25 | } 26 | 27 | if t.FirstNode == nil { // empty analysis for empty tree 28 | return &analysis, nil 29 | } 30 | 31 | analysis.StartBlockHeight = t.FirstNode.Block.Number 32 | analysis.EndBlockHeight = t.LatestNodes[0].Block.Number 33 | 34 | if len(t.LatestNodes) > 1 { 35 | analysis.IsSplitOngoing = true 36 | } 37 | 38 | analysis.NumBlocks = len(t.NodeByHash) 39 | analysis.NumBlocksMainChain = len(t.MainChainNodeByHash) 40 | 41 | // Find reorgs 42 | for _, node := range t.NodeByHash { 43 | if len(node.Children) > 1 { 44 | reorg, err := NewReorg(node) 45 | if err != nil { 46 | return nil, err 47 | } 48 | analysis.Reorgs[reorg.Id()] = reorg 49 | } 50 | } 51 | 52 | return &analysis, nil 53 | } 54 | 55 | func (a *TreeAnalysis) Print() { 56 | fmt.Printf("TreeAnalysis %d - %d, nodes: %d, mainchain: %d, reorgs: %d\n", a.StartBlockHeight, a.EndBlockHeight, a.NumBlocks, a.NumBlocksMainChain, len(a.Reorgs)) 57 | if a.IsSplitOngoing { 58 | fmt.Println("- split ongoing") 59 | } 60 | 61 | for _, reorg := range a.Reorgs { 62 | fmt.Println("") 63 | fmt.Println(reorg.String()) 64 | fmt.Printf("- common parent: %d %s, first block after: %d %s\n", reorg.CommonParent.Number, reorg.CommonParent.Hash, reorg.FirstBlockAfterReorg.Number, reorg.FirstBlockAfterReorg.Hash) 65 | 66 | for chainKey, chain := range reorg.Chains { 67 | if chainKey == reorg.MainChainHash { 68 | fmt.Printf("- mainchain l=%d: ", len(chain)) 69 | } else { 70 | fmt.Printf("- sidechain l=%d: ", len(chain)) 71 | } 72 | for _, block := range chain { 73 | fmt.Printf("%s ", block.Hash) 74 | } 75 | fmt.Print("\n") 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /analysis/treenode.go: -------------------------------------------------------------------------------- 1 | package analysis 2 | 3 | import "fmt" 4 | 5 | type TreeNode struct { 6 | Block *Block 7 | Parent *TreeNode 8 | Children []*TreeNode 9 | 10 | IsFirst bool 11 | IsMainChain bool 12 | } 13 | 14 | func NewTreeNode(block *Block, parent *TreeNode) *TreeNode { 15 | return &TreeNode{ 16 | Block: block, 17 | Parent: parent, 18 | Children: []*TreeNode{}, 19 | IsFirst: parent == nil, 20 | } 21 | } 22 | 23 | func (tn *TreeNode) String() string { 24 | return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Number, tn.Block.Hash, tn.IsMainChain, tn.IsFirst, len(tn.Children)) 25 | } 26 | 27 | func (tn *TreeNode) AddChild(node *TreeNode) { 28 | tn.Children = append(tn.Children, node) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/db-utils/add-missing-uncles/main.go: -------------------------------------------------------------------------------- 1 | // In case of service downtime, add at least the known uncles to the database 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "math/big" 10 | "os" 11 | "sync" 12 | "time" 13 | 14 | "github.com/ethereum/go-ethereum/common" 15 | "github.com/ethereum/go-ethereum/core/types" 16 | "github.com/ethereum/go-ethereum/crypto" 17 | "github.com/ethereum/go-ethereum/ethclient" 18 | "github.com/flashbots/reorg-monitor/analysis" 19 | "github.com/flashbots/reorg-monitor/database" 20 | "github.com/flashbots/reorg-monitor/reorgutils" 21 | "github.com/metachris/flashbotsrpc" 22 | ) 23 | 24 | var ( 25 | db *database.DatabaseService 26 | rpc *flashbotsrpc.FlashbotsRPC 27 | client *ethclient.Client 28 | mevGethClient *ethclient.Client 29 | callBundlePrivKey, _ = crypto.GenerateKey() 30 | ethUriPtr *string 31 | ) 32 | 33 | func main() { 34 | log.SetOutput(os.Stdout) 35 | 36 | ethUriPtr = flag.String("eth", "", "mev-geth node URI") 37 | mevGethUriPtr := flag.String("mevgeth", "", "mev-geth node URI") 38 | startBlock := flag.Int64("startblock", 0, "start blockheight") 39 | endBlock := flag.Int64("endblock", 0, "end blockheight") 40 | flag.Parse() 41 | 42 | if *ethUriPtr == "" { 43 | log.Fatal("Missing -eth argument") 44 | } 45 | 46 | if *mevGethUriPtr == "" { 47 | log.Fatal("Missing -mevgeth argument") 48 | } 49 | 50 | if *startBlock == 0 { 51 | log.Fatal("Missing -startblock argument") 52 | } 53 | 54 | if *endBlock == 0 { 55 | log.Fatal("Missing -endblock argument") 56 | } 57 | 58 | var err error 59 | fmt.Printf("Connecting to %s...", *ethUriPtr) 60 | client, err = ethclient.Dial(*ethUriPtr) 61 | reorgutils.Perror(err) 62 | fmt.Printf(" ok\n") 63 | 64 | fmt.Printf("Connecting to %s...", *mevGethUriPtr) 65 | mevGethClient, err = ethclient.Dial(*mevGethUriPtr) 66 | reorgutils.Perror(err) 67 | fmt.Printf(" ok\n") 68 | 69 | rpc = flashbotsrpc.NewFlashbotsRPC(*mevGethUriPtr) 70 | 71 | db, err = database.NewDatabaseService(os.Getenv("POSTGRES_DSN")) 72 | reorgutils.Perror(err) 73 | fmt.Println("Connected to database") 74 | 75 | blockChan := make(chan *types.Block, 100) 76 | 77 | // Start block processor 78 | var analyzeLock sync.Mutex 79 | go func() { 80 | analyzeLock.Lock() 81 | defer analyzeLock.Unlock() // we unlock when done 82 | 83 | for block := range blockChan { 84 | fmt.Printf("Block %d %s \t miner: %s \t tx=%-4d \t gas=%d \t %d\n", block.NumberU64(), block.Hash(), block.Coinbase(), len(block.Transactions()), block.GasUsed(), len(block.Uncles())) 85 | 86 | // Download the uncles for processing later 87 | for _, uncleHeader := range block.Uncles() { 88 | fmt.Printf("Downloading uncle %s...\n", uncleHeader.Hash()) 89 | uncleBlock, err := mevGethClient.BlockByHash(context.Background(), uncleHeader.Hash()) 90 | if err != nil { 91 | fmt.Println("- error:", err) 92 | continue 93 | } 94 | 95 | addUncle(uncleBlock, block) 96 | } 97 | } 98 | }() 99 | 100 | // Start getting blocks 101 | reorgutils.GetBlocks(blockChan, client, *startBlock, *endBlock, 15) 102 | 103 | // Wait until all blocks have been processed 104 | close(blockChan) 105 | analyzeLock.Lock() 106 | } 107 | 108 | func addUncle(uncleBlock, mainchainBlock *types.Block) { 109 | fmt.Println("addUncle", uncleBlock.Hash()) 110 | 111 | // Add to database now 112 | _, err := db.BlockEntry(uncleBlock.Hash()) 113 | if err == nil { // already exists 114 | fmt.Println("- block already exists in db, skipping update") 115 | return 116 | } 117 | 118 | // get next block 119 | nextHeight := new(big.Int).Add(uncleBlock.Number(), common.Big1) 120 | mainChainBlockChild1, err := client.BlockByNumber(context.Background(), nextHeight) 121 | reorgutils.Perror(err) 122 | 123 | observed := time.Now().UTC().UnixNano() 124 | reorg := analysis.Reorg{ 125 | IsFinished: true, 126 | SeenLive: false, 127 | StartBlockHeight: uncleBlock.NumberU64(), 128 | EndBlockHeight: uncleBlock.NumberU64(), 129 | Chains: make(map[common.Hash][]*analysis.Block), 130 | Depth: 1, 131 | BlocksInvolved: make(map[common.Hash]*analysis.Block), 132 | MainChainHash: mainchainBlock.Hash(), 133 | MainChainBlocks: make(map[common.Hash]*analysis.Block), 134 | NumReplacedBlocks: 1, 135 | EthNodesInvolved: make(map[string]bool), 136 | FirstBlockAfterReorg: analysis.NewBlock(mainChainBlockChild1, analysis.OriginUncle, *ethUriPtr, observed), 137 | } 138 | 139 | _uncleBlock := analysis.NewBlock(uncleBlock, analysis.OriginUncle, *ethUriPtr, observed) 140 | _mainChainBlock := analysis.NewBlock(mainchainBlock, analysis.OriginSubscription, *ethUriPtr, observed) 141 | 142 | // Update reorg details 143 | reorg.Chains[_mainChainBlock.Hash] = []*analysis.Block{_mainChainBlock} 144 | reorg.Chains[_uncleBlock.Hash] = []*analysis.Block{_uncleBlock} 145 | reorg.BlocksInvolved[_mainChainBlock.Hash] = _mainChainBlock 146 | reorg.BlocksInvolved[_uncleBlock.Hash] = _uncleBlock 147 | reorg.MainChainBlocks[_mainChainBlock.Hash] = _mainChainBlock 148 | reorg.EthNodesInvolved[*ethUriPtr] = true 149 | 150 | // Create block entries 151 | fmt.Println("- simulating uncle block...") 152 | uncleSimRes, err := rpc.FlashbotsSimulateBlock(callBundlePrivKey, uncleBlock, 0) 153 | if err != nil { 154 | fmt.Println("-", err) 155 | return 156 | } 157 | uncleBlockEntry := database.NewBlockEntry(_uncleBlock, &reorg) 158 | uncleBlockEntry.UpdateWitCallBundleResponse(uncleSimRes) 159 | 160 | fmt.Println("- simulating mainchain block...") 161 | mainChainSimRes, err := rpc.FlashbotsSimulateBlock(callBundlePrivKey, uncleBlock, 0) 162 | if err != nil { 163 | fmt.Println("-", err) 164 | return 165 | } 166 | mainChainBlockEntry := database.NewBlockEntry(_mainChainBlock, &reorg) 167 | mainChainBlockEntry.UpdateWitCallBundleResponse(mainChainSimRes) 168 | 169 | // Time to use for created_at is time of next block 170 | t := time.Unix(int64(mainChainBlockChild1.Header().Time), 0).UTC() 171 | tf := t.Format("2006-01-02 15:04:05") 172 | 173 | // Save 174 | reorgEntry := database.NewReorgEntry(&reorg) 175 | 176 | err = db.AddReorgEntry(reorgEntry) 177 | if err != nil { 178 | fmt.Println("-", err) 179 | return 180 | } 181 | 182 | _, err = db.DB.Exec("Update reorg_summary SET Created_At=$1 WHERE Key=$2", tf, reorgEntry.Key) 183 | if err != nil { 184 | fmt.Println("-", err) 185 | return 186 | } 187 | 188 | err = db.AddBlockEntry(uncleBlockEntry) 189 | if err != nil { 190 | fmt.Println("-", err) 191 | return 192 | } 193 | 194 | err = db.AddBlockEntry(mainChainBlockEntry) 195 | if err != nil { 196 | fmt.Println("-", err) 197 | return 198 | } 199 | 200 | _, err = db.DB.Exec("Update reorg_block SET Created_At=$1 WHERE Reorg_Key=$2", tf, reorgEntry.Key) 201 | if err != nil { 202 | fmt.Println("-", err) 203 | return 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /cmd/db-utils/simulate-missing/main.go: -------------------------------------------------------------------------------- 1 | // Fetch all blocks that haven't been simulated from the database -> simulate it -> update the db entry 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/crypto" 13 | "github.com/ethereum/go-ethereum/ethclient" 14 | "github.com/flashbots/reorg-monitor/database" 15 | "github.com/flashbots/reorg-monitor/reorgutils" 16 | "github.com/metachris/flashbotsrpc" 17 | ) 18 | 19 | var ( 20 | db *database.DatabaseService 21 | client *ethclient.Client 22 | rpc *flashbotsrpc.FlashbotsRPC 23 | callBundlePrivKey, _ = crypto.GenerateKey() 24 | ) 25 | 26 | func main() { 27 | log.SetOutput(os.Stdout) 28 | 29 | ethUriPtr := flag.String("mevgeth", "", "mev-geth node URI") 30 | flag.Parse() 31 | 32 | if *ethUriPtr == "" { 33 | log.Fatal("Missing mev-geth node uri") 34 | } 35 | 36 | var err error 37 | fmt.Printf("Connecting to %s...", *ethUriPtr) 38 | client, err = ethclient.Dial(*ethUriPtr) 39 | reorgutils.Perror(err) 40 | fmt.Printf(" ok\n") 41 | 42 | rpc = flashbotsrpc.NewFlashbotsRPC(*ethUriPtr) 43 | 44 | db, err = database.NewDatabaseService(os.Getenv("POSTGRES_DSN")) 45 | reorgutils.Perror(err) 46 | fmt.Println("Connected to database") 47 | 48 | blockEntries := []database.BlockEntry{} 49 | db.DB.Select(&blockEntries, "SELECT * FROM reorg_block WHERE MevGeth_CoinbaseDiffWei=0 AND NumTx>0 ORDER BY id DESC") 50 | if len(blockEntries) == 0 { 51 | return 52 | } 53 | fmt.Println(len(blockEntries)) 54 | 55 | for _, blockEntry := range blockEntries { 56 | fmt.Println("Checking block", blockEntry.BlockNumber, blockEntry.BlockHash, blockEntry.NumTx) 57 | ethBlock, err := client.BlockByHash(context.Background(), common.HexToHash(blockEntry.BlockHash)) 58 | if err != nil { 59 | fmt.Println("-", err) 60 | continue 61 | } 62 | 63 | if len(ethBlock.Transactions()) == 0 { 64 | blockEntry.MevGeth_CoinbaseDiffWei = "0" 65 | blockEntry.MevGeth_GasFeesWei = "0" 66 | blockEntry.MevGeth_EthSentToCoinbaseWei = "0" 67 | blockEntry.MevGeth_EthSentToCoinbase = "0" 68 | blockEntry.MevGeth_CoinbaseDiffEth = "0" 69 | } else { 70 | res, err := rpc.FlashbotsSimulateBlock(callBundlePrivKey, ethBlock, 0) 71 | if err != nil { 72 | fmt.Println("-", err) 73 | continue 74 | } 75 | blockEntry.UpdateWitCallBundleResponse(res) 76 | } 77 | 78 | _, err = db.DB.Exec("Update reorg_block SET MevGeth_CoinbaseDiffWei=$1, MevGeth_GasFeesWei=$2, MevGeth_EthSentToCoinbaseWei=$3, MevGeth_EthSentToCoinbase=$4, MevGeth_CoinbaseDiffEth=$5 WHERE id=$6", 79 | blockEntry.MevGeth_CoinbaseDiffWei, blockEntry.MevGeth_GasFeesWei, blockEntry.MevGeth_EthSentToCoinbaseWei, blockEntry.MevGeth_EthSentToCoinbase, blockEntry.MevGeth_CoinbaseDiffEth, blockEntry.Id) 80 | reorgutils.Perror(err) 81 | fmt.Println("updated block", blockEntry.Id) 82 | // return 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cmd/db-utils/test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/flashbots/reorg-monitor/analysis" 11 | "github.com/flashbots/reorg-monitor/database" 12 | "github.com/flashbots/reorg-monitor/reorgutils" 13 | "github.com/flashbots/reorg-monitor/testutils" 14 | ) 15 | 16 | var db *database.DatabaseService 17 | 18 | func main() { 19 | var ( 20 | postgresDsn = "postgres://user1:password@localhost:5432/reorg?sslmode=disable" 21 | err error 22 | ) 23 | 24 | db, err = database.NewDatabaseService(postgresDsn) 25 | reorgutils.Perror(err) 26 | 27 | log.SetOutput(os.Stdout) 28 | 29 | ethUriPtr := flag.String("eth", os.Getenv("GETH_ETH1LOCAL"), "Geth node URI") 30 | flag.Parse() 31 | 32 | if *ethUriPtr == "" { 33 | log.Fatal("Missing eth node uri") 34 | } 35 | 36 | testutils.EthNodeUri = *ethUriPtr 37 | _, err = testutils.ConnectClient(*ethUriPtr) 38 | reorgutils.Perror(err) 39 | 40 | // Test(testutils.Test_12996760_12996760_d1_b2) 41 | // Test(testutils.Test_12996750_12996750_d1_b3) 42 | // Test(testutils.Test_12991732_12991733_d2_b4) 43 | // Test(testutils.Test_12969887_12969889_d3_b6) 44 | // Test(testutils.Test_13017535_13017536_d2_b5) 45 | // Test(testutils.Test_13018369_13018370_d2_b4) 46 | 47 | // test := testutils.Test_13018369_13018370_d2_b4 48 | // test := testutils.Test_12996750_12996750_d1_b3_twouncles 49 | // test := testutils.Test3xD1 50 | test := testutils.TestCase{ 51 | BlockInfo: []string{ 52 | "13400397", 53 | "0xe11507e3ab485efa72d4221e5a2e58d35809425f0bb486d702dc38efe894c830", 54 | "0x5824ce21441e5d30e8685a90cb70bb24d09ca6533fb3063f35bd5b9a05e480b8", 55 | "13400402", 56 | }, 57 | } 58 | testutils.ResetMon() 59 | 60 | // Add the blocks 61 | for _, ethBlock := range testutils.BlocksForStrings(test.BlockInfo) { 62 | observed := time.Now().UTC().UnixNano() 63 | block := analysis.NewBlock(ethBlock, analysis.OriginSubscription, *ethUriPtr, observed) 64 | testutils.Monitor.AddBlock(block) 65 | } 66 | 67 | analysis, err := testutils.Monitor.AnalyzeTree(100, 0) 68 | if err != nil { 69 | fmt.Println(err) 70 | return 71 | } 72 | 73 | // analysis.Tree.Print() 74 | // fmt.Println("") 75 | analysis.Print() 76 | fmt.Println("") 77 | 78 | for _, reorg := range analysis.Reorgs { 79 | fmt.Println(reorg) 80 | 81 | entry := database.NewReorgEntry(reorg) 82 | db.AddReorgEntry(entry) 83 | 84 | // blocks 85 | for _, block := range reorg.BlocksInvolved { 86 | blockEntry := database.NewBlockEntry(block, reorg) 87 | db.AddBlockEntry(blockEntry) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cmd/reorg-monitor-test/main.go: -------------------------------------------------------------------------------- 1 | // Test various aspects of the reorg monitor based on custom block inputs (block height and hash) 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/flashbots/reorg-monitor/analysis" 13 | "github.com/flashbots/reorg-monitor/monitor" 14 | "github.com/flashbots/reorg-monitor/reorgutils" 15 | "github.com/flashbots/reorg-monitor/testutils" 16 | ) 17 | 18 | var mon *monitor.ReorgMonitor 19 | 20 | func main() { 21 | log.SetOutput(os.Stdout) 22 | 23 | ethUriPtr := flag.String("eth", os.Getenv("ETH_NODES"), "Ethereum node URI") 24 | flag.Parse() 25 | 26 | ethUris := strings.Split(*ethUriPtr, ",") 27 | if len(ethUris) == 0 { 28 | log.Fatal("Missing eth node uri") 29 | } 30 | 31 | // Connect a geth client to fetch the custom blocks 32 | _, err := testutils.ConnectClient(ethUris[0]) 33 | reorgutils.Perror(err) 34 | 35 | // Create a new monitor instance (and connect its geth clients to fetch further blocks if required) 36 | mon = monitor.NewReorgMonitor(ethUris, make(chan *analysis.Reorg), true, 100) 37 | numConnectedClients := mon.ConnectClients() 38 | if numConnectedClients == 0 { 39 | log.Fatal("could not connect to any clients") 40 | } 41 | 42 | CheckReorg([]string{ 43 | "0xd690a9bb26d27d665768f67b8839ea6571a1e10c95ad69e38dac16d9686f5d9f", 44 | "0xc24323315f3735017026f754b3f6ee1bca3cdbb8e3f6b2476dd09aec4e40cb38", 45 | "0x1eba1d62712b43067cb8f913e682190eabf2dd020a4e954fdd4460a6099f09a7", 46 | }) 47 | } 48 | 49 | // blockIds can include block numbers and hashes 50 | func CheckReorg(blockIds []string) { 51 | // Add the blocks 52 | for _, ethBlock := range testutils.BlocksForStrings(blockIds) { 53 | block := analysis.NewBlock(ethBlock, analysis.OriginSubscription, testutils.EthNodeUri, time.Now().UTC().UnixNano()) 54 | mon.AddBlock(block) 55 | } 56 | 57 | // Analyze 58 | analysis, err := mon.AnalyzeTree(0, 0) 59 | if err != nil { 60 | fmt.Println(err) 61 | return 62 | } 63 | 64 | // Print tree and result 65 | fmt.Println("") 66 | analysis.Tree.Print() 67 | fmt.Println("") 68 | analysis.Print() 69 | } 70 | 71 | func TestAndVerify(testCase testutils.TestCase) { 72 | // Create a new monitor 73 | testutils.ResetMon() 74 | 75 | // Add the blocks 76 | for _, ethBlock := range testutils.BlocksForStrings(testCase.BlockInfo) { 77 | block := analysis.NewBlock(ethBlock, analysis.OriginSubscription, testutils.EthNodeUri, time.Now().UTC().UnixNano()) 78 | testutils.Monitor.AddBlock(block) 79 | } 80 | 81 | // reorgs := testutils.ReorgCheckAndPrint() 82 | // testutils.Pcheck("NumReorgs", len(reorgs), 1) 83 | 84 | // reorg := reorgs[0] 85 | // testutils.Pcheck("StartBlock", reorg.StartBlockHeight, testCase.ExpectedResult.StartBlock) 86 | // testutils.Pcheck("EndBlock", reorg.EndBlockHeight, testCase.ExpectedResult.EndBlock) 87 | // testutils.Pcheck("Depth", reorg.Depth, testCase.ExpectedResult.Depth) 88 | // testutils.Pcheck("NumBlocks", len(reorg.BlocksInvolved), testCase.ExpectedResult.NumBlocks) 89 | // testutils.Pcheck("NumReplacedBlocks", reorg.NumReplacedBlocks, testCase.ExpectedResult.NumReplacedBlocks) 90 | 91 | // if testCase.ExpectedResult.MustBeLive { 92 | // testutils.Pcheck("MustBeLive", reorg.SeenLive, true) 93 | // } 94 | 95 | // fmt.Println(reorg.MermaidSyntax()) 96 | } 97 | -------------------------------------------------------------------------------- /cmd/reorg-monitor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/flashbots/reorg-monitor/analysis" 10 | "github.com/flashbots/reorg-monitor/database" 11 | "github.com/flashbots/reorg-monitor/monitor" 12 | "github.com/metachris/flashbotsrpc" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | const ( 18 | AppName = "reorg-monitor" 19 | 20 | // default values for CLI flags 21 | defaultMaxBlocks = 200 22 | defaultSimulateBlocks = false 23 | defaultEnableDebug = false 24 | 25 | // flag related constants 26 | // NOTE: Be sure to match the flag with its associated struct tag in config for `monitor.Config` 27 | flagEnableDebug = "debug" 28 | usageDebug = "enable debug mode for additional logging" 29 | 30 | flagEthereumJsonRpcURIs = "ethereum-jsonrpc-uris" 31 | usageEthereumJsonRpcURIs = "comma separated list of Ethereum JSON-RPC URIs used for monitoring network" 32 | 33 | flagListenAddress = "listen-address" 34 | usageListenAddress = "TCP network address used to launch monitoring web server that displays block status information" 35 | 36 | flagMaxBlocks = "max-blocks" 37 | usageMaxBlocks = "maximum number of blocks to hold in memory" 38 | 39 | flagMevGethURI = "mev-geth-uri" 40 | usageMevGethURI = "URI for mev-geth used for block simulation" 41 | 42 | flagPostgresDSN = "postgres-dsn" 43 | usagePostgresDSN = "data source name (DSN) for PostgreSQL database used for storing reorged blocks" 44 | 45 | flagSimulateBlocks = "simulate-blocks" 46 | usageSimulateBlocks = "toggles block simulation and updates database with response metadata if enabled" 47 | ) 48 | 49 | var ( 50 | ColorGreen = "\033[1;32m%s\033[0m" 51 | 52 | version = "dev" // is set during build process 53 | 54 | db *database.DatabaseService 55 | rpc *flashbotsrpc.FlashbotsRPC 56 | callBundlePrivKey, _ = crypto.GenerateKey() 57 | ) 58 | 59 | func main() { 60 | if err := MonitorCmd().Execute(); err != nil { 61 | log.Fatal(err) 62 | } 63 | } 64 | 65 | func handleReorg(simulateBlocks bool, db *database.DatabaseService, reorg *analysis.Reorg) { 66 | log.Println(reorg.String()) 67 | fmt.Println("- common parent: ", reorg.CommonParent.Hash) 68 | fmt.Println("- first block after:", reorg.FirstBlockAfterReorg.Hash) 69 | for chainKey, chain := range reorg.Chains { 70 | if chainKey == reorg.MainChainHash { 71 | fmt.Printf("- mainchain l=%d: ", len(chain)) 72 | } else { 73 | fmt.Printf("- sidechain l=%d: ", len(chain)) 74 | } 75 | 76 | for _, block := range chain { 77 | fmt.Printf("%s ", block.Hash) 78 | } 79 | fmt.Print("\n") 80 | } 81 | 82 | if db != nil { 83 | entry := database.NewReorgEntry(reorg) 84 | err := db.AddReorgEntry(entry) 85 | if err != nil { 86 | log.Printf("error at db.AddReorgEntry: %+v\n", err) 87 | } 88 | 89 | for _, block := range reorg.BlocksInvolved { 90 | blockEntry := database.NewBlockEntry(block, reorg) 91 | 92 | // If block has no transactions, then it has 0 miner value (no need to simulate) 93 | if simulateBlocks && len(block.Block.Transactions()) > 0 { 94 | res, err := rpc.FlashbotsSimulateBlock(callBundlePrivKey, block.Block, 0) 95 | if err != nil { 96 | log.Println("error: sim failed of block", block.Hash, "-", err) 97 | } else { 98 | log.Printf("- sim of block %s: CoinbaseDiff=%20s, GasFees=%20s, EthSentToCoinbase=%20s\n", block.Hash, res.CoinbaseDiff, res.GasFees, res.EthSentToCoinbase) 99 | blockEntry.UpdateWitCallBundleResponse(res) 100 | } 101 | } 102 | 103 | err := db.AddBlockEntry(blockEntry) 104 | if err != nil { 105 | log.Println("error at db.AddBlockEntry:", err) 106 | } 107 | } 108 | } 109 | 110 | if reorg.NumReplacedBlocks > 1 { 111 | fmt.Println(reorg.MermaidSyntax()) 112 | } 113 | fmt.Println("") 114 | } 115 | 116 | // MonitorCmd defines top level command to instantiate and run reorg monitoring server based on 117 | // input environment variables and command line flags 118 | func MonitorCmd() *cobra.Command { 119 | conf := new(monitor.Config) 120 | 121 | cmd := &cobra.Command{ 122 | Use: AppName, 123 | SilenceUsage: true, 124 | Short: "Monitor for re-orgs on Ethereum network across execution layer (EL) and consensus layer (CL)", 125 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 126 | // Load environment variables and cli flags into configuration structure using viper 127 | v := viper.New() 128 | // Replace dashes with underscores to support reading in environment variables 129 | v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 130 | if err := v.BindPFlags(cmd.PersistentFlags()); err != nil { 131 | return err 132 | } 133 | v.AutomaticEnv() 134 | 135 | return v.Unmarshal(conf) 136 | }, 137 | RunE: func(cmd *cobra.Command, args []string) error { 138 | log.Printf("initializing %s [version: %s]", AppName, version) 139 | 140 | if len(conf.EthereumJsonRpcURIs) == 0 { 141 | return fmt.Errorf("ethereum json-rpc endpoints not found") 142 | } 143 | 144 | if conf.SimulateBlocks { 145 | if conf.MevGethURI == "" { 146 | return fmt.Errorf("block simulation enabled but mev-geth URI not found") 147 | } 148 | 149 | rpc = flashbotsrpc.NewFlashbotsRPC(conf.MevGethURI) 150 | rpc.Debug = conf.EnableDebug 151 | log.Printf("Using mev-geth node at %s for block simulations", conf.MevGethURI) 152 | } 153 | 154 | if conf.PostgresDSN != "" { 155 | var err error 156 | db, err = database.NewDatabaseService(conf.PostgresDSN) 157 | endpoint := strings.Split(conf.PostgresDSN, "@")[1] 158 | if err != nil { 159 | return fmt.Errorf("error initializing database service with endpoint %s - %v", endpoint, err) 160 | } 161 | log.Println("Connected to database at", "uri", endpoint) 162 | } 163 | 164 | // Channel to receive reorgs from monitor 165 | reorgChan := make(chan *analysis.Reorg) 166 | 167 | // Setup and start the monitor 168 | mon := monitor.NewReorgMonitor(conf.EthereumJsonRpcURIs, reorgChan, true, conf.MaxBlocks) 169 | if mon.ConnectClients() == 0 { 170 | return fmt.Errorf("%s could not connect to any clients", AppName) 171 | } 172 | 173 | if conf.ListenAddress != "" { 174 | log.Printf("Starting webserver on %s\n", conf.ListenAddress) 175 | ws := monitor.NewMonitorWebserver(mon, conf.ListenAddress) 176 | go ws.ListenAndServe() 177 | } 178 | 179 | // In the background, subscribe to new blocks and listen for updates 180 | go mon.SubscribeAndListen() 181 | 182 | // Wait for reorgs 183 | for reorg := range reorgChan { 184 | handleReorg(conf.SimulateBlocks, db, reorg) 185 | } 186 | return nil 187 | }, 188 | } 189 | 190 | cmd.PersistentFlags().StringSliceVar(&conf.EthereumJsonRpcURIs, flagEthereumJsonRpcURIs, nil, usageEthereumJsonRpcURIs) 191 | cmd.PersistentFlags().StringVar(&conf.PostgresDSN, flagPostgresDSN, "", usagePostgresDSN) 192 | cmd.PersistentFlags().StringVar(&conf.ListenAddress, flagListenAddress, "", usageListenAddress) 193 | cmd.PersistentFlags().BoolVar(&conf.SimulateBlocks, flagSimulateBlocks, defaultSimulateBlocks, usageSimulateBlocks) 194 | cmd.PersistentFlags().StringVar(&conf.MevGethURI, flagMevGethURI, "", usageMevGethURI) 195 | cmd.PersistentFlags().IntVar(&conf.MaxBlocks, flagMaxBlocks, defaultMaxBlocks, usageMaxBlocks) 196 | cmd.PersistentFlags().BoolVar(&conf.EnableDebug, flagEnableDebug, defaultEnableDebug, usageDebug) 197 | return cmd 198 | } 199 | -------------------------------------------------------------------------------- /database/dbservice.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/flashbots/reorg-monitor/analysis" 9 | "github.com/jmoiron/sqlx" 10 | ) 11 | 12 | const driverName = "postgres" 13 | 14 | type DatabaseService struct { 15 | DB *sqlx.DB 16 | } 17 | 18 | func NewDatabaseService(dsn string) (*DatabaseService, error) { 19 | db, err := sqlx.Connect(driverName, dsn) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | _, err = db.Exec(Schema) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &DatabaseService{ 30 | DB: db, 31 | }, nil 32 | } 33 | 34 | func (s *DatabaseService) Reset() { 35 | s.DB.MustExec(`DROP TABLE "reorg_summary";`) 36 | s.DB.MustExec(`DROP TABLE "reorg_block";`) 37 | s.DB.MustExec(Schema) 38 | } 39 | 40 | func (s *DatabaseService) Close() { 41 | s.DB.Close() 42 | } 43 | 44 | func (s *DatabaseService) ReorgEntry(key string) (entry ReorgEntry, err error) { 45 | err = s.DB.Get(&entry, "SELECT * FROM reorg_summary WHERE Key=$1", key) 46 | return entry, err 47 | } 48 | 49 | func (s *DatabaseService) BlockEntry(hash common.Hash) (entry BlockEntry, err error) { 50 | err = s.DB.Get(&entry, "SELECT * FROM reorg_block WHERE BlockHash=$1", strings.ToLower(hash.Hex())) 51 | return entry, err 52 | } 53 | 54 | func (s *DatabaseService) AddReorgEntry(entry ReorgEntry) error { 55 | var count int 56 | err := s.DB.QueryRow("SELECT COUNT(*) FROM reorg_summary WHERE Key = $1", entry.Key).Scan(&count) 57 | if err != nil { 58 | return err 59 | } 60 | if count > 0 { // already exists 61 | return fmt.Errorf("a reorg with key %s already exists", entry.Key) 62 | } 63 | 64 | // Insert 65 | _, err = s.DB.Exec("INSERT INTO reorg_summary (Key, SeenLive, StartBlockNumber, EndBlockNumber, Depth, NumChains, NumBlocksInvolved, NumBlocksReplaced, MermaidSyntax) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", entry.Key, entry.SeenLive, entry.StartBlockNumber, entry.EndBlockNumber, entry.Depth, entry.NumChains, entry.NumBlocksInvolved, entry.NumBlocksReplaced, entry.MermaidSyntax) 66 | return err 67 | } 68 | 69 | func (s *DatabaseService) AddBlockEntry(entry BlockEntry) error { 70 | var count int 71 | err := s.DB.QueryRow("SELECT COUNT(*) FROM reorg_block WHERE Reorg_Key = $1 AND BlockHash = $2", entry.Reorg_Key, strings.ToLower(entry.BlockHash)).Scan(&count) 72 | if err != nil { 73 | return err 74 | } 75 | if count > 0 { // already exists 76 | return fmt.Errorf("a block with hash %s for reorg %s already exists", entry.BlockHash, entry.Reorg_Key) 77 | } 78 | 79 | // Insert 80 | _, err = s.DB.Exec("INSERT INTO reorg_block (Reorg_Key, Origin, NodeUri, BlockNumber, BlockHash, ParentHash, BlockTimestamp, CoinbaseAddress, Difficulty, NumUncles, NumTx, IsPartOfReorg, IsMainChain, IsFirst, MevGeth_CoinbaseDiffEth, MevGeth_CoinbaseDiffWei, MevGeth_GasFeesWei, MevGeth_EthSentToCoinbaseWei, MevGeth_EthSentToCoinbase) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", 81 | entry.Reorg_Key, entry.Origin, entry.NodeUri, entry.BlockNumber, entry.BlockHash, entry.ParentHash, entry.BlockTimestamp, entry.CoinbaseAddress, entry.Difficulty, entry.NumUncles, entry.NumTx, entry.IsPartOfReorg, entry.IsMainChain, entry.IsFirst, entry.MevGeth_CoinbaseDiffEth, entry.MevGeth_CoinbaseDiffWei, entry.MevGeth_GasFeesWei, entry.MevGeth_EthSentToCoinbaseWei, entry.MevGeth_EthSentToCoinbase) 82 | return err 83 | } 84 | 85 | func (s *DatabaseService) AddReorgWithBlocks(reorg *analysis.Reorg) error { 86 | // First add the reorg summary 87 | err := s.AddReorgEntry(NewReorgEntry(reorg)) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | // Then add all the blocks 93 | for _, block := range reorg.BlocksInvolved { 94 | err := s.AddBlockEntry(NewBlockEntry(block, reorg)) 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (s *DatabaseService) DeleteReorgWithBlocks(entry ReorgEntry) error { 104 | _, err := s.DB.Exec("DELETE FROM reorg_block WHERE Reorg_Key=$1", entry.Key) 105 | if err != nil { 106 | return err 107 | } 108 | _, err = s.DB.Exec("DELETE FROM reorg_summary WHERE id=$1", entry.Id) 109 | return err 110 | } 111 | 112 | func (s *DatabaseService) DeleteReorgEntry(entry ReorgEntry) error { 113 | _, err := s.DB.Exec("DELETE FROM reorg_summary WHERE id=$1", entry.Id) 114 | return err 115 | } 116 | 117 | func (s *DatabaseService) DeleteBlockEntry(entry BlockEntry) error { 118 | _, err := s.DB.Exec("DELETE FROM reorg_block WHERE id=$1", entry.Id) 119 | return err 120 | } 121 | -------------------------------------------------------------------------------- /database/types.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "math/big" 6 | 7 | "github.com/flashbots/reorg-monitor/analysis" 8 | "github.com/flashbots/reorg-monitor/reorgutils" 9 | _ "github.com/lib/pq" 10 | "github.com/metachris/flashbotsrpc" 11 | ) 12 | 13 | var Schema = ` 14 | CREATE TABLE IF NOT EXISTS reorg_summary ( 15 | Id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 16 | Created_At timestamp NOT NULL default current_timestamp, 17 | 18 | Key VARCHAR (40) NOT NULL UNIQUE, 19 | SeenLive boolean NOT NULL, 20 | 21 | StartBlockNumber integer NOT NULL, 22 | EndBlockNumber integer NOT NULL, 23 | Depth integer NOT NULL, 24 | NumChains integer NOT NULL, 25 | NumBlocksInvolved integer NOT NULL, 26 | NumBlocksReplaced integer NOT NULL, 27 | MermaidSyntax text NOT NULL 28 | ); 29 | 30 | CREATE TABLE IF NOT EXISTS reorg_block ( 31 | Id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 32 | Created_At timestamp NOT NULL default current_timestamp, 33 | 34 | Reorg_Key VARCHAR (40) REFERENCES reorg_summary (Key) NOT NULL, 35 | 36 | Origin VARCHAR (20) NOT NULL, 37 | NodeUri text NOT NULL, 38 | 39 | BlockNumber integer NOT NULL, 40 | BlockHash text NOT NULL, 41 | ParentHash text NOT NULL, 42 | BlockTimestamp integer NOT NULL, 43 | CoinbaseAddress text NOT NULL, 44 | 45 | Difficulty bigint NOT NULL, 46 | NumUncles integer NOT NULL, 47 | NumTx integer NOT NULL, 48 | 49 | IsPartOfReorg boolean NOT NULL, 50 | IsMainChain boolean NOT NULL, 51 | IsFirst boolean NOT NULL, 52 | 53 | MevGeth_CoinbaseDiffWei NUMERIC(48, 0), 54 | MevGeth_GasFeesWei NUMERIC(48, 0), 55 | MevGeth_EthSentToCoinbaseWei NUMERIC(48, 0), 56 | 57 | MevGeth_CoinbaseDiffEth VARCHAR(10), 58 | MevGeth_EthSentToCoinbase VARCHAR(10) 59 | ); 60 | ` 61 | 62 | type ReorgEntry struct { 63 | Id int 64 | Created_At sql.NullTime 65 | 66 | Key string 67 | SeenLive bool 68 | 69 | StartBlockNumber uint64 70 | EndBlockNumber uint64 71 | Depth int 72 | NumChains int 73 | NumBlocksInvolved int 74 | NumBlocksReplaced int 75 | MermaidSyntax string 76 | } 77 | 78 | func NewReorgEntry(reorg *analysis.Reorg) ReorgEntry { 79 | return ReorgEntry{ 80 | Key: reorg.Id(), 81 | SeenLive: reorg.SeenLive, 82 | StartBlockNumber: reorg.StartBlockHeight, 83 | EndBlockNumber: reorg.EndBlockHeight, 84 | Depth: reorg.Depth, 85 | NumChains: len(reorg.Chains), 86 | NumBlocksInvolved: len(reorg.BlocksInvolved), 87 | NumBlocksReplaced: reorg.NumReplacedBlocks, 88 | MermaidSyntax: reorg.MermaidSyntax(), 89 | } 90 | } 91 | 92 | type BlockEntry struct { 93 | Id int 94 | Created_At sql.NullTime 95 | 96 | Reorg_Key string 97 | Origin string 98 | NodeUri string 99 | 100 | BlockNumber uint64 101 | BlockHash string 102 | ParentHash string 103 | BlockTimestamp uint64 104 | CoinbaseAddress string 105 | 106 | Difficulty uint64 107 | NumUncles int 108 | NumTx int 109 | 110 | IsPartOfReorg bool 111 | IsMainChain bool 112 | IsFirst bool 113 | 114 | MevGeth_CoinbaseDiffWei string 115 | MevGeth_GasFeesWei string 116 | MevGeth_EthSentToCoinbaseWei string 117 | 118 | MevGeth_CoinbaseDiffEth string 119 | MevGeth_EthSentToCoinbase string 120 | } 121 | 122 | func NewBlockEntry(block *analysis.Block, reorg *analysis.Reorg) BlockEntry { 123 | _, isPartOfReorg := reorg.BlocksInvolved[block.Hash] 124 | _, isMainChain := reorg.MainChainBlocks[block.Hash] 125 | 126 | blockEntry := BlockEntry{ 127 | Reorg_Key: reorg.Id(), 128 | Origin: string(block.Origin), 129 | NodeUri: block.NodeUri, 130 | 131 | BlockNumber: block.Number, 132 | BlockHash: block.Hash.String(), 133 | ParentHash: block.ParentHash.String(), 134 | BlockTimestamp: block.Block.Time(), 135 | CoinbaseAddress: block.Block.Coinbase().String(), 136 | 137 | Difficulty: block.Block.Difficulty().Uint64(), 138 | NumUncles: len(block.Block.Uncles()), 139 | NumTx: len(block.Block.Transactions()), 140 | 141 | IsPartOfReorg: isPartOfReorg, 142 | IsMainChain: isMainChain, 143 | IsFirst: block.Number == reorg.StartBlockHeight, 144 | 145 | MevGeth_CoinbaseDiffWei: "0", 146 | MevGeth_GasFeesWei: "0", 147 | MevGeth_EthSentToCoinbaseWei: "0", 148 | 149 | MevGeth_CoinbaseDiffEth: "0.000000", 150 | MevGeth_EthSentToCoinbase: "0.000000", 151 | } 152 | 153 | return blockEntry 154 | } 155 | 156 | func (e *BlockEntry) UpdateWitCallBundleResponse(callBundleResponse flashbotsrpc.FlashbotsCallBundleResponse) { 157 | coinbaseDiffWei := new(big.Int) 158 | coinbaseDiffWei.SetString(callBundleResponse.CoinbaseDiff, 10) 159 | coinbaseDiffEth := reorgutils.WeiToEth(coinbaseDiffWei) 160 | 161 | ethSentToCoinbaseWei := new(big.Int) 162 | ethSentToCoinbaseWei.SetString(callBundleResponse.EthSentToCoinbase, 10) 163 | ethSentToCoinbase := reorgutils.WeiToEth(ethSentToCoinbaseWei) 164 | 165 | e.MevGeth_CoinbaseDiffWei = callBundleResponse.CoinbaseDiff 166 | e.MevGeth_GasFeesWei = callBundleResponse.GasFees 167 | e.MevGeth_EthSentToCoinbaseWei = callBundleResponse.EthSentToCoinbase 168 | 169 | e.MevGeth_CoinbaseDiffEth = coinbaseDiffEth.Text('f', 6) 170 | e.MevGeth_EthSentToCoinbase = ethSentToCoinbase.Text('f', 6) 171 | } 172 | -------------------------------------------------------------------------------- /deploy/k8s/README.md: -------------------------------------------------------------------------------- 1 | # Reorg Monitor Kubernetes Deployment 2 | 3 | ### Dependencies 4 | - [docker](https://www.docker.com/products/docker-desktop/) 5 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 6 | - [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) 7 | - [kind](https://kind.sigs.k8s.io/) (Kubernetes in Docker) 8 | - [make](https://www.gnu.org/software/make/) 9 | 10 | ## [Development] Running Locally 11 | 12 | #### [Prerequisite] Initialize kind cluster 13 | ```shell 14 | kind create cluster 15 | ``` 16 | 17 | 1. Build docker image and load into kind cluster 18 | ```shell 19 | DOCKER_TAG=flashbots/reorg-monitor:0.1.0 make docker-image 20 | 21 | kind load docker-image flashbots/reorg-monitor:0.1.0 22 | ``` 23 | 2. [OPTIONAL] Navigate to development directory and inspect kubernetes manifests to ensure they are valid 24 | ```shell 25 | cd reorg-monitor/deploy/k8s/dev 26 | 27 | kubectl kustomize . 28 | ``` 29 | 3. [OPTIONAL] Create namespace if it doesn't exist 30 | ```shell 31 | # NOTE: Be sure to match the namespace name with value defined in manifests or kustomization file 32 | kubectl create namespace reorg-monitor 33 | ``` 34 | 4. Deploy services to namespace using relevant manifests 35 | ```shell 36 | kubectl apply -k deploy/k8s/dev 37 | ``` -------------------------------------------------------------------------------- /deploy/k8s/base/default.env: -------------------------------------------------------------------------------- 1 | ETHEREUM_JSONRPC_URIS= 2 | POSTGRES_DSN=postgres://user1:password@postgres.reorg-monitor.svc.cluster.local:5432/reorg?sslmode=disable 3 | LISTEN_ADDRESS=0.0.0.0:9090 4 | 5 | POSTGRES_DB=reorg 6 | POSTGRES_USER=user1 7 | POSTGRES_PASSWORD=password 8 | -------------------------------------------------------------------------------- /deploy/k8s/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | namespace: reorg-monitor 5 | 6 | images: 7 | - name: flashbots/reorg-monitor 8 | newTag: 0.1.0 9 | 10 | replicas: 11 | - name: reorg-monitor 12 | count: 1 13 | - name: postgres 14 | count: 0 15 | 16 | resources: 17 | - reorg-monitor-deployment.yaml 18 | - reorg-monitor-service.yaml 19 | - postgres-statefulset.yaml 20 | - postgres-service.yaml 21 | 22 | secretGenerator: 23 | - name: reorg-monitor-secrets 24 | envs: 25 | - default.env 26 | -------------------------------------------------------------------------------- /deploy/k8s/base/postgres-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres 5 | spec: 6 | type: ClusterIP 7 | selector: 8 | app.kubernetes.io/name: postgres 9 | app.kubernetes.io/component: database 10 | app.kubernetes.io/part-of: reorg-monitor 11 | ports: 12 | - name: postgresql 13 | port: 5432 14 | targetPort: postgresql 15 | -------------------------------------------------------------------------------- /deploy/k8s/base/postgres-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: postgres 5 | spec: 6 | replicas: 0 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: postgres 10 | app.kubernetes.io/component: database 11 | app.kubernetes.io/part-of: reorg-monitor 12 | serviceName: postgres 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: postgres 17 | app.kubernetes.io/component: database 18 | app.kubernetes.io/part-of: reorg-monitor 19 | spec: 20 | containers: 21 | - name: postgres 22 | image: postgres:14.7 23 | envFrom: 24 | - secretRef: 25 | name: reorg-monitor-secrets 26 | ports: 27 | - name: postgresql 28 | containerPort: 5432 29 | volumeMounts: 30 | - name: postgres-pvc 31 | mountPath: /var/lib/postgresql/data 32 | volumes: 33 | - name: postgres-pvc 34 | persistentVolumeClaim: 35 | claimName: postgres-pvc 36 | volumeClaimTemplates: 37 | - metadata: 38 | name: postgres-pvc 39 | spec: 40 | accessModes: 41 | - ReadWriteOnce 42 | resources: 43 | requests: 44 | storage: 10Gi -------------------------------------------------------------------------------- /deploy/k8s/base/reorg-monitor-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: reorg-monitor 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: reorg-monitor 10 | app.kubernetes.io/component: monitor 11 | app.kubernetes.io/part-of: reorg-monitor 12 | template: 13 | metadata: 14 | labels: 15 | app.kubernetes.io/name: reorg-monitor 16 | app.kubernetes.io/component: monitor 17 | app.kubernetes.io/part-of: reorg-monitor 18 | spec: 19 | containers: 20 | - name: reorg-monitor 21 | image: flashbots/reorg-monitor:latest 22 | envFrom: 23 | - secretRef: 24 | name: reorg-monitor-secrets 25 | ports: 26 | - name: webserver 27 | containerPort: 9090 -------------------------------------------------------------------------------- /deploy/k8s/base/reorg-monitor-secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: reorg-monitor-secrets 5 | namespace: reorg-monitor 6 | -------------------------------------------------------------------------------- /deploy/k8s/base/reorg-monitor-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: reorg-monitor 5 | spec: 6 | type: LoadBalancer 7 | externalTrafficPolicy: Local 8 | selector: 9 | app.kubernetes.io/name: reorg-monitor 10 | app.kubernetes.io/component: monitor 11 | app.kubernetes.io/part-of: reorg-monitor 12 | ports: 13 | - name: webserver 14 | port: 9090 15 | targetPort: webserver -------------------------------------------------------------------------------- /deploy/k8s/dev/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../base 6 | 7 | replicas: 8 | - name: postgres 9 | count: 1 10 | 11 | patchesStrategicMerge: 12 | - reorg-monitor-service-patch.yaml -------------------------------------------------------------------------------- /deploy/k8s/dev/reorg-monitor-service-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: reorg-monitor 5 | spec: 6 | type: ClusterIP 7 | externalTrafficPolicy: null -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | volumes: 4 | psql_data: 5 | driver: local 6 | 7 | services: 8 | db: 9 | container_name: db 10 | image: postgres 11 | restart: always 12 | volumes: 13 | - 'psql_data:/var/lib/postgresql/data' 14 | ports: 15 | - "5432:5432" 16 | environment: 17 | POSTGRES_USER: user1 18 | POSTGRES_PASSWORD: password 19 | POSTGRES_DB: reorg 20 | 21 | adminer: 22 | container_name: adminer 23 | image: adminer 24 | restart: always 25 | depends_on: 26 | - db 27 | ports: 28 | - "8092:8080" 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flashbots/reorg-monitor 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.12.2 7 | github.com/jmoiron/sqlx v1.3.5 8 | github.com/lib/pq v1.10.9 9 | github.com/metachris/flashbotsrpc v0.6.0 10 | github.com/pkg/errors v0.9.1 11 | github.com/spf13/cobra v1.7.0 12 | github.com/spf13/viper v1.16.0 13 | 14 | ) 15 | 16 | require ( 17 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect 18 | github.com/deckarep/golang-set/v2 v2.3.0 // indirect 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 20 | github.com/fsnotify/fsnotify v1.6.0 // indirect 21 | github.com/go-ole/go-ole v1.2.6 // indirect 22 | github.com/go-stack/stack v1.8.1 // indirect 23 | github.com/gorilla/websocket v1.5.0 // indirect 24 | github.com/hashicorp/hcl v1.0.0 // indirect 25 | github.com/holiman/uint256 v1.2.3 // indirect 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 | github.com/magiconair/properties v1.8.7 // indirect 28 | github.com/mitchellh/mapstructure v1.5.0 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 30 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect 31 | github.com/spf13/afero v1.9.5 // indirect 32 | github.com/spf13/cast v1.5.1 // indirect 33 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 34 | github.com/spf13/pflag v1.0.5 // indirect 35 | github.com/subosito/gotenv v1.4.2 // indirect 36 | github.com/tklauser/go-sysconf v0.3.11 // indirect 37 | github.com/tklauser/numcpus v0.6.1 // indirect 38 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 39 | golang.org/x/crypto v0.10.0 // indirect 40 | golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect 41 | golang.org/x/sys v0.9.0 // indirect 42 | golang.org/x/text v0.10.0 // indirect 43 | gopkg.in/ini.v1 v1.67.0 // indirect 44 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | 48 | replace ( 49 | github.com/btcsuite/btcd => github.com/btcsuite/btcd v0.23.3 50 | github.com/btcsuite/btcd/chaincfg/chainhash => github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 51 | ) 52 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= 42 | github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= 43 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 44 | github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= 45 | github.com/btcsuite/btcd v0.23.3 h1:4KH/JKy9WiCd+iUS9Mu0Zp7Dnj17TGdKrg9xc/FGj24= 46 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= 47 | github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= 48 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9w277ABlgELO7H04IM= 49 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 50 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 51 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 52 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 53 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 54 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 55 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 56 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 57 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 58 | github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8= 59 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= 60 | github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoGMWEhDvS3zToKcDpRsLuRolQJBVGdozk= 61 | github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= 62 | github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= 63 | github.com/consensys/gnark-crypto v0.10.0 h1:zRh22SR7o4K35SoNqouS9J/TKHTyU2QWaj5ldehyXtA= 64 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 65 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 66 | github.com/crate-crypto/go-kzg-4844 v0.3.0 h1:UBlWE0CgyFqqzTI+IFyCzA7A3Zw4iip6uzRv5NIXG0A= 67 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 68 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 69 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 70 | github.com/deckarep/golang-set/v2 v2.3.0 h1:qs18EKUfHm2X9fA50Mr/M5hccg2tNnVqsiBImnyDs0g= 71 | github.com/deckarep/golang-set/v2 v2.3.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= 72 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 73 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 74 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 75 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 76 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 77 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 78 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 79 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 80 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 81 | github.com/ethereum/c-kzg-4844 v0.3.1 h1:sR65+68+WdnMKxseNWxSJuAv2tsUrihTpVBTfM/U5Zg= 82 | github.com/ethereum/go-ethereum v1.12.2 h1:eGHJ4ij7oyVqUQn48LBz3B7pvQ8sV0wGJiIE6gDq/6Y= 83 | github.com/ethereum/go-ethereum v1.12.2/go.mod h1:1cRAEV+rp/xX0zraSCBnu9Py3HQ+geRMj3HdR+k0wfI= 84 | github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= 85 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 86 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 87 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 88 | github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= 89 | github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= 90 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 91 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 92 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 93 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 94 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 95 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 96 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 97 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 98 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 99 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 100 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 101 | github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= 102 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 103 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 104 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 105 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 106 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 107 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 108 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 109 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 110 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 111 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 112 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 113 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 114 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 115 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 116 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 117 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 118 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 119 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 120 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 121 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 122 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 123 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 124 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 125 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 126 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 127 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 128 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 129 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 130 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 131 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 132 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 133 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 134 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 135 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 141 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 142 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 143 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 144 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 145 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 146 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 147 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 148 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 149 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 150 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 151 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 152 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 153 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 154 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 155 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 156 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 157 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 158 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 159 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 160 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 161 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 162 | github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= 163 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 164 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 165 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 166 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 167 | github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= 168 | github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= 169 | github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= 170 | github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= 171 | github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= 172 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 173 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 174 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 175 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 176 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 177 | github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= 178 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 179 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 180 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 181 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 182 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 183 | github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= 184 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 185 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 186 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 187 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 188 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 189 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 190 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 191 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 192 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 193 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 194 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 195 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 196 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 197 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 198 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 199 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= 200 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 201 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 202 | github.com/metachris/flashbotsrpc v0.6.0 h1:EnMdkd/jgct8kaDYpuMgEZpOew92+ok8Elr4qxbjmu8= 203 | github.com/metachris/flashbotsrpc v0.6.0/go.mod h1:UrS249kKA1PK27sf12M6tUxo/M4ayfFrBk7IMFY1TNw= 204 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 205 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 206 | github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= 207 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 208 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 209 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 210 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 211 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 212 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 213 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 214 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 215 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 216 | github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= 217 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 218 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 219 | github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= 220 | github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= 221 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 222 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 223 | github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= 224 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 225 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 226 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= 227 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 228 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= 229 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 230 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 231 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 232 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 233 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 234 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 235 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 236 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 237 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 238 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= 239 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= 240 | github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= 241 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 242 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 243 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 244 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 245 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 246 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 247 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 248 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 249 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 250 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 251 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 252 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 253 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 254 | github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= 255 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= 256 | github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU= 257 | github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= 258 | github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8= 259 | github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= 260 | github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= 261 | github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= 262 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 263 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 264 | github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= 265 | github.com/urfave/cli/v2 v2.24.1 h1:/QYYr7g0EhwXEML8jO+8OYt5trPnLHS0p3mrgExJ5NU= 266 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 267 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 268 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 269 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 270 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 271 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 272 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 273 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 274 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 275 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 276 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 277 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 278 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 279 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 280 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 281 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 282 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 283 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 284 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 285 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 286 | golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= 287 | golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= 288 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 289 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 290 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 291 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 292 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 293 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 294 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 295 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 296 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 297 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 298 | golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= 299 | golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 300 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 301 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 302 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 303 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 304 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 305 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 306 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 307 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 308 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 309 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 310 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 311 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 312 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 313 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 314 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 315 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 316 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 317 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 318 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 319 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 320 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 321 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 322 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 323 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 324 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 325 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 326 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 327 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 328 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 329 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 330 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 331 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 332 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 333 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 334 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 335 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 336 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 337 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 338 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 339 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 340 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 341 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 342 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 343 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 344 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 345 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 346 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 347 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 348 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 349 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 350 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 351 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 352 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 353 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 354 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 355 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 356 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 357 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 358 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 359 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 360 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 361 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 362 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 363 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 364 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 365 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 366 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 367 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 368 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 369 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 370 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 371 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 372 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 373 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 374 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 375 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 376 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 377 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 411 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 412 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 413 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 414 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 415 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 416 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 417 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 418 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 419 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 420 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 421 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 422 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 423 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 424 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 425 | golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= 426 | golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 427 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 428 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 429 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 430 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 431 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 432 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 433 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 434 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 435 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 436 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 437 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 438 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 439 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 440 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 441 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 442 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 443 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 444 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 445 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 446 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 447 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 448 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 449 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 450 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 451 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 452 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 453 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 454 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 455 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 456 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 457 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 458 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 459 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 460 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 461 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 462 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 463 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 464 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 465 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 466 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 467 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 468 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 469 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 470 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 471 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 472 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 473 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 474 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 475 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 476 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 477 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 478 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 479 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 480 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 481 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 482 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 483 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 484 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 485 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 486 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 487 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 488 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 489 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 490 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 491 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 492 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 493 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 494 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 495 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 496 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 497 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 498 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 499 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 500 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 501 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 502 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 503 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 504 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 505 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 506 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 507 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 508 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 509 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 510 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 511 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 512 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 513 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 514 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 515 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 516 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 517 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 518 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 519 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 520 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 521 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 522 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 523 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 524 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 525 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 526 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 527 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 528 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 529 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 530 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 531 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 532 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 533 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 534 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 535 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 536 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 537 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 538 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 539 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 540 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 541 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 542 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 543 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 544 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 545 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 546 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 547 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 548 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 549 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 550 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 551 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 552 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 553 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 554 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 555 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 556 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 557 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 558 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 559 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 560 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 561 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 562 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 563 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 564 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 565 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 566 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 567 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 568 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 569 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 570 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 571 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 572 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 573 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 574 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 575 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 576 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 577 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= 578 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= 579 | gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= 580 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 581 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 582 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 583 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 584 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 585 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 586 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 587 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 588 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 589 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 590 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 591 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 592 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 593 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 594 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 595 | -------------------------------------------------------------------------------- /monitor/config.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | // Config defines attributes used by reorg monitor server and used for reading in 4 | // environment variables and CLI flags. 5 | type Config struct { // NOTE: ensure struct tags match flag names for each attribute 6 | EthereumJsonRpcURIs []string `mapstructure:"ethereum-jsonrpc-uris"` 7 | PostgresDSN string `mapstructure:"postgres-dsn"` 8 | ListenAddress string `mapstructure:"listen-address"` 9 | SimulateBlocks bool `mapstructure:"simulate-blocks"` 10 | MevGethURI string `mapstructure:"mev-geth-uri"` 11 | MaxBlocks int `mapstructure:"max-blocks"` 12 | EnableDebug bool `mapstructure:"debug"` 13 | } 14 | -------------------------------------------------------------------------------- /monitor/gethconnection.go: -------------------------------------------------------------------------------- 1 | // Package monitor wraps an Ethereum connection and tries to reconnect on error 2 | package monitor 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "github.com/ethereum/go-ethereum/core/types" 11 | "github.com/ethereum/go-ethereum/ethclient" 12 | "github.com/flashbots/reorg-monitor/analysis" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type GethConnection struct { 17 | NodeUri string 18 | Client *ethclient.Client 19 | NewBlockChan chan<- *analysis.Block 20 | 21 | IsConnected bool 22 | IsSubscribed bool 23 | NextRetryTimeoutSec int64 // Wait time before retry. Starts at 5 seconds and doubles after each unsuccessful retry (max: 3 min). 24 | 25 | NumResubscribes int64 26 | NumReconnects int64 27 | NumBlocks uint64 28 | } 29 | 30 | func NewGethConnection(nodeUri string, newBlockChan chan<- *analysis.Block) (*GethConnection, error) { 31 | conn := GethConnection{ 32 | NodeUri: nodeUri, 33 | NewBlockChan: newBlockChan, 34 | NextRetryTimeoutSec: 5, 35 | } 36 | 37 | err := conn.Connect() 38 | return &conn, err 39 | } 40 | 41 | func (conn *GethConnection) Connect() (err error) { 42 | fmt.Printf("[%25s] Connecting to geth node... ", conn.NodeUri) 43 | conn.Client, err = ethclient.Dial(conn.NodeUri) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | syncProgress, err := conn.Client.SyncProgress(context.Background()) 49 | if err != nil { 50 | return errors.Wrap(err, "error at SyncProgress") 51 | } 52 | 53 | if syncProgress != nil { 54 | return fmt.Errorf("error: sync in progress") 55 | } 56 | 57 | fmt.Printf("ok\n") 58 | conn.IsConnected = true 59 | return nil 60 | } 61 | 62 | func (conn *GethConnection) Subscribe() error { 63 | if !conn.IsConnected { 64 | conn.ResubscribeAfterTimeout() 65 | return nil 66 | } 67 | 68 | headers := make(chan *types.Header) 69 | sub, err := conn.Client.SubscribeNewHead(context.Background(), headers) 70 | if err != nil { 71 | fmt.Printf("[conn %s] SubscribeNewHead error: %+v\n", conn.NodeUri, err) 72 | conn.IsSubscribed = false 73 | return err 74 | } 75 | 76 | conn.IsSubscribed = true 77 | conn.NextRetryTimeoutSec = 5 78 | 79 | for { 80 | select { 81 | case err := <-sub.Err(): 82 | fmt.Printf("[conn %s] Subscription error: %+v\n", conn.NodeUri, err) 83 | conn.IsSubscribed = false 84 | conn.ResubscribeAfterTimeout() 85 | return err 86 | case header := <-headers: 87 | observed := time.Now().UTC().UnixNano() 88 | conn.NumBlocks += 1 89 | 90 | // Fetch full block information from same client 91 | ethBlock, err := conn.Client.BlockByHash(context.Background(), header.Hash()) 92 | if err != nil { 93 | log.Printf("[conn %s] BlockByHash error: %+v (header %d %s)\n", conn.NodeUri, err, header.Number.Uint64(), header.Hash()) 94 | continue 95 | } 96 | 97 | // Add the block 98 | newBlock := analysis.NewBlock(ethBlock, analysis.OriginSubscription, conn.NodeUri, observed) 99 | conn.NewBlockChan <- newBlock 100 | } 101 | } 102 | } 103 | 104 | func (conn *GethConnection) ResubscribeAfterTimeout() { 105 | var err error 106 | 107 | conn.NumResubscribes += 1 108 | log.Printf("[conn %s] resubscribing in %d seconds...\n", conn.NodeUri, conn.NextRetryTimeoutSec) 109 | time.Sleep(time.Duration(conn.NextRetryTimeoutSec) * time.Second) 110 | log.Printf("[conn %s] resubscribing...\n", conn.NodeUri) 111 | 112 | // Now double time until next retry (max 3 min) 113 | conn.NextRetryTimeoutSec *= 2 114 | if conn.NextRetryTimeoutSec > 60*3 { 115 | conn.NextRetryTimeoutSec = 60 * 3 116 | } 117 | 118 | // step 1: get sync status 119 | if conn.Client == nil || !conn.IsConnected { 120 | conn.NumReconnects += 1 121 | conn.Client, err = ethclient.Dial(conn.NodeUri) 122 | if err != nil { 123 | log.Printf("[conn %s] err at ResubscribeAfterTimeout syncProgressCheck->reconnect: %v\n", conn.NodeUri, err) 124 | conn.IsConnected = false 125 | conn.ResubscribeAfterTimeout() 126 | return 127 | } 128 | conn.IsConnected = true 129 | } 130 | 131 | syncProgress, err := conn.Client.SyncProgress(context.Background()) 132 | if err != nil { 133 | log.Printf("[conn %s] err at ResubscribeAfterTimeout syncProgressCheck: %v\n", conn.NodeUri, err) 134 | 135 | // Reconnect 136 | conn.NumReconnects += 1 137 | conn.Client, err = ethclient.Dial(conn.NodeUri) 138 | if err != nil { 139 | log.Printf("[conn %s] err at ResubscribeAfterTimeout syncProgressCheck->reconnect: %v\n", conn.NodeUri, err) 140 | conn.IsConnected = false 141 | conn.ResubscribeAfterTimeout() 142 | return 143 | } 144 | 145 | conn.IsConnected = true 146 | syncProgress, err = conn.Client.SyncProgress(context.Background()) 147 | if err != nil { 148 | log.Printf("[conn %s] err at ResubscribeAfterTimeout syncProgressCheck: %v\n", conn.NodeUri, err) 149 | conn.ResubscribeAfterTimeout() 150 | return 151 | } 152 | } 153 | 154 | if syncProgress != nil { 155 | log.Printf("[conn %s] err at ResubscribeAfterTimeout syncProgressCheck: sync in progress\n", conn.NodeUri) 156 | conn.ResubscribeAfterTimeout() 157 | return 158 | } 159 | 160 | // step 2: subscribe 161 | err = conn.Subscribe() 162 | if err != nil { 163 | log.Printf("[conn %s] err at ResubscribeAfterTimeout Subscribe: %v\n", conn.NodeUri, err) 164 | conn.ResubscribeAfterTimeout() 165 | return 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /monitor/monitor.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/flashbots/reorg-monitor/analysis" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type ReorgMonitor struct { 15 | maxBlocksInCache int 16 | 17 | gethNodeUris []string 18 | connections map[string]*GethConnection 19 | verbose bool 20 | 21 | NewBlockChan chan *analysis.Block 22 | NewReorgChan chan<- *analysis.Reorg 23 | 24 | BlockByHash map[common.Hash]*analysis.Block 25 | BlocksByHeight map[uint64]map[common.Hash]*analysis.Block 26 | 27 | EarliestBlockNumber uint64 28 | LatestBlockNumber uint64 29 | 30 | KnownReorgs map[string]uint64 // key: reorgId, value: endBlockNumber 31 | } 32 | 33 | func NewReorgMonitor(gethNodeUris []string, reorgChan chan<- *analysis.Reorg, verbose bool, maxBlocks int) *ReorgMonitor { 34 | return &ReorgMonitor{ 35 | verbose: verbose, 36 | maxBlocksInCache: maxBlocks, 37 | 38 | gethNodeUris: gethNodeUris, 39 | connections: make(map[string]*GethConnection), 40 | 41 | NewBlockChan: make(chan *analysis.Block, 100), 42 | NewReorgChan: reorgChan, 43 | 44 | BlockByHash: make(map[common.Hash]*analysis.Block), 45 | BlocksByHeight: make(map[uint64]map[common.Hash]*analysis.Block), 46 | KnownReorgs: make(map[string]uint64), 47 | } 48 | } 49 | 50 | func (mon *ReorgMonitor) String() string { 51 | return fmt.Sprintf("ReorgMonitor: %d - %d, %d / %d blocks, %d reorgcache", mon.EarliestBlockNumber, mon.LatestBlockNumber, len(mon.BlockByHash), len(mon.BlocksByHeight), len(mon.KnownReorgs)) 52 | } 53 | 54 | func (mon *ReorgMonitor) ConnectClients() (connectedClients int) { 55 | for _, nodeUri := range mon.gethNodeUris { 56 | gethConn, err := NewGethConnection(nodeUri, mon.NewBlockChan) 57 | if err != nil { // in case of an error, just print it but still continue to add it 58 | fmt.Println(err) 59 | } else { 60 | connectedClients += 1 61 | } 62 | mon.connections[nodeUri] = gethConn 63 | } 64 | 65 | return connectedClients 66 | } 67 | 68 | // SubscribeAndListen is the main monitor loop: subscribes to new blocks from all geth connections, and waits for new blocks to process. 69 | // After adding a new block, a reorg check takes place. If a new completed reorg is detected, it is sent to the channel. 70 | func (mon *ReorgMonitor) SubscribeAndListen() { 71 | // Subscribe to new blocks from all clients 72 | for _, conn := range mon.connections { 73 | go conn.Subscribe() 74 | } 75 | 76 | // Wait for new blocks and process them (blocking) 77 | lastBlockHeight := uint64(0) 78 | for block := range mon.NewBlockChan { 79 | mon.AddBlock(block) 80 | 81 | // Do nothing if block is at previous height 82 | if block.Number == lastBlockHeight { 83 | continue 84 | } 85 | 86 | if len(mon.BlocksByHeight) < 3 { 87 | continue 88 | } 89 | 90 | // Analyze blocks once a new height has been reached 91 | lastBlockHeight = block.Number 92 | analysis, err := mon.AnalyzeTree(0, 2) 93 | if err != nil { 94 | log.Println("error in SubscribeAndListen->AnalyzeTree", err) 95 | continue 96 | } 97 | 98 | for _, reorg := range analysis.Reorgs { 99 | if !reorg.IsFinished { // don't care about unfinished reorgs 100 | continue 101 | } 102 | 103 | // Send new finished reorgs to channel 104 | if _, isKnownReorg := mon.KnownReorgs[reorg.Id()]; !isKnownReorg { 105 | mon.KnownReorgs[reorg.Id()] = reorg.EndBlockHeight 106 | mon.NewReorgChan <- reorg 107 | } 108 | } 109 | } 110 | } 111 | 112 | // AddBlock adds a block to history if it hasn't been seen before, and download unknown referenced blocks (parent, uncles). 113 | func (mon *ReorgMonitor) AddBlock(block *analysis.Block) bool { 114 | defer mon.TrimCache() 115 | 116 | // If known, then only overwrite if known was by uncle 117 | knownBlock, isKnown := mon.BlockByHash[block.Hash] 118 | if isKnown && knownBlock.Origin != analysis.OriginUncle { 119 | return false 120 | } 121 | 122 | // Only accept blocks that are after the earliest known (some nodes might be further back) 123 | if block.Number < mon.EarliestBlockNumber { 124 | return false 125 | } 126 | 127 | // Print 128 | blockInfo := fmt.Sprintf("[%25s] Add%s \t %-12s \t %s", block.NodeUri, block.String(), block.Origin, mon) 129 | log.Println(blockInfo) 130 | 131 | // Add for access by hash 132 | mon.BlockByHash[block.Hash] = block 133 | 134 | // Create array of blocks at this height, if necessary 135 | if _, found := mon.BlocksByHeight[block.Number]; !found { 136 | mon.BlocksByHeight[block.Number] = make(map[common.Hash]*analysis.Block) 137 | } 138 | 139 | // Add to map of blocks at this height 140 | mon.BlocksByHeight[block.Number][block.Hash] = block 141 | 142 | // Set earliest block 143 | if mon.EarliestBlockNumber == 0 || block.Number < mon.EarliestBlockNumber { 144 | mon.EarliestBlockNumber = block.Number 145 | } 146 | 147 | // Set latest block 148 | if block.Number > mon.LatestBlockNumber { 149 | mon.LatestBlockNumber = block.Number 150 | } 151 | 152 | // Check if further blocks can be downloaded from this one 153 | if block.Number > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block 154 | err := mon.CheckBlockForReferences(block) 155 | if err != nil { 156 | log.Println(err) 157 | } 158 | } 159 | 160 | return true 161 | } 162 | 163 | func (mon *ReorgMonitor) TrimCache() { 164 | // Trim reorg history 165 | for reorgId, reorgEndBlockheight := range mon.KnownReorgs { 166 | if reorgEndBlockheight < mon.EarliestBlockNumber { 167 | delete(mon.KnownReorgs, reorgId) 168 | } 169 | } 170 | 171 | for currentHeight := mon.EarliestBlockNumber; currentHeight < mon.LatestBlockNumber; currentHeight++ { 172 | blocks, heightExists := mon.BlocksByHeight[currentHeight] 173 | if !heightExists { 174 | continue 175 | } 176 | 177 | // Set new lowest block number 178 | mon.EarliestBlockNumber = currentHeight 179 | 180 | // Stop if trimmed enough 181 | if len(mon.BlockByHash) <= mon.maxBlocksInCache { 182 | return 183 | } 184 | 185 | // Trim 186 | for hash := range blocks { 187 | delete(mon.BlocksByHeight[currentHeight], hash) 188 | delete(mon.BlockByHash, hash) 189 | } 190 | delete(mon.BlocksByHeight, currentHeight) 191 | } 192 | } 193 | 194 | func (mon *ReorgMonitor) CheckBlockForReferences(block *analysis.Block) error { 195 | // Check parent 196 | _, found := mon.BlockByHash[block.ParentHash] 197 | if !found { 198 | // fmt.Printf("- parent of %d %s not found (%s), downloading...\n", block.Number, block.Hash, block.ParentHash) 199 | _, _, err := mon.EnsureBlock(block.ParentHash, analysis.OriginGetParent, block.NodeUri) 200 | if err != nil { 201 | return errors.Wrap(err, "get-parent error") 202 | } 203 | } 204 | 205 | // Check uncles 206 | for _, uncleHeader := range block.Block.Uncles() { 207 | // fmt.Printf("- block %d %s has uncle: %s\n", block.Number, block.Hash, uncleHeader.Hash()) 208 | _, _, err := mon.EnsureBlock(uncleHeader.Hash(), analysis.OriginUncle, block.NodeUri) 209 | if err != nil { 210 | return errors.Wrap(err, "get-uncle error") 211 | } 212 | } 213 | 214 | // ro.DebugPrintln(fmt.Sprintf("- added block %d %s", block.NumberU64(), block.Hash())) 215 | return nil 216 | } 217 | 218 | func (mon *ReorgMonitor) EnsureBlock(blockHash common.Hash, origin analysis.BlockOrigin, nodeUri string) (block *analysis.Block, alreadyExisted bool, err error) { 219 | // Check and potentially download block 220 | var found bool 221 | block, found = mon.BlockByHash[blockHash] 222 | if found { 223 | return block, true, nil 224 | } 225 | 226 | fmt.Printf("- block %s (%s) not found, downloading from %s...\n", blockHash, origin, nodeUri) 227 | conn := mon.connections[nodeUri] 228 | ethBlock, err := conn.Client.BlockByHash(context.Background(), blockHash) 229 | if err != nil { 230 | fmt.Println("- err block not found:", blockHash, err) // todo: try other clients 231 | msg := fmt.Sprintf("EnsureBlock error for hash %s", blockHash) 232 | return nil, false, errors.Wrap(err, msg) 233 | } 234 | 235 | block = analysis.NewBlock(ethBlock, origin, nodeUri, time.Now().UTC().UnixNano()) 236 | 237 | // Add a new block without sending to channel, because that makes reorg.AddBlock() asynchronous, but we want reorg.AddBlock() to wait until all references are added. 238 | mon.AddBlock(block) 239 | return block, false, nil 240 | } 241 | 242 | func (mon *ReorgMonitor) AnalyzeTree(maxBlocks, distanceToLastBlockHeight uint64) (*analysis.TreeAnalysis, error) { 243 | // Set end height of search 244 | endBlockNumber := mon.LatestBlockNumber - distanceToLastBlockHeight 245 | 246 | // Set start height of search 247 | startBlockNumber := mon.EarliestBlockNumber 248 | if maxBlocks > 0 && endBlockNumber-maxBlocks > mon.EarliestBlockNumber { 249 | startBlockNumber = endBlockNumber - maxBlocks 250 | } 251 | 252 | // Build tree datastructure 253 | tree := analysis.NewBlockTree() 254 | for height := startBlockNumber; height <= endBlockNumber; height++ { 255 | numBlocksAtHeight := len(mon.BlocksByHeight[height]) 256 | if numBlocksAtHeight == 0 { 257 | err := fmt.Errorf("error in monitor.AnalyzeTree: no blocks at height %d", height) 258 | return nil, err 259 | } 260 | 261 | // Start tree only when 1 block at this height. If more blocks then skip. 262 | if tree.FirstNode == nil && numBlocksAtHeight > 1 { 263 | continue 264 | } 265 | 266 | // Add all blocks at this height to the tree 267 | for _, currentBlock := range mon.BlocksByHeight[height] { 268 | err := tree.AddBlock(currentBlock) 269 | if err != nil { 270 | return nil, errors.Wrap(err, "monitor.AnalyzeTree->tree.AddBlock error") 271 | } 272 | } 273 | } 274 | 275 | // Get analysis of tree 276 | analysis, err := analysis.NewTreeAnalysis(tree) 277 | if err != nil { 278 | return nil, errors.Wrap(err, "monitor.AnalyzeTree->NewTreeAnalysis error") 279 | } 280 | 281 | return analysis, nil 282 | } 283 | -------------------------------------------------------------------------------- /monitor/webserver.go: -------------------------------------------------------------------------------- 1 | package monitor 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | _ "net/http/pprof" 7 | "time" 8 | ) 9 | 10 | type MonitorWebserver struct { 11 | Monitor *ReorgMonitor 12 | Addr string 13 | TimeStarted time.Time 14 | } 15 | 16 | // API response 17 | type StatusResponse struct { 18 | Monitor MonitorInfo 19 | Connections []ConnectionInfo 20 | } 21 | 22 | type MonitorInfo struct { 23 | Id string 24 | NumBlocks int 25 | EarliestBlockNumber uint64 26 | LatestBlockNumber uint64 27 | TimeStarted string 28 | } 29 | 30 | type ConnectionInfo struct { 31 | NodeUri string 32 | IsConnected bool 33 | IsSubscribed bool 34 | NumBlocks uint64 35 | NumReconnects int64 36 | NumResubscribes int64 37 | NextTimeout int64 38 | } 39 | 40 | func NewMonitorWebserver(monitor *ReorgMonitor, listenAddr string) *MonitorWebserver { 41 | return &MonitorWebserver{ 42 | Monitor: monitor, 43 | Addr: listenAddr, 44 | TimeStarted: time.Now().UTC(), 45 | } 46 | } 47 | 48 | func (ws *MonitorWebserver) HandleStatusRequest(w http.ResponseWriter, r *http.Request) { 49 | // fmt.Fprintf(w, "Monitor: %s\n", ws.Monitor.String()) 50 | res := StatusResponse{ 51 | Monitor: MonitorInfo{ 52 | Id: ws.Monitor.String(), 53 | NumBlocks: len(ws.Monitor.BlockByHash), 54 | EarliestBlockNumber: ws.Monitor.EarliestBlockNumber, 55 | LatestBlockNumber: ws.Monitor.LatestBlockNumber, 56 | TimeStarted: ws.TimeStarted.String(), 57 | }, 58 | Connections: make([]ConnectionInfo, 0), 59 | } 60 | 61 | for _, c := range ws.Monitor.connections { 62 | connInfo := ConnectionInfo{ 63 | NodeUri: c.NodeUri, 64 | IsConnected: c.IsConnected, 65 | IsSubscribed: c.IsSubscribed, 66 | NumBlocks: c.NumBlocks, 67 | NumReconnects: c.NumReconnects, 68 | NumResubscribes: c.NumResubscribes, 69 | NextTimeout: c.NextRetryTimeoutSec, 70 | } 71 | res.Connections = append(res.Connections, connInfo) 72 | } 73 | 74 | w.Header().Set("Content-Type", "application/json") 75 | json.NewEncoder(w).Encode(res) 76 | } 77 | 78 | func (ws *MonitorWebserver) ListenAndServe() error { 79 | http.HandleFunc("/", ws.HandleStatusRequest) 80 | return http.ListenAndServe(ws.Addr, nil) 81 | } 82 | -------------------------------------------------------------------------------- /reorgutils/reorgutils.go: -------------------------------------------------------------------------------- 1 | package reorgutils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math" 8 | "math/big" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/ethereum/go-ethereum/core/types" 14 | "github.com/ethereum/go-ethereum/ethclient" 15 | ) 16 | 17 | func Perror(err error) { 18 | if err != nil { 19 | panic(err) 20 | } 21 | } 22 | 23 | func FileExists(path string) bool { 24 | _, err := os.Stat(path) 25 | return !os.IsNotExist(err) 26 | } 27 | 28 | func BalanceToEth(balance *big.Int) *big.Float { 29 | fbalance := new(big.Float) 30 | fbalance.SetInt(balance) 31 | // fbalance.SetString(balance) 32 | ethValue := new(big.Float).Quo(fbalance, big.NewFloat(math.Pow10(18))) 33 | return ethValue 34 | } 35 | 36 | func BalanceToEthStr(balance *big.Int) string { 37 | if balance == nil { 38 | return "nil" 39 | } 40 | return BalanceToEth(balance).Text('f', 4) 41 | } 42 | 43 | func SprintBlock(block *types.Block) string { 44 | t := time.Unix(int64(block.Time()), 0).UTC() 45 | return fmt.Sprintf("Block %s %s \t %s \t tx: %3d, uncles: %d", block.Number(), block.Hash(), t, len(block.Transactions()), len(block.Uncles())) 46 | } 47 | 48 | func WeiToEth(wei *big.Int) (ethValue *big.Float) { 49 | // wei / 10^18 50 | fbalance := new(big.Float) 51 | fbalance.SetString(wei.String()) 52 | ethValue = new(big.Float).Quo(fbalance, big.NewFloat(1e18)) 53 | return 54 | } 55 | 56 | var ColorGreen = "\033[1;32m%s\033[0m" 57 | 58 | func ColorPrintf(color, format string, a ...interface{}) { 59 | str := fmt.Sprintf(format, a...) 60 | fmt.Printf(string(color), str) 61 | } 62 | 63 | func GetBlocks(blockChan chan<- *types.Block, client *ethclient.Client, startBlock, endBlock int64, concurrency int) { 64 | var blockWorkerWg sync.WaitGroup 65 | blockHeightChan := make(chan int64, 100) // blockHeight to fetch with receipts 66 | 67 | // Start eth client thread pool 68 | for w := 1; w <= concurrency; w++ { 69 | blockWorkerWg.Add(1) 70 | 71 | // Worker gets a block height from blockHeightChan, downloads it, and puts it in the blockChan 72 | go func() { 73 | defer blockWorkerWg.Done() 74 | for blockHeight := range blockHeightChan { 75 | // fmt.Println(blockHeight) 76 | block, err := client.BlockByNumber(context.Background(), big.NewInt(blockHeight)) 77 | if err != nil { 78 | log.Println("Error getting block:", blockHeight, err) 79 | continue 80 | } 81 | blockChan <- block 82 | } 83 | }() 84 | } 85 | 86 | // Push blocks into channel, for workers to pick up 87 | for currentBlockNumber := startBlock; currentBlockNumber <= endBlock; currentBlockNumber++ { 88 | blockHeightChan <- currentBlockNumber 89 | } 90 | 91 | // Close worker channel and wait for workers to finish 92 | close(blockHeightChan) 93 | blockWorkerWg.Wait() 94 | } 95 | -------------------------------------------------------------------------------- /testutils/testcases.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | type TestCase struct { 4 | Name string 5 | BlockInfo []string 6 | ExpectedResult ReorgTestResult 7 | } 8 | 9 | type ReorgTestResult struct { 10 | MustBeLive bool 11 | StartBlock uint64 12 | EndBlock uint64 13 | Depth int 14 | NumBlocks int 15 | NumReplacedBlocks int 16 | } 17 | 18 | var TestD1and2 = TestCase{ 19 | // https://mermaid-js.github.io/mermaid-live-editor/view/#eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgMHhkMzE0NTMyNjgyZWM1Mjc3MzMxYjg1N2E2OGJmMjIwM2I1MjBhNGZkMWM4NWJlNzViMDMyOTY1OGYxOTY2YjAyIC0tPiAweDdkZTk2OWNkNGI4M2MwYTdmN2E0MjMxYmY2MDY0N2MyNDBiYzE2YTAxZmRmZGNhYjE1ZWU4Zjg3MjA1MDFjOTRcbiAgICAweGQzMTQ1MzI2ODJlYzUyNzczMzFiODU3YTY4YmYyMjAzYjUyMGE0ZmQxYzg1YmU3NWIwMzI5NjU4ZjE5NjZiMDIgLS0-IDB4MGFhMmMzYWI4MmQ4ODI3ZWUzYTIyNTIxZjQyNDE5NDFmYzdhZTAwZjVkZjk5NGNlNmY4OGZhZGQyNDRkZjQwYlxuICAgIDB4N2RlOTY5Y2Q0YjgzYzBhN2Y3YTQyMzFiZjYwNjQ3YzI0MGJjMTZhMDFmZGZkY2FiMTVlZThmODcyMDUwMWM5NCAtLT4gMHhhOTdjY2MwM2U4NmUzODVkYzRiOWJjMDNhZWU5OTc2YWQ5ODliYWZmMTExN2JhYmZhNDdhNWFlZjg3ZTg2ODAzXG4gICAgMHgwYWEyYzNhYjgyZDg4MjdlZTNhMjI1MjFmNDI0MTk0MWZjN2FlMDBmNWRmOTk0Y2U2Zjg4ZmFkZDI0NGRmNDBiIC0tPiAweGY5MTAxMzA0NDQ1YWViYzFkMmNlNmFlYzczNWM0YWU1OTY5Y2Q1YjQ1YmUxNzYzNzZiNjgwMmE2MTAwYjc1YzNcbiAgICAweGU2NWEwMTYzZWNkODI5YWVkODkwZWZkMDg4NWIwMGM3Yzg4NjEwZGVmYzFkYjZiNTU4NTRlYjc3M2RkOTg1N2UgLS0-IDB4ZDMxNDUzMjY4MmVjNTI3NzMzMWI4NTdhNjhiZjIyMDNiNTIwYTRmZDFjODViZTc1YjAzMjk2NThmMTk2NmIwMlxuICAgIDB4ZTY1YTAxNjNlY2Q4MjlhZWQ4OTBlZmQwODg1YjAwYzdjODg2MTBkZWZjMWRiNmI1NTg1NGViNzczZGQ5ODU3ZSAtLT4gMHhhODVmZjhlYzc2YWRjYWE4MGE0ZjhjNzkzMzUyYWYwNmY0M2ZjNWZlZTY3ZjE5NjFkOTkyODgyMjg0ODA3YzUxXG4gICAgMHhmOTEwMTMwNDQ0NWFlYmMxZDJjZTZhZWM3MzVjNGFlNTk2OWNkNWI0NWJlMTc2Mzc2YjY4MDJhNjEwMGI3NWMzIC0tPiAweGYzMGYwMjRkZjc5NzE0N2ZmNjFiMDZhNzRmMGM1NjllNzg0MGExYjRkOTc1ODMxNmUzOGJiYmQ5MDhiMzdlYjgiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjp0cnVlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6dHJ1ZX0 20 | BlockInfo: []string{ 21 | "0xe65a0163ecd829aed890efd0885b00c7c88610defc1db6b55854eb773dd9857e", 22 | "0xd314532682ec5277331b857a68bf2203b520a4fd1c85be75b0329658f1966b02", 23 | "0xa85ff8ec76adcaa80a4f8c793352af06f43fc5fee67f1961d992882284807c51", 24 | "0xa97ccc03e86e385dc4b9bc03aee9976ad989baff1117babfa47a5aef87e86803", 25 | "0xf30f024df797147ff61b06a74f0c569e7840a1b4d9758316e38bbbd908b37eb8", 26 | }, 27 | } 28 | 29 | var Test3xD1 = TestCase{ 30 | // https://mermaid-js.github.io/mermaid-live-editor/view/#eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyXG4gICAgMHgwOGYxM2JjNzRkMDQwYzA5ZjAxNmY0ZDZjNzJlMThlZTc5NzJiYTQyMmViNTg5MmNjMDhkYjc3YmM0OGQ0MjRlIC0tPiAweDk2MTU1YmNiZjBjZjcyMWRkZWJhOWQxYzA0YWJmOGZhY2ZkODFjODQyN2EzZjI3Y2Q5MzU1Y2NkODRmYTg2MWVcbiAgICAweDA4ZjEzYmM3NGQwNDBjMDlmMDE2ZjRkNmM3MmUxOGVlNzk3MmJhNDIyZWI1ODkyY2MwOGRiNzdiYzQ4ZDQyNGUgLS0-IDB4YmNhYTNhY2JlOGQ3NTMwNWRlNTBlZDM5NGVmOTIzYzVlNDVmMDYyOGYyMzJmYWE4YjA5YmIzMjdjN2ZhZGI5ZVxuICAgIDB4OTYxNTViY2JmMGNmNzIxZGRlYmE5ZDFjMDRhYmY4ZmFjZmQ4MWM4NDI3YTNmMjdjZDkzNTVjY2Q4NGZhODYxZSAtLT4gMHhjMDc1MWIyNGRkZTU3MzU1ZDAyY2VhY2YzNmJjNWYwZTM3MzY3ZDkxOTA3ODUwYzQzYTc0NzNmNjcwMWZjZmUxXG4gICAgMHg5NjE1NWJjYmYwY2Y3MjFkZGViYTlkMWMwNGFiZjhmYWNmZDgxYzg0MjdhM2YyN2NkOTM1NWNjZDg0ZmE4NjFlIC0tPiAweGU0YmZlMmYyNWNkZjM4YzZmNDE0YWY2ZDgxMTFlYjUzNDJiYjk2YmY3NDhiM2RkMGJiYTU4OWJlYzhkNGJjZmVcbiAgICAweGMwNzUxYjI0ZGRlNTczNTVkMDJjZWFjZjM2YmM1ZjBlMzczNjdkOTE5MDc4NTBjNDNhNzQ3M2Y2NzAxZmNmZTEgLS0-IDB4NGViYjY4MDQwMTM1ZjNlNmZkNTg3ZmM1N2U3ZDUxZjEwNzBkNWU4YzgzMGZlODk3OTkxMWM2MTQ3NDEzMDJhZFxuICAgIDB4YzA3NTFiMjRkZGU1NzM1NWQwMmNlYWNmMzZiYzVmMGUzNzM2N2Q5MTkwNzg1MGM0M2E3NDczZjY3MDFmY2ZlMSAtLT4gMHg5NDMyMTk5ZWIxMjYwMTk0NTI4ZmM1MmY0ZGQ5MWUzNTUwOTMwODY2ZmU4NzA0Y2ViY2MyNzhlM2UzOTUzMjRmXG4gICAgMHg0ZWJiNjgwNDAxMzVmM2U2ZmQ1ODdmYzU3ZTdkNTFmMTA3MGQ1ZThjODMwZmU4OTc5OTExYzYxNDc0MTMwMmFkIC0tPiAweDExMzY3MGIyYWNlY2M0Y2FmNjhkOWEzNWFhZDEwNmM2NjA1NzliM2U5YmNmODRlZjEwNjJkYWZlMWVjZTA4YmEiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjp0cnVlLCJhdXRvU3luYyI6dHJ1ZSwidXBkYXRlRGlhZ3JhbSI6dHJ1ZX0 31 | BlockInfo: []string{ 32 | "0x08f13bc74d040c09f016f4d6c72e18ee7972ba422eb5892cc08db77bc48d424e", 33 | "0x113670b2acecc4caf68d9a35aad106c660579b3e9bcf84ef1062dafe1ece08ba", 34 | "0x9432199eb1260194528fc52f4dd91e3550930866fe8704cebcc278e3e395324f", 35 | "0xe4bfe2f25cdf38c6f414af6d8111eb5342bb96bf748b3dd0bba589bec8d4bcfe", 36 | "0xbcaa3acbe8d75305de50ed394ef923c5e45f0628f232faa8b09bb327c7fadb9e", 37 | }, 38 | } 39 | 40 | var TestD3 = TestCase{ 41 | // https://mermaid-js.github.io/mermaid-live-editor/view/#eyJjb2RlIjoic3RhdGVEaWFncmFtLXYyIDB4ZDY5MGE5YmIyNmQyN2Q2NjU3NjhmNjdiODgzOWVhNjU3MWExZTEwYzk1YWQ2OWUzOGRhYzE2ZDk2ODZmNWQ5ZiAtLT4gMHgzY2QyMzZlMTc4ZmM4M2ZlNzNiNTdjZGI3ODAyMTg3ZTFkNWIzYWNlYTJjMWQ0MDYxYzA5NmUyYjY4ODYzMzBkIDB4ZDY5MGE5YmIyNmQyN2Q2NjU3NjhmNjdiODgzOWVhNjU3MWExZTEwYzk1YWQ2OWUzOGRhYzE2ZDk2ODZmNWQ5ZiAtLT4gMHhmNTA0MjY0ZWUyZTllNjQ3MGY1ZTYwMjY2MzA0N2EzYTJmNDU0MWJjYzNmNDZjODVmZTBkZGFhMmZmNTZkZDIwIDB4ZjUwNDI2NGVlMmU5ZTY0NzBmNWU2MDI2NjMwNDdhM2EyZjQ1NDFiY2MzZjQ2Yzg1ZmUwZGRhYTJmZjU2ZGQyMCAtLT4gMHgyYTg0YmQ1MTIzOWVkMzZmMGU5YjczZmI1NDJkNzVlMzI2YTBhMGFjYTQ3ZDJjY2U4OGQ5NzI2OTZhZGVhYTIwIDB4M2NkMjM2ZTE3OGZjODNmZTczYjU3Y2RiNzgwMjE4N2UxZDViM2FjZWEyYzFkNDA2MWMwOTZlMmI2ODg2MzMwZCAtLT4gMHg5OTAxMWJiYzk1OWJmZTIzYjEzZTNiMzMwYmM0YTRkMjNmNmVjODEzOWUyMzQ2MGM1M2UzYTkxZjdiYTFmZWY1IDB4OTkwMTFiYmM5NTliZmUyM2IxM2UzYjMzMGJjNGE0ZDIzZjZlYzgxMzllMjM0NjBjNTNlM2E5MWY3YmExZmVmNSAtLT4gMHhjMjQzMjMzMTVmMzczNTAxNzAyNmY3NTRiM2Y2ZWUxYmNhM2NkYmI4ZTNmNmIyNDc2ZGQwOWFlYzRlNDBjYjM4IDB4MmE4NGJkNTEyMzllZDM2ZjBlOWI3M2ZiNTQyZDc1ZTMyNmEwYTBhY2E0N2QyY2NlODhkOTcyNjk2YWRlYWEyMCAtLT4gMHgwZjk3ZGY5NTU2NzJmODA3MjEyNGE3OTU5MDNkOGMyOTA0ODA2ZmJmM2JjZGRjMmU5ZjgyMjFkNDNjNjYyZDA2IDB4MGY5N2RmOTU1NjcyZjgwNzIxMjRhNzk1OTAzZDhjMjkwNDgwNmZiZjNiY2RkYzJlOWY4MjIxZDQzYzY2MmQwNiAtLT4gMHgxZWJhMWQ2MjcxMmI0MzA2N2NiOGY5MTNlNjgyMTkwZWFiZjJkZDAyMGE0ZTk1NGZkZDQ0NjBhNjA5OWYwOWE3XG5cbiIsIm1lcm1haWQiOiJ7XG4gIFwidGhlbWVcIjogXCJkZWZhdWx0XCJcbn0iLCJ1cGRhdGVFZGl0b3IiOnRydWUsImF1dG9TeW5jIjp0cnVlLCJ1cGRhdGVEaWFncmFtIjp0cnVlfQ 42 | BlockInfo: []string{ 43 | "0xd690a9bb26d27d665768f67b8839ea6571a1e10c95ad69e38dac16d9686f5d9f", 44 | "0xc24323315f3735017026f754b3f6ee1bca3cdbb8e3f6b2476dd09aec4e40cb38", 45 | "0x1eba1d62712b43067cb8f913e682190eabf2dd020a4e954fdd4460a6099f09a7", 46 | }, 47 | } 48 | 49 | var TestX = TestCase{ 50 | BlockInfo: []string{ 51 | "13123066", 52 | "13123067", 53 | "13123068", 54 | "13123069", 55 | "13123070", 56 | }, 57 | } 58 | 59 | var Test_Tmp = TestCase{ 60 | Name: "uncletest", 61 | BlockInfo: []string{"13090277", "13090279"}, 62 | ExpectedResult: ReorgTestResult{StartBlock: 13090277, EndBlock: 13090279, Depth: 1, NumBlocks: 3, NumReplacedBlocks: 2}, 63 | } 64 | 65 | var Test_12996760_12996760_d1_b2 = TestCase{ 66 | Name: "1 uncle", 67 | BlockInfo: []string{"12996760", "12996763"}, 68 | ExpectedResult: ReorgTestResult{StartBlock: 12996760, EndBlock: 12996760, Depth: 1, NumBlocks: 2, NumReplacedBlocks: 1}, 69 | } 70 | 71 | var Test_12996750_12996750_d1_b3_twouncles = TestCase{ 72 | Name: "2 uncles", 73 | BlockInfo: []string{"12996749", "12996751"}, 74 | ExpectedResult: ReorgTestResult{StartBlock: 12996750, EndBlock: 12996750, Depth: 1, NumBlocks: 3, NumReplacedBlocks: 2}, 75 | } 76 | 77 | var Test_12991732_12991733_d2_b4 = TestCase{ 78 | BlockInfo: []string{ 79 | "12991730", 80 | "0xc5d7c2d6da0a4dba574ca6b7697b5850477d646fdb067b20d908060b0d5651c7", // 12991733 81 | "0x61d0546aba46a166c185c584673e5afe911673e22ca75a754f165454b161e72a", // 12991734 82 | "12991736", 83 | }, 84 | ExpectedResult: ReorgTestResult{StartBlock: 12991732, EndBlock: 12991733, Depth: 2, NumBlocks: 4, NumReplacedBlocks: 2}, 85 | } 86 | 87 | var Test_12969887_12969889_d3_b6 = TestCase{ 88 | BlockInfo: []string{ 89 | "12969885", 90 | "0xae396e35c045b8603de015e182ce1349c579c68bb00396bfb8a7b5946a4fa87c", // 12969889 91 | "0xdca194ddb314c1c4e3de10ccfcb88bf9183a78118a393e1b3860e5eb10dd7c6c", // 12969889 92 | "12969891", 93 | }, 94 | ExpectedResult: ReorgTestResult{StartBlock: 12969887, EndBlock: 12969889, Depth: 3, NumBlocks: 6, NumReplacedBlocks: 3}, 95 | } 96 | 97 | var Test_13017535_13017536_d2_b5 = TestCase{ // 3 blocks at 13017535, 2 blocks at 13017536 98 | BlockInfo: []string{ 99 | "0xd633f8b768ae1e6975eb0fbd8f5d7ef7b06151a9106a23c17b0ee1b4f74a9bed", // 13017533 100 | "0xab672fe4e5ca25f44d8cf5c8be556a155d976ddc27a21e069172b3dda7335dad", // 13017534 101 | "0xd24bb816d9416fe504dea1d2480e560f31d59a50035cd967142cbb118782a015", // 13017535 102 | "0xfa5314344ed60908988e30524fbcdf4b1fef23a050339368c53c96c5461c956b", // 13017535 103 | "0x990e488c4eebcb83d17c739311b639c41c200c3906093b6d80ed10d2a75c503b", // 13017536 104 | "0xdc9a6e449e959ca888da7365d529ac9e05d98d9f7e88adc0e5016da13cef10b7", // 13017535 105 | "0x3e0e26323edfe6728a4ded45716c138b9e85df50342eea4059f2354ac2937d08", // 13017536 106 | "0xebf21cef1a406e30bb7b4d482591ca82e444f779efcb76ce67d09c2f548b4c82", // 13017537 107 | "0xb22ff4c5759adb7e14da8644d2dfdef98bb0e43f3d548cdcdbb0c7fa78675413", // 13017538 108 | }, 109 | ExpectedResult: ReorgTestResult{StartBlock: 13017535, EndBlock: 13017536, Depth: 2, NumBlocks: 5, NumReplacedBlocks: 3}, 110 | } 111 | 112 | var Test_13018369_13018370_d2_b4 = TestCase{ 113 | BlockInfo: []string{ 114 | "0xae416859b2ae32ac70dee15d3b164d81f27c5990312b72419bd0d15c856911bc", // 13018368 115 | "0x9282169b84cde985685d6157438ef5a4ff7fa83a895ff31a4893c9400e87b0c9", // 13018369 116 | "0xd9fb42a0296ebb85924366ada87d5acf8eae2069c408111c52c585b48bbb0ec0", // 13018369 117 | "0xf06bf47c3332361f93cc24c45954949755fa4474631bdfeaa176b48929a56663", // 13018370 118 | "0x94e290ab3ddaea782b826ea66094c429db1aeb632f80fe7fd003faad9e0a2001", // 13018371 119 | "0x15d97a6e60b229e143e20bd1a810c3568f13b59a7c9a1f1098928e17389c355d", // 13018370 120 | "0x6a5706073f58b14949fbb47c9107c480fbb34f9b96c72b34385ae3d06de489b6", // 13018372 121 | "0x6b501f2591c5f16398497cf71ea7fcc845029847a39f90ebc01f76408a3c665f", // 13018373 122 | "0xcfba4b54e919631d3b678ab82c9f22bc0cfc4e26341825088a447830d46a7ba1", // 13018374 123 | }, 124 | ExpectedResult: ReorgTestResult{StartBlock: 13018369, EndBlock: 13018370, Depth: 2, NumBlocks: 4, NumReplacedBlocks: 2}, 125 | } 126 | 127 | var Test_13033424_13033425_d2_b5 = TestCase{ 128 | BlockInfo: []string{ 129 | "13033421", 130 | "0x29af3e566af450ec7443b1857944a7c266aed19707f348e24657bc4abc32ec9f", // 13033423, parent of reorg 1 131 | "0x42087d1b5230fd48172c19d301691aa0866d62739b00804e21ad9668a30fa461", // 13033424, reorg 1, child 1 132 | "0x5996b0838dbd0d23458664633d1f7beef77be74a69abafdd588825b01ab1f15a", // 13033424, reorg 1, child 2 133 | 134 | "0x448b1495889c0a1759e83d08b5550b14c7992b6dd84b0eda84414bbd62675337", 135 | "0x5c4aaa79df3c48f53282340c1893a4547d4d42c4ca3af5376dba34646832925d", 136 | "0xd608ccc185b058eae7c12547712244c38ddd98e515e8ba1df7e0af29b468bd3c", 137 | "0xb63bb5b0ee3bffb39c6afec9e7569150c9a9b170aba84f5a34c960ffb27046e4", 138 | // "13033428", 139 | }, 140 | ExpectedResult: ReorgTestResult{StartBlock: 13033424, EndBlock: 13033425, Depth: 2, NumBlocks: 5, NumReplacedBlocks: 3}, 141 | } 142 | -------------------------------------------------------------------------------- /testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/big" 8 | "reflect" 9 | "strconv" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/ethereum/go-ethereum/core/types" 13 | "github.com/ethereum/go-ethereum/ethclient" 14 | "github.com/flashbots/reorg-monitor/analysis" 15 | "github.com/flashbots/reorg-monitor/monitor" 16 | "github.com/flashbots/reorg-monitor/reorgutils" 17 | ) 18 | 19 | var ( 20 | Client *ethclient.Client 21 | EthNodeUri string 22 | Monitor *monitor.ReorgMonitor 23 | ) 24 | 25 | func ConnectClient(uri string) (client *ethclient.Client, err error) { 26 | EthNodeUri = uri 27 | 28 | // Connect to geth node 29 | fmt.Printf("Connecting to %s...", uri) 30 | Client, err = ethclient.Dial(uri) 31 | if err != nil { 32 | return nil, err 33 | } 34 | fmt.Printf(" ok\n") 35 | return Client, nil 36 | } 37 | 38 | func ResetMon() { 39 | reorgChan := make(chan *analysis.Reorg) 40 | Monitor = monitor.NewReorgMonitor([]string{EthNodeUri}, reorgChan, true, 100) 41 | numConnectedClients := Monitor.ConnectClients() 42 | if numConnectedClients == 0 { 43 | log.Fatal("could not connect to any clients") 44 | } 45 | } 46 | 47 | func BlocksForStrings(blockStrings []string) (ret []*types.Block) { 48 | ret = make([]*types.Block, len(blockStrings)) 49 | for i, blockStr := range blockStrings { 50 | if len(blockStr) < 10 { 51 | blockNum, err := strconv.Atoi(blockStr) 52 | reorgutils.Perror(err) 53 | ret[i] = GetBlockByNumber(int64(blockNum)) 54 | } else { 55 | ret[i] = GetBlockByHashStr(blockStr) 56 | } 57 | } 58 | return ret 59 | } 60 | 61 | func ReorgCheckAndPrint() { 62 | fmt.Println("\n---\n ") 63 | analysis, err := Monitor.AnalyzeTree(100, 0) 64 | if err != nil { 65 | fmt.Println(err) 66 | return 67 | } 68 | 69 | analysis.Tree.Print() 70 | fmt.Println("") 71 | analysis.Print() 72 | } 73 | 74 | func GetBlockByHashStr(hashStr string) *types.Block { 75 | hash := common.HexToHash(hashStr) 76 | block, err := Client.BlockByHash(context.Background(), hash) 77 | if err != nil { 78 | log.Fatalf("GetBlockByHashStr couldn't find block %s: %v\n", hash, err) 79 | } 80 | reorgutils.Perror(err) 81 | return block 82 | } 83 | 84 | func GetBlockByNumber(number int64) *types.Block { 85 | block, err := Client.BlockByNumber(context.Background(), big.NewInt(number)) 86 | reorgutils.Perror(err) 87 | return block 88 | } 89 | 90 | func Assert(condition bool, errorMsg string) { 91 | if !condition { 92 | log.Fatal(errorMsg) 93 | } 94 | } 95 | 96 | func Check(f string, got, want interface{}) error { 97 | if !reflect.DeepEqual(got, want) { 98 | return fmt.Errorf("%s mismatch: got %v, want %v", f, got, want) 99 | } 100 | return nil 101 | } 102 | 103 | func Pcheck(f string, got, want interface{}) { 104 | err := Check(f, got, want) 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | } 109 | --------------------------------------------------------------------------------