diff --git a/.circleci/config.yml b/.circleci/config.yml index e0418e5..7ac83a4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,7 @@ workflows: # mapping: | op-conductor-mon/.* run-build-op-conductor-mon true + op-ufm/.* run-build-op-ufm true .circleci/.* run-all true .github/.* run-all true diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index e1b1c0f..adcc7af 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -10,6 +10,9 @@ parameters: run-build-op-conductor-mon: type: boolean default: false + run-build-op-ufm: + type: boolean + default: false run-all: type: boolean default: false @@ -224,7 +227,11 @@ jobs: - run: name: run lint command: | - golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint -e "errors.As" -e "errors.Is" --timeout "3m0s" ./... + if [ -f .golangci.yml ]; then + golangci-lint run -c .golangci.yml -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint -e "errors.As" -e "errors.Is" --timeout "3m0s" ./... + else + golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint -e "errors.As" -e "errors.Is" --timeout "3m0s" ./... + fi working_directory: <> go-test: @@ -357,3 +364,27 @@ workflows: - oplabs-gcr requires: - op-conductor-mon-docker-build + op-ufm: + when: + or: [<< pipeline.parameters.run-build-op-ufm >>, << pipeline.parameters.run-all >>] + jobs: + - go-lint: + name: op-conductor-mon-lint + module: op-ufm + - go-test: + name: op-conductor-mon-tests + module: op-ufm + - docker-build: + name: op-ufm-docker-build + docker_file: op-ufm/Dockerfile + docker_name: op-ufm + docker_tags: <>,<> + docker_context: . + - docker-publish: + name: op-ufm-docker-publish + docker_name: op-ufm + docker_tags: <>,<> + context: + - oplabs-gcr + requires: + - op-ufm-docker-build diff --git a/op-ufm/.gitignore b/op-ufm/.gitignore new file mode 100644 index 0000000..47ea0d9 --- /dev/null +++ b/op-ufm/.gitignore @@ -0,0 +1,4 @@ +bin +tls + +config.toml diff --git a/op-ufm/.golangci.yml b/op-ufm/.golangci.yml new file mode 100644 index 0000000..e19ed9b --- /dev/null +++ b/op-ufm/.golangci.yml @@ -0,0 +1,10 @@ +linters-settings: + staticcheck: + checks: ["all"] + +issues: + exclude-rules: + - path: "pkg/provider/roundtrip.go" + linters: + - staticcheck + text: "SA4006" diff --git a/op-ufm/Dockerfile b/op-ufm/Dockerfile new file mode 100644 index 0000000..0a2644e --- /dev/null +++ b/op-ufm/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.21.1-alpine3.18 as builder + +ARG GITCOMMIT=docker +ARG GITDATE=docker +ARG GITVERSION=docker + +RUN apk add make jq git gcc musl-dev linux-headers + +COPY ./op-ufm /app + +WORKDIR /app + +RUN make ufm + +FROM alpine:3.18 + +COPY --from=builder /app/entrypoint.sh /bin/entrypoint.sh +COPY --from=builder /app/bin/ufm /bin/ufm + +RUN apk update && \ + chmod +x /bin/entrypoint.sh + +RUN apk add ca-certificates jq curl bind-tools + +VOLUME /etc/ufm + +EXPOSE 8080 + +ENTRYPOINT ["/bin/entrypoint.sh"] +CMD ["/bin/ufm", "/etc/ufm/config.toml"] diff --git a/op-ufm/Makefile b/op-ufm/Makefile new file mode 100644 index 0000000..7dbd96f --- /dev/null +++ b/op-ufm/Makefile @@ -0,0 +1,36 @@ +LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT) +LDFLAGSSTRING +=-X main.GitDate=$(GITDATE) +LDFLAGSSTRING +=-X main.GitVersion=$(GITVERSION) +LDFLAGS := -ldflags "$(LDFLAGSSTRING)" + +ufm: + go build -v $(LDFLAGS) -o ./bin/ufm ./cmd/ufm +.PHONY: ufm + +fmt: + go mod tidy + gofmt -w . +.PHONY: fmt + +test: + go test -race -v ./... +.PHONY: test + +lint: + go vet ./... +.PHONY: lint + +tls: + kubectl get secrets op-ufm-client-tls -o yaml | yq '.data."tls.key"' | base64 --decode > tls/tls.key + kubectl get secrets op-ufm-client-tls -o yaml | yq '.data."tls.crt"' | base64 --decode > tls/tls.crt + kubectl get secrets op-ufm-client-tls -o yaml | yq '.data."ca.crt"' | base64 --decode > tls/ca.crt +.PHONY: tls + +mod-tidy: + # Below GOPRIVATE line allows mod-tidy to be run immediately after + # releasing new versions. This bypasses the Go modules proxy, which + # can take a while to index new versions. + # + # See https://proxy.golang.org/ for more info. + export GOPRIVATE="github.com/ethereum-optimism" && go mod tidy +.PHONY: mod-tidy diff --git a/op-ufm/README.md b/op-ufm/README.md new file mode 100644 index 0000000..235273a --- /dev/null +++ b/op-ufm/README.md @@ -0,0 +1,23 @@ +# OP User Facing Monitoring + +This project simulates a synthetic user interacting with a OP Stack chain. + +It is intended to be used as a tool for monitoring +the health of the network by measuring end-to-end transaction latency. + + +## Metrics + +* Round-trip duration time to get transaction receipt (from creation timestamp) + +* First-seen duration time (from creation timestamp) + + +## Usage + +Run `make ufm` to build the binary. No additional dependencies are necessary. + +Copy `example.config.toml` to `config.toml` and edit the file to configure the service. + +Start the service with `ufm config.toml`. + diff --git a/op-ufm/cmd/ufm/main.go b/op-ufm/cmd/ufm/main.go new file mode 100644 index 0000000..f4f753a --- /dev/null +++ b/op-ufm/cmd/ufm/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/signal" + "syscall" + + "golang.org/x/exp/slog" + + "github.com/ethereum/go-ethereum/log" + + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum-optimism/optimism/op-ufm/pkg/config" + "github.com/ethereum-optimism/optimism/op-ufm/pkg/service" +) + +var ( + GitVersion = "" + GitCommit = "" + GitDate = "" +) + +func main() { + oplog.SetGlobalLogHandler(slog.NewJSONHandler( + os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + + log.Info("initializing", + "version", GitVersion, + "commit", GitCommit, + "date", GitDate) + + if len(os.Args) < 2 { + log.Crit("must specify a config file on the command line") + } + cfg := initConfig(os.Args[1]) + + ctx := context.Background() + svc := service.New(cfg) + svc.Start(ctx) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + recvSig := <-sig + log.Info("caught signal, shutting down", + "signal", recvSig) + + svc.Shutdown() +} + +func initConfig(cfgFile string) *config.Config { + cfg, err := config.New(cfgFile) + if err != nil { + log.Crit("error reading config file", + "file", cfgFile, + "err", err) + } + + // update log level from config + logLevel, err := oplog.LevelFromString(cfg.LogLevel) + if err != nil { + logLevel = log.LevelInfo + if cfg.LogLevel != "" { + log.Warn("invalid server.log_level", + "log_level", cfg.LogLevel) + } + } + oplog.SetGlobalLogHandler(slog.NewJSONHandler( + os.Stdout, &slog.HandlerOptions{Level: logLevel})) + + // readable parsed config + jsonCfg, _ := json.MarshalIndent(cfg, "", " ") + fmt.Printf("%s", string(jsonCfg)) + + err = cfg.Validate() + if err != nil { + log.Crit("invalid config", + "err", err) + } + + return cfg +} diff --git a/op-ufm/entrypoint.sh b/op-ufm/entrypoint.sh new file mode 100644 index 0000000..ef83fa8 --- /dev/null +++ b/op-ufm/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +echo "Updating CA certificates." +update-ca-certificates +echo "Running CMD." +exec "$@" \ No newline at end of file diff --git a/op-ufm/example.config.toml b/op-ufm/example.config.toml new file mode 100644 index 0000000..13b62a3 --- /dev/null +++ b/op-ufm/example.config.toml @@ -0,0 +1,83 @@ +# Log level. +# Possible values: trace | debug | info | warn | error | crit +# Default: debug +log_level = "debug" + +[signer_service] +# URL to the signer service +url = "http://localhost:1234" +tls_ca_cert = "tls/ca.crt" +tls_cert = "tls/tls.crt" +tls_key = "tls/tls.key" + +[healthz] +# Whether or not to enable healthz endpoint +enabled = true +# Host for the healthz endpoint to listen on +host = "0.0.0.0" +# Port for the above. +port = "8080" + +[metrics] +# Whether or not to enable Prometheus metrics +enabled = true +# Host for the Prometheus metrics endpoint to listen on. +host = "0.0.0.0" +# Port for the above. +port = "9761" + +[wallets.default] +# OP Stack Chain ID +# see https://community.optimism.io/docs/useful-tools/networks/ +chain_id = 420 +# Signer method to use +# Possible values: signer | static +signer_method = "static" +# Address used to send transactions +address = "0x0000000000000000000000000000000000000000" +# For static signer method, the private key to use +private_key = "0000000000000000000000000000000000000000000000000000000000000000" +# Transaction value in wei +tx_value = 100000000000000 + +[providers.p1] +# URL to the RPC provider +url = "http://localhost:8551" +# Read only providers are only used to check for transactions +read_only = true +# Interval to poll the provider for expected transactions +read_interval = "10s" +# Interval to submit new transactions to the provider +send_interval = "30s" +# Interval between send transaction when we get "already known" txpool err +send_transaction_retry_interval = "100ms" +# Max time to retry +send_transaction_retry_timeout = "5s" +# Interval between each send transaction to the same network +send_transaction_cool_down = "30s" +# Interval between receipt retrieval +receipt_retrieval_interval = "500ms" +# Max time to check for receipt +receipt_retrieval_timeout = "2m" + +[providers.p2] +# Uncomment to disable this provider +# disabled=true +# URL to the RPC provider +url = "http://localhost:8552" +# Read only providers are only used to check for transactions +read_only = false +# Interval to poll the provider for expected transactions +read_interval = "10s" +# Interval to submit new transactions to the provider +send_interval = "30s" +# Interval between send transaction when we get "already known" txpool err +send_transaction_retry_interval = "100ms" +# Max time to retry +send_transaction_retry_timeout = "5s" +# Interval between each send transaction to the same network +send_transaction_cool_down = "30s" +# Interval between receipt retrieval +receipt_retrieval_interval = "500ms" +# Max time to check for receipt +receipt_retrieval_timeout = "2m" diff --git a/op-ufm/go.mod b/op-ufm/go.mod new file mode 100644 index 0000000..92d5e36 --- /dev/null +++ b/op-ufm/go.mod @@ -0,0 +1,101 @@ +module github.com/ethereum-optimism/optimism/op-ufm + +go 1.21 + +toolchain go1.21.6 + +require ( + cloud.google.com/go/kms v1.12.1 + github.com/BurntSushi/toml v1.3.2 + github.com/ethereum-optimism/optimism v1.7.7-0.20240529212843-af0999ccc31d + github.com/ethereum/go-ethereum v1.13.15 + github.com/gorilla/mux v1.8.0 + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.19.0 + github.com/rs/cors v1.9.0 + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 +) + +require ( + cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v1.1.0 // indirect + github.com/DataDog/zstd v1.5.2 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/VictoriaMetrics/fastcache v1.12.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect + github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240522134500-19555bdbdc95 // indirect + github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect + github.com/getsentry/sentry-go v0.18.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/s2a-go v0.1.4 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/holiman/uint256 v1.2.4 // indirect + github.com/klauspost/compress v1.17.6 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rivo/uniseg v0.4.3 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/urfave/cli/v2 v2.27.1 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/tools v0.17.0 // indirect + google.golang.org/api v0.132.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/grpc v1.56.3 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + rsc.io/tmplfunc v0.0.3 // indirect +) + +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101315.1-rc.5 diff --git a/op-ufm/go.sum b/op-ufm/go.sum new file mode 100644 index 0000000..1165628 --- /dev/null +++ b/op-ufm/go.sum @@ -0,0 +1,461 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk= +cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= +cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= +cloud.google.com/go/kms v1.12.1 h1:xZmZuwy2cwzsocmKDOPu4BL7umg8QXagQx6fKVmf45U= +cloud.google.com/go/kms v1.12.1/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= +github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= +github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= +github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= +github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd v0.24.0 h1:gL3uHE/IaFj6fcZSu03SvqPMSx7s/dPzfpG/atRwWdo= +github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= +github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= +github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4 h1:PuHFhOUMnD62r80dN+Ik5qco2drekgsUSVdcHsvllec= +github.com/cockroachdb/pebble v0.0.0-20231018212520-f6cde3fc2fa4/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= +github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-kzg-4844 v0.7.0 h1:C0vgZRk4q4EZ/JgPfzuSoxdCq3C3mOZMBShovmncxvA= +github.com/crate-crypto/go-kzg-4844 v0.7.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum-optimism/op-geth v1.101315.1-rc.5 h1:yaBvV/GfDuZOecDsGXyzsa/g8+AEBG+Bj1+NFyZRkdw= +github.com/ethereum-optimism/op-geth v1.101315.1-rc.5/go.mod h1:8tQ6r0e1NNJbSVHzYKafQqf62gV9BzZR+SKkXRckjLM= +github.com/ethereum-optimism/optimism v1.7.7-0.20240529212843-af0999ccc31d h1:bVre/IJ14z9JtVlEHw6q1hIhE6O8o4C8jt69TmRWrAU= +github.com/ethereum-optimism/optimism v1.7.7-0.20240529212843-af0999ccc31d/go.mod h1:BgTeAhCkyj+v7PQPoDyvgXnjqAiwr+qcdMIeggigcC4= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240522134500-19555bdbdc95 h1:GjXKQg6u6WkEIcY0dvW2IKhMRY8cVjwdw+rNKhduAo8= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240522134500-19555bdbdc95/go.mod h1:7xh2awFQqsiZxFrHKTgEd+InVfDRrkKVUIuK8SAFHp0= +github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= +github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/fjl/memsize v0.0.2 h1:27txuSD9or+NZlnOWdKUxeBzTAUkWCVh+4Gf2dWFOzA= +github.com/fjl/memsize v0.0.2/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= +github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= +github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/go-bexpr v0.1.11 h1:6DqdA/KBjurGby9yTY0bmkathya0lfwF2SeuubCI7dY= +github.com/hashicorp/go-bexpr v0.1.11/go.mod h1:f03lAo0duBlDIUMGCuad8oLcgejw4m7U+N8T+6Kz1AE= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= +github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= +github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw= +github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo= +github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= +github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.132.0 h1:8t2/+qZ26kAOGSmOiHwVycqVaDg7q3JDILrNi/Z6rvc= +google.golang.org/api v0.132.0/go.mod h1:AeTBC6GpJnJSRJjktDcPX0QwtS8pGYZOV6MSuSCusw0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= diff --git a/op-ufm/pkg/config/config.go b/op-ufm/pkg/config/config.go new file mode 100644 index 0000000..e0c80d6 --- /dev/null +++ b/op-ufm/pkg/config/config.go @@ -0,0 +1,171 @@ +package config + +import ( + "math/big" + + "github.com/BurntSushi/toml" + "github.com/pkg/errors" +) + +type Config struct { + LogLevel string `toml:"log_level"` + + Signer SignerServiceConfig `toml:"signer_service"` + Metrics MetricsConfig `toml:"metrics"` + Healthz HealthzConfig `toml:"healthz"` + + Wallets map[string]*WalletConfig `toml:"wallets"` + Providers map[string]*ProviderConfig `toml:"providers"` +} + +type SignerServiceConfig struct { + URL string `toml:"url"` + TLSCaCert string `toml:"tls_ca_cert"` + TLSCert string `toml:"tls_cert"` + TLSKey string `toml:"tls_key"` +} + +type MetricsConfig struct { + Enabled bool `toml:"enabled"` + Debug bool `toml:"debug"` + Host string `toml:"host"` + Port string `toml:"port"` +} + +type HealthzConfig struct { + Enabled bool `toml:"enabled"` + Host string `toml:"host"` + Port string `toml:"port"` +} + +type WalletConfig struct { + ChainID big.Int `toml:"chain_id"` + + // signer | static + SignerMethod string `toml:"signer_method"` + Address string `toml:"address"` + // private key is used for static signing + PrivateKey string `toml:"private_key"` + + // transaction parameters + TxValue big.Int `toml:"tx_value"` +} + +type ProviderConfig struct { + Network string `toml:"network"` + URL string `toml:"url"` + + ReadOnly bool `toml:"read_only"` + ReadInterval TOMLDuration `toml:"read_interval"` + + SendInterval TOMLDuration `toml:"send_interval"` + SendTransactionRetryInterval TOMLDuration `toml:"send_transaction_retry_interval"` + SendTransactionRetryTimeout TOMLDuration `toml:"send_transaction_retry_timeout"` + SendTransactionCoolDown TOMLDuration `toml:"send_transaction_cool_down"` + ReceiptRetrievalInterval TOMLDuration `toml:"receipt_retrieval_interval"` + ReceiptRetrievalTimeout TOMLDuration `toml:"receipt_retrieval_timeout"` + + Wallet string `toml:"wallet"` +} + +func New(file string) (*Config, error) { + cfg := &Config{} + if _, err := toml.DecodeFile(file, cfg); err != nil { + return nil, err + } + return cfg, nil +} + +func (c *Config) Validate() error { + if c.Metrics.Enabled { + if c.Metrics.Host == "" || c.Metrics.Port == "" { + return errors.New("metrics is enabled but host or port are missing") + } + } + if c.Healthz.Enabled { + if c.Healthz.Host == "" || c.Healthz.Port == "" { + return errors.New("healthz is enabled but host or port are missing") + } + } + + if len(c.Wallets) == 0 { + return errors.New("at least one wallet must be set") + } + + if len(c.Providers) == 0 { + return errors.New("at least one provider must be set") + } + + for name, wallet := range c.Wallets { + if wallet.ChainID.BitLen() == 0 { + return errors.Errorf("wallet [%s] chain_id is missing", name) + } + if wallet.SignerMethod != "signer" && wallet.SignerMethod != "static" { + return errors.Errorf("wallet [%s] signer_method is invalid", name) + } + if wallet.SignerMethod == "signer" { + if c.Signer.URL == "" { + return errors.New("signer url is missing") + } + if c.Signer.TLSCaCert == "" { + return errors.New("signer tls_ca_cert is missing") + } + if c.Signer.TLSCert == "" { + return errors.New("signer tls_cert is missing") + } + if c.Signer.TLSKey == "" { + return errors.New("signer tls_key is missing") + } + } + if wallet.SignerMethod == "static" { + if wallet.PrivateKey == "" { + return errors.Errorf("wallet [%s] private_key is missing", name) + } + } + if wallet.Address == "" { + return errors.Errorf("wallet [%s] address is missing", name) + } + if wallet.TxValue.BitLen() == 0 { + return errors.Errorf("wallet [%s] tx_value is missing", name) + } + } + + for name, provider := range c.Providers { + if provider.URL == "" { + return errors.Errorf("provider [%s] url is missing", name) + } + if provider.ReadInterval == 0 { + return errors.Errorf("provider [%s] read_interval is missing", name) + } + if provider.SendInterval == 0 { + return errors.Errorf("provider [%s] send_interval is missing", name) + } + if provider.SendTransactionRetryInterval == 0 { + return errors.Errorf("provider [%s] send_transaction_retry_interval is missing", name) + } + if provider.SendTransactionRetryTimeout == 0 { + return errors.Errorf("provider [%s] send_transaction_retry_timeout is missing", name) + } + if provider.SendTransactionCoolDown == 0 { + return errors.Errorf("provider [%s] send_transaction_cool_down is missing", name) + } + if provider.ReceiptRetrievalInterval == 0 { + return errors.Errorf("provider [%s] receipt_retrieval_interval is missing", name) + } + if provider.ReceiptRetrievalTimeout == 0 { + return errors.Errorf("provider [%s] receipt_retrieval_timeout is missing", name) + } + if provider.Wallet == "" { + return errors.Errorf("provider [%s] wallet is missing", name) + } + if _, ok := c.Wallets[provider.Wallet]; !ok { + return errors.Errorf("provider [%s] has an invalid wallet [%s]", name, provider.Wallet) + } + } + + if c.LogLevel == "" { + c.LogLevel = "debug" + } + + return nil +} diff --git a/op-ufm/pkg/config/toml_duration.go b/op-ufm/pkg/config/toml_duration.go new file mode 100644 index 0000000..64fe368 --- /dev/null +++ b/op-ufm/pkg/config/toml_duration.go @@ -0,0 +1,15 @@ +package config + +import "time" + +type TOMLDuration time.Duration + +func (t *TOMLDuration) UnmarshalText(b []byte) error { + d, err := time.ParseDuration(string(b)) + if err != nil { + return err + } + + *t = TOMLDuration(d) + return nil +} diff --git a/op-ufm/pkg/metrics/clients/eth.go b/op-ufm/pkg/metrics/clients/eth.go new file mode 100644 index 0000000..92d7c25 --- /dev/null +++ b/op-ufm/pkg/metrics/clients/eth.go @@ -0,0 +1,123 @@ +package clients + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" +) + +type InstrumentedEthClient struct { + c *ethclient.Client + providerName string +} + +func Dial(providerName string, url string) (*InstrumentedEthClient, error) { + start := time.Now() + c, err := ethclient.Dial(url) + if err != nil { + metrics.RecordErrorDetails(providerName, "ethclient.Dial", err) + return nil, err + } + metrics.RecordRPCLatency(providerName, "ethclient", "Dial", time.Since(start)) + return &InstrumentedEthClient{c: c, providerName: providerName}, nil +} + +func (i *InstrumentedEthClient) TransactionByHash(ctx context.Context, hash common.Hash) (*types.Transaction, bool, error) { + start := time.Now() + tx, isPending, err := i.c.TransactionByHash(ctx, hash) + if err != nil { + if !i.ignorableErrors(err) { + metrics.RecordErrorDetails(i.providerName, "ethclient.TransactionByHash", err) + } + return nil, false, err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "TransactionByHash", time.Since(start)) + return tx, isPending, err +} + +func (i *InstrumentedEthClient) PendingNonceAt(ctx context.Context, address string) (uint64, error) { + start := time.Now() + nonce, err := i.c.PendingNonceAt(ctx, common.HexToAddress(address)) + if err != nil { + metrics.RecordErrorDetails(i.providerName, "ethclient.PendingNonceAt", err) + return 0, err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "PendingNonceAt", time.Since(start)) + return nonce, err +} + +func (i *InstrumentedEthClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + start := time.Now() + receipt, err := i.c.TransactionReceipt(ctx, txHash) + if err != nil { + if !i.ignorableErrors(err) { + metrics.RecordErrorDetails(i.providerName, "ethclient.TransactionReceipt", err) + } + return nil, err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "TransactionReceipt", time.Since(start)) + return receipt, err +} + +func (i *InstrumentedEthClient) SendTransaction(ctx context.Context, tx *types.Transaction) error { + start := time.Now() + err := i.c.SendTransaction(ctx, tx) + if err != nil { + if !i.ignorableErrors(err) { + metrics.RecordErrorDetails(i.providerName, "ethclient.SendTransaction", err) + } + return err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "SendTransaction", time.Since(start)) + return err +} + +func (i *InstrumentedEthClient) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) { + start := time.Now() + gas, err := i.c.EstimateGas(ctx, msg) + if err != nil { + metrics.RecordErrorDetails(i.providerName, "ethclient.EstimateGas", err) + return 0, err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "EstimateGas", time.Since(start)) + return gas, err +} + +func (i *InstrumentedEthClient) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + start := time.Now() + gasTipCap, err := i.c.SuggestGasTipCap(ctx) + if err != nil { + metrics.RecordErrorDetails(i.providerName, "ethclient.SuggestGasTipCap", err) + return nil, err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "SuggestGasTipCap", time.Since(start)) + return gasTipCap, err +} + +func (i *InstrumentedEthClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + start := time.Now() + header, err := i.c.HeaderByNumber(ctx, number) + if err != nil { + metrics.RecordErrorDetails(i.providerName, "ethclient.HeaderByNumber", err) + return nil, err + } + metrics.RecordRPCLatency(i.providerName, "ethclient", "HeaderByNumber", time.Since(start)) + return header, err +} + +func (i *InstrumentedEthClient) ignorableErrors(err error) bool { + msg := err.Error() + // we dont use errors.Is because eth client actually uses errors.New, + // therefore creating an incomparable instance :( + return msg == ethereum.NotFound.Error() || + msg == txpool.ErrAlreadyKnown.Error() || + msg == core.ErrNonceTooLow.Error() +} diff --git a/op-ufm/pkg/metrics/clients/signer.go b/op-ufm/pkg/metrics/clients/signer.go new file mode 100644 index 0000000..6d0c413 --- /dev/null +++ b/op-ufm/pkg/metrics/clients/signer.go @@ -0,0 +1,42 @@ +package clients + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics" + "github.com/ethereum/go-ethereum/common" + + signer "github.com/ethereum-optimism/optimism/op-service/signer" + optls "github.com/ethereum-optimism/optimism/op-service/tls" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +type InstrumentedSignerClient struct { + c *signer.SignerClient + providerName string +} + +func NewSignerClient(providerName string, logger log.Logger, endpoint string, tlsConfig optls.CLIConfig) (*InstrumentedSignerClient, error) { + start := time.Now() + c, err := signer.NewSignerClient(logger, endpoint, tlsConfig) + if err != nil { + metrics.RecordErrorDetails(providerName, "signer.NewSignerClient", err) + return nil, err + } + metrics.RecordRPCLatency(providerName, "signer", "NewSignerClient", time.Since(start)) + return &InstrumentedSignerClient{c: c, providerName: providerName}, nil +} + +func (i *InstrumentedSignerClient) SignTransaction(ctx context.Context, chainId *big.Int, from *common.Address, tx *types.Transaction) (*types.Transaction, error) { + start := time.Now() + tx, err := i.c.SignTransaction(ctx, chainId, *from, tx) + if err != nil { + metrics.RecordErrorDetails(i.providerName, "signer.SignTransaction", err) + return nil, err + } + metrics.RecordRPCLatency(i.providerName, "signer", "SignTransaction", time.Since(start)) + return tx, err +} diff --git a/op-ufm/pkg/metrics/metrics.go b/op-ufm/pkg/metrics/metrics.go new file mode 100644 index 0000000..6c8a64a --- /dev/null +++ b/op-ufm/pkg/metrics/metrics.go @@ -0,0 +1,166 @@ +package metrics + +import ( + fmt "fmt" + "regexp" + "strings" + "time" + + "github.com/ethereum/go-ethereum/log" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +const ( + MetricsNamespace = "ufm" +) + +var ( + Debug bool + + errorsTotal = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "errors_total", + Help: "Count of errors", + }, []string{ + "provider", + "error", + }) + + rpcLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "rpc_latency", + Help: "RPC latency per provider, client and method (ms)", + }, []string{ + "provider", + "client", + "method", + }) + + roundTripLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "roundtrip_latency", + Help: "Round trip latency per provider (ms)", + }, []string{ + "provider", + }) + + gasUsed = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "gas_used", + Help: "Gas used per provider", + }, []string{ + "provider", + }) + + firstSeenLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "first_seen_latency", + Help: "First seen latency latency per provider (ms)", + }, []string{ + "provider_source", + "provider_seen", + }) + + providerToProviderLatency = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "provider_to_provider_latency", + Help: "Provider to provider latency (ms)", + }, []string{ + "provider_source", + "provider_seen", + }) + + networkTransactionsInFlight = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: MetricsNamespace, + Name: "transactions_inflight", + Help: "Transactions in flight, per network", + }, []string{ + "network", + }) +) + +var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z ]+`) + +func RecordError(provider string, errorLabel string) { + if Debug { + log.Debug("metric inc", + "m", "errors_total", + "provider", provider, + "error", errorLabel) + } + errorsTotal.WithLabelValues(provider, errorLabel).Inc() +} + +// RecordErrorDetails concats the error message to the label removing non-alpha chars +func RecordErrorDetails(provider string, label string, err error) { + errClean := nonAlphanumericRegex.ReplaceAllString(err.Error(), "") + errClean = strings.ReplaceAll(errClean, " ", "_") + errClean = strings.ReplaceAll(errClean, "__", "_") + label = fmt.Sprintf("%s.%s", label, errClean) + RecordError(provider, label) +} + +func RecordRPCLatency(provider string, client string, method string, latency time.Duration) { + if Debug { + log.Debug("metric set", + "m", "rpc_latency", + "provider", provider, + "client", client, + "method", method, + "latency", latency) + } + rpcLatency.WithLabelValues(provider, client, method).Set(float64(latency.Milliseconds())) +} + +func RecordRoundTripLatency(provider string, latency time.Duration) { + if Debug { + log.Debug("metric set", + "m", "roundtrip_latency", + "provider", provider, + "latency", latency) + } + roundTripLatency.WithLabelValues(provider).Set(float64(latency.Milliseconds())) +} + +func RecordGasUsed(provider string, val uint64) { + if Debug { + log.Debug("metric add", + "m", "gas_used", + "provider", provider, + "val", val) + } + gasUsed.WithLabelValues(provider).Set(float64(val)) +} + +func RecordFirstSeenLatency(providerSource string, providerSeen string, latency time.Duration) { + if Debug { + log.Debug("metric set", + "m", "first_seen_latency", + "provider_source", providerSource, + "provider_seen", providerSeen, + "latency", latency) + } + firstSeenLatency.WithLabelValues(providerSource, providerSeen).Set(float64(latency.Milliseconds())) +} + +func RecordProviderToProviderLatency(providerSource string, providerSeen string, latency time.Duration) { + if Debug { + log.Debug("metric set", + "m", "provider_to_provider_latency", + "provider_source", providerSource, + "provider_seen", providerSeen, + "latency", latency) + } + providerToProviderLatency.WithLabelValues(providerSource, providerSeen).Set(float64(latency.Milliseconds())) +} + +func RecordTransactionsInFlight(network string, count int) { + if Debug { + log.Debug("metric set", + "m", "transactions_inflight", + "network", network, + "count", count) + } + networkTransactionsInFlight.WithLabelValues(network).Set(float64(count)) +} diff --git a/op-ufm/pkg/provider/heartbeat.go b/op-ufm/pkg/provider/heartbeat.go new file mode 100644 index 0000000..ddc6ed2 --- /dev/null +++ b/op-ufm/pkg/provider/heartbeat.go @@ -0,0 +1,104 @@ +package provider + +import ( + "context" + "time" + + "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics" + "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics/clients" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/log" + "github.com/pkg/errors" +) + +// Heartbeat polls for expected in-flight transactions +func (p *Provider) Heartbeat(ctx context.Context) { + log.Debug("heartbeat", + "provider", p.name, + "count", len(p.txPool.Transactions)) + + metrics.RecordTransactionsInFlight(p.config.Network, len(p.txPool.Transactions)) + + // let's exclude transactions already seen by this provider, or originated by it + expectedTransactions := make([]*TransactionState, 0, len(p.txPool.Transactions)) + alreadySeen := 0 + for _, st := range p.txPool.Transactions { + if st.ProviderSource == p.name { + continue + } + if _, exist := st.SeenBy[p.name]; exist { + alreadySeen++ + continue + } + expectedTransactions = append(expectedTransactions, st) + } + + if len(expectedTransactions) == 0 { + log.Debug("no expected txs", + "count", len(p.txPool.Transactions), + "provider", p.name, + "alreadySeen", alreadySeen) + return + } + + client, err := clients.Dial(p.name, p.config.URL) + if err != nil { + log.Error("cant dial to provider", + "provider", p.name, + "url", p.config.URL, + "err", err) + } + + log.Debug("checking in-flight tx", + "count", len(p.txPool.Transactions), + "provider", p.name, + "alreadySeen", alreadySeen) + for _, st := range expectedTransactions { + hash := st.Hash.Hex() + + _, isPending, err := client.TransactionByHash(ctx, st.Hash) + if err != nil && !errors.Is(err, ethereum.NotFound) { + log.Error("cant check transaction", + "provider", p.name, + "hash", hash, + "url", p.config.URL, + "err", err) + continue + } + + log.Debug("got transaction", + "provider", p.name, + "hash", hash, + "isPending", isPending) + + // mark transaction as seen by this provider + st.M.Lock() + latency := time.Since(st.SentAt) + if st.FirstSeen.IsZero() { + st.FirstSeen = time.Now() + metrics.RecordFirstSeenLatency(st.ProviderSource, p.name, latency) + log.Info("transaction first seen", + "hash", hash, + "firstSeenLatency", latency, + "providerSource", st.ProviderSource, + "providerSeen", p.name) + } + if _, exist := st.SeenBy[p.name]; !exist { + st.SeenBy[p.name] = time.Now() + metrics.RecordProviderToProviderLatency(st.ProviderSource, p.name, latency) + } + st.M.Unlock() + + // check if transaction have been seen by all providers + p.txPool.M.Lock() + if len(st.SeenBy) == p.txPool.Expected { + log.Debug("transaction seen by all", + "hash", hash, + "expected", p.txPool.Expected, + "seenBy", len(st.SeenBy)) + delete(p.txPool.Transactions, st.Hash.Hex()) + } + p.txPool.M.Unlock() + } +} diff --git a/op-ufm/pkg/provider/provider.go b/op-ufm/pkg/provider/provider.go new file mode 100644 index 0000000..31626e3 --- /dev/null +++ b/op-ufm/pkg/provider/provider.go @@ -0,0 +1,72 @@ +package provider + +import ( + "context" + "time" + + "github.com/ethereum-optimism/optimism/op-ufm/pkg/config" +) + +type Provider struct { + name string + config *config.ProviderConfig + signerConfig *config.SignerServiceConfig + walletConfig *config.WalletConfig + txPool *NetworkTransactionPool + + cancelFunc context.CancelFunc +} + +func New(name string, cfg *config.ProviderConfig, + signerConfig *config.SignerServiceConfig, + walletConfig *config.WalletConfig, + txPool *NetworkTransactionPool) *Provider { + p := &Provider{ + name: name, + config: cfg, + signerConfig: signerConfig, + walletConfig: walletConfig, + txPool: txPool, + } + return p +} + +func (p *Provider) Start(ctx context.Context) { + providerCtx, cancelFunc := context.WithCancel(ctx) + p.cancelFunc = cancelFunc + + schedule(providerCtx, time.Duration(p.config.ReadInterval), p.Heartbeat) + if !p.config.ReadOnly { + schedule(providerCtx, time.Duration(p.config.SendInterval), p.RoundTrip) + } +} + +func (p *Provider) Shutdown() { + if p.cancelFunc != nil { + p.cancelFunc() + } +} + +func (p *Provider) Name() string { + return p.name +} + +func (p *Provider) URL() string { + return p.config.URL +} + +func schedule(ctx context.Context, interval time.Duration, handler func(ctx context.Context)) { + go func() { + for { + timer := time.NewTimer(interval) + handler(ctx) + + select { + case <-timer.C: + case <-ctx.Done(): + timer.Stop() + return + } + } + }() +} diff --git a/op-ufm/pkg/provider/roundtrip.go b/op-ufm/pkg/provider/roundtrip.go new file mode 100644 index 0000000..04b9af0 --- /dev/null +++ b/op-ufm/pkg/provider/roundtrip.go @@ -0,0 +1,312 @@ +package provider + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics" + iclients "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics/clients" + "github.com/ethereum/go-ethereum/core" + + "github.com/ethereum-optimism/optimism/op-service/tls" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// RoundTrip send a new transaction to measure round trip latency +func (p *Provider) RoundTrip(ctx context.Context) { + log.Debug("RoundTrip", + "provider", p.name) + + client, err := iclients.Dial(p.name, p.config.URL) + if err != nil { + log.Error("cant dial to provider", + "provider", p.name, + "url", p.config.URL, + "err", err) + return + } + + p.txPool.ExclusiveSend.Lock() + defer p.txPool.ExclusiveSend.Unlock() + + // lint:ignore SA4006 txHash is set and used within tx sending loop + txHash := common.Hash{} + attempt := 0 + nonce := uint64(0) + + // used for timeout + firstAttemptAt := time.Now() + // used for actual round trip time (disregard retry time) + var roundTripStartedAt time.Time + for { + + // sleep until we get a clear to send + for { + coolDown := time.Duration(p.config.SendTransactionCoolDown) - time.Since(p.txPool.LastSend) + if coolDown > 0 { + time.Sleep(coolDown) + } else { + break + } + } + + from, tx, err := p.createTx(ctx, client, nonce) + if err != nil { + log.Error("cant create tx", + "provider", p.name, + "nonce", nonce, + "err", err) + return + } + nonce = tx.Nonce() + + signedTx, err := p.sign(ctx, from, tx) + if err != nil { + log.Error("cant sign tx", + "provider", p.name, + "tx", tx, + "err", err) + return + } + txHash = signedTx.Hash() + + roundTripStartedAt = time.Now() + err = client.SendTransaction(ctx, signedTx) + if err != nil { + if err.Error() == txpool.ErrAlreadyKnown.Error() || + err.Error() == txpool.ErrReplaceUnderpriced.Error() || + err.Error() == core.ErrNonceTooLow.Error() { + + log.Warn("cant send transaction (retryable)", + "provider", p.name, + "err", err, + "nonce", nonce) + + if time.Since(firstAttemptAt) >= time.Duration(p.config.SendTransactionRetryTimeout) { + log.Error("send transaction timed out (known already)", + "provider", p.name, + "hash", txHash.Hex(), + "nonce", nonce, + "elapsed", time.Since(firstAttemptAt), + "attempt", attempt) + metrics.RecordErrorDetails(p.name, "send.timeout", err) + return + } + + log.Warn("tx already known, incrementing nonce and trying again", + "provider", p.name, + "nonce", nonce) + time.Sleep(time.Duration(p.config.SendTransactionRetryInterval)) + + nonce++ + attempt++ + if attempt%10 == 0 { + log.Debug("retrying send transaction...", + "provider", p.name, + "attempt", attempt, + "nonce", nonce, + "elapsed", time.Since(firstAttemptAt)) + } + } else { + log.Error("cant send transaction", + "provider", p.name, + "nonce", nonce, + "err", err) + metrics.RecordErrorDetails(p.name, "ethclient.SendTransaction", err) + return + } + } else { + break + } + } + + log.Info("transaction sent", + "provider", p.name, + "hash", txHash.Hex(), + "nonce", nonce) + + // add to pool + sentAt := time.Now() + p.txPool.M.Lock() + p.txPool.Transactions[txHash.Hex()] = &TransactionState{ + Hash: txHash, + ProviderSource: p.name, + SentAt: sentAt, + SeenBy: make(map[string]time.Time), + } + p.txPool.LastSend = sentAt + p.txPool.M.Unlock() + + var receipt *types.Receipt + attempt = 0 + for receipt == nil { + if time.Since(sentAt) >= time.Duration(p.config.ReceiptRetrievalTimeout) { + log.Error("receipt retrieval timed out", + "provider", p.name, + "hash", txHash, + "nonce", nonce, + "elapsed", time.Since(sentAt)) + metrics.RecordErrorDetails(p.name, "receipt.timeout", err) + return + } + time.Sleep(time.Duration(p.config.ReceiptRetrievalInterval)) + if attempt%10 == 0 { + log.Debug("checking for receipt...", + "provider", p.name, + "hash", txHash, + "nonce", nonce, + "attempt", attempt, + "elapsed", time.Since(sentAt)) + } + receipt, err = client.TransactionReceipt(ctx, txHash) + if err != nil && !errors.Is(err, ethereum.NotFound) { + log.Error("cant get receipt for transaction", + "provider", p.name, + "hash", txHash.Hex(), + "nonce", nonce, + "err", err) + return + } + attempt++ + } + + roundTripLatency := time.Since(roundTripStartedAt) + + metrics.RecordRoundTripLatency(p.name, roundTripLatency) + metrics.RecordGasUsed(p.name, receipt.GasUsed) + + log.Info("got transaction receipt", + "hash", txHash.Hex(), + "nonce", nonce, + "roundTripLatency", roundTripLatency, + "provider", p.name, + "blockNumber", receipt.BlockNumber, + "blockHash", receipt.BlockHash, + "gasUsed", receipt.GasUsed) +} + +func (p *Provider) createTx(ctx context.Context, client *iclients.InstrumentedEthClient, nonce uint64) (*common.Address, *types.Transaction, error) { + var err error + if nonce == 0 { + nonce, err = client.PendingNonceAt(ctx, p.walletConfig.Address) + if err != nil { + log.Error("cant get nonce", + "provider", p.name, + "nonce", nonce, + "err", err) + return nil, nil, err + } + } + + gasTipCap, err := client.SuggestGasTipCap(ctx) + if err != nil { + log.Error("cant get gas tip cap", + "provider", p.name, + "err", err) + return nil, nil, err + } + + // adjust gas tip cap by 110% + const GasTipCapAdjustmentMultiplier = 110 + const GasTipCapAdjustmentDivisor = 100 + gasTipCap = new(big.Int).Mul(gasTipCap, big.NewInt(GasTipCapAdjustmentMultiplier)) + gasTipCap = new(big.Int).Div(gasTipCap, big.NewInt(GasTipCapAdjustmentDivisor)) + + head, err := client.HeaderByNumber(ctx, nil) + if err != nil { + log.Error("cant get base fee from head", + "provider", p.name, + "err", err) + return nil, nil, err + } + baseFee := head.BaseFee + + gasFeeCap := new(big.Int).Add( + gasTipCap, + new(big.Int).Mul(baseFee, big.NewInt(2))) + + addr := common.HexToAddress(p.walletConfig.Address) + var data []byte + dynamicTx := &types.DynamicFeeTx{ + ChainID: &p.walletConfig.ChainID, + Nonce: nonce, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + To: &addr, + Value: &p.walletConfig.TxValue, + Data: data, + } + + gas, err := client.EstimateGas(ctx, ethereum.CallMsg{ + From: addr, + To: &addr, + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Data: dynamicTx.Data, + Value: dynamicTx.Value, + }) + if err != nil { + log.Error("cant estimate gas", + "provider", p.name, + "err", err) + return nil, nil, err + } + dynamicTx.Gas = gas + tx := types.NewTx(dynamicTx) + + log.Info("tx created", + "provider", p.name, + "from", addr, + "to", dynamicTx.To, + "nonce", dynamicTx.Nonce, + "value", dynamicTx.Value, + "gas", dynamicTx.Gas, + "gasTipCap", dynamicTx.GasTipCap, + "gasFeeCap", dynamicTx.GasFeeCap, + ) + + return &addr, tx, nil +} + +func (p *Provider) sign(ctx context.Context, from *common.Address, tx *types.Transaction) (*types.Transaction, error) { + if p.walletConfig.SignerMethod == "static" { + log.Debug("using static signer") + privateKey, err := crypto.HexToECDSA(p.walletConfig.PrivateKey) + if err != nil { + log.Error("failed to parse private key", "err", err) + return nil, err + } + return types.SignTx(tx, types.LatestSignerForChainID(&p.walletConfig.ChainID), privateKey) + } else if p.walletConfig.SignerMethod == "signer" { + tlsConfig := tls.CLIConfig{ + TLSCaCert: p.signerConfig.TLSCaCert, + TLSCert: p.signerConfig.TLSCert, + TLSKey: p.signerConfig.TLSKey, + } + client, err := iclients.NewSignerClient(p.name, log.Root(), p.signerConfig.URL, tlsConfig) + if err != nil || client == nil { + log.Error("failed to create signer client", "err", err) + } + + if client == nil { + return nil, errors.New("could not initialize signer client") + } + + signedTx, err := client.SignTransaction(ctx, &p.walletConfig.ChainID, from, tx) + if err != nil { + return nil, err + } + + return signedTx, nil + } else { + return nil, errors.New("invalid signer method") + } +} diff --git a/op-ufm/pkg/provider/tx_pool.go b/op-ufm/pkg/provider/tx_pool.go new file mode 100644 index 0000000..2ea5f6a --- /dev/null +++ b/op-ufm/pkg/provider/tx_pool.go @@ -0,0 +1,40 @@ +package provider + +import ( + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +// TransactionPool is used locally to share transactions between providers under the same pool +type TransactionPool map[string]*NetworkTransactionPool + +// NetworkTransactionPool is used locally to share transactions between providers under the same network +type NetworkTransactionPool struct { + M sync.Mutex + Transactions map[string]*TransactionState + Expected int + + // Last time a transaction was sent + LastSend time.Time + // Prevents concurrent transaction send + ExclusiveSend sync.Mutex +} + +type TransactionState struct { + // Transaction hash + Hash common.Hash + + // Mutex + M sync.Mutex + + SentAt time.Time + ProviderSource string + + FirstSeen time.Time + + // Map of providers that have seen this transaction, and when + // Once all providers have seen the transaction it is removed from the pool + SeenBy map[string]time.Time +} diff --git a/op-ufm/pkg/service/healthz_server.go b/op-ufm/pkg/service/healthz_server.go new file mode 100644 index 0000000..2fceea6 --- /dev/null +++ b/op-ufm/pkg/service/healthz_server.go @@ -0,0 +1,42 @@ +package service + +import ( + "context" + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/cors" + + "github.com/ethereum/go-ethereum/log" +) + +type HealthzServer struct { + ctx context.Context + server *http.Server +} + +func (h *HealthzServer) Start(ctx context.Context, addr string) error { + hdlr := mux.NewRouter() + hdlr.HandleFunc("/healthz", h.Handle).Methods("GET") + c := cors.New(cors.Options{ + AllowedOrigins: []string{"*"}, + }) + server := &http.Server{ + Handler: c.Handler(hdlr), + Addr: addr, + } + h.server = server + h.ctx = ctx + return h.server.ListenAndServe() +} + +func (h *HealthzServer) Shutdown() error { + return h.server.Shutdown(h.ctx) +} + +func (h *HealthzServer) Handle(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("OK")) + if err != nil { + log.Error("error handling HealthzServer response") + } +} diff --git a/op-ufm/pkg/service/metrics_server.go b/op-ufm/pkg/service/metrics_server.go new file mode 100644 index 0000000..4371d48 --- /dev/null +++ b/op-ufm/pkg/service/metrics_server.go @@ -0,0 +1,27 @@ +package service + +import ( + "context" + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type MetricsServer struct { + ctx context.Context + server *http.Server +} + +func (m *MetricsServer) Start(ctx context.Context, addr string) error { + server := &http.Server{ + Handler: promhttp.Handler(), + Addr: addr, + } + m.server = server + m.ctx = ctx + return m.server.ListenAndServe() +} + +func (m *MetricsServer) Shutdown() error { + return m.server.Shutdown(m.ctx) +} diff --git a/op-ufm/pkg/service/service.go b/op-ufm/pkg/service/service.go new file mode 100644 index 0000000..c131209 --- /dev/null +++ b/op-ufm/pkg/service/service.go @@ -0,0 +1,111 @@ +package service + +import ( + "context" + "net" + + "github.com/ethereum-optimism/optimism/op-ufm/pkg/config" + "github.com/ethereum-optimism/optimism/op-ufm/pkg/metrics" + "github.com/ethereum-optimism/optimism/op-ufm/pkg/provider" + + "github.com/ethereum/go-ethereum/log" +) + +type Service struct { + Config *config.Config + Healthz *HealthzServer + Metrics *MetricsServer + Providers map[string]*provider.Provider +} + +func New(cfg *config.Config) *Service { + s := &Service{ + Config: cfg, + Healthz: &HealthzServer{}, + Metrics: &MetricsServer{}, + Providers: make(map[string]*provider.Provider, len(cfg.Providers)), + } + return s +} + +func (s *Service) Start(ctx context.Context) { + log.Info("service starting") + if s.Config.Healthz.Enabled { + addr := net.JoinHostPort(s.Config.Healthz.Host, s.Config.Healthz.Port) + log.Info("starting healthz server", + "addr", addr) + go func() { + if err := s.Healthz.Start(ctx, addr); err != nil { + log.Error("error starting healthz server", + "err", err) + } + }() + } + + metrics.Debug = s.Config.Metrics.Debug + if s.Config.Metrics.Enabled { + addr := net.JoinHostPort(s.Config.Metrics.Host, s.Config.Metrics.Port) + log.Info("starting metrics server", + "addr", addr) + go func() { + if err := s.Metrics.Start(ctx, addr); err != nil { + log.Error("error starting metrics server", + "err", err) + } + }() + } + + // map networks to its providers + networks := make(map[string][]string) + for name, providerConfig := range s.Config.Providers { + networks[providerConfig.Network] = append(networks[providerConfig.Network], name) + } + + txpool := &provider.TransactionPool{} + for name, providers := range networks { + if len(providers) == 1 { + log.Warn("can't measure first seen for network, please another provider", + "network", name) + } + (*txpool)[name] = &provider.NetworkTransactionPool{} + (*txpool)[name].Transactions = make(map[string]*provider.TransactionState) + // set expected number of providers for this network + // -1 since we don't wait for acking from the same provider + (*txpool)[name].Expected = len(providers) - 1 + } + + for name, providerConfig := range s.Config.Providers { + s.Providers[name] = provider.New(name, + providerConfig, + &s.Config.Signer, + s.Config.Wallets[providerConfig.Wallet], + (*txpool)[providerConfig.Network]) + s.Providers[name].Start(ctx) + log.Info("provider started", + "provider", name) + } + + log.Info("service started") +} + +func (s *Service) Shutdown() { + log.Info("service shutting down") + if s.Config.Healthz.Enabled { + if err := s.Healthz.Shutdown(); err != nil { + log.Error("Error shutting down healthz server", err) + } + log.Info("healthz stopped") + } + if s.Config.Metrics.Enabled { + if err := s.Metrics.Shutdown(); err != nil { + log.Error("Error shutting down metrics server", err) + } + log.Info("metrics stopped") + } + for name, provider := range s.Providers { + provider.Shutdown() + log.Info("provider stopped", + "provider", name) + } + log.Info("service stopped") +} diff --git a/op-ufm/tools/kmstool/main.go b/op-ufm/tools/kmstool/main.go new file mode 100644 index 0000000..b5ff40e --- /dev/null +++ b/op-ufm/tools/kmstool/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "os" + + kms "cloud.google.com/go/kms/apiv1" + "cloud.google.com/go/kms/apiv1/kmspb" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func main() { + println("kmstool - usage: kmstool ") + + if len(os.Args) < 2 { + panic("missing ") + } + + keyName := os.Args[1] + + ctx := context.Background() + client, err := kms.NewKeyManagementClient(ctx) + if err != nil { + panic(fmt.Errorf("failed to create kms client: %w", err)) + } + defer client.Close() + + addr, err := resolveAddr(ctx, client, keyName) + if err != nil { + panic(fmt.Errorf("failed to retrieve the key: %w", err)) + } + fmt.Printf("ethereum addr: %s", addr) + println() + println() +} + +func resolveAddr(ctx context.Context, client *kms.KeyManagementClient, keyName string) (common.Address, error) { + resp, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: keyName}) + if err != nil { + return common.Address{}, fmt.Errorf("google kms public key %q lookup: %w", keyName, err) + } + keyPem := resp.Pem + + block, _ := pem.Decode([]byte(keyPem)) + if block == nil { + return common.Address{}, fmt.Errorf("google kms public key %q pem empty: %.130q", keyName, keyPem) + } + + var info struct { + AlgID pkix.AlgorithmIdentifier + Key asn1.BitString + } + _, err = asn1.Unmarshal(block.Bytes, &info) + if err != nil { + return common.Address{}, fmt.Errorf("google kms public key %q pem block %q: %w", keyName, block.Type, err) + } + + return pubKeyAddr(info.Key.Bytes), nil +} + +// PubKeyAddr returns the Ethereum address for the (uncompressed) key bytes. +func pubKeyAddr(bytes []byte) common.Address { + digest := crypto.Keccak256(bytes[1:]) + var addr common.Address + copy(addr[:], digest[12:]) + return addr +}