From e3dc26a3530212f0a022e7222491f565a15bb4ce Mon Sep 17 00:00:00 2001 From: Quang Le Date: Fri, 10 Jul 2026 18:17:55 +0700 Subject: [PATCH 1/3] test(app): add smoke test --- test-infra/compose/README.md | 119 + test-infra/compose/alert.go | 121 + test-infra/compose/auto.go | 303 ++ test-infra/compose/compose/main.go | 217 ++ test-infra/compose/compose_internal_test.go | 135 + test-infra/compose/config.go | 265 ++ test-infra/compose/define.go | 426 +++ test-infra/compose/docker-compose.template | 129 + test-infra/compose/go.mod | 184 + test-infra/compose/go.sum | 1022 ++++++ test-infra/compose/lock.go | 184 + test-infra/compose/new.go | 27 + test-infra/compose/new_test.go | 29 + test-infra/compose/run.go | 139 + test-infra/compose/smoke/smoke_test.go | 226 ++ .../compose/static/grafana/dash_alerts.json | 812 +++++ .../static/grafana/dash_charon_overview.json | 3104 +++++++++++++++++ .../static/grafana/dash_duty_details.json | 2275 ++++++++++++ .../compose/static/grafana/dashboards.yml | 8 + .../compose/static/grafana/datasource.yml | 43 + test-infra/compose/static/grafana/grafana.ini | 9 + .../compose/static/grafana/notifiers.yml | 14 + .../compose/static/lighthouse/Dockerfile | 16 + test-infra/compose/static/lighthouse/run.sh | 27 + test-infra/compose/static/lodestar/Dockerfile | 11 + test-infra/compose/static/lodestar/run.sh | 31 + test-infra/compose/static/loki/loki.yml | 47 + .../compose/static/prometheus/prometheus.yml | 25 + .../compose/static/prometheus/rules.yml | 44 + test-infra/compose/static/tempo/tempo.yaml | 25 + test-infra/compose/static/vouch/Dockerfile | 9 + test-infra/compose/static/vouch/run.sh | 50 + test-infra/compose/static/vouch/vouch.yml | 36 + test-infra/compose/template.go | 92 + ...ockerCompose_define_create_template.golden | 20 + ...TestDockerCompose_define_create_yml.golden | 18 + ...stDockerCompose_define_dkg_template.golden | 57 + .../TestDockerCompose_define_dkg_yml.golden | 28 + ...e_lock_create_pluto_keygen_template.golden | 65 + ...ompose_lock_create_pluto_keygen_yml.golden | 32 + ...tDockerCompose_lock_create_template.golden | 65 + .../TestDockerCompose_lock_create_yml.golden | 30 + ...mpose_lock_dkg_mixed_impls_template.golden | 221 ++ ...kerCompose_lock_dkg_mixed_impls_yml.golden | 103 + ...TestDockerCompose_lock_dkg_template.golden | 221 ++ .../TestDockerCompose_lock_dkg_yml.golden | 101 + ...kerCompose_run_mixed_impls_template.golden | 494 +++ ...stDockerCompose_run_mixed_impls_yml.golden | 259 ++ .../TestDockerCompose_run_template.golden | 494 +++ .../testdata/TestDockerCompose_run_yml.golden | 257 ++ .../testdata/TestNewDefaultConfig.golden | 32 + 51 files changed, 12701 insertions(+) create mode 100644 test-infra/compose/README.md create mode 100644 test-infra/compose/alert.go create mode 100644 test-infra/compose/auto.go create mode 100644 test-infra/compose/compose/main.go create mode 100644 test-infra/compose/compose_internal_test.go create mode 100644 test-infra/compose/config.go create mode 100644 test-infra/compose/define.go create mode 100644 test-infra/compose/docker-compose.template create mode 100644 test-infra/compose/go.mod create mode 100644 test-infra/compose/go.sum create mode 100644 test-infra/compose/lock.go create mode 100644 test-infra/compose/new.go create mode 100644 test-infra/compose/new_test.go create mode 100644 test-infra/compose/run.go create mode 100644 test-infra/compose/smoke/smoke_test.go create mode 100644 test-infra/compose/static/grafana/dash_alerts.json create mode 100644 test-infra/compose/static/grafana/dash_charon_overview.json create mode 100644 test-infra/compose/static/grafana/dash_duty_details.json create mode 100644 test-infra/compose/static/grafana/dashboards.yml create mode 100644 test-infra/compose/static/grafana/datasource.yml create mode 100644 test-infra/compose/static/grafana/grafana.ini create mode 100644 test-infra/compose/static/grafana/notifiers.yml create mode 100644 test-infra/compose/static/lighthouse/Dockerfile create mode 100755 test-infra/compose/static/lighthouse/run.sh create mode 100644 test-infra/compose/static/lodestar/Dockerfile create mode 100755 test-infra/compose/static/lodestar/run.sh create mode 100644 test-infra/compose/static/loki/loki.yml create mode 100644 test-infra/compose/static/prometheus/prometheus.yml create mode 100644 test-infra/compose/static/prometheus/rules.yml create mode 100644 test-infra/compose/static/tempo/tempo.yaml create mode 100644 test-infra/compose/static/vouch/Dockerfile create mode 100644 test-infra/compose/static/vouch/run.sh create mode 100644 test-infra/compose/static/vouch/vouch.yml create mode 100644 test-infra/compose/template.go create mode 100644 test-infra/compose/testdata/TestDockerCompose_define_create_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_define_create_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_define_dkg_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_define_dkg_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_create_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_create_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_dkg_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_lock_dkg_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_yml.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_run_template.golden create mode 100644 test-infra/compose/testdata/TestDockerCompose_run_yml.golden create mode 100644 test-infra/compose/testdata/TestNewDefaultConfig.golden diff --git a/test-infra/compose/README.md b/test-infra/compose/README.md new file mode 100644 index 00000000..ae06caff --- /dev/null +++ b/test-infra/compose/README.md @@ -0,0 +1,119 @@ +# Pluto Compose + +> Run, test, and debug a developer-focussed insecure local pluto/charon cluster using docker compose + +This is a port of charon's [`testutil/compose`](https://github.com/ObolNetwork/charon/tree/main/testutil/compose) +(pinned at `v1.7.1`, the pluto parity reference) with one extra axis: each node in the +cluster can run either **charon** or **pluto**, so clusters of N charon + M pluto nodes +can be composed for cross-implementation testing. + +Compose is a tool that generates `docker-compose.yml` files such that different clusters +can be created and run. The aim is for developers to be able to debug features and check +functionality of clusters on their local machines. + +The `compose` command should be executed in sequential steps: + 1. `compose new`: Creates a new config.json that defines what will be composed. + 2. `compose define`: Creates a docker-compose.yml that executes `create dkg` if keygen==dkg. + 3. `compose lock`: Creates a docker-compose.yml that executes `create cluster` or `dkg`. + 4. `compose run`: Creates a docker-compose.yml that executes `run`. + +The `compose` command also includes some convenience functions. +- `compose clean`: Cleans the compose directory of existing files. +- `compose auto`: Runs `compose define && compose lock && compose run`. + +Note that compose automatically runs `docker compose up` at the end of each command. This can be disabled via `--up=false`. + +## Node implementations + +Node implementations are assigned round-robin from `--node-impls` (like validator types): + +``` +compose new --node-impls=charon # all charon (default) +compose new --node-impls=pluto # all pluto +compose new --nodes=4 --node-impls=charon,charon,pluto,pluto # 2 charon + 2 pluto +``` + +- Charon nodes run `obolnetwork/charon:{tag}` (default `latest`, or `local` built from + `CHARON_REPO` with `--build-local`). +- Pluto nodes run `pluto:{tag}` (default `local`), built automatically from the repo + root `Dockerfile` during the define step. This requires the `PLUTO_REPO` env var + pointing at the pluto repo. +- `--keygen-impl` selects which implementation runs the single-container keygen steps + (`create cluster` / `create dkg`); it defaults to node0's implementation. +- The relay always runs the charon node-base image. + +Pluto accepts `CHARON_*` env vars and charon-compatible flags by design (CLI parity), +so the generated docker-compose.yml services are identical for both implementations +apart from the image. + +Note: scenarios where pluto nodes `run` require `pluto run` to be implemented; keygen +steps (`create cluster`, `create dkg`, `dkg`) work today. + +## Usage Examples + +Install the `compose` binary: +``` +# From inside the pluto repo +go install ./test-infra/compose/compose + +# If `which compose` fails, then fix your environment: `export PATH=$PATH:$(go env GOPATH)/bin`. Or see https://go.dev/doc/gopath_code +``` + +Create a pluto compose workspace folder: +``` +cd /tmp +mkdir pluto-compose +cd pluto-compose +``` + +Create the default cluster: +``` +export PLUTO_REPO=/path/to/pluto +compose clean && compose new --node-impls=pluto && compose define && compose lock && compose run +``` + +Monitor the cluster via `grafana`: +``` +open http://localhost:3000/d/charon_overview_dashboard/charon-overview # Open Grafana simnet dashboard +``` + +Creating a DKG based cluster that uses a locally built charon binary: +``` +compose new --keygen=dkg --build-local +compose auto +``` + +## Smoke tests + +`smoke/smoke_test.go` mirrors charon's compose smoke tests: each scenario generates +and runs a full cluster with a mock beacon node (simnet), while a Prometheus container +evaluates the alert rules in `static/prometheus/rules.yml`. A scenario fails if any +alert fires within the test timeout. + +``` +cd test-infra/compose + +# Charon-only scenario matrix (pulls obolnetwork/charon:v1.7.1): +go test ./smoke -v -integration -run 'TestSmoke/default_alpha' + +# Include pluto scenarios that work today (pluto keygen + charon runtime): +PLUTO_REPO=$(git rev-parse --show-toplevel) go test ./smoke -v -integration + +# Full matrix including scenarios running pluto nodes (requires `pluto run` support): +PLUTO_REPO=$(git rev-parse --show-toplevel) go test ./smoke -v -integration -pluto-run + +# Keep docker-compose logs per scenario: +go test ./smoke -v -integration -log-dir=. +``` + +Scenarios running pluto nodes (`all_pluto`, `mixed_2_charon_2_pluto`, `pluto_dkg`) skip +until enabled with `-pluto-run`; flip the flag on (and later, the default) once +`pluto run` lands. + +## Versioning + +Charon is pinned to the pluto parity reference (`v1.7.1`): both the Go library in +`go.mod` and the docker image tag used by smoke tests. The two `replace` directives in +`go.mod` are copied from charon's own `go.mod` (Go does not propagate a dependency's +replaces) and must be kept in sync when bumping charon. Bump deliberately alongside the +parity target, not to track charon main. diff --git a/test-infra/compose/alert.go b/test-infra/compose/alert.go new file mode 100644 index 00000000..f8cb62ec --- /dev/null +++ b/test-infra/compose/alert.go @@ -0,0 +1,121 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "bytes" + "context" + "encoding/json" + "os/exec" + "time" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" +) + +const alertsPolled = "alerts_polled" + +// startAlertCollector starts a goroutine that polls prometheus alerts until the context is closed and returns +// a channel on which the received alert descriptions will be sent. +func startAlertCollector(ctx context.Context, dir string) chan string { + resp := make(chan string, 100) + + go func() { + var ( + success bool + dedup = make(map[string]bool) + ) + + defer close(resp) + + const iterSleep = time.Second * 2 + + // Time required to wait for prometheus container to start. + time.Sleep(time.Second * 10) + + for ; ctx.Err() == nil; time.Sleep(iterSleep) { // Sleep for iterSleep before next iteration. + //nolint:revive // tls not required for testing. + cmd := exec.CommandContext(ctx, "docker", "compose", "exec", "-T", "curl", "curl", "-s", "http://prometheus:9090/api/v1/rules?type=alert") + cmd.Dir = dir + + out, err := cmd.CombinedOutput() + if ctx.Err() != nil { + return + } else if err != nil { + log.Error(ctx, "Exec curl alerts", err, z.Str("out", string(out))) + continue + } + + var alerts promAlerts + if err := json.Unmarshal(bytes.TrimSpace(out), &alerts); err != nil { + resp <- errors.Wrap(err, "unmarshal alerts", z.Str("out", string(out))).Error() + continue + } + + if alerts.Status != "success" { + resp <- "non success status from prometheus alerts: " + alerts.Status + continue + } + + if !success { + resp <- alertsPolled // Push initial "fake alert" so logic can fail is not alerts polled. + + success = true + } + + for _, active := range getActiveAlerts(alerts) { + if dedup[active] { + continue + } + + dedup[active] = true + log.Info(ctx, "Detected new alert", z.Str("alert", active)) + + resp <- active + } + } + }() + + return resp +} + +func getActiveAlerts(alerts promAlerts) []string { + var resp []string + + for _, group := range alerts.Data.Groups { + for _, rule := range group.Rules { + for _, alert := range rule.Alerts { + // Prometheus reports alert states as inactive/pending/firing. + // Charon matches "active" here, which never occurs, so its + // alert gate silently passes everything (upstream bug). + if alert.State != "firing" { + continue + } + + resp = append(resp, alert.Annotations.Description) + } + } + } + + return resp +} + +// promAlerts is the json response returned by querying prometheus alerts. +type promAlerts struct { + Status string `json:"status"` + Data struct { + Groups []struct { + Name string `json:"name"` + Rules []struct { + Name string `json:"name"` + Alerts []struct { + State string `json:"state"` + Annotations struct { + Description string `json:"description"` + } `json:"annotations"` + } `json:"alerts"` + } `json:"rules"` + } `json:"groups"` + } `json:"data"` +} diff --git a/test-infra/compose/auto.go b/test-infra/compose/auto.go new file mode 100644 index 00000000..d0f29df0 --- /dev/null +++ b/test-infra/compose/auto.go @@ -0,0 +1,303 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "context" + "io" + "io/fs" + "os" + "os/exec" + "time" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" +) + +type AutoConfig struct { + // Dir is the directory to use for compose artifacts. + Dir string + // AlertTimeout is the timeout to collect alerts before shutdown. Zero disables timeout. + AlertTimeout time.Duration + // SudoPerms enables changing all compose artefacts file permissions using sudo. + SudoPerms bool + // Print generated docker-compose.yml files. + PrintYML bool + // RunTmplFunc allows arbitrary overrides in the run step template. + RunTmplFunc func(*TmplData) + // DefineTmplFunc allows arbitrary overrides if the define step template. + DefineTmplFunc func(*TmplData) + // LogFile enables writing (appending) docker compose output to this file path instead of stdout. + LogFile string +} + +// Auto runs all three steps (define,lock,run) sequentially with support for detecting alerts. +func Auto(ctx context.Context, conf AutoConfig) error { + ctx = log.WithTopic(ctx, "auto") + + w, closeFunc, err := newLogWriter(conf.LogFile) + if err != nil { + return err + } + defer closeFunc() //nolint:errcheck // non-critical + + steps := []struct { + Name string + RunFunc RunFunc + TmplFunc func(*TmplData) + RunStep bool + }{ + { + Name: "define", + RunFunc: Define, + TmplFunc: conf.DefineTmplFunc, + }, { + Name: "lock", + RunFunc: Lock, + }, { + Name: "run", + RunFunc: Run, + TmplFunc: conf.RunTmplFunc, + RunStep: true, + }, + } + + for _, step := range steps { + run := NewRunnerFunc(step.Name, conf.Dir, false, step.RunFunc) + + tmpl, err := run(ctx) + if err != nil { + return err + } + + if conf.SudoPerms { + if err := fixPerms(ctx, conf.Dir); err != nil { + return err + } + } + + if step.TmplFunc != nil { + step.TmplFunc(&tmpl) + + err := WriteDockerCompose(conf.Dir, tmpl) + if err != nil { + return err + } + } + + if conf.PrintYML { + if err := printDockerCompose(ctx, conf.Dir); err != nil { + return err + } + } + + if step.RunStep { // Continue below if final run step. + break + } + + _, _ = w.Write([]byte("===== " + step.Name + " step: docker compose up =====\n")) + + if err := execUp(ctx, conf.Dir, w); err != nil { + return err + } + } + + // Ensure everything is clean before we start with alert test. + _ = execDown(ctx, conf.Dir) + + _, _ = w.Write([]byte("===== run step: docker compose up --no-start --build =====\n")) + + // Build and create docker compose services before executing docker compose up. + if err = execBuildAndCreate(ctx, conf.Dir); err != nil { + return err + } + + if conf.AlertTimeout > 0 { + var cancel context.CancelFunc + + ctx, cancel = context.WithTimeout(ctx, conf.AlertTimeout) + defer cancel() + } + + alerts := startAlertCollector(ctx, conf.Dir) + + defer func() { + _ = execDown(context.Background(), conf.Dir) + }() + + _, _ = w.Write([]byte("===== run step: docker compose up =====\n")) + + if err = execUp(ctx, conf.Dir, w); err != nil && !errors.Is(err, context.DeadlineExceeded) { + return err + } + + var ( + alertMsgs []string + alertSuccess bool + ) + + for alert := range alerts { + if alert == alertsPolled { + alertSuccess = true + } else { + alertMsgs = append(alertMsgs, alert) + } + } + + if !alertSuccess { + return errors.New("alerts couldn't be polled") + } else if len(alertMsgs) > 0 { + return errors.New("alerts detected", z.Any("alerts", alertMsgs)) + } + + log.Info(ctx, "No alerts detected") + + return nil +} + +// printDockerCompose prints the docker-compose.yml file to stdout. +func printDockerCompose(ctx context.Context, dir string) error { + log.Info(ctx, "Printing docker-compose.yml") + cmd := exec.CommandContext(ctx, "cat", "docker-compose.yml") + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return errors.Wrap(err, "exec cat docker-compose.yml") + } + + return nil +} + +// fixPerms fixes file permissions as a workaround for linux docker by removing +// all restrictions using sudo chmod. +func fixPerms(ctx context.Context, dir string) error { + cmd := exec.CommandContext(ctx, "sudo", "chmod", "-R", "a+wrX", ".") + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + return errors.Wrap(err, "exec sudo chmod") + } + + return nil +} + +// execDown executes `docker compose down`. +func execDown(ctx context.Context, dir string) error { + // Charon runs `sudo chown -R runner:docker` here to fix file ownership on + // its GitHub Actions runner. Dropped: `runner:docker` only exists in CI, so + // locally it just prompts for a sudo password and errors ("illegal group + // name"). Use -sudo-perms if a real permission fix is ever needed. + log.Info(ctx, "Executing docker compose down") + + cmd := exec.CommandContext(ctx, "docker", "compose", "down", + "--remove-orphans", + "--timeout=2", + ) + cmd.Dir = dir + cmd.Stdout = os.Stdout + + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "run down") + } + + return nil +} + +// execUp executes `docker compose up` and it writes docker compose logs to the given out io.Writer. +func execUp(ctx context.Context, dir string, out io.Writer) error { + // Build first so containers start at the same time below. + log.Info(ctx, "Executing docker compose build") + cmd := exec.CommandContext(ctx, "docker", "compose", "build", "--parallel") + + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + return errors.Wrap(err, "exec docker compose build", z.Str("output", string(out))) + } + + log.Info(ctx, "Executing docker compose up") + cmd = exec.CommandContext(ctx, "docker", "compose", "up", + "--remove-orphans", + "--abort-on-container-exit", + "--quiet-pull", + ) + cmd.Dir = dir + cmd.Stdout = out + cmd.Stderr = out + + if err := cmd.Run(); err != nil { + if ctx.Err() != nil { + err = ctx.Err() + } + + return errors.Wrap(err, "exec docker compose up") + } + + return nil +} + +// execBuildAndCreate builds and creates containers. It should be called before execUp for run step. +func execBuildAndCreate(ctx context.Context, dir string) error { + log.Info(ctx, "Executing docker compose up --no-start --build") + cmd := exec.CommandContext(ctx, "docker", "compose", "up", "--no-start", "--build") + + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + return errors.Wrap(err, "exec docker compose up --no-start --build", z.Str("output", string(out))) + } + + return nil +} + +// RunFunc defines a function that generates docker-compose.yml from config and returns the template data. +type RunFunc func(context.Context, string, Config) (TmplData, error) + +// NewRunnerFunc returns a function that wraps and runs a run function. +func NewRunnerFunc(topic string, dir string, up bool, runFunc RunFunc, +) func(ctx context.Context) (data TmplData, err error) { + return func(ctx context.Context) (data TmplData, err error) { + ctx = log.WithTopic(ctx, topic) + + conf, err := LoadConfig(dir) + if errors.Is(err, fs.ErrNotExist) { + return TmplData{}, errors.New("compose config.json not found; maybe try `compose new` first", z.Str("dir", dir)) + } else if err != nil { + return TmplData{}, err + } + + log.Info(ctx, "Running compose command", z.Str("command", topic)) + + data, err = runFunc(ctx, dir, conf) + if err != nil { + return TmplData{}, err + } + + if up { + return data, execUp(ctx, dir, os.Stdout) + } + + return data, nil + } +} + +// newLogWriter returns io writer and a close function or an error. +func newLogWriter(logFile string) (io.WriteCloser, func() error, error) { + if logFile == "" { + return os.Stdout, func() error { return nil }, nil + } + + // Preparing log file. + file, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return nil, nil, errors.Wrap(err, "open log file") + } + + return file, file.Close, nil +} diff --git a/test-infra/compose/compose/main.go b/test-infra/compose/compose/main.go new file mode 100644 index 00000000..304fc8ff --- /dev/null +++ b/test-infra/compose/compose/main.go @@ -0,0 +1,217 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +// Command compose provides a tool to run, test, debug local charon clusters +// using docker compose. +// +// It consists of multiple steps: +// - compose new: Creates a new config.json that defines what will be composed. +// - compose define: Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg. +// - compose lock: Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`. +// - compose run: Creates a docker-compose.yml that executes `charon run`. +package main + +import ( + "context" + "os/signal" + "syscall" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/NethermindEth/pluto/test-infra/compose" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/cluster" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + cobra.CheckErr(newRootCmd().ExecuteContext(ctx)) +} + +func newRootCmd() *cobra.Command { + root := &cobra.Command{ + Use: "compose", + Short: "Charon Compose - Run, test, and debug a developer-focussed insecure local charon cluster using docker compose", + } + + root.AddCommand(newNewCmd()) + root.AddCommand(newCleanCmd()) + root.AddCommand(newBuildLocalCmd()) + root.AddCommand(newAutoCmd()) + root.AddCommand(newDockerCmd( + "define", + "Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg", + compose.Define, + )) + root.AddCommand(newDockerCmd( + "lock", + "Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`", + compose.Lock, + )) + root.AddCommand(newDockerCmd( + "run", + "Creates a docker-compose.yml that executes `charon run`", + compose.Run, + )) + + return root +} + +// newDockerCmd returns a cobra command that generates docker-compose.yml files and executes it. +func newDockerCmd(use string, short string, runFunc compose.RunFunc) *cobra.Command { + cmd := &cobra.Command{ + Use: use, + Short: short, + } + + up := addUpFlag(cmd.Flags()) + dir := addDirFlag(cmd.Flags()) + cmd.RunE = func(cmd *cobra.Command, _ []string) error { + _, err := compose.NewRunnerFunc(use, *dir, *up, runFunc)(cmd.Context()) + if err != nil { + log.Error(cmd.Context(), "Fatal error", err) + } + + return err + } + + return cmd +} + +func newAutoCmd() *cobra.Command { + var conf compose.AutoConfig + + cmd := &cobra.Command{ + Use: "auto", + Short: "Convenience function that runs `compose define && compose lock && compose run`", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { //nolint:revive // keep args variable name for clarity + err := compose.Auto(cmd.Context(), conf) + if err != nil { + log.Error(cmd.Context(), "auto command fatal error", err) + return err + } + + return nil + }, + } + + cmd.Flags().StringVar(&conf.Dir, "compose-dir", ".", "Directory to use for compose artifacts") + cmd.Flags().DurationVar(&conf.AlertTimeout, "alert-timeout", 0, "Timeout to collect alerts before shutdown. Zero disables timeout.") + cmd.Flags().BoolVar(&conf.SudoPerms, "sudo-perms", false, "Enables changing all compose artefacts file permissions using sudo.") + cmd.Flags().BoolVar(&conf.PrintYML, "print-yml", false, "Print generated docker-compose.yml files.") + + return cmd +} + +func newBuildLocalCmd() *cobra.Command { + return &cobra.Command{ + Use: "build-local", + Short: "Builds the obolnetwork/charon:local docker container from the local source code. Note this requires the CHARON_REPO env var.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { //nolint:revive // keep args variable name for clarity + return compose.BuildLocal(cmd.Context()) + }, + } +} + +func newNewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "new", + Short: "Creates a new config.json that defines what will be composed", + } + + conf := compose.NewDefaultConfig() + + dir := addDirFlag(cmd.Flags()) + keygen := cmd.Flags().String("keygen", string(conf.KeyGen), "Key generation process: create, split, dkg") + buildLocal := cmd.Flags().Bool("build-local", conf.BuildLocal, "Enables building a local charon container from source. Note this requires the CHARON_REPO env var.") + beaconNode := cmd.Flags().String("beacon-nodes", conf.BeaconNodes, "Beacon node URL endpoints or 'mock' for simnet.") + extRelay := cmd.Flags().String("external-relay", "", "Optional external relay HTTP url.") + splitKeys := cmd.Flags().String("split-keys-dir", conf.SplitKeysDir, "Directory containing keys to split for keygen==create, or empty not to split.") + featureSet := cmd.Flags().String("feature-set", conf.FeatureSet, "Minimum feature set to enable: alpha, beta, stable") + numVals := cmd.Flags().Int("num-validators", conf.NumValidators, "Number of distributed validators.") + vcTypes := cmd.Flags().StringSlice("validator-types", conf.VCStrings(), "Validator types to include.") + nodes := cmd.Flags().Int("nodes", conf.NumNodes, "Number of charon nodes in the cluster.") + insecureKeys := cmd.Flags().Bool("insecure-keys", conf.InsecureKeys, "To generate keys quickly.") + slotDuration := cmd.Flags().Duration("simnet-slot-duration", time.Second, "Configures slot duration in simnet beacon mock.") + beaconFuzz := cmd.Flags().Bool("beacon-fuzz", false, "Configures simnet beaconmock to return fuzzed responses.") + p2pFuzz := cmd.Flags().Bool("p2p-fuzz", false, "Configures charon p2p network to return fuzzed responses of one of the nodes in the cluster.") + nodeImpls := cmd.Flags().StringSlice("node-impls", nil, "Node implementations (charon or pluto) assigned round-robin to nodes; e.g. charon,pluto for alternating. Empty defaults to all charon.") + keygenImpl := cmd.Flags().String("keygen-impl", string(conf.KeyGenImpl), "Implementation running single-container keygen steps: charon or pluto. Empty defaults to node0's implementation.") + plutoImageTag := cmd.Flags().String("pluto-image-tag", conf.PlutoImageTag, "Pluto docker image tag: pluto:{tag}. Tag 'local' is built from source, which requires the PLUTO_REPO env var.") + + cmd.RunE = func(cmd *cobra.Command, _ []string) error { + conf.KeyGen = compose.KeyGen(*keygen) + conf.BuildLocal = *buildLocal + conf.BeaconNodes = *beaconNode + conf.SplitKeysDir = *splitKeys + conf.FeatureSet = *featureSet + conf.ExternalRelay = *extRelay + conf.NumValidators = *numVals + conf.NumNodes = *nodes + conf.Threshold = cluster.Threshold(conf.NumNodes) + conf.InsecureKeys = *insecureKeys + conf.SlotDuration = *slotDuration + conf.BeaconFuzz = *beaconFuzz + conf.P2PFuzz = *p2pFuzz + conf.KeyGenImpl = compose.NodeImpl(*keygenImpl) + conf.PlutoImageTag = *plutoImageTag + + if len(*nodeImpls) > 0 { + var impls []compose.NodeImpl + for _, impl := range *nodeImpls { + impls = append(impls, compose.NodeImpl(impl)) + } + + conf.NodeImpls = impls + } + + if conf.BuildLocal { + conf.ImageTag = "local" + } + + var vcs []compose.VCType + for _, vc := range *vcTypes { + vcs = append(vcs, compose.VCType(vc)) + } + + conf.VCs = vcs + + ctx := log.WithTopic(cmd.Context(), "new") + if err := compose.New(ctx, *dir, conf); err != nil { + log.Error(ctx, "Fatal error", err) + return err + } + + return nil + } + + return cmd +} + +func newCleanCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "clean", + Short: "Convenience function that cleans the compose directory", + } + + dir := addDirFlag(cmd.Flags()) + + cmd.RunE = func(cmd *cobra.Command, _ []string) error { + return compose.Clean(cmd.Context(), *dir) + } + + return cmd +} + +func addDirFlag(flags *pflag.FlagSet) *string { + return flags.String("compose-dir", ".", "Directory to use for compose artifacts") +} + +func addUpFlag(flags *pflag.FlagSet) *bool { + return flags.Bool("up", true, "Execute `docker compose up` when compose command completes") +} diff --git a/test-infra/compose/compose_internal_test.go b/test-infra/compose/compose_internal_test.go new file mode 100644 index 00000000..c7f3f306 --- /dev/null +++ b/test-infra/compose/compose_internal_test.go @@ -0,0 +1,135 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "bytes" + "context" + "os" + "path" + "testing" + "text/template" + + k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/testutil" +) + +//go:generate go test . -update -clean + +func TestDockerCompose(t *testing.T) { + tests := []struct { + Name string + ConfFunc func(*Config) + RunFunc func(ctx context.Context, dir string, conf Config) (TmplData, error) + }{ + { + Name: "define dkg", + ConfFunc: func(conf *Config) { + conf.KeyGen = KeyGenDKG + }, + RunFunc: Define, + }, + { + Name: "define create", + ConfFunc: func(conf *Config) { + conf.KeyGen = KeyGenCreate + }, + RunFunc: Define, + }, + { + Name: "lock dkg", + ConfFunc: func(conf *Config) { + conf.Step = stepDefined + conf.KeyGen = KeyGenDKG + }, + RunFunc: Lock, + }, + { + Name: "lock create", + ConfFunc: func(conf *Config) { + conf.Step = stepDefined + conf.KeyGen = KeyGenCreate + }, + RunFunc: Lock, + }, + { + Name: "run", + ConfFunc: func(conf *Config) { + conf.NumValidators = 2 + conf.Step = stepLocked + }, + RunFunc: Run, + }, + { + Name: "lock dkg mixed impls", + ConfFunc: func(conf *Config) { + conf.Step = stepDefined + conf.KeyGen = KeyGenDKG + conf.NodeImpls = []NodeImpl{ImplCharon, ImplPluto} + }, + RunFunc: Lock, + }, + { + Name: "run mixed impls", + ConfFunc: func(conf *Config) { + conf.NumValidators = 2 + conf.Step = stepLocked + conf.NodeImpls = []NodeImpl{ImplCharon, ImplCharon, ImplPluto, ImplPluto} + }, + RunFunc: Run, + }, + { + Name: "lock create pluto keygen", + ConfFunc: func(conf *Config) { + conf.Step = stepDefined + conf.KeyGen = KeyGenCreate + conf.KeyGenImpl = ImplPluto + }, + RunFunc: Lock, + }, + } + + const seed = 0 + + keyGenFunc = func() (*k1.PrivateKey, error) { + return testutil.GenerateInsecureK1Key(t, seed), nil + } + noPull = true + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + dir := t.TempDir() + + conf := NewDefaultConfig() + if test.ConfFunc != nil { + test.ConfFunc(&conf) + } + + data, err := test.RunFunc(context.Background(), dir, conf) + require.NoError(t, err) + + t.Run("yml", func(t *testing.T) { + b, err := os.ReadFile(path.Join(dir, "docker-compose.yml")) + require.NoError(t, err) + + b = bytes.ReplaceAll(b, []byte(dir), []byte("testdir")) + testutil.RequireGoldenBytes(t, b) + }) + + t.Run("template", func(t *testing.T) { + data.ComposeDir = "testdir" + testutil.RequireGoldenJSON(t, data) + }) + }) + } +} + +func TestParseTemplate(t *testing.T) { + _, err := template.New("").Parse(string(tmpl)) + require.NoError(t, err) + + _, err = getVC(VCTeku, 0, 1, false, true) + require.NoError(t, err) +} diff --git a/test-infra/compose/config.go b/test-infra/compose/config.go new file mode 100644 index 00000000..4cda0072 --- /dev/null +++ b/test-infra/compose/config.go @@ -0,0 +1,265 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "strings" + "time" +) + +const ( + version = "obol/charon/compose/1.0.0" + configFile = "config.json" + defaultImageTag = "latest" + defaultBeaconNode = "mock" + defaultKeyGen = KeyGenCreate + defaultNumVals = 1 + defaultNumNodes = 4 + defaultThreshold = 3 + defaultFeatureSet = "alpha" + + charonImage = "obolnetwork/charon" + plutoImage = "pluto" + cmdRun = "run" + cmdUnsafeRun = "[unsafe,run]" + // cmdDKG delays shutdown after completion to allow other nodes to finish. + // Uses a flag instead of charon's `sh -c '... && sleep 2'` since the pluto image is distroless (no shell). + cmdDKG = "[dkg,--shutdown-delay=2s]" + cmdCreateCluster = "[create,cluster]" + cmdCreateDKG = "[create,dkg]" +) + +var charonPorts = []port{ + {External: 3600, Internal: 3600}, // # Validator API + {External: 3610, Internal: 3610}, // # Libp2p + {External: 3620, Internal: 3620}, // # Monitoring + {External: 3630, Internal: 3630}, // # Discv5 +} + +// VCType defines a validator client type. +type VCType string + +const ( + VCMock VCType = "mock" + VCTeku VCType = "teku" + VCLighthouse VCType = "lighthouse" + VCVouch VCType = "vouch" + VCLodestar VCType = "lodestar" +) + +// KeyGen defines a key generation process. +type KeyGen string + +const ( + KeyGenDKG KeyGen = "dkg" + KeyGenCreate KeyGen = "create" +) + +// NodeImpl defines the implementation (charon or pluto) running a node. +type NodeImpl string + +const ( + ImplCharon NodeImpl = "charon" + ImplPluto NodeImpl = "pluto" +) + +// step defines the current completed compose step. +type step string + +const ( + stepNew step = "new" + stepDefined step = "defined" + stepLocked step = "locked" +) + +// Config defines a local compose cluster; including both keygen and running a cluster. +type Config struct { + // Version defines the compose config version. + Version string `json:"version"` + + // Step defines the current completed compose step. + Step step `json:"step"` + + // NumNodes is the number of charon nodes in the cluster. + NumNodes int `json:"num_nodes"` + + // Threshold required for signature reconstruction. Defaults to safe value for number of nodes/peers. + Threshold int `json:"threshold"` + + // NumValidators is the number of DVs to be created in the cluster lock file. + NumValidators int `json:"num_validators"` + + // ImageTag defines the charon docker image tag: obolnetwork/charon:{ImageTag}. + ImageTag string `json:"image_tag"` + + // BuildLocal enables building a local charon docker container from source overriding ImageTag with 'local'. + BuildLocal bool `json:"build_local"` + + // NodeImpls defines the implementation (charon or pluto) of each node. + // Nodes are assigned round-robin like VCs; node{i} runs NodeImpls[i%len(NodeImpls)]. + // Empty defaults to all charon. + NodeImpls []NodeImpl `json:"node_impls"` + + // KeyGenImpl defines the implementation running single-container keygen steps + // (`create cluster` and `create dkg`). Empty defaults to the implementation of node0. + KeyGenImpl NodeImpl `json:"keygen_impl"` + + // PlutoImageTag defines the pluto docker image tag: pluto:{PlutoImageTag}. + // The image is built from source (PLUTO_REPO env var) by the define step when a pluto impl is used. + PlutoImageTag string `json:"pluto_image_tag"` + + // KeyGen defines the key generation process. + KeyGen KeyGen `json:"key_gen"` + + // SplitKeysDir directory containing keys to split for keygen==create. + SplitKeysDir string `json:"split_keys_dir"` + + // BeaconNodes url endpoint or "mock" for simnet. + BeaconNodes string `json:"beacon_nodes"` + + // ExternalRelay HTTP url endpoint or empty to disable. + ExternalRelay string `json:"external_relay"` + + // VCs define the types of validator clients to use. + VCs []VCType `json:"validator_clients"` + + // FeatureSet defines the minimum feature set to enable. + FeatureSet string `json:"feature_set"` + + // DisableMonitoringPorts defines whether to disable prometheus and jaeger monitoring port binding. + DisableMonitoringPorts bool `json:"disable_monitoring_ports"` + + // InsecureKeys generates insecure keys. Useful when testing large validator sets + // as it speeds up keystore encryption and decryption. + InsecureKeys bool `json:"insecure_keys"` + + // SlotDuration configures slot duration on simnet beacon mock for all the nodes in the cluster. + SlotDuration time.Duration `json:"slot_duration"` + + // BeaconFuzz configures simnet beaconmock to return fuzzed responses. + BeaconFuzz bool `json:"beacon-fuzz"` + + // P2PFuzz configures charon p2p network to send and receive fuzzed messages. + P2PFuzz bool `json:"p2p-fuzz"` + + // SyntheticBlockProposals configures use of synthetic block proposals in simnet cluster. + SyntheticBlockProposals bool `json:"synthetic_block_proposals"` + + // Monitoring enables monitoring stack for the compose cluster. It includes grafana, loki and jaeger services. + Monitoring bool `json:"monitoring"` + + // BuilderAPI enables the builder API for the compose cluster. + BuilderAPI bool `json:"builder_api"` +} + +// VCStrings returns the VCs field as a slice of strings. +func (c Config) VCStrings() []string { + var resp []string + for _, vc := range c.VCs { + resp = append(resp, string(vc)) + } + + return resp +} + +// NodeImpl returns the implementation of node{index}, assigned round-robin like VCs. +func (c Config) NodeImpl(index int) NodeImpl { + if len(c.NodeImpls) == 0 { + return ImplCharon + } + + return c.NodeImpls[index%len(c.NodeImpls)] +} + +// KeygenImpl returns the implementation running single-container keygen steps. +func (c Config) KeygenImpl() NodeImpl { + if c.KeyGenImpl != "" { + return c.KeyGenImpl + } + + return c.NodeImpl(0) +} + +// ImplImage returns the full docker image reference for the provided implementation. +func (c Config) ImplImage(impl NodeImpl) string { + if impl == ImplPluto { + return plutoImage + ":" + c.PlutoImageTag + } + + return charonImage + ":" + c.ImageTag +} + +// ImageOverride returns the per-node image override for the provided implementation, +// or empty to use the default charon node-base image. +func (c Config) ImageOverride(impl NodeImpl) string { + if impl == ImplPluto { + return c.ImplImage(impl) + } + + return "" +} + +// flagsCommand renders a bracketed docker-compose command with the key-values +// as explicit command-line flags. Pluto's `create cluster` and `create dkg` +// commands do not read CHARON_* env vars like charon does (missing clap env +// bindings), so pluto keygen containers get their configuration as flags. +func flagsCommand(cmd string, kvs []kv) string { + args := strings.Split(strings.Trim(cmd, "[]"), ",") + + for _, kv := range kvs { + val := strings.Trim(kv.Value, `"`) + flag := "--" + strings.ReplaceAll(kv.Key, "_", "-") + + switch val { + case "", "false": // Omit empty values and false bools (bool flags take no value). + continue + case "true": + args = append(args, flag) + default: + arg := flag + "=" + val + if strings.Contains(arg, ",") { + arg = "'" + arg + "'" // Quote args with commas so the YAML flow sequence stays intact. + } + + args = append(args, arg) + } + } + + return "[" + strings.Join(args, ",") + "]" +} + +// UsesPluto returns true if any node or keygen step runs pluto. +func (c Config) UsesPluto() bool { + if c.KeygenImpl() == ImplPluto { + return true + } + + for i := range c.NumNodes { + if c.NodeImpl(i) == ImplPluto { + return true + } + } + + return false +} + +// NewDefaultConfig returns a new default config. +func NewDefaultConfig() Config { + return Config{ + Version: version, + NumNodes: defaultNumNodes, + Threshold: defaultThreshold, + NumValidators: defaultNumVals, + ImageTag: defaultImageTag, + NodeImpls: []NodeImpl{ImplCharon}, + PlutoImageTag: "local", + VCs: []VCType{VCLighthouse, VCLighthouse, VCMock}, + KeyGen: defaultKeyGen, + BeaconNodes: defaultBeaconNode, + Step: stepNew, + FeatureSet: defaultFeatureSet, + SlotDuration: time.Second, + SyntheticBlockProposals: true, + Monitoring: true, + } +} diff --git a/test-infra/compose/define.go b/test-infra/compose/define.go new file mode 100644 index 00000000..36573219 --- /dev/null +++ b/test-infra/compose/define.go @@ -0,0 +1,426 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strconv" + "strings" + + k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/k1util" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/eth2util" + "github.com/obolnetwork/charon/eth2util/enr" +) + +// zeroAddress is not owned by any user, is often associated with token burn & mint/genesis events and used as a generic null address. +// See https://etherscan.io/address/0x0000000000000000000000000000000000000000. +const zeroAddress = `"0x0000000000000000000000000000000000000000"` + +// Clean deletes all compose directory files and artifacts. +func Clean(ctx context.Context, dir string) error { + ctx = log.WithTopic(ctx, "clean") + + files, err := filepath.Glob(path.Join(dir, "*")) + if err != nil { + return errors.Wrap(err, "glob dir") + } + + // Make sure we ONLY delete compose artifacts. + var ( + configFound bool + goFound bool + ) + + for _, file := range files { + if file == configFile { + configFound = true + } else if strings.HasSuffix(file, ".go") || strings.HasPrefix(file, "go.") { + goFound = true + } + } + + if !configFound { + log.Info(ctx, "Not cleaning since config.json not found") + return nil + } else if goFound { + return errors.New("go files found, compose dir incorrect", z.Str("dir", dir)) + } + + log.Info(ctx, "Cleaning compose dir", z.Int("files", len(files))) + + for _, file := range files { + if strings.Contains(file, "key") { + // Do not delete root folder with key in the name, since it might be long-lived split keys folder. + log.Info(ctx, "Not deleting *key* folder", z.Str("path", file)) + continue + } + + if err := os.RemoveAll(file); err != nil { + return errors.Wrap(err, "remove file") + } + } + + return nil +} + +// noPull allows disabling pulling during unit tests. +var noPull bool + +// Define defines a compose cluster; including both keygen and running definitions. +func Define(ctx context.Context, dir string, conf Config) (TmplData, error) { + if conf.Step != stepNew { + return TmplData{}, errors.New("compose config not new, so can't be defined", z.Any("step", conf.Step)) + } + + if conf.BuildLocal { + if err := BuildLocal(ctx); err != nil { + return TmplData{}, err + } + } + + if !noPull && !conf.BuildLocal && conf.ImageTag == "latest" { + if err := pullLatest(ctx); err != nil { + return TmplData{}, err + } + } + + if !noPull && conf.UsesPluto() && conf.PlutoImageTag == "local" { + if err := BuildLocalPluto(ctx); err != nil { + return TmplData{}, err + } + } + + if conf.SplitKeysDir != "" { + if err := validateSplitKeysDir(dir, conf.SplitKeysDir); err != nil { + return TmplData{}, err + } + } + + var data TmplData + + if conf.KeyGen == KeyGenDKG { + log.Info(ctx, "Creating node*/charon-enr-private-key for ENRs required for charon create dkg") + + // charon create dkg requires operator ENRs, so we need to create p2pkeys now. + p2pkeys, err := newP2PKeys(conf.NumNodes) + if err != nil { + return TmplData{}, err + } + + var enrs []string + + for i, key := range p2pkeys { + // Best effort creation of folder, rather fail when saving p2pkey file next. + _ = os.MkdirAll(nodeFile(dir, i, ""), 0o755) + + err := k1util.Save(key, nodeFile(dir, i, "charon-enr-private-key")) + if err != nil { + return TmplData{}, errors.Wrap(err, "save charon-enr-private-key") + } + + record, err := enr.New(key) + if err != nil { + return TmplData{}, err + } + + enrs = append(enrs, record.String()) + } + + kvs := []kv{ + {"name", "compose"}, + {"num_validators", strconv.Itoa(conf.NumValidators)}, + {"operator_enrs", strings.Join(enrs, ",")}, + {"threshold", strconv.Itoa(conf.Threshold)}, + {"withdrawal_addresses", zeroAddress}, + {"fee-recipient_addresses", zeroAddress}, + {"dkg_algorithm", "frost"}, + {"output_dir", "/compose"}, + {"network", eth2util.Goerli.Name}, + } + + n := TmplNode{Image: conf.ImageOverride(conf.KeygenImpl()), EnvVars: kvs} + if conf.KeygenImpl() == ImplPluto { + n.Command = flagsCommand(cmdCreateDKG, kvs) + } + + data = TmplData{ + ComposeDir: dir, + CharonImageTag: conf.ImageTag, + CharonCommand: cmdCreateDKG, + Nodes: []TmplNode{n}, + } + } else { + // Other keygens only need a noop docker compose, since charon-compose.yml + // is used directly in their compose lock. + data = TmplData{ + ComposeDir: dir, + CharonImageTag: conf.ImageTag, + CharonEntrypoint: "echo", + CharonCommand: fmt.Sprintf("No charon commands needed for keygen=%s define step", conf.KeyGen), + Nodes: []TmplNode{{}}, + } + } + + log.Info(ctx, "Creating config.json") + + conf.Step = stepDefined + if err := WriteConfig(dir, conf); err != nil { + return TmplData{}, err + } + + if err := copyStaticFolders(dir); err != nil { + return TmplData{}, err + } + + if err := writePrometheusConfig(dir, conf); err != nil { + return TmplData{}, err + } + + log.Info(ctx, "Creating docker-compose.yml") + log.Info(ctx, "Create cluster definition: docker compose up") + + if err := WriteDockerCompose(dir, data); err != nil { + return TmplData{}, err + } + + return data, nil +} + +// validateSplitKeysDir returns an error if the split keys dir is not a child of dir. +func validateSplitKeysDir(dir string, spitKeysDir string) error { + rel, err := getRelSplitKeysDir(dir, spitKeysDir) + if err != nil { + return err + } else if strings.HasPrefix(rel, "..") { + return errors.New("split-keys-dir must be a child of compose dir", z.Str("relative", rel)) + } + + return nil +} + +// getRelSplitKeysDir returns the splitKeysDir as a relative path to dir. +func getRelSplitKeysDir(dir, splitKeysDir string) (string, error) { + if splitKeysDir == "" { + return "", nil + } + + dir, err := filepath.Abs(dir) + if err != nil { + return "", errors.Wrap(err, "abs dir") + } + + splitKeysDir, err = filepath.Abs(splitKeysDir) + if err != nil { + return "", errors.Wrap(err, "abs dir") + } + + rel, err := filepath.Rel(dir, splitKeysDir) + if err != nil { + return "", errors.Wrap(err, "relative split keys dir") + } + + return rel, nil +} + +// pullLatest pulls the latest charon docker image. +func pullLatest(ctx context.Context) error { + log.Info(ctx, "Pulling latest charon docker image") + + cmd := exec.CommandContext(ctx, "docker", "pull", charonImage+":latest") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "run docker pull") + } + + return nil +} + +// BuildLocal builds an `obolnetwork/charon:local` docker container from source. Note this requires CHARON_REPO env var. +func BuildLocal(ctx context.Context) error { + repo, ok := os.LookupEnv("CHARON_REPO") + if !ok || repo == "" { + return errors.New("cannot build local charon binary; CHARON_REPO env var, the path to the charon repo, is not set") + } + + log.Info(ctx, "Building `obolnetwork/charon:local` docker container", z.Str("repo", repo)) + + var out bytes.Buffer // Only log output if there is an error. + + cmd := exec.CommandContext(ctx, "docker", "build", "-t", "obolnetwork/charon:local", ".") + cmd.Stdout = &out + cmd.Stderr = &out + cmd.Dir = repo + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "exec docker build", z.Str("output", out.String())) + } + + return nil +} + +// BuildLocalPluto builds a `pluto:local` docker container from source. Note this requires PLUTO_REPO env var. +func BuildLocalPluto(ctx context.Context) error { + repo, ok := os.LookupEnv("PLUTO_REPO") + if !ok || repo == "" { + return errors.New("cannot build local pluto binary; PLUTO_REPO env var, the path to the pluto repo, is not set") + } + + log.Info(ctx, "Building `pluto:local` docker container", z.Str("repo", repo)) + + var out bytes.Buffer // Only log output if there is an error. + + cmd := exec.CommandContext(ctx, "docker", "build", "-t", "pluto:local", ".") + cmd.Stdout = &out + cmd.Stderr = &out + cmd.Dir = repo + + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "exec docker build", z.Str("output", out.String())) + } + + return nil +} + +// copyStaticFolders copies the embedded static folders to the compose dir. +func copyStaticFolders(dir string) error { + const staticRoot = "static" + + dirs, err := static.ReadDir(staticRoot) + if err != nil { + return errors.Wrap(err, "read dirs") + } + + for _, d := range dirs { + if !d.IsDir() { + return errors.New("static files not supported") + } + + if err := os.MkdirAll(path.Join(dir, d.Name()), 0o755); err != nil { + return errors.Wrap(err, "mkdir all") + } + + files, err := static.ReadDir(path.Join(staticRoot, d.Name())) + if err != nil { + return errors.Wrap(err, "read files") + } + + for _, f := range files { + if f.IsDir() { + return errors.New("child static dirs not supported") + } + + b, err := static.ReadFile(path.Join(staticRoot, d.Name(), f.Name())) + if err != nil { + return errors.Wrap(err, "read file") + } + + var mode os.FileMode = 0o644 + if strings.HasSuffix(f.Name(), ".sh") { + mode = 0o755 + } + + if err := os.WriteFile(path.Join(dir, d.Name(), f.Name()), b, mode); err != nil { + return errors.Wrap(err, "write file") + } + } + } + + return nil +} + +// writePrometheusConfig writes prometheus scrape configs for the actual +// cluster size, replacing the static 4-node default copied from static/. +// Unlike charon's static config, this scrapes the relay (not a non-existent +// "bootnode") and covers all NumNodes so the `up == 0` alert works. +func writePrometheusConfig(dir string, conf Config) error { + var b strings.Builder + + b.WriteString(`global: + scrape_interval: 5s + evaluation_interval: 5s + +scrape_configs: + - job_name: 'relay' + static_configs: + - targets: [ 'relay:3620' ] +`) + + for i := range conf.NumNodes { + fmt.Fprintf(&b, ` - job_name: 'node%d' + static_configs: + - targets: ['node%d:3620'] +`, i, i) + } + + b.WriteString(` +rule_files: + - /etc/prometheus/rules.yml +`) + + err := os.WriteFile(path.Join(dir, "prometheus", "prometheus.yml"), []byte(b.String()), 0o644) //nolint:gosec + if err != nil { + return errors.Wrap(err, "write prometheus.yml") + } + + return nil +} + +// keyGenFunc can be overridden in tests for deterministic p2pkeys. +var keyGenFunc = func() (*k1.PrivateKey, error) { + privkey, err := k1.GeneratePrivateKey() + if err != nil { + return nil, errors.Wrap(err, "new priv key") + } + + return privkey, nil +} + +// newP2PKeys returns a slice of newly generated secp256k1 private keys. +func newP2PKeys(n int) ([]*k1.PrivateKey, error) { + var resp []*k1.PrivateKey + + for range n { + key, err := keyGenFunc() + if err != nil { + return nil, errors.Wrap(err, "new key") + } + + resp = append(resp, key) + } + + return resp, nil +} + +// nodeFile returns the path to a file in a node folder. +func nodeFile(dir string, i int, file string) string { + return path.Join(dir, fmt.Sprintf("node%d", i), file) +} + +// WriteConfig writes the config as yaml to disk. +func WriteConfig(dir string, conf Config) error { + b, err := json.MarshalIndent(conf, "", " ") + if err != nil { + return errors.Wrap(err, "marshal config") + } + + err = os.WriteFile(path.Join(dir, configFile), b, 0o755) //nolint:gosec + if err != nil { + return errors.Wrap(err, "write config") + } + + return nil +} diff --git a/test-infra/compose/docker-compose.template b/test-infra/compose/docker-compose.template new file mode 100644 index 00000000..58f0a25a --- /dev/null +++ b/test-infra/compose/docker-compose.template @@ -0,0 +1,129 @@ +x-node-base: &node-base + image: obolnetwork/charon:{{.CharonImageTag}} + {{if .CharonEntrypoint }}entrypoint: {{.CharonEntrypoint}} + {{end -}} + command: {{.CharonCommand}} + networks: [compose] + volumes: [{{.ComposeDir}}:/compose] + {{if .Relay }}depends_on: [relay]{{end}} + +services: + {{- range $i, $node := .Nodes}} + node{{$i}}: + <<: *node-base + container_name: node{{$i}} + {{if .Image}}image: {{.Image}} + {{end -}} + {{if .Entrypoint}}entrypoint: {{.Entrypoint}} + {{end -}} + {{if .Command}}command: {{.Command}} + {{end -}} + {{- if .EnvVars}} + environment: + {{- range $node.EnvVars}} + CHARON_{{.EnvKey}}: {{.Value}} + {{- end}} + {{end -}} + {{if .Ports}} + ports: + {{- range $node.Ports}} + - "{{.External}}:{{.Internal}}" + {{end -}} + {{end -}} + {{end -}} + + {{- if .Relay }} + relay: + <<: *node-base + container_name: relay + command: relay + depends_on: [] + environment: + CHARON_HTTP_ADDRESS: 0.0.0.0:3640 + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_DATA_DIR: /compose/relay + CHARON_P2P_RELAYS: "" + CHARON_P2P_EXTERNAL_HOSTNAME: relay + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_UDP_ADDRESS: 0.0.0.0:3630 + CHARON_P2P_ADVERTISE_PRIVATE_ADDRESSES: "true" + CHARON_LOKI_ADDRESS: http://loki:3100/loki/api/v1/push + {{end -}} + + {{- range $i, $vc := .VCs}} + {{- if $vc.Label}} + vc{{$i}}-{{$vc.Label}}: + container_name: vc{{$i}}-{{$vc.Label}} + {{if $vc.Build}}build: {{$vc.Build}} + {{end -}} + {{if $vc.Image}}image: {{$vc.Image}} + {{end -}} + {{if $vc.Command}}command: {{$vc.Command}} + {{end -}} + networks: [compose] + depends_on: [node{{$i}}] + environment: + NODE: node{{$i}} + volumes: + - .:/compose + {{end -}} + {{end -}} + + {{if .Alerting}} + curl: + container_name: curl + # Can be used to curl services; e.g. docker compose exec curl curl http://prometheus:9090/api/v1/rules\?type\=alert + image: curlimages/curl:latest + command: sleep 1d + networks: [compose] + + prometheus: + container_name: prometheus + image: prom/prometheus:${PROMETHEUS_VERSION:-v2.50.1} + {{if .MonitoringPorts}}ports: + - "9090:9090" + {{end -}} + networks: [compose] + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - ./prometheus/rules.yml:/etc/prometheus/rules.yml + {{end}} + + {{if .Monitoring}} + grafana: + container_name: grafana + image: grafana/grafana:${GRAFANA_VERSION:-10.4.2} + {{if .MonitoringPorts}}ports: + - "3000:3000" + {{end -}} + networks: [compose] + volumes: + - ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml + - ./grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/datasource.yml + - ./grafana/notifiers.yml:/etc/grafana/provisioning/notifiers/notifiers.yml + - ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro + - ./grafana/dash_charon_overview.json:/etc/dashboards/dash_charon_overview.json + - ./grafana/dash_duty_details.json:/etc/dashboards/dash_duty_details.json + - ./grafana/dash_alerts.json:/etc/dashboards/dash_alerts.json + + tempo: + container_name: tempo + image: grafana/tempo:${TEMPO_VERSION:-2.7.1} + networks: [compose] + user: ":" + command: -config.file=/opt/tempo/tempo.yaml + volumes: + - ./tempo:/opt/tempo + + loki: + container_name: loki + image: grafana/loki:${LOKI_VERSION:-2.8.2} + networks: [compose] + user: ":" + command: -config.file=/opt/loki/loki.yml + volumes: + - ./loki:/opt/loki + {{end}} + +networks: + compose: diff --git a/test-infra/compose/go.mod b/test-infra/compose/go.mod new file mode 100644 index 00000000..a10d76fc --- /dev/null +++ b/test-infra/compose/go.mod @@ -0,0 +1,184 @@ +module github.com/NethermindEth/pluto/test-infra/compose + +go 1.25 + +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 + github.com/obolnetwork/charon v1.7.1 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/attestantio/go-eth2-client v0.27.1 // indirect + github.com/benbjohnson/clock v1.3.5 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/elastic/gosigar v0.14.3 // indirect + github.com/emicklei/dot v1.8.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.3 // indirect + github.com/ethereum/go-ethereum v1.16.4 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/ferranbt/fastssz v1.0.0 // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/goccy/go-yaml v1.17.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/herumi/bls-eth-go-binary v1.36.4 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/huandu/go-clone v1.7.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ipfs/go-cid v0.5.0 // indirect + github.com/ipfs/go-log/v2 v2.8.1 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/jsternberg/zap-logfmt v1.3.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/koron/go-ssdp v0.0.5 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-flow-metrics v0.2.0 // indirect + github.com/libp2p/go-libp2p v0.41.1 // indirect + github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/libp2p/go-netroute v0.2.2 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v5 v5.0.0 // indirect + github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.64 // indirect + github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect + github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr v0.16.1 // indirect + github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-multistream v0.6.0 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/ginkgo/v2 v2.23.3 // indirect + github.com/opencontainers/runtime-spec v1.2.1 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.9 // indirect + github.com/pion/interceptor v0.1.39 // indirect + github.com/pion/logging v0.2.3 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtp v1.8.18 // indirect + github.com/pion/sctp v1.8.37 // indirect + github.com/pion/sdp/v3 v3.0.11 // indirect + github.com/pion/srtp/v3 v3.0.4 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn/v4 v4.0.0 // indirect + github.com/pion/webrtc/v4 v4.0.14 // indirect + github.com/pk910/dynamic-ssz v0.0.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/protolambda/eth2-shuffle v1.1.0 // indirect + github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.50.1 // indirect + github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect + github.com/r3labs/sse/v2 v2.10.0 // indirect + github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/dig v1.18.1 // indirect + go.uber.org/fx v1.23.0 // indirect + go.uber.org/mock v0.5.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect + gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/blake3 v1.4.0 // indirect +) + +// Copied from charon v1.7.1 go.mod (Go does not propagate a dependency's replaces). +// Keep in sync when bumping the charon version. +replace github.com/coinbase/kryptology => github.com/ObolNetwork/kryptology v0.1.0 + +replace github.com/attestantio/go-eth2-client => github.com/ObolNetwork/go-eth2-client v0.27.1-obol.1 diff --git a/test-infra/compose/go.sum b/test-infra/compose/go.sum new file mode 100644 index 00000000..8f40199f --- /dev/null +++ b/test-infra/compose/go.sum @@ -0,0 +1,1022 @@ +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1 h1:f6miF8tK6H+Ktad24WpnNfpHO75GRGk0rhJ1mxPXqgA= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250121211742-6d880cc6cc8d.1/go.mod h1:rvbyamNtvJ4o3ExeCmaG5/6iHnu0vy0E+UQ+Ph0om8s= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1 h1:zgJPqo17m28+Lf5BW4xv3PvU20BnrmTcGYrog22lLIU= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250307204501-0409229c3780.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 h1:1SDs5tEGoWWv2vmKLx2B0Bp+yfhlxiU4DaZUII8+Pvs= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1/go.mod h1:o2AgVM1j3MczvxnMqfZTpiqGwK1VD4JbEagseY0QcjE= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1 h1:O1sbHpYA7yAIZpDWSEw0mNibv1gov2KH8mSzPruCNhk= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250116203702-1c024d64352b.1/go.mod h1:ee69ieBAzwc/oY/Vde0K4r6JWvrk093q4Z/FXexPMmA= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 h1:trcsXBDm8exui7mvndZnvworCyBq1xuMnod2N0j79K8= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1/go.mod h1:OUbhXurY+VHFGn9FBxcRy8UB7HXk9NvJ2qCgifOMypQ= +buf.build/go/bufplugin v0.8.0 h1:YgR1+CNGmzR69jt85oRWTa5FioZoX/tOrHV+JxfNnnk= +buf.build/go/bufplugin v0.8.0/go.mod h1:rcm0Esd3P/GM2rtYTvz3+9Gf8w9zdo7rG8dKSxYHHIE= +buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4= +buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg= +buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= +buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.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.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI= +connectrpc.com/otelconnect v0.7.2/go.mod h1:JS7XUKfuJs2adhCnXhNHPHLz6oAaZniCJdSF00OZSew= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/attestantio/go-builder-client v0.7.2 h1:bOrtysEIZd9bEM+mAeT6OtAo6LSAft/qylBLwFoFwZ0= +github.com/attestantio/go-builder-client v0.7.2/go.mod h1:+NADxbaknI5yxl+0mCkMa/VciVsesxRMGNP/poDfV08= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bufbuild/buf v1.51.0 h1:k2we7gmuSDeIqxkv16F/8s5Kk0l2ZfvMHpvC1n6o5Rk= +github.com/bufbuild/buf v1.51.0/go.mod h1:TbX4Df3BfE0Lugd3Y3sFr7QTxqmCfPkuiEexe29KZeE= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= +github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= +github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d h1:Y6Yp/LwSaRG8gw9GyyQD7jensL9NXqPlkbuulaAvCEE= +github.com/bufbuild/protovalidate-go v0.9.3-0.20250317160558-38a17488914d/go.mod h1:SZN6Qr3lPWuKMoQtIhKdhESkb+3m2vk0lqN9WMuZDDU= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGznx5A= +github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +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.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A= +github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= +github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314= +github.com/cockroachdb/redact v1.1.6/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/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= +github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= +github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= +github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= +github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/ethereum/c-kzg-4844/v2 v2.1.3 h1:DQ21UU0VSsuGy8+pcMJHDS0CV1bKmJmxsJYK8l3MiLU= +github.com/ethereum/c-kzg-4844/v2 v2.1.3/go.mod h1:fyNcYI/yAuLWJxf4uzVtS8VDKeoAaRM8G/+ADz/pRdA= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.4 h1:H6dU0r2p/amA7cYg6zyG9Nt2JrKKH6oX2utfcqrSpkQ= +github.com/ethereum/go-ethereum v1.16.4/go.mod h1:P7551slMFbjn2zOQaKrJShZVN/d8bGxp4/I6yZVlb5w= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= +github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= +github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-yaml v1.17.0 h1:JJhayi67p5LeTLh9UJYFhayPIOGDZjAqQNoEzHhYvik= +github.com/goccy/go-yaml v1.17.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +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.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/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.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= +github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= +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.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.6.0 h1:tHuViEiKFvs9TSjiisqeBQAxld1mscgF0D/czoHVV30= +github.com/graph-gophers/graphql-go v1.6.0/go.mod h1:mVu5xmLns4x/D4XH7R6bepK2bMF4I4J1BBTum2VDbWU= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hashicorp/go-bexpr v0.1.14 h1:uKDeyuOhWhT1r5CiMTjdVY4Aoxdxs6EtwgTGnlosyp4= +github.com/hashicorp/go-bexpr v0.1.14/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8= +github.com/herumi/bls-eth-go-binary v1.36.4 h1:yff41RSbfyZwfE1NF/qddP5nXhgdU0c3RGOpYOoM7YM= +github.com/herumi/bls-eth-go-binary v1.36.4/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +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.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo= +github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= +github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4= +github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/ipfs/go-log/v2 v2.8.1 h1:Y/X36z7ASoLJaYIJAL4xITXgwf7RVeqb1+/25aq/Xk0= +github.com/ipfs/go-log/v2 v2.8.1/go.mod h1:NyhTBcZmh2Y55eWVjOeKf8M7e4pnJYM3yDZNxQBWEEY= +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/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= +github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= +github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= +github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE= +github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= +github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= +github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= +github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= +github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/obolnetwork/charon v1.7.1 h1:FK5VSSFWopuPQMzTn3JY3sshreXOtIp5A1SUKTU3lG4= +github.com/obolnetwork/charon v1.7.1/go.mod h1:vzMNosnmZiM6OheKPlx7ZwmqAYVbHJsmadKx2by9Nbo= +github.com/ObolNetwork/go-eth2-client v0.27.1-obol.1 h1:bnUqEOoHVnIDXpDp8YwOUWWZuqr2xB9FGtuzqF/+rSI= +github.com/ObolNetwork/go-eth2-client v0.27.1-obol.1/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= +github.com/ObolNetwork/kryptology v0.1.0 h1:AhoG4My70+xMhEJSpVaJay/t+T/vIUNHQYLjsDJHulI= +github.com/ObolNetwork/kryptology v0.1.0/go.mod h1:/Wl7Js2f676GyXZDTaojf/O+l0fxFPWudbyjdFhkpSA= +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.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +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 v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= +github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk= +github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.39 h1:Y6k0bN9Y3Lg/Wb21JBWp480tohtns8ybJ037AGr9UuA= +github.com/pion/interceptor v0.1.39/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU= +github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= +github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI= +github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= +github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= +github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg= +github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk= +github.com/pk910/dynamic-ssz v0.0.6 h1:Tu97LSc2TtCyqRfoSbhG9XuR/FbA7CkKeAnlkgUydFY= +github.com/pk910/dynamic-ssz v0.0.6/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/protolambda/eth2-shuffle v1.1.0 h1:gixIBI84IeugTwwHXm8vej1bSSEhueBCSryA4lAKRLU= +github.com/protolambda/eth2-shuffle v1.1.0/go.mod h1:FhA2c0tN15LTC+4T9DNVm+55S7uXTTjQ8TQnBuXlkF8= +github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= +github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= +github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w= +github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +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/showwin/speedtest-go v1.7.10 h1:9o5zb7KsuzZKn+IE2//z5btLKJ870JwO6ETayUkqRFw= +github.com/showwin/speedtest-go v1.7.10/go.mod h1:Ei7OCTmNPdWofMadzcfgq1rUO7mvJy9Jycj//G7vyfA= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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.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.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vektra/mockery/v2 v2.53.3 h1:yBU8XrzntcZdcNRRv+At0anXgSaFtgkyVUNm3f4an3U= +github.com/vektra/mockery/v2 v2.53.3/go.mod h1:hIFFb3CvzPdDJJiU7J4zLRblUMv7OuezWsHPmswriwo= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/wealdtech/go-eth2-types/v2 v2.8.2 h1:b5aXlNBLKgjAg/Fft9VvGlqAUCQMP5LzYhlHRrr4yPg= +github.com/wealdtech/go-eth2-types/v2 v2.8.2/go.mod h1:IAz9Lz1NVTaHabQa+4zjk2QDKMv8LVYo0n46M9o/TXw= +github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1 h1:9j7bpwjT9wmwBb54ZkBhTm1uNIlFFcCJXefd/YskZPw= +github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4 v1.4.1/go.mod h1:+tI1VD76E1WINI+Nstg7RVGpUolL5ql10nu2YztMO/4= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.11.0 h1:yX9+FfUXvPDvZ8Q5bhF+64AWrQwh4a3/HpfTx99DnZc= +github.com/wealdtech/go-eth2-wallet-types/v2 v2.11.0/go.mod h1:UVP9YFcnPiIzHqbmCMW3qrQ3TK5FOqr1fmKqNT9JGr8= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +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.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI= +go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw= +go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg= +go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ= +go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo= +go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es= +go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +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-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/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-20200813134508-3edf25e44fcc/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-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +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-20190227155943-e225da77a7e6/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-20210220032951-036812b2e83c/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20181029174526-d69651ed3497/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +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.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/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= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= +lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= +pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo= +pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/test-infra/compose/lock.go b/test-infra/compose/lock.go new file mode 100644 index 00000000..f35a52ee --- /dev/null +++ b/test-infra/compose/lock.go @@ -0,0 +1,184 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "strconv" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" + "github.com/obolnetwork/charon/eth2util" +) + +// Lock creates a docker-compose.yml from a charon-compose.yml for generating keys and a cluster lock file. +func Lock(ctx context.Context, dir string, conf Config) (TmplData, error) { + if conf.Step != stepDefined { + return TmplData{}, errors.New("compose config not defined, so can't be locked", z.Any("step", conf.Step)) + } + + var data TmplData + + switch conf.KeyGen { + case KeyGenCreate: + splitKeysDir, err := getRelSplitKeysDir(dir, conf.SplitKeysDir) + if err != nil { + return TmplData{}, err + } else if splitKeysDir != "" { + splitKeysDir = path.Join("/compose", splitKeysDir) + } + + // Only single node to call charon create cluster generate keys + kvs := []kv{ + {"name", fmt.Sprintf("compose-%d-%d", conf.NumNodes, conf.NumValidators)}, + {"threshold", strconv.Itoa(conf.Threshold)}, + {"nodes", strconv.Itoa(conf.NumNodes)}, + {"cluster-dir", "/compose"}, + {"split-existing-keys", fmt.Sprintf(`"%v"`, conf.SplitKeysDir != "")}, + {"split-keys-dir", splitKeysDir}, + {"num-validators", strconv.Itoa(conf.NumValidators)}, + {"insecure-keys", fmt.Sprintf(`"%v"`, conf.InsecureKeys)}, + {"withdrawal-addresses", zeroAddress}, + {"fee-recipient-addresses", zeroAddress}, + {"network", eth2util.Goerli.Name}, + } + + n := TmplNode{Image: conf.ImageOverride(conf.KeygenImpl()), EnvVars: kvs} + if conf.KeygenImpl() == ImplPluto { + n.Command = flagsCommand(cmdCreateCluster, kvs) + } + + data = TmplData{ + ComposeDir: dir, + CharonImageTag: conf.ImageTag, + CharonCommand: cmdCreateCluster, + Nodes: []TmplNode{n}, + } + case KeyGenDKG: + var nodes []TmplNode + for i := range conf.NumNodes { + n := TmplNode{ + EnvVars: newNodeEnvs(i, conf, ""), + Image: conf.ImageOverride(conf.NodeImpl(i)), + Command: cmdDKG, + } + nodes = append(nodes, n) + } + + data = TmplData{ + ComposeDir: dir, + CharonImageTag: conf.ImageTag, + CharonCommand: "not used", + Relay: true, + Nodes: nodes, + } + default: + return TmplData{}, errors.New("unsupported keygen", z.Any("keygen", conf.KeyGen)) + } + + log.Info(ctx, "Creating docker-compose.yml") + log.Info(ctx, "Create keys and cluster lock with: docker compose up") + + conf.Step = stepLocked + if err := WriteConfig(dir, conf); err != nil { + return TmplData{}, err + } + + if err := WriteDockerCompose(dir, data); err != nil { + return TmplData{}, err + } + + return data, nil +} + +// newNodeEnvs returns the default node environment variable to run a charon docker container. +func newNodeEnvs(index int, conf Config, vcType VCType) []kv { + beaconMock := false + + beaconNode := conf.BeaconNodes + if beaconNode == "mock" { + beaconMock = true + beaconNode = "" + } + + lockFile := fmt.Sprintf("/compose/node%d/cluster-lock.json", index) + + // The path-less URL form (multiaddrs response) instead of charon-compose's + // /enr path: pluto's relay parsing roundtrips URLs through a multiaddr, + // which cannot represent a URL path. Charon supports both forms. + //nolint:revive // tls not required for testing. + p2pRelayAddr := "http://relay:3640" + if conf.ExternalRelay != "" { + p2pRelayAddr = conf.ExternalRelay + } + + // Common config + kvs := []kv{ + {"private-key-file", fmt.Sprintf("/compose/node%d/charon-enr-private-key", index)}, + {"monitoring-address", "0.0.0.0:3620"}, + {"p2p-external-hostname", fmt.Sprintf("node%d", index)}, + {"p2p-tcp-address", "0.0.0.0:3610"}, + {"p2p-relays", p2pRelayAddr}, + {"log-level", "debug"}, + {"log-color", "force"}, + {"feature-set", conf.FeatureSet}, + } + + if conf.Step == stepDefined { + // Define lock config + return append(kvs, + kv{"data-dir", fmt.Sprintf("/compose/node%d", index)}, + kv{"definition-file", "/compose/cluster-definition.json"}, + kv{"insecure-keys", fmt.Sprintf(`"%v"`, conf.InsecureKeys)}, + ) + } + + // Define run config + kvs = append(kvs, + kv{"lock-file", lockFile}, + kv{"validator-api-address", "0.0.0.0:3600"}, + kv{"beacon-node-endpoints", beaconNode}, + kv{"simnet-beacon_mock", fmt.Sprintf(`"%v"`, beaconMock)}, + kv{"simnet-validator-mock", fmt.Sprintf(`"%v"`, vcType == VCMock)}, + kv{"simnet-slot-duration", conf.SlotDuration.String()}, + kv{"simnet-validator-keys-dir", fmt.Sprintf("/compose/node%d/validator_keys", index)}, + kv{"simnet-beacon-mock-fuzz", fmt.Sprintf(`"%v"`, conf.BeaconFuzz)}, + kv{"synthetic-block-proposals", fmt.Sprintf(`"%v"`, conf.SyntheticBlockProposals)}, + kv{"builder-api", fmt.Sprintf(`"%v"`, conf.BuilderAPI)}, + ) + + // Unlike charon's compose, only point nodes at loki/tempo when the + // monitoring stack actually runs: failed pushes to absent services are + // logged as errors, tripping the Error Log Rate alert. + if conf.Monitoring { + //nolint:revive // tls not required for testing. + kvs = append(kvs, + kv{"otlp-address", "tempo:4317"}, + kv{"otlp-service-name", fmt.Sprintf("node%d", index)}, + kv{"loki-addresses", "http://loki:3100/loki/api/v1/push"}, + kv{"loki-service", fmt.Sprintf("node%d", index)}, + ) + } + + return kvs +} + +// LoadConfig returns the config loaded from disk. +func LoadConfig(dir string) (Config, error) { + b, err := os.ReadFile(path.Join(dir, configFile)) + if err != nil { + return Config{}, errors.Wrap(err, "load config") + } + + var resp Config + if err := json.Unmarshal(b, &resp); err != nil { + return Config{}, errors.Wrap(err, "unmarshal Config") + } + + return resp, nil +} diff --git a/test-infra/compose/new.go b/test-infra/compose/new.go new file mode 100644 index 00000000..ac1e17a7 --- /dev/null +++ b/test-infra/compose/new.go @@ -0,0 +1,27 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "context" + "fmt" + + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" +) + +// New creates a new compose config file from flags. +func New(ctx context.Context, dir string, conf Config) error { + if err := Clean(ctx, dir); err != nil { + return err + } + + conf.Step = stepNew + + log.Info(ctx, "Writing config to compose dir", + z.Str("dir", dir), + z.Str("config", fmt.Sprintf("%#v", conf)), + ) + + return WriteConfig(dir, conf) +} diff --git a/test-infra/compose/new_test.go b/test-infra/compose/new_test.go new file mode 100644 index 00000000..992309b7 --- /dev/null +++ b/test-infra/compose/new_test.go @@ -0,0 +1,29 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose_test + +import ( + "context" + "os" + "path" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/NethermindEth/pluto/test-infra/compose" + "github.com/obolnetwork/charon/testutil" +) + +//go:generate go test . -update -clean + +func TestNewDefaultConfig(t *testing.T) { + dir := t.TempDir() + + err := compose.New(context.Background(), dir, compose.NewDefaultConfig()) + require.NoError(t, err) + + conf, err := os.ReadFile(path.Join(dir, "config.json")) + require.NoError(t, err) + + testutil.RequireGoldenBytes(t, conf) +} diff --git a/test-infra/compose/run.go b/test-infra/compose/run.go new file mode 100644 index 00000000..bf4c3375 --- /dev/null +++ b/test-infra/compose/run.go @@ -0,0 +1,139 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "bytes" + "context" + "fmt" + "text/template" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/log" + "github.com/obolnetwork/charon/app/z" +) + +// Run creates a docker-compose.yml from config.json to run the cluster. +func Run(ctx context.Context, dir string, conf Config) (TmplData, error) { + if conf.Step != stepLocked { + return TmplData{}, errors.New("compose config not locked, so can't be run", z.Any("step", conf.Step)) + } + + var ( + nodes []TmplNode + vcs []TmplVC + ) + + for i := range conf.NumNodes { + typ := conf.VCs[i%len(conf.VCs)] + + vc, err := getVC(typ, i, conf.NumValidators, conf.InsecureKeys, conf.BuilderAPI) + if err != nil { + return TmplData{}, err + } + + vcs = append(vcs, vc) + + n := TmplNode{EnvVars: newNodeEnvs(i, conf, typ), Image: conf.ImageOverride(conf.NodeImpl(i))} + if !conf.DisableMonitoringPorts { + for _, p := range charonPorts { + p.External += 10000 * i + n.Ports = append(n.Ports, p) + } + } + + nodes = append(nodes, n) + } + + charonCmd := cmdRun + + if conf.P2PFuzz { + nodes[0].EnvVars = append(nodes[0].EnvVars, kv{"p2p-fuzz", fmt.Sprintf(`"%v"`, conf.P2PFuzz)}) + charonCmd = cmdUnsafeRun + } + + data := TmplData{ + ComposeDir: dir, + CharonImageTag: conf.ImageTag, + CharonCommand: charonCmd, + Nodes: nodes, + Relay: true, + Monitoring: conf.Monitoring, + Alerting: true, + MonitoringPorts: !conf.DisableMonitoringPorts, + VCs: vcs, + } + + log.Info(ctx, "Created docker-compose.yml") + log.Info(ctx, "Run the cluster with: docker compose up") + + if err := WriteDockerCompose(dir, data); err != nil { + return TmplData{}, err + } + + return data, nil +} + +// getVC returns the validator client template data for the provided type and index. +func getVC(typ VCType, nodeIdx int, numVals int, insecure, builderAPI bool) (TmplVC, error) { + vcByType := map[VCType]TmplVC{ + VCVouch: { + Label: string(VCVouch), + Build: "vouch", + }, + VCLighthouse: { + Label: string(VCLighthouse), + Build: "lighthouse", + }, + VCLodestar: { + Label: string(VCLodestar), + Build: "lodestar", + }, + VCTeku: { + Label: string(VCTeku), + Image: "consensys/teku:latest", + Command: `| + validator-client + --network=auto + --beacon-node-api-endpoint="http://node{{.NodeIdx}}:3600" + {{range .TekuKeys}}--validator-keys="{{.}}" + {{end -}} + --validators-proposer-default-fee-recipient="0x0000000000000000000000000000000000000000" + --validators-proposer-blinded-blocks-enabled={{.BuilderAPI}}`, + }, + } + + resp := vcByType[typ] + if typ == VCTeku { + var keys []string + + for i := range numVals { + if insecure { + keys = append(keys, fmt.Sprintf("/compose/node%d/validator_keys/keystore-insecure-%d.json:/compose/node%d/validator_keys/keystore-insecure-%d.txt", nodeIdx, i, nodeIdx, i)) + } else { + keys = append(keys, fmt.Sprintf("/compose/node%d/validator_keys/keystore-%d.json:/compose/node%d/validator_keys/keystore-%d.txt", nodeIdx, i, nodeIdx, i)) + } + } + + data := struct { + TekuKeys []string + NodeIdx int + BuilderAPI bool + }{ + NodeIdx: nodeIdx, + TekuKeys: keys, + BuilderAPI: builderAPI, + } + + var buf bytes.Buffer + + err := template.Must(template.New("").Parse(resp.Command)).Execute(&buf, data) + if err != nil { + return TmplVC{}, errors.Wrap(err, "teku template") + } + + resp.Command = buf.String() + } + + return resp, nil +} diff --git a/test-infra/compose/smoke/smoke_test.go b/test-infra/compose/smoke/smoke_test.go new file mode 100644 index 00000000..c5059435 --- /dev/null +++ b/test-infra/compose/smoke/smoke_test.go @@ -0,0 +1,226 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package smoke_test + +import ( + "context" + "flag" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/obolnetwork/charon/testutil" + + "github.com/NethermindEth/pluto/test-infra/compose" +) + +//go:generate go test . -run=TestSmoke -integration -v + +var ( + integration = flag.Bool("integration", false, "Enable docker based integration test") + plutoRun = flag.Bool("pluto-run", false, "Enable scenarios that run pluto nodes. Requires `pluto run` support and the PLUTO_REPO env var.") + sudoPerms = flag.Bool("sudo-perms", false, "Enables changing all compose artefacts file permissions using sudo.") + logDir = flag.String("log-dir", "", "Specifies the directory to store test docker-compose logs. Empty defaults to stdout.") +) + +// charonImageTag pins the charon reference version pluto is ported from. +const charonImageTag = "v1.7.1" + +func TestSmoke(t *testing.T) { + if !*integration { + t.Skip("Skipping smoke integration test") + } + + const defaultTimeout = time.Minute + + tests := []struct { + Name string + ConfigFunc func(*compose.Config) + RunTmplFunc func(*compose.TmplData) + DefineTmplFunc func(*compose.TmplData) + PrintYML bool + Timeout time.Duration + RequirePluto bool // Scenario needs the pluto docker image (PLUTO_REPO env var). + RequirePlutoRun bool // Scenario runs pluto nodes, which requires `pluto run` support (-pluto-run flag). + }{ + { + Name: "default_alpha", + PrintYML: true, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.FeatureSet = "alpha" + }, + }, + { + Name: "default_beta", + ConfigFunc: func(conf *compose.Config) { + conf.NumNodes = 3 + conf.Threshold = 2 + conf.KeyGen = compose.KeyGenCreate + conf.FeatureSet = "beta" + }, + }, + { + Name: "default_stable", + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.FeatureSet = "stable" + }, + }, + { + Name: "dkg", + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenDKG + conf.VCs = []compose.VCType{compose.VCMock} + }, + }, + { + Name: "very_large", + ConfigFunc: func(conf *compose.Config) { + conf.NumNodes = 10 + conf.Threshold = 7 + conf.NumValidators = 100 + conf.KeyGen = compose.KeyGenCreate + conf.VCs = []compose.VCType{compose.VCMock} + conf.SlotDuration = time.Second * 6 + conf.SyntheticBlockProposals = false + }, + Timeout: time.Minute * 2, + }, + { + Name: "1_of_4_down", + RunTmplFunc: func(data *compose.TmplData) { + node0 := data.Nodes[0] + for i := range len(node0.EnvVars) { + if strings.HasPrefix(node0.EnvVars[i].Key, "p2p") { + data.Nodes[0].EnvVars[i].Key = node0.EnvVars[i].Key + "-unset" // Zero p2p flags to it cannot communicate + } + } + }, + }, + { + Name: "1_of_3_down", + ConfigFunc: func(conf *compose.Config) { + conf.NumNodes = 3 + conf.Threshold = 2 + }, + RunTmplFunc: func(data *compose.TmplData) { + node0 := data.Nodes[0] + for i := range len(node0.EnvVars) { + if strings.HasPrefix(node0.EnvVars[i].Key, "p2p") { + data.Nodes[0].EnvVars[i].Key = node0.EnvVars[i].Key + "-unset" // Zero p2p flags to it cannot communicate + } + } + }, + }, + { + Name: "blinded_blocks_vmock", + ConfigFunc: func(conf *compose.Config) { + conf.BuilderAPI = true + }, + }, + { + // Pluto generates the keys and cluster lock, charon nodes run them. + // Validates pluto `create cluster` artifacts against the charon runtime. + Name: "pluto_keygen_create", + RequirePluto: true, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.KeyGenImpl = compose.ImplPluto + conf.VCs = []compose.VCType{compose.VCMock} + }, + }, + { + Name: "all_pluto", + RequirePluto: true, + RequirePlutoRun: true, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.NodeImpls = []compose.NodeImpl{compose.ImplPluto} + conf.VCs = []compose.VCType{compose.VCMock} + // `pluto run` fails fast on --synthetic-block-proposals. + conf.SyntheticBlockProposals = false + }, + }, + { + // Threshold 3 of 4 forces both implementations to participate in every duty. + Name: "mixed_2_charon_2_pluto", + RequirePluto: true, + RequirePlutoRun: true, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.NodeImpls = []compose.NodeImpl{ + compose.ImplCharon, compose.ImplCharon, + compose.ImplPluto, compose.ImplPluto, + } + conf.VCs = []compose.VCType{compose.VCMock} + // `pluto run` fails fast on --synthetic-block-proposals. + conf.SyntheticBlockProposals = false + }, + }, + { + Name: "pluto_dkg", + RequirePluto: true, + RequirePlutoRun: true, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenDKG + conf.NodeImpls = []compose.NodeImpl{compose.ImplPluto} + conf.VCs = []compose.VCType{compose.VCMock} + // `pluto run` fails fast on --synthetic-block-proposals. + conf.SyntheticBlockProposals = false + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + if test.RequirePluto && os.Getenv("PLUTO_REPO") == "" { + t.Skip("Skipping pluto scenario since PLUTO_REPO env var is not set") + } + + if test.RequirePlutoRun && !*plutoRun { + t.Skip("Skipping scenario running pluto nodes; enable with -pluto-run once `pluto run` is supported") + } + + dir := t.TempDir() + + conf := compose.NewDefaultConfig() + conf.Monitoring = false + conf.DisableMonitoringPorts = true + conf.ImageTag = charonImageTag + + conf.InsecureKeys = true + if test.ConfigFunc != nil { + test.ConfigFunc(&conf) + } + + require.NoError(t, compose.WriteConfig(dir, conf)) + + os.Args = []string{"cobra.test"} + + if test.Timeout == 0 { + test.Timeout = defaultTimeout + } + + autoConfig := compose.AutoConfig{ + Dir: dir, + AlertTimeout: test.Timeout, + SudoPerms: *sudoPerms, + PrintYML: test.PrintYML, + RunTmplFunc: test.RunTmplFunc, + DefineTmplFunc: test.DefineTmplFunc, + } + + if *logDir != "" { + autoConfig.LogFile = path.Join(*logDir, test.Name+".log") + } + + err := compose.Auto(context.Background(), autoConfig) + testutil.RequireNoError(t, err) + }) + } +} diff --git a/test-infra/compose/static/grafana/dash_alerts.json b/test-infra/compose/static/grafana/dash_alerts.json new file mode 100644 index 00000000..d0f61654 --- /dev/null +++ b/test-infra/compose/static/grafana/dash_alerts.json @@ -0,0 +1,812 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0m", + "frequency": "15s", + "handler": 1, + "name": "Error Log Rate alert", + "noDataState": "ok", + "notifications": [] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "increase(app_log_error_total[30s]) ", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "op": "gt", + "value": 5, + "visible": true + } + ], + "title": "Error Log Rate", + "type": "timeseries" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0", + "frequency": "15s", + "handler": 1, + "name": "Warn Log Rate alert", + "noDataState": "ok", + "notifications": [] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 9, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "increase(app_log_warn_total[30s]) ", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "op": "gt", + "value": 5, + "visible": true + } + ], + "title": "Warn Log Rate", + "type": "timeseries" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 1 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0", + "frequency": "15s", + "handler": 1, + "name": "Validator API Error Rate alert", + "noDataState": "ok", + "notifications": [] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 0, + "y": 6 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "increase(core_validatorapi_request_error_total{endpoint!=\"proxy\"}[30s]) ", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "op": "gt", + "value": 1, + "visible": true + } + ], + "title": "Validator API Error Rate", + "type": "timeseries" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 5 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0s", + "frequency": "15s", + "handler": 1, + "name": "Proxy API Error Rate alert", + "noDataState": "ok", + "notifications": [] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 9, + "y": 6 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "increase(core_validatorapi_request_error_total{endpoint=\"proxy\"}[30s]) ", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "op": "gt", + "value": 5, + "visible": true + } + ], + "title": "Proxy API Error Rate", + "type": "timeseries" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 0.5 + ], + "type": "lt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0s", + "frequency": "30s", + "handler": 1, + "name": "Broadcast Duty Rate alert", + "noDataState": "ok", + "notifications": [] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 0, + "y": 12 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "increase(core_bcast_broadcast_total[30s])", + "interval": "", + "legendFormat": "{{job}} - {{type}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "op": "lt", + "value": 0.5, + "visible": true + } + ], + "title": "Broadcast Duty Rate", + "type": "timeseries" + }, + { + "alert": { + "alertRuleTags": {}, + "conditions": [ + { + "evaluator": { + "params": [ + 40 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "max" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "0s", + "frequency": "15s", + "handler": 1, + "name": "Outstanding Duty Count alert", + "noDataState": "ok", + "notifications": [] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 9, + "y": 12 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "exemplar": true, + "expr": "core_bcast_broadcast_total - core_scheduler_duty_total", + "interval": "", + "legendFormat": "{{job}} - {{type}}", + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "op": "gt", + "value": 40, + "visible": true + } + ], + "title": "Outstanding Duty Count", + "type": "timeseries" + } + ], + "schemaVersion": 35, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Alerts", + "uid": "a9mqsmrnk", + "version": 1, + "weekStart": "" +} diff --git a/test-infra/compose/static/grafana/dash_charon_overview.json b/test-infra/compose/static/grafana/dash_charon_overview.json new file mode 100644 index 00000000..f8882d7d --- /dev/null +++ b/test-infra/compose/static/grafana/dash_charon_overview.json @@ -0,0 +1,3104 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": false, + "title": "Explore Charon Logs", + "tooltip": "Navigate to Charon Logs Explorer", + "type": "link", + "url": "/explore?orgId=1&left=%7B%22datasource%22:%22loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22expr%22:%22%7Bcluster_name%3D%5C%22$cluster_name%5C%22,%20cluster_hash%3D%5C%22$cluster_hash%5C%22,%20cluster_peer%3D%5C%22$cluster_peer%5C%22%7D%20%7C%20logfmt%20%7C%20line_format%20%60%7B%7B.pretty%7D%7D%60%22,%22queryType%22:%22range%22,%22datasource%22:%7B%22type%22:%22loki%22,%22uid%22:%22loki%22%7D,%22editorMode%22:%22builder%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "cluster-labels" + ], + "targetBlank": false, + "title": "New link", + "tooltip": "", + "type": "dashboards", + "url": "" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "json-view" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 0, + "y": 0 + }, + "id": 19, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": false + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(\n sum(app_git_commit{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (git_hash)\n)\n + on(job) group_left(version)\n(\n 0 * sum(app_version{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (version)\n)\n + on(job) group_left(peer_name)\n(\n 0 * sum(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer_name)\n)\n + on(job) group_left(network)\n(\n 0 * sum(cluster_network{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (network)\n)\n + on(job) group_left(cluster_hash)\n(\n 0 * sum(cluster_operators{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (cluster_hash)\n) \n + on(job) group_left(cluster_name)\n(\n 0 * sum(cluster_operators{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (cluster_name)\n) ", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "range": false, + "refId": "B" + } + ], + "title": "App Info", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "mode": "rows" + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "mappings": [], + "max": 10, + "min": 0, + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "P2P Reachability" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "color": "text", + "index": 0, + "text": "Unknown" + }, + "1": { + "color": "green", + "index": 1, + "text": "Public" + }, + "2": { + "color": "yellow", + "index": 2, + "text": "Private" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Uptime" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "decimals", + "value": 1 + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 5, + "y": 0 + }, + "id": 28, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "text": { + "titleSize": 15, + "valueSize": 15 + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "time() - max(app_start_time_secs{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "hide": false, + "instant": true, + "legendFormat": "Uptime", + "range": false, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "core_scheduler_current_slot{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "format": "time_series", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "Slot", + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "core_scheduler_current_epoch{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "legendFormat": "Epoch", + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "core_scheduler_validators_active{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "legendFormat": "Validators Active", + "range": false, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "hide": false, + "instant": true, + "legendFormat": "Charon Peers Connected", + "range": false, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(p2p_relay_connections{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) ", + "hide": false, + "instant": true, + "legendFormat": "Relays Connected", + "range": false, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "app_beacon_node_peers{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "legendFormat": "Beacon Node Peers", + "range": false, + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "p2p_reachability_status{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "legendFormat": "P2P Reachability", + "range": false, + "refId": "F" + } + ], + "title": "Current Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Node is healthy if Beacon Node connected and synced and at least quorum peers are connected.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "text", + "index": 1, + "text": "Unknown" + }, + "1": { + "color": "green", + "index": 0, + "text": "OK" + }, + "2": { + "color": "red", + "index": 3, + "text": "BeaconNode Down" + }, + "3": { + "color": "orange", + "index": 4, + "text": "BeaconNode Syncing" + }, + "4": { + "color": "orange", + "index": 5, + "text": "Insufficient Peers" + }, + "5": { + "color": "orange", + "index": 6, + "text": "VC not connected" + }, + "6": { + "color": "orange", + "index": 7, + "text": "VC missing some validators" + }, + "7": { + "color": "orange", + "index": 8, + "text": "BeaconNode has zero peers" + }, + "8": { + "color": "orange", + "index": 9, + "text": "BeaconNode far behind" + } + }, + "type": "value" + }, + { + "options": { + "match": "null", + "result": { + "color": "text", + "index": 2, + "text": "Dead" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 9, + "y": 0 + }, + "id": 76, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "interval": "", + "legendFormat": "", + "range": false, + "refId": "A" + } + ], + "title": "Healthy", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 0.2, + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.15 + }, + { + "color": "orange", + "value": 0.3 + }, + { + "color": "red", + "value": 0.5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 11, + "x": 13, + "y": 0 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(p2p_ping_latency_secs_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (le,peer)) ", + "interval": "", + "legendFormat": "{{peer}}", + "refId": "A" + } + ], + "title": "Peer ping latency (90%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Information about the charon cluster", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 9, + "y": 3 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": { + "titleSize": 15, + "valueSize": 15 + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "cluster_operators{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "instant": true, + "legendFormat": "Operators", + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "cluster_threshold{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "legendFormat": "Threshold", + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "cluster_validators{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "hide": false, + "instant": true, + "legendFormat": "Validators", + "range": false, + "refId": "C" + } + ], + "title": "Cluster Info", + "transformations": [], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "- **Peer**: The peer's name (inferred from their *charon-enr-private-key*)\n- **You**: ⭐️ is this local charon node\n- **Connected**: Whether you are currently connected to this peer.\n- **Direct**: Whether the connection is *direct* (👍) or *relay* (👎) \n- **Latency**: The time messages take to travel to/from the peer.\n- **Uptime**\" The duration since peer was restarted.\n- **ClockDiff**: Difference between local and peer's clock time. More than 2s is bad.\n- **Attest**: Number of attestation duties performed in the time window.\n- **PrepareAgg**: Number of aggregation preparation duties performed in the time window.\n- **Aggregate**: Number of aggregations duties performed in the time window.\n- **Propose**: Number of block proposal duties performed in the time window.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "inspect": false, + "minWidth": 0, + "width": 90 + }, + "decimals": 0, + "mappings": [], + "noValue": "--", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Latency" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 150 + }, + "type": "range" + }, + { + "options": { + "from": 150, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 300 + }, + "type": "range" + }, + { + "options": { + "from": 300, + "result": { + "color": "orange", + "index": 2 + }, + "to": 500 + }, + "type": "range" + }, + { + "options": { + "from": 500, + "result": { + "color": "red", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "lcd", + "type": "gauge" + } + }, + { + "id": "custom.width", + "value": 200 + }, + { + "id": "max", + "value": 700 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "You" + }, + "properties": [ + { + "id": "noValue", + "value": "⭐" + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": "--" + }, + "1": { + "index": 1, + "text": "--" + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.width", + "value": 60 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Connected" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": " ❌" + }, + "1": { + "index": 1, + "text": "✅" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ClockDiff" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": "🆗" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Direct" + }, + "properties": [ + { + "id": "custom.width", + "value": 55 + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": "--" + } + }, + "type": "value" + }, + { + "options": { + "from": 1, + "result": { + "index": 1, + "text": "👎" + }, + "to": 9 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "index": 2, + "text": "👍" + }, + "to": 1000 + }, + "type": "range" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Peer" + }, + "properties": [ + { + "id": "custom.width", + "value": 150 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Uptime" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Index" + }, + "properties": [ + { + "id": "custom.width", + "value": 30 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Builder API" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": " ❌" + }, + "1": { + "index": 1, + "text": "✅" + } + }, + "type": "value" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 54, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "You" + } + ] + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.90, sum(rate(p2p_ping_latency_secs_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (le,peer)) * 1000", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "\nsum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"attester\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"proposer\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"randao\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "", + "range": false, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"prepare_aggregator\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"aggregator\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"exit\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "I" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(round(app_peerinfo_clock_offset_seconds{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"},0.1)*1000) by (peer) ", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "J" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(max(p2p_peer_connection_types{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",type=\"direct\"}) by (peer)*10 + \nmax(p2p_peer_connection_types{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",type=\"relay\"}) by (peer))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "", + "range": false, + "refId": "K" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(app_peerinfo_version{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer,version) ", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "L" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_peerinfo_git_commit{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer,git_hash) > 0", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "M" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"sync_message\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "N" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"sync_contribution\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "O" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"prepare_sync_contribution\"}[$__range])) by (peer) > 0", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "P" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "time() - max(app_peerinfo_start_time_secs{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} > 0) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "Q" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_peerinfo_index{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "R" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_peerinfo_builder_api_enabled{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "S", + "useBackend": false + } + ], + "title": "Peer Connectivity and Duty Participation", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "peer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 10": true, + "Time 11": true, + "Time 12": true, + "Time 13": true, + "Time 14": true, + "Time 15": true, + "Time 16": true, + "Time 17": true, + "Time 2": true, + "Time 3": true, + "Time 4": true, + "Time 5": true, + "Time 6": true, + "Time 7": true, + "Time 8": true, + "Time 9": true, + "Value #C": true, + "Value #D": true, + "Value #E": true, + "Value #F": false, + "Value #G": true, + "Value #L": true, + "Value #M": true, + "Value #N": true, + "Value #P": true + }, + "indexByName": { + "Time 1": 11, + "Time 10": 28, + "Time 11": 29, + "Time 12": 30, + "Time 13": 31, + "Time 14": 32, + "Time 15": 33, + "Time 16": 34, + "Time 2": 13, + "Time 3": 14, + "Time 4": 15, + "Time 5": 16, + "Time 6": 17, + "Time 7": 18, + "Time 8": 19, + "Time 9": 27, + "Value #A": 3, + "Value #B": 5, + "Value #C": 21, + "Value #D": 22, + "Value #E": 23, + "Value #F": 2, + "Value #G": 24, + "Value #J": 6, + "Value #K": 4, + "Value #L": 12, + "Value #M": 20, + "Value #N": 25, + "Value #P": 26, + "Value #Q": 9, + "Value #R": 1, + "Value #S": 10, + "git_hash": 8, + "peer": 0, + "version": 7 + }, + "renameByName": { + "Value #A": "Connected", + "Value #B": "Latency", + "Value #C": "Attest", + "Value #D": "Propose", + "Value #E": "Randao", + "Value #F": "You", + "Value #G": "PrepareAgg", + "Value #H": "Aggregate", + "Value #I": "Exit", + "Value #J": "ClockDiff", + "Value #K": "Direct", + "Value #L": "", + "Value #N": "SyncMsg", + "Value #O": "SyncContrib", + "Value #P": "PrepareContrib", + "Value #Q": "Uptime", + "Value #R": "Index", + "Value #S": "Builder API", + "git_hash": "GitCommit", + "peer": "Peer", + "version": "Version" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 100, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 0 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 7, + "options": { + "colWidth": 0.5, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.43, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "increase(core_bcast_broadcast_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval]) > 0", + "instant": false, + "interval": "", + "legendFormat": "{{duty}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(clamp_max(increase(core_scheduler_current_epoch{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval]), 0.2)) > 0", + "hide": false, + "interval": "1m", + "legendFormat": "epoch boundry", + "range": true, + "refId": "B" + } + ], + "title": "Completed duties by type ", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Failed duties by type over time. Use data link to show logs.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 100, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 0 + }, + "links": [ + { + "targetBlank": true, + "title": "Show in log explorer", + "url": "/explore?orgId=1&left=%7B%22datasource%22:%22loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22builder%22,%22expr%22:%22%7Bcompose_service%3D%5C%22$job%5C%22%7D%20%7C%3D%20%60Duty%20failed%60%20%7C%3D%20%60${__field.labels.duty}%60%20%7C%20logfmt%20%7C%20line_format%20%60%7B%7B.pretty%7D%7D%60%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 67, + "options": { + "colWidth": 0.5, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.43, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_failed_duties_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (duty) > 0", + "instant": false, + "interval": "1m", + "legendFormat": "{{duty}}", + "range": true, + "refId": "A" + } + ], + "title": "Failed duties by type ", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Most frequent warnings and errors grouped by topic. ", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [ + { + "targetBlank": true, + "title": "Show in log explorer", + "url": "/explore?orgId=1&left=%7B%22datasource%22:%22loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22builder%22,%22expr%22:%22%7Bcompose_service%3D%5C%22$job%5C%22%7D%20%7C%3D%20%60${__field.labels.msg}%60%20%7C%20logfmt%20%7C%20line_format%20%60%7B%7B.pretty%7D%7D%60%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D" + } + ], + "mappings": [], + "min": 0, + "noValue": "No warnings", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 20 + }, + "id": 68, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "none", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "max(increase(app_log_warn_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (topic) > 0", + "legendFormat": "{{topic}}", + "queryType": "range", + "range": true, + "refId": "A", + "resolution": 10 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(increase(app_log_error_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (topic) > 0", + "hide": false, + "legendFormat": "{{topic}}", + "range": true, + "refId": "B" + } + ], + "title": "Top Warns and Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Average beacon node score for the whole time window.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 0.5 + }, + { + "color": "green", + "value": 0.8 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 12, + "y": 20 + }, + "id": 70, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "(clamp(app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 1 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 4 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 5 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 6, 1, 1) OR on() vector(0))\n*\n(\n (\n0.5 * (1.0 - 10*(sum(increase(app_eth2_errors_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__range])) / (sum(increase(app_eth2_latency_seconds_count{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__range]))))) OR on() vector(0.5)\n )\n +\n (\n 0.5 * (1.0 - clamp_max(histogram_quantile(0.99, sum(rate(app_eth2_latency_seconds_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__range])) by (le)),1))\n )\n)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Avg Beacon Node Score", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 0.01 + }, + { + "color": "red", + "value": 0.5 + }, + { + "color": "#EAB839", + "value": 0.51 + }, + { + "color": "yellow", + "value": 0.8 + }, + { + "color": "green", + "value": 0.81 + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byFrameRefID", + "options": "B" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#8b8b8b", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byFrameRefID", + "options": "C" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#ebebeb", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 15, + "y": 20 + }, + "id": 72, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "clamp(app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 1 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 4 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 5 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 6, 1, 1) OR on() vector(0)", + "hide": true, + "interval": "", + "legendFormat": "status_score", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "0.5 * (1.0 - 10*(sum(increase(app_eth2_errors_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) / (sum(increase(app_eth2_latency_seconds_count{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval]))))) OR on() vector(0.5)", + "hide": false, + "interval": "", + "legendFormat": "error_score", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "0.5 * (1.0 - clamp_max(histogram_quantile(0.99, sum(rate(app_eth2_latency_seconds_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (le)),1))", + "hide": false, + "interval": "", + "legendFormat": "latency_score", + "range": true, + "refId": "C" + } + ], + "title": "Beacon Node Score Weights", + "transformations": [], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 26 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "increase(app_eth2_errors_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval]) > 0", + "interval": "", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Beacon API Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 26 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(app_eth2_latency_seconds_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (le,endpoint)) ", + "interval": "", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Beacon API request latency (90%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 33 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(rate(core_validatorapi_request_latency_seconds_count{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (endpoint) > 0", + "interval": "", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Validator API requests rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 33 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(core_validatorapi_request_latency_seconds_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (le,endpoint)) ", + "interval": "", + "legendFormat": "{{endpoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Validator API request latency (90%)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Peer count of the beacon node connected to this charon instance", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 74, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "app_beacon_node_peers{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}", + "legendFormat": "Peer count", + "range": true, + "refId": "A" + } + ], + "title": "Beacon node peer count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "File Descriptors" + }, + "properties": [ + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 49, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(go_goroutines{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "interval": "", + "legendFormat": "Go Routines", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(process_open_fds{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "hide": false, + "legendFormat": "File Descriptors", + "range": true, + "refId": "B" + } + ], + "title": "Go Routines and File Descriptors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(go_memstats_alloc_bytes{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "interval": "", + "legendFormat": "Allocated Memory", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(go_memstats_heap_inuse_bytes{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "hide": true, + "legendFormat": "Inuse Heap", + "range": true, + "refId": "B" + } + ], + "title": "Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 52, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.0.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(irate(process_cpu_seconds_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval]))", + "intervalFactor": 2, + "legendFormat": "CPU", + "metric": "go_gc_duration_seconds", + "range": true, + "refId": "A", + "step": 4 + } + ], + "title": "CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total balance of validators with links to beaconcha.in for each public key.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "light-blue" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Balance" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "from": 32, + "result": { + "color": "green", + "index": 0 + }, + "to": 100 + }, + "type": "range" + }, + { + "options": { + "from": 30, + "result": { + "color": "orange", + "index": 1 + }, + "to": 32 + }, + "type": "range" + }, + { + "options": { + "from": 0, + "result": { + "color": "red", + "index": 2 + }, + "to": 30 + }, + "type": "range" + } + ] + }, + { + "id": "unit", + "value": "ETH" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Public Key" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "beaconcha.in", + "url": "http://${__data.fields[\"cluster_network\"]}.beaconcha.in/validator/${__data.fields[\"Public Key\"]}" + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 54 + }, + "id": 56, + "options": { + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "core_scheduler_validator_balance_gwei{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} / 1000000000", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "{{pubkey}}", + "range": false, + "refId": "B" + } + ], + "title": "Validators", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Time 1": true, + "Time 2": true, + "__name__": true, + "__name__ 1": true, + "__name__ 2": true, + "cluster_hash": true, + "cluster_name": true, + "cluster_network": false, + "cluster_peer": true, + "instance": true, + "instance 1": true, + "instance 2": true, + "job": true, + "job 1": true, + "job 2": true, + "pubkey": true, + "pubkey_full": false + }, + "indexByName": { + "Time": 0, + "Value": 9, + "cluster_hash": 1, + "cluster_name": 2, + "cluster_network": 8, + "cluster_peer": 3, + "instance": 4, + "job": 5, + "pubkey": 6, + "pubkey_full": 7 + }, + "renameByName": { + "Value": "Balance", + "Value #A": "Effectiveness", + "Value #B": "Balance", + "cluster_network": "Network", + "pubkey": "Public Key", + "pubkey_full": "Public Key" + } + } + } + ], + "type": "table" + } + ], + "refresh": "30s", + "revision": 1, + "schemaVersion": 38, + "tags": [ + "charon", + "cluster-labels" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "compose-4-1", + "value": "compose-4-1" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name, cluster_name)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "Cluster Name", + "multi": false, + "name": "cluster_name", + "options": [], + "query": { + "query": "label_values(app_peer_name, cluster_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "1467820", + "value": "1467820" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name{cluster_name=\"$cluster_name\"}, cluster_hash)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "Cluster Hash", + "multi": false, + "name": "cluster_hash", + "options": [], + "query": { + "query": "label_values(app_peer_name{cluster_name=\"$cluster_name\"}, cluster_hash)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "anxious-leaves", + "value": "anxious-leaves" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\"}, cluster_peer)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "Cluster Peer", + "multi": false, + "name": "cluster_peer", + "options": [], + "query": { + "query": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\"}, cluster_peer)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "node0", + "value": "node0" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"},job)", + "description": "Prometheus job (can be ignored)", + "hide": 2, + "includeAll": false, + "label": "--", + "multi": false, + "name": "job", + "options": [], + "query": { + "query": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"},job)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Charon Overview", + "uid": "charon_overview_dashboard", + "version": 1, + "weekStart": "" +} diff --git a/test-infra/compose/static/grafana/dash_duty_details.json b/test-infra/compose/static/grafana/dash_duty_details.json new file mode 100644 index 00000000..611b7449 --- /dev/null +++ b/test-infra/compose/static/grafana/dash_duty_details.json @@ -0,0 +1,2275 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": true, + "tags": [], + "targetBlank": false, + "title": "Explore Charon Logs", + "tooltip": "Navigate to Charon Logs Exporer", + "type": "link", + "url": "/explore?orgId=1&left=%7B%22datasource%22:%22loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22builder%22,%22expr%22:%22%7Bcompose_service%3D%5C%22$job%5C%22%7D%20%7C%20logfmt%20%7C%20line_format%20%60%7B%7B.pretty%7D%7D%60%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": true, + "keepTime": true, + "tags": [ + "cluster-labels" + ], + "targetBlank": false, + "title": "", + "tooltip": "", + "type": "dashboards", + "url": "" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 83, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "never", + "tooltip": { + "mode": "none", + "sort": "none" + } + }, + "pluginVersion": "9.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_version{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (version)", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "{{version}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_git_commit{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (git_hash)", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "{{git_hash}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "rate(app_start_time_secs{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) > 0", + "hide": false, + "instant": false, + "interval": "$interval", + "legendFormat": "Restarts", + "range": true, + "refId": "C" + } + ], + "title": "Versions and Git Hashes", + "type": "state-timeline" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Node is healthy if Beacon Node connected and synced and at least quorum peers are connected.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [ + { + "options": { + "0": { + "color": "text", + "index": 1, + "text": "Unknown" + }, + "1": { + "color": "green", + "index": 0, + "text": "OK" + }, + "2": { + "color": "red", + "index": 3, + "text": "BeaconNode Down" + }, + "3": { + "color": "orange", + "index": 4, + "text": "BeaconNode Syncing" + }, + "4": { + "color": "orange", + "index": 5, + "text": "Insufficient Peers" + } + }, + "type": "value" + }, + { + "options": { + "match": "null", + "result": { + "color": "text", + "index": 2, + "text": "Dead" + } + }, + "type": "special" + } + ], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 75, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Readyz Health Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.15 + }, + { + "color": "orange", + "value": 0.3 + }, + { + "color": "red", + "value": 0.5 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "histogram_quantile(0.90, sum(rate(p2p_ping_latency_secs_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",peer=~\"$peer\"}[$interval])) by (le,peer)) ", + "interval": "", + "legendFormat": "{{peer}}", + "range": true, + "refId": "A" + } + ], + "title": "Peer ping latency (90%) (time series)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "- **Peer**: The peer's name (inferred from their *charon-enr-private-key*)\n- **You**: ⭐️ is this local charon node\n- **Connected**: Whether you are currently connected to this peer.\n- **Direct**: Whether the connection is *direct* (👍) or *relay* (👎) \n- **Latency**: The time messages take to travel to/from the peer.\n- **Uptime**\" The duration since peer was restarted.\n- **ClockDiff**: Difference between local and peer's clock time. More than 2s is bad.\n- **Attest**: Number of attestation duties performed in the time window.\n- **PrepareAgg**: Number of aggregation preparation duties performed in the time window.\n- **Aggregate**: Number of aggregations duties performed in the time window.\n- **Propose**: Number of block proposal duties performed in the time window.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false, + "minWidth": 0, + "width": 90 + }, + "decimals": 0, + "mappings": [], + "noValue": "--", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Latency" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 0 + }, + "to": 150 + }, + "type": "range" + }, + { + "options": { + "from": 150, + "result": { + "color": "yellow", + "index": 1 + }, + "to": 300 + }, + "type": "range" + }, + { + "options": { + "from": 300, + "result": { + "color": "orange", + "index": 2 + }, + "to": 500 + }, + "type": "range" + }, + { + "options": { + "from": 500, + "result": { + "color": "red", + "index": 3 + }, + "to": 100000 + }, + "type": "range" + } + ] + }, + { + "id": "custom.displayMode", + "value": "lcd-gauge" + }, + { + "id": "custom.width", + "value": 200 + }, + { + "id": "max", + "value": 700 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "You" + }, + "properties": [ + { + "id": "noValue", + "value": "⭐" + }, + { + "id": "custom.displayMode", + "value": "auto" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": "--" + }, + "1": { + "index": 1, + "text": "--" + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.width", + "value": 60 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Connected" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": " ❌" + }, + "1": { + "index": 1, + "text": "✅" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ClockDiff" + }, + "properties": [ + { + "id": "unit", + "value": "ms" + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": "🆗" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Direct" + }, + "properties": [ + { + "id": "custom.width", + "value": 55 + }, + { + "id": "mappings", + "value": [ + { + "options": { + "0": { + "index": 0, + "text": "--" + } + }, + "type": "value" + }, + { + "options": { + "from": 1, + "result": { + "index": 1, + "text": "👎" + }, + "to": 9 + }, + "type": "range" + }, + { + "options": { + "from": 10, + "result": { + "index": 2, + "text": "👍" + }, + "to": 1000 + }, + "type": "range" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Peer" + }, + "properties": [ + { + "id": "custom.width", + "value": 150 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Uptime" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Index" + }, + "properties": [ + { + "id": "custom.width", + "value": 30 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 85, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "You" + } + ] + }, + "pluginVersion": "9.3.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "histogram_quantile(0.90, sum(rate(p2p_ping_latency_secs_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])) by (le,peer)) * 1000", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "\nsum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"attester\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"proposer\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"randao\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "", + "range": false, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"prepare_aggregator\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"aggregator\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"exit\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "I" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(round(app_peerinfo_clock_offset_seconds{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"},0.1)*1000) by (peer) ", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "J" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(max(p2p_peer_connection_types{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",type=\"direct\"}) by (peer)*10 + \nmax(p2p_peer_connection_types{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",type=\"relay\"}) by (peer))", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "", + "range": false, + "refId": "K" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(app_peerinfo_version{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer,version) ", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "L" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_peerinfo_git_commit{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer,git_hash) > 0", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "M" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"sync_message\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "N" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"sync_contribution\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "O" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"prepare_sync_contribution\"}[$__range])) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "P" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "time() - max(app_peerinfo_start_time_secs{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} > 0) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "Q" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "max(app_peerinfo_index{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}) by (peer)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "R" + } + ], + "title": "Peer Connectivity and Duty Participation", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "peer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 10": true, + "Time 11": true, + "Time 12": true, + "Time 13": true, + "Time 14": true, + "Time 15": true, + "Time 16": true, + "Time 17": true, + "Time 2": true, + "Time 3": true, + "Time 4": true, + "Time 5": true, + "Time 6": true, + "Time 7": true, + "Time 8": true, + "Time 9": true, + "Value #F": false, + "Value #L": true, + "Value #M": true + }, + "indexByName": { + "Time 1": 10, + "Time 10": 29, + "Time 11": 30, + "Time 12": 31, + "Time 13": 32, + "Time 14": 33, + "Time 15": 34, + "Time 16": 35, + "Time 17": 36, + "Time 2": 12, + "Time 3": 13, + "Time 4": 14, + "Time 5": 15, + "Time 6": 16, + "Time 7": 17, + "Time 8": 18, + "Time 9": 28, + "Value #A": 3, + "Value #B": 5, + "Value #C": 20, + "Value #D": 21, + "Value #E": 22, + "Value #F": 2, + "Value #G": 23, + "Value #H": 24, + "Value #J": 6, + "Value #K": 4, + "Value #L": 11, + "Value #M": 19, + "Value #N": 25, + "Value #O": 27, + "Value #P": 26, + "Value #Q": 9, + "Value #R": 1, + "git_hash": 8, + "peer": 0, + "version": 7 + }, + "renameByName": { + "Value #A": "Connected", + "Value #B": "Latency", + "Value #C": "Attest", + "Value #D": "Propose", + "Value #E": "Randao", + "Value #F": "You", + "Value #G": "PrepareAgg", + "Value #H": "Aggregate", + "Value #I": "Exit", + "Value #J": "ClockDiff", + "Value #K": "Direct", + "Value #L": "", + "Value #N": "SyncMsg", + "Value #O": "SyncContrib", + "Value #P": "PrepareContrib", + "Value #Q": "Uptime", + "Value #R": "Index", + "git_hash": "GitCommit", + "peer": "Peer", + "version": "Version" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Beacon node score over time", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "#ffffff", + "mode": "fixed" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 32, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 0.5 + }, + { + "color": "green", + "value": 0.8 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 0, + "y": 11 + }, + "id": 89, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "clamp_min((clamp(app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 1 OR app_monitoring_readyz{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"} == 4, 1, 1) OR on() vector(0))\n*\n(\n (\n0.5 * (1.0 - 10*(sum(increase(app_eth2_errors_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$interval])) / (sum(increase(app_eth2_latency_seconds_count{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$interval]))))) OR on() vector(0.5)\n )\n +\n (\n 0.5 * (1.0 - clamp_max(histogram_quantile(0.99, sum(rate(app_eth2_latency_seconds_bucket{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$interval])) by (le)),1))\n )\n), 0)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Beacon Node Score", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": null + }, + { + "color": "green", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 87, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "never", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(p2p_peer_connection_types{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",type=\"relay\",peer=~\"$peer\"}) by (peer) + (100 * max(p2p_peer_connection_types{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",type=\"direct\",peer=~\"$peer\"}) by (peer)) > 0", + "format": "time_series", + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Peer Connections (🟡=relay, 🟢=direct)", + "type": "state-timeline" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Completed duties by type over time.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 100, + "lineWidth": 0 + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 92, + "options": { + "colWidth": 0.5, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.43, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_bcast_broadcast_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$interval])) by (duty) > 0", + "instant": false, + "interval": "$interval", + "legendFormat": "{{duty}}", + "range": true, + "refId": "A" + } + ], + "title": "✅ Completed duties by type ", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 81, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.9, + "showValue": "never", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",peer=~\"$peer\"}[$interval])) by (peer) > 0 ", + "interval": "$interval", + "legendFormat": "{{peer}}", + "range": true, + "refId": "A" + } + ], + "title": "Peer Participation All Duties", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Failed duties by type over time. Use data link to show logs.", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "red", + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 100, + "lineWidth": 0 + }, + "links": [ + { + "targetBlank": true, + "title": "Show in log explorer", + "url": "/explore?orgId=1&left=%7B%22datasource%22:%22loki%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22editorMode%22:%22builder%22,%22expr%22:%22%7Bcompose_service%3D%5C%22$job%5C%22%7D%20%7C%3D%20%60Duty%20failed%60%20%7C%3D%20%60${__field.labels.duty}%60%20%7C%20logfmt%20%7C%20line_format%20%60%7B%7B.pretty%7D%7D%60%22,%22queryType%22:%22range%22%7D%5D,%22range%22:%7B%22from%22:%22now-1h%22,%22to%22:%22now%22%7D%7D" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 91, + "options": { + "colWidth": 0.5, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.43, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(increase(core_tracker_failed_duties_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$interval])) by (duty) > 0", + "instant": false, + "interval": "$interval", + "legendFormat": "{{duty}}", + "range": true, + "refId": "A" + } + ], + "title": "🚨Failed duties by type ", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 93, + "options": { + "colWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "rowHeight": 0.9, + "showValue": "never", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(core_tracker_participation_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",peer=~\"$peer\",duty=\"$duty\"}[$interval])) by (peer) > 0 ", + "interval": "$interval", + "legendFormat": "{{peer}}", + "range": true, + "refId": "A" + } + ], + "title": "Peer Participation By Single Duty: $duty ", + "type": "status-history" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "orange", + "value": 4 + }, + { + "color": "red", + "value": 8 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 28 + }, + "id": 95, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(core_consensus_decided_rounds{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "hide": false, + "legendFormat": "Max All Duties", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max(core_consensus_decided_rounds{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\", duty=\"$duty\"})", + "hide": false, + "legendFormat": "$duty", + "range": true, + "refId": "B" + } + ], + "title": "Consensus Rounds", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Number of consenus instance timeouts per duty", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 28 + }, + "id": 99, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "increase(core_consensus_timeout_total{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$__rate_interval])", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Consensus timeouts: $duty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "dashed+area" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 2 + }, + { + "color": "orange", + "value": 5 + }, + { + "color": "red", + "value": 10 + } + ] + }, + "unit": "slots" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 34 + }, + "id": 94, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(core_tracker_inclusion_delay{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"})", + "interval": "", + "legendFormat": "Instant", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg_over_time(core_tracker_inclusion_delay{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"}[$interval])", + "hide": false, + "interval": "", + "legendFormat": "Avg", + "range": true, + "refId": "B" + } + ], + "title": "Attestation Inclusion Delay (slots)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Duration of consensus instance by duty", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 34 + }, + "id": 97, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.90, sum(rate(core_consensus_duration_seconds_bucket{cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\",duty=\"$duty\"}[$interval])) by (le,duty))", + "legendFormat": "{{duty}}", + "range": true, + "refId": "A" + } + ], + "title": "Consensus duration", + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [ + "cluster-labels" + ], + "templating": { + "list": [ + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name, cluster_name)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "Cluster Name", + "multi": false, + "name": "cluster_name", + "options": [], + "query": { + "query": "label_values(app_peer_name, cluster_name)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "6576466", + "value": "6576466" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name{cluster_name=\"$cluster_name\"}, cluster_hash)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "Cluster Hash", + "multi": false, + "name": "cluster_hash", + "options": [], + "query": { + "query": "label_values(app_peer_name{cluster_name=\"$cluster_name\"}, cluster_hash)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "important-pen", + "value": "important-pen" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\"}, cluster_peer)", + "description": "", + "hide": 0, + "includeAll": false, + "label": "Cluster Peer", + "multi": false, + "name": "cluster_peer", + "options": [], + "query": { + "query": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\"}, cluster_peer)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "node3", + "value": "node3" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"},job)", + "description": "Prometheus job (can be ignored)", + "hide": 2, + "includeAll": false, + "label": "--", + "multi": false, + "name": "job", + "options": [], + "query": { + "query": "label_values(app_peer_name{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\",cluster_peer=\"$cluster_peer\"},job)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\", cluster_peer=\"$cluster_peer\"}, peer)", + "description": "", + "hide": 2, + "includeAll": true, + "label": "P2P Peer", + "multi": false, + "name": "peer", + "options": [], + "query": { + "query": "label_values(p2p_ping_success{cluster_name=\"$cluster_name\",cluster_hash=\"$cluster_hash\", cluster_peer=\"$cluster_peer\"}, peer)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "auto": true, + "auto_count": 30, + "auto_min": "1m", + "current": { + "selected": false, + "text": "30m", + "value": "30m" + }, + "hide": 0, + "label": "Interval", + "name": "interval", + "options": [ + { + "selected": false, + "text": "auto", + "value": "$__auto_interval_interval" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": true, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "queryValue": "", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + }, + { + "current": { + "selected": true, + "text": "attester", + "value": "attester" + }, + "hide": 0, + "includeAll": false, + "label": "Duty", + "multi": false, + "name": "duty", + "options": [ + { + "selected": true, + "text": "attester", + "value": "attester" + }, + { + "selected": false, + "text": "prepare_aggregator", + "value": "prepare_aggregator" + }, + { + "selected": false, + "text": "aggregator", + "value": "aggregator" + }, + { + "selected": false, + "text": "sync_message", + "value": "sync_message" + }, + { + "selected": false, + "text": "prepare_sync_contribution", + "value": "prepare_sync_contribution" + }, + { + "selected": false, + "text": "sync_contribution", + "value": "sync_contribution" + }, + { + "selected": false, + "text": "proposer", + "value": "proposer" + }, + { + "selected": false, + "text": "randao", + "value": "randao" + }, + { + "selected": false, + "text": "exit", + "value": "exit" + }, + { + "selected": false, + "text": "builder_proposer", + "value": "builder_proposer" + }, + { + "selected": false, + "text": "builder_registration", + "value": "builder_registration" + }, + { + "selected": false, + "text": "info_sync", + "value": "info_sync" + } + ], + "query": "attester,prepare_aggregator,aggregator,sync_message,prepare_sync_contribution,sync_contribution,proposer,randao,exit,builder_proposer,builder_registration,info_sync", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Charon Duty Details", + "uid": "-su_K8K4z", + "version": 1, + "weekStart": "" +} diff --git a/test-infra/compose/static/grafana/dashboards.yml b/test-infra/compose/static/grafana/dashboards.yml new file mode 100644 index 00000000..6bbd5e73 --- /dev/null +++ b/test-infra/compose/static/grafana/dashboards.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +providers: + - name: dashboards + type: file + updateIntervalSeconds: 30 + options: + path: /etc/dashboards diff --git a/test-infra/compose/static/grafana/datasource.yml b/test-infra/compose/static/grafana/datasource.yml new file mode 100644 index 00000000..5ba50e11 --- /dev/null +++ b/test-infra/compose/static/grafana/datasource.yml @@ -0,0 +1,43 @@ +apiVersion: 1 + +deleteDatasources: + - name: Prometheus + orgId: 1 + - name: Loki + orgId: 1 + + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + orgId: 1 + url: http://prometheus:9090 + basicAuth: false + isDefault: true + jsonData: + graphiteVersion: "1.1" + tlsAuth: false + tlsAuthWithCACert: false + version: 1 + editable: true + + - name: Loki + type: loki + uid: loki + orgId: 1 + url: http://loki:3100 + basicAuth: false + isDefault: false + version: 1 + editable: true + + - name: Tempo + type: tempo + uid: tempo + orgId: 1 + url: http://tempo:3200 + basicAuth: false + isDefault: false + version: 1 + editable: true diff --git a/test-infra/compose/static/grafana/grafana.ini b/test-infra/compose/static/grafana/grafana.ini new file mode 100644 index 00000000..2ab0213a --- /dev/null +++ b/test-infra/compose/static/grafana/grafana.ini @@ -0,0 +1,9 @@ +[auth.anonymous] +enabled = true +org_role = Admin + +[log] +level = warn + +[unified_alerting] +enabled = false diff --git a/test-infra/compose/static/grafana/notifiers.yml b/test-infra/compose/static/grafana/notifiers.yml new file mode 100644 index 00000000..be27b1dc --- /dev/null +++ b/test-infra/compose/static/grafana/notifiers.yml @@ -0,0 +1,14 @@ +apiVersion: 1 + +notifiers: + - name: compose-webhook + type: webhook + uid: compose-webhook + org_id: 1 + is_default: true + send_reminder: false + frequency: 1s + disable_resolve_message: true + settings: + url: http://host.docker.internal:26354 + httpMethod: POST diff --git a/test-infra/compose/static/lighthouse/Dockerfile b/test-infra/compose/static/lighthouse/Dockerfile new file mode 100644 index 00000000..73a7dbc3 --- /dev/null +++ b/test-infra/compose/static/lighthouse/Dockerfile @@ -0,0 +1,16 @@ +FROM sigp/lighthouse:v7.1.0 + +ENV YQ_VERSION=v4.42.1 + +RUN apt-get update && apt-get install -y curl jq wget + +RUN \ + march="$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)"; \ + case "$march" in \ + arm64) export YQ_BINARY=yq_linux_arm64 ;; \ + amd64) export YQ_BINARY=yq_linux_amd64 ;; \ + esac; \ + wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY} -O /usr/bin/yq \ + && chmod +x /usr/bin/yq + +ENTRYPOINT ["/compose/lighthouse/run.sh"] diff --git a/test-infra/compose/static/lighthouse/run.sh b/test-infra/compose/static/lighthouse/run.sh new file mode 100755 index 00000000..192a2730 --- /dev/null +++ b/test-infra/compose/static/lighthouse/run.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +while ! curl "http://${NODE}:3600/up" 2>/dev/null; do + echo "Waiting for http://${NODE}:3600/up to become available..." + sleep 5 +done + +echo "Creating testnet config" +rm -rf /tmp/testnet || true +mkdir /tmp/testnet/ +curl "http://${NODE}:3600/eth/v1/config/spec" | jq -r .data | yq -P > /tmp/testnet/config.yaml +echo "0" > /tmp/testnet/deploy_block.txt +echo "0" > /tmp/testnet/deposit_contract_block.txt + +for f in /compose/"${NODE}"/validator_keys/keystore-*.json; do + echo "Importing key ${f}" + cat "$(echo "${f}" | sed 's/json/txt/')" | lighthouse account validator import \ + --testnet-dir "/tmp/testnet" \ + --keystore "${f}" +done + + +echo "Starting lighthouse validator client for ${NODE}" +exec lighthouse validator \ + --testnet-dir "/tmp/testnet" \ + --beacon-nodes "http://${NODE}:3600" \ + --suggested-fee-recipient "0x0000000000000000000000000000000000000000" diff --git a/test-infra/compose/static/lodestar/Dockerfile b/test-infra/compose/static/lodestar/Dockerfile new file mode 100644 index 00000000..ae8a70b0 --- /dev/null +++ b/test-infra/compose/static/lodestar/Dockerfile @@ -0,0 +1,11 @@ +FROM chainsafe/lodestar:v1.34.1 + +RUN apt-get update && apt-get install -y curl jq wget + +ENV YQ_VERSION=v4.23.1 +ENV YQ_BINARY=yq_linux_amd64 + +RUN wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY} -O /usr/bin/yq \ + && chmod +x /usr/bin/yq + +ENTRYPOINT ["/compose/lodestar/run.sh"] diff --git a/test-infra/compose/static/lodestar/run.sh b/test-infra/compose/static/lodestar/run.sh new file mode 100755 index 00000000..ea04d5b9 --- /dev/null +++ b/test-infra/compose/static/lodestar/run.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +while ! curl "http://${NODE}:3600/up" 2>/dev/null; do + echo "Waiting for http://${NODE}:3600/up to become available..." + sleep 5 +done + +echo "Creating testnet config" +rm -rf /tmp/testnet || true +mkdir /tmp/testnet/ +curl "http://${NODE}:3600/eth/v1/config/spec" | jq -r .data | yq -P > /tmp/testnet/config.yaml + +for f in /compose/"${NODE}"/validator_keys/keystore-*.json; do + echo "Importing key ${f}" + + # Import keystore with password. + node /usr/app/packages/cli/bin/lodestar validator import \ + --network="dev" \ + --importKeystores="$f" \ + --importKeystoresPassword="${f%.json}.txt" +done + +echo "Imported all keys" + +node /usr/app/packages/cli/bin/lodestar validator \ + --network="dev" \ + --metrics=true \ + --metrics.address="0.0.0.0" \ + --metrics.port=5064 \ + --beaconNodes="http://$NODE:3600" \ + --distributed diff --git a/test-infra/compose/static/loki/loki.yml b/test-infra/compose/static/loki/loki.yml new file mode 100644 index 00000000..1bdf3684 --- /dev/null +++ b/test-infra/compose/static/loki/loki.yml @@ -0,0 +1,47 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +ingester: + lifecycler: + address: 127.0.0.1 + ring: + kvstore: + store: inmemory + replication_factor: 1 + final_sleep: 0s + wal: + enabled: false + chunk_idle_period: 5m + chunk_retain_period: 30s + max_transfer_retries: 0 + +schema_config: + configs: + - from: 2022-01-01 + store: boltdb + object_store: filesystem + schema: v11 + index: + prefix: index_ + period: 168h + +storage_config: + boltdb: + directory: /opt/loki/index + + filesystem: + directory: /opt/loki/chunks + +limits_config: + enforce_metric_name: false + reject_old_samples: true + reject_old_samples_max_age: 168h + +chunk_store_config: + max_look_back_period: 0s + +table_manager: + retention_deletes_enabled: false + retention_period: 0s diff --git a/test-infra/compose/static/prometheus/prometheus.yml b/test-infra/compose/static/prometheus/prometheus.yml new file mode 100644 index 00000000..ff306eb0 --- /dev/null +++ b/test-infra/compose/static/prometheus/prometheus.yml @@ -0,0 +1,25 @@ +global: + scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute. + evaluation_interval: 5s # Evaluate rules every 15 seconds. The default is every 1 minute. + +scrape_configs: + # Charon's compose scrapes a non-existent 'bootnode' target; the relay is + # the service actually exposing monitoring on 3620. + - job_name: 'relay' + static_configs: + - targets: [ 'relay:3620' ] + - job_name: 'node0' + static_configs: + - targets: ['node0:3620'] + - job_name: 'node1' + static_configs: + - targets: ['node1:3620'] + - job_name: 'node2' + static_configs: + - targets: ['node2:3620'] + - job_name: 'node3' + static_configs: + - targets: ['node3:3620'] + +rule_files: + - /etc/prometheus/rules.yml diff --git a/test-infra/compose/static/prometheus/rules.yml b/test-infra/compose/static/prometheus/rules.yml new file mode 100644 index 00000000..583c784a --- /dev/null +++ b/test-infra/compose/static/prometheus/rules.yml @@ -0,0 +1,44 @@ +groups: +- name: pluto + rules: + - alert: Pluto Down + expr: up == 0 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} is down" + + - alert: Error Log Rate + expr: app_log_error_total > 0 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} has a high error rate" + + - alert: Warn Log Rate + expr: increase(app_log_warn_total[30s]) > 2 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} has a high warning rate" + + - alert: Validator API Error Rate + expr: increase(core_validatorapi_request_error_total{endpoint!="proxy"}[30s]) > 1 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} validator API a high error rate" + + - alert: Proxy API Error Rate + expr: increase(core_validatorapi_request_error_total{endpoint="proxy"}[30s]) > 5 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} proxy API a high error rate" + + - alert: Broadcast Duty Rate + expr: increase(core_bcast_broadcast_total[30s]) < 0.5 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} is not broadcasting enough duties" + + - alert: Outstanding Duty Rate + expr: core_bcast_broadcast_total - core_scheduler_duty_total > 50 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} has too many outstanding duties" diff --git a/test-infra/compose/static/tempo/tempo.yaml b/test-infra/compose/static/tempo/tempo.yaml new file mode 100644 index 00000000..6b3e882b --- /dev/null +++ b/test-infra/compose/static/tempo/tempo.yaml @@ -0,0 +1,25 @@ +server: + http_listen_port: 3200 + grpc_listen_port: 9095 + +distributor: + receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +ingester: + trace_idle_period: 10s + max_block_bytes: 1_048_576 + max_block_duration: 5m + +compactor: + compaction: + block_retention: 24h + +storage: + trace: + backend: local + local: + path: /tmp/tempo/traces diff --git a/test-infra/compose/static/vouch/Dockerfile b/test-infra/compose/static/vouch/Dockerfile new file mode 100644 index 00000000..e45da485 --- /dev/null +++ b/test-infra/compose/static/vouch/Dockerfile @@ -0,0 +1,9 @@ +FROM wealdtech/ethdo:1.35.2 as ethdo + +FROM attestant/vouch:1.11.1 + +COPY --from=ethdo /app/ethdo /app/ethdo + +RUN apt-get update && apt-get install -y curl jq wget + +ENTRYPOINT ["/compose/vouch/run.sh"] diff --git a/test-infra/compose/static/vouch/run.sh b/test-infra/compose/static/vouch/run.sh new file mode 100644 index 00000000..685f674a --- /dev/null +++ b/test-infra/compose/static/vouch/run.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +# Running vouch VC is split into three steps: +# 1. Converting keys into a format which vouch understands. This is what ethdo does. +# 2. Creating configuration for vouch (vouch.yml). +# 3. Actually running the vouch validator client. + +BASE_DIR="/tmp/vouch" +KEYS_DIR="/tmp/vouch/keys" +ACCOUNT_PASSPHRASE="secret" # Hardcoded ethdo account passphrase + +rm -rf /tmp/vouch || true +mkdir ${BASE_DIR} +mkdir ${KEYS_DIR} + +cp /compose/vouch/vouch.yml ${BASE_DIR} + +# Create an ethdo wallet within the keys folder. +wallet="validators" +/app/ethdo --base-dir="${KEYS_DIR}" wallet create --wallet ${wallet} + +# Import keys into the ethdo wallet. +account=0 +for f in /compose/"${NODE}"/validator_keys/keystore-*.json; do + accountName="account-${account}" + echo "Importing key ${f} into ethdo wallet: ${wallet}/${accountName}" + + KEYSTORE_PASSPHRASE=$(cat "${f//json/txt}") + /app/ethdo \ + --base-dir="${KEYS_DIR}" account import \ + --account="${wallet}"/"${accountName}" \ + --keystore="$f" \ + --passphrase="$ACCOUNT_PASSPHRASE" \ + --keystore-passphrase="$KEYSTORE_PASSPHRASE" \ + --allow-weak-passphrases + + # Increment account. + # shellcheck disable=SC2003 + account=$(expr "$account" + 1) +done + +# Log wallet info. +echo "Starting vouch validator client. Wallet info:" +/app/ethdo wallet info \ +--wallet="${wallet}" \ +--base-dir="${KEYS_DIR}" \ +--verbose + +# Now run vouch. +exec /app/vouch --base-dir=${BASE_DIR} --beacon-node-address="http://${NODE}:3600" diff --git a/test-infra/compose/static/vouch/vouch.yml b/test-infra/compose/static/vouch/vouch.yml new file mode 100644 index 00000000..075af4c7 --- /dev/null +++ b/test-infra/compose/static/vouch/vouch.yml @@ -0,0 +1,36 @@ +# This is a sample config to run vouch VC. It is used by the run.sh script +# to generate a custom config for each vouch VC connected to a charon node. +# Refer: https://github.com/attestantio/vouch/blob/master/docs/configuration.md. + +# The wallet account manager obtains account information from local wallets, and signs locally. +# It supports wallets created by ethdo. +accountmanager: + wallet: + locations: /tmp/vouch/keys + accounts: validators + passphrases: secret + +# metrics is the module that logs metrics, in this case using prometheus. Note that vouch doesn't emit metrics if +# the following block is not provided. +metrics: + prometheus: + # listen-address is the address on which prometheus listens for metrics requests. + listen-address: 0.0.0.0:8081 + +# Allow sufficient time (10s) to block while fetching duties for DVT. +strategies: + beaconblockproposal: + timeout: 10s + blindedbeaconblockproposal: + timeout: 10s + attestationdata: + timeout: 10s + aggregateattestation: + timeout: 10s + synccommitteecontribution: + timeout: 10s + +# blockrelay provides information about working with local execution clients and remote relays for block proposals. +# fallback-fee-recipient is required field for vouch if no execution configuration is provided. +blockrelay: + fallback-fee-recipient: '0x0000000000000000000000000000000000000001' diff --git a/test-infra/compose/template.go b/test-infra/compose/template.go new file mode 100644 index 00000000..f5d84cd9 --- /dev/null +++ b/test-infra/compose/template.go @@ -0,0 +1,92 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "bytes" + "embed" + "os" + "path" + "strings" + "text/template" + + "github.com/obolnetwork/charon/app/errors" +) + +//go:embed docker-compose.template +var tmpl []byte + +//go:embed static +var static embed.FS + +// TmplData is the docker-compose.yml template data. +type TmplData struct { + ComposeDir string + + CharonImageTag string + CharonEntrypoint string + CharonCommand string + + Nodes []TmplNode + VCs []TmplVC + + Relay bool + Monitoring bool + Alerting bool + MonitoringPorts bool +} + +// TmplVC represents a validator client service in a docker-compose.yml. +type TmplVC struct { + Label string + Image string + Build string + Command string + Ports []port +} + +// TmplNode represents a charon or pluto TmplNode service in a docker-compose.yml. +type TmplNode struct { + Image string // Image is a full image reference, empty by default, resulting in obolnetwork/charon:{CharonImageTag} being used. + Entrypoint string // Entrypoint is empty by default, resulting in CharonEntrypoint being used. + Command string // Command is empty by default, resulting in CharonCommand being used. + EnvVars []kv + Ports []port +} + +// kv is a key value pair. +type kv struct { + Key string + Value string +} + +// EnvKey returns the key formatted as env var: "data-dir" -> "DATA_DIR". +func (kv kv) EnvKey() string { + return strings.ReplaceAll(strings.ToUpper(kv.Key), "-", "_") +} + +// port is a port mapping in a docker-compose.yml. +type port struct { + External int + Internal int +} + +// WriteDockerCompose generates the docker-compose.yml template and writes it to disk. +func WriteDockerCompose(dir string, data TmplData) error { + tpl, err := template.New("").Parse(string(tmpl)) + if err != nil { + return errors.Wrap(err, "new template") + } + + var buf bytes.Buffer + if err := tpl.Execute(&buf, data); err != nil { + return errors.Wrap(err, "exec template") + } + + err = os.WriteFile(path.Join(dir, "docker-compose.yml"), buf.Bytes(), 0o755) //nolint:gosec + if err != nil { + return errors.Wrap(err, "write docker-compose.yml") + } + + return nil +} diff --git a/test-infra/compose/testdata/TestDockerCompose_define_create_template.golden b/test-infra/compose/testdata/TestDockerCompose_define_create_template.golden new file mode 100644 index 00000000..cebe6ca5 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_define_create_template.golden @@ -0,0 +1,20 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "echo", + "CharonCommand": "No charon commands needed for keygen=create define step", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": null, + "Ports": null + } + ], + "VCs": null, + "Relay": false, + "Monitoring": false, + "Alerting": false, + "MonitoringPorts": false +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_define_create_yml.golden b/test-infra/compose/testdata/TestDockerCompose_define_create_yml.golden new file mode 100644 index 00000000..feb6b363 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_define_create_yml.golden @@ -0,0 +1,18 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + entrypoint: echo + command: No charon commands needed for keygen=create define step + networks: [compose] + volumes: [testdir:/compose] + + +services: + node0: + <<: *node-base + container_name: node0 + + + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_define_dkg_template.golden b/test-infra/compose/testdata/TestDockerCompose_define_dkg_template.golden new file mode 100644 index 00000000..a564224e --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_define_dkg_template.golden @@ -0,0 +1,57 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "[create,dkg]", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "name", + "Value": "compose" + }, + { + "Key": "num_validators", + "Value": "1" + }, + { + "Key": "operator_enrs", + "Value": "enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48,enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48,enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48,enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48" + }, + { + "Key": "threshold", + "Value": "3" + }, + { + "Key": "withdrawal_addresses", + "Value": "\"0x0000000000000000000000000000000000000000\"" + }, + { + "Key": "fee-recipient_addresses", + "Value": "\"0x0000000000000000000000000000000000000000\"" + }, + { + "Key": "dkg_algorithm", + "Value": "frost" + }, + { + "Key": "output_dir", + "Value": "/compose" + }, + { + "Key": "network", + "Value": "goerli" + } + ], + "Ports": null + } + ], + "VCs": null, + "Relay": false, + "Monitoring": false, + "Alerting": false, + "MonitoringPorts": false +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_define_dkg_yml.golden b/test-infra/compose/testdata/TestDockerCompose_define_dkg_yml.golden new file mode 100644 index 00000000..75984835 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_define_dkg_yml.golden @@ -0,0 +1,28 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: [create,dkg] + networks: [compose] + volumes: [testdir:/compose] + + +services: + node0: + <<: *node-base + container_name: node0 + + environment: + CHARON_NAME: compose + CHARON_NUM_VALIDATORS: 1 + CHARON_OPERATOR_ENRS: enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48,enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48,enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48,enr:-HW4QEp-BLhP30tqTGFbR9n2PdUKWP9qc0zphIRmn8_jpm4BYkgekztXQaPA_znRW8RvNYHo0pUwyPEwUGGeZu26XlKAgmlkgnY0iXNlY3AyNTZrMaEDG4TFVnsSZECZXT7VqroFZdceGDRgSBn_nBf16dXdB48 + CHARON_THRESHOLD: 3 + CHARON_WITHDRAWAL_ADDRESSES: "0x0000000000000000000000000000000000000000" + CHARON_FEE_RECIPIENT_ADDRESSES: "0x0000000000000000000000000000000000000000" + CHARON_DKG_ALGORITHM: frost + CHARON_OUTPUT_DIR: /compose + CHARON_NETWORK: goerli + + + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_template.golden b/test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_template.golden new file mode 100644 index 00000000..dedeff49 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_template.golden @@ -0,0 +1,65 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "[create,cluster]", + "Nodes": [ + { + "Image": "pluto:local", + "Entrypoint": "", + "Command": "[create,cluster,--name=compose-4-1,--threshold=3,--nodes=4,--cluster-dir=/compose,--num-validators=1,--withdrawal-addresses=0x0000000000000000000000000000000000000000,--fee-recipient-addresses=0x0000000000000000000000000000000000000000,--network=goerli]", + "EnvVars": [ + { + "Key": "name", + "Value": "compose-4-1" + }, + { + "Key": "threshold", + "Value": "3" + }, + { + "Key": "nodes", + "Value": "4" + }, + { + "Key": "cluster-dir", + "Value": "/compose" + }, + { + "Key": "split-existing-keys", + "Value": "\"false\"" + }, + { + "Key": "split-keys-dir", + "Value": "" + }, + { + "Key": "num-validators", + "Value": "1" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + }, + { + "Key": "withdrawal-addresses", + "Value": "\"0x0000000000000000000000000000000000000000\"" + }, + { + "Key": "fee-recipient-addresses", + "Value": "\"0x0000000000000000000000000000000000000000\"" + }, + { + "Key": "network", + "Value": "goerli" + } + ], + "Ports": null + } + ], + "VCs": null, + "Relay": false, + "Monitoring": false, + "Alerting": false, + "MonitoringPorts": false +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_yml.golden b/test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_yml.golden new file mode 100644 index 00000000..4d3cbf2a --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_create_pluto_keygen_yml.golden @@ -0,0 +1,32 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: [create,cluster] + networks: [compose] + volumes: [testdir:/compose] + + +services: + node0: + <<: *node-base + container_name: node0 + image: pluto:local + command: [create,cluster,--name=compose-4-1,--threshold=3,--nodes=4,--cluster-dir=/compose,--num-validators=1,--withdrawal-addresses=0x0000000000000000000000000000000000000000,--fee-recipient-addresses=0x0000000000000000000000000000000000000000,--network=goerli] + + environment: + CHARON_NAME: compose-4-1 + CHARON_THRESHOLD: 3 + CHARON_NODES: 4 + CHARON_CLUSTER_DIR: /compose + CHARON_SPLIT_EXISTING_KEYS: "false" + CHARON_SPLIT_KEYS_DIR: + CHARON_NUM_VALIDATORS: 1 + CHARON_INSECURE_KEYS: "false" + CHARON_WITHDRAWAL_ADDRESSES: "0x0000000000000000000000000000000000000000" + CHARON_FEE_RECIPIENT_ADDRESSES: "0x0000000000000000000000000000000000000000" + CHARON_NETWORK: goerli + + + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_create_template.golden b/test-infra/compose/testdata/TestDockerCompose_lock_create_template.golden new file mode 100644 index 00000000..7ad52b2a --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_create_template.golden @@ -0,0 +1,65 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "[create,cluster]", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "name", + "Value": "compose-4-1" + }, + { + "Key": "threshold", + "Value": "3" + }, + { + "Key": "nodes", + "Value": "4" + }, + { + "Key": "cluster-dir", + "Value": "/compose" + }, + { + "Key": "split-existing-keys", + "Value": "\"false\"" + }, + { + "Key": "split-keys-dir", + "Value": "" + }, + { + "Key": "num-validators", + "Value": "1" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + }, + { + "Key": "withdrawal-addresses", + "Value": "\"0x0000000000000000000000000000000000000000\"" + }, + { + "Key": "fee-recipient-addresses", + "Value": "\"0x0000000000000000000000000000000000000000\"" + }, + { + "Key": "network", + "Value": "goerli" + } + ], + "Ports": null + } + ], + "VCs": null, + "Relay": false, + "Monitoring": false, + "Alerting": false, + "MonitoringPorts": false +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_create_yml.golden b/test-infra/compose/testdata/TestDockerCompose_lock_create_yml.golden new file mode 100644 index 00000000..f485f9f5 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_create_yml.golden @@ -0,0 +1,30 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: [create,cluster] + networks: [compose] + volumes: [testdir:/compose] + + +services: + node0: + <<: *node-base + container_name: node0 + + environment: + CHARON_NAME: compose-4-1 + CHARON_THRESHOLD: 3 + CHARON_NODES: 4 + CHARON_CLUSTER_DIR: /compose + CHARON_SPLIT_EXISTING_KEYS: "false" + CHARON_SPLIT_KEYS_DIR: + CHARON_NUM_VALIDATORS: 1 + CHARON_INSECURE_KEYS: "false" + CHARON_WITHDRAWAL_ADDRESSES: "0x0000000000000000000000000000000000000000" + CHARON_FEE_RECIPIENT_ADDRESSES: "0x0000000000000000000000000000000000000000" + CHARON_NETWORK: goerli + + + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_template.golden b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_template.golden new file mode 100644 index 00000000..0a971b3f --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_template.golden @@ -0,0 +1,221 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "not used", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node0/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node0" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node0" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + }, + { + "Image": "pluto:local", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node1/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node1" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node1" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + }, + { + "Image": "", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node2/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node2" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node2" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + }, + { + "Image": "pluto:local", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node3/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node3" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node3" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + } + ], + "VCs": null, + "Relay": true, + "Monitoring": false, + "Alerting": false, + "MonitoringPorts": false +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_yml.golden b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_yml.golden new file mode 100644 index 00000000..408a78a0 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_mixed_impls_yml.golden @@ -0,0 +1,103 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: not used + networks: [compose] + volumes: [testdir:/compose] + depends_on: [relay] + +services: + node0: + <<: *node-base + container_name: node0 + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node0/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node0 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node0 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + node1: + <<: *node-base + container_name: node1 + image: pluto:local + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node1/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node1 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node1 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + node2: + <<: *node-base + container_name: node2 + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node2/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node2 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node2 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + node3: + <<: *node-base + container_name: node3 + image: pluto:local + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node3/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node3 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node3 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + relay: + <<: *node-base + container_name: relay + command: relay + depends_on: [] + environment: + CHARON_HTTP_ADDRESS: 0.0.0.0:3640 + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_DATA_DIR: /compose/relay + CHARON_P2P_RELAYS: "" + CHARON_P2P_EXTERNAL_HOSTNAME: relay + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_UDP_ADDRESS: 0.0.0.0:3630 + CHARON_P2P_ADVERTISE_PRIVATE_ADDRESSES: "true" + CHARON_LOKI_ADDRESS: http://loki:3100/loki/api/v1/push + + + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_dkg_template.golden b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_template.golden new file mode 100644 index 00000000..53c50a34 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_template.golden @@ -0,0 +1,221 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "not used", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node0/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node0" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node0" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + }, + { + "Image": "", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node1/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node1" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node1" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + }, + { + "Image": "", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node2/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node2" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node2" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + }, + { + "Image": "", + "Entrypoint": "", + "Command": "[dkg,--shutdown-delay=2s]", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node3/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node3" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "data-dir", + "Value": "/compose/node3" + }, + { + "Key": "definition-file", + "Value": "/compose/cluster-definition.json" + }, + { + "Key": "insecure-keys", + "Value": "\"false\"" + } + ], + "Ports": null + } + ], + "VCs": null, + "Relay": true, + "Monitoring": false, + "Alerting": false, + "MonitoringPorts": false +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_lock_dkg_yml.golden b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_yml.golden new file mode 100644 index 00000000..c829bf1a --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_lock_dkg_yml.golden @@ -0,0 +1,101 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: not used + networks: [compose] + volumes: [testdir:/compose] + depends_on: [relay] + +services: + node0: + <<: *node-base + container_name: node0 + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node0/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node0 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node0 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + node1: + <<: *node-base + container_name: node1 + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node1/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node1 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node1 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + node2: + <<: *node-base + container_name: node2 + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node2/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node2 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node2 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + node3: + <<: *node-base + container_name: node3 + command: [dkg,--shutdown-delay=2s] + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node3/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node3 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_DATA_DIR: /compose/node3 + CHARON_DEFINITION_FILE: /compose/cluster-definition.json + CHARON_INSECURE_KEYS: "false" + + relay: + <<: *node-base + container_name: relay + command: relay + depends_on: [] + environment: + CHARON_HTTP_ADDRESS: 0.0.0.0:3640 + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_DATA_DIR: /compose/relay + CHARON_P2P_RELAYS: "" + CHARON_P2P_EXTERNAL_HOSTNAME: relay + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_UDP_ADDRESS: 0.0.0.0:3630 + CHARON_P2P_ADVERTISE_PRIVATE_ADDRESSES: "true" + CHARON_LOKI_ADDRESS: http://loki:3100/loki/api/v1/push + + + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_template.golden b/test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_template.golden new file mode 100644 index 00000000..afad120a --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_template.golden @@ -0,0 +1,494 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "run", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node0/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node0" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node0/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"false\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node0/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node0" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node0" + } + ], + "Ports": [ + { + "External": 3600, + "Internal": 3600 + }, + { + "External": 3610, + "Internal": 3610 + }, + { + "External": 3620, + "Internal": 3620 + }, + { + "External": 3630, + "Internal": 3630 + } + ] + }, + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node1/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node1" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node1/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"false\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node1/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node1" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node1" + } + ], + "Ports": [ + { + "External": 13600, + "Internal": 3600 + }, + { + "External": 13610, + "Internal": 3610 + }, + { + "External": 13620, + "Internal": 3620 + }, + { + "External": 13630, + "Internal": 3630 + } + ] + }, + { + "Image": "pluto:local", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node2/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node2" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node2/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node2/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node2" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node2" + } + ], + "Ports": [ + { + "External": 23600, + "Internal": 3600 + }, + { + "External": 23610, + "Internal": 3610 + }, + { + "External": 23620, + "Internal": 3620 + }, + { + "External": 23630, + "Internal": 3630 + } + ] + }, + { + "Image": "pluto:local", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node3/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node3" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node3/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"false\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node3/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node3" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node3" + } + ], + "Ports": [ + { + "External": 33600, + "Internal": 3600 + }, + { + "External": 33610, + "Internal": 3610 + }, + { + "External": 33620, + "Internal": 3620 + }, + { + "External": 33630, + "Internal": 3630 + } + ] + } + ], + "VCs": [ + { + "Label": "lighthouse", + "Image": "", + "Build": "lighthouse", + "Command": "", + "Ports": null + }, + { + "Label": "lighthouse", + "Image": "", + "Build": "lighthouse", + "Command": "", + "Ports": null + }, + { + "Label": "", + "Image": "", + "Build": "", + "Command": "", + "Ports": null + }, + { + "Label": "lighthouse", + "Image": "", + "Build": "lighthouse", + "Command": "", + "Ports": null + } + ], + "Relay": true, + "Monitoring": true, + "Alerting": true, + "MonitoringPorts": true +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_yml.golden b/test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_yml.golden new file mode 100644 index 00000000..5d9e3abf --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_run_mixed_impls_yml.golden @@ -0,0 +1,259 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: run + networks: [compose] + volumes: [testdir:/compose] + depends_on: [relay] + +services: + node0: + <<: *node-base + container_name: node0 + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node0/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node0 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node0/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "false" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node0/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node0 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node0 + + ports: + - "3600:3600" + + - "3610:3610" + + - "3620:3620" + + - "3630:3630" + + node1: + <<: *node-base + container_name: node1 + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node1/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node1 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node1/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "false" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node1/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node1 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node1 + + ports: + - "13600:3600" + + - "13610:3610" + + - "13620:3620" + + - "13630:3630" + + node2: + <<: *node-base + container_name: node2 + image: pluto:local + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node2/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node2 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node2/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "true" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node2/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node2 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node2 + + ports: + - "23600:3600" + + - "23610:3610" + + - "23620:3620" + + - "23630:3630" + + node3: + <<: *node-base + container_name: node3 + image: pluto:local + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node3/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node3 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node3/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "false" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node3/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node3 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node3 + + ports: + - "33600:3600" + + - "33610:3610" + + - "33620:3620" + + - "33630:3630" + + relay: + <<: *node-base + container_name: relay + command: relay + depends_on: [] + environment: + CHARON_HTTP_ADDRESS: 0.0.0.0:3640 + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_DATA_DIR: /compose/relay + CHARON_P2P_RELAYS: "" + CHARON_P2P_EXTERNAL_HOSTNAME: relay + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_UDP_ADDRESS: 0.0.0.0:3630 + CHARON_P2P_ADVERTISE_PRIVATE_ADDRESSES: "true" + CHARON_LOKI_ADDRESS: http://loki:3100/loki/api/v1/push + + vc0-lighthouse: + container_name: vc0-lighthouse + build: lighthouse + networks: [compose] + depends_on: [node0] + environment: + NODE: node0 + volumes: + - .:/compose + + vc1-lighthouse: + container_name: vc1-lighthouse + build: lighthouse + networks: [compose] + depends_on: [node1] + environment: + NODE: node1 + volumes: + - .:/compose + + vc3-lighthouse: + container_name: vc3-lighthouse + build: lighthouse + networks: [compose] + depends_on: [node3] + environment: + NODE: node3 + volumes: + - .:/compose + + curl: + container_name: curl + # Can be used to curl services; e.g. docker compose exec curl curl http://prometheus:9090/api/v1/rules\?type\=alert + image: curlimages/curl:latest + command: sleep 1d + networks: [compose] + + prometheus: + container_name: prometheus + image: prom/prometheus:${PROMETHEUS_VERSION:-v2.50.1} + ports: + - "9090:9090" + networks: [compose] + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - ./prometheus/rules.yml:/etc/prometheus/rules.yml + + + + grafana: + container_name: grafana + image: grafana/grafana:${GRAFANA_VERSION:-10.4.2} + ports: + - "3000:3000" + networks: [compose] + volumes: + - ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml + - ./grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/datasource.yml + - ./grafana/notifiers.yml:/etc/grafana/provisioning/notifiers/notifiers.yml + - ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro + - ./grafana/dash_charon_overview.json:/etc/dashboards/dash_charon_overview.json + - ./grafana/dash_duty_details.json:/etc/dashboards/dash_duty_details.json + - ./grafana/dash_alerts.json:/etc/dashboards/dash_alerts.json + + tempo: + container_name: tempo + image: grafana/tempo:${TEMPO_VERSION:-2.7.1} + networks: [compose] + user: ":" + command: -config.file=/opt/tempo/tempo.yaml + volumes: + - ./tempo:/opt/tempo + + loki: + container_name: loki + image: grafana/loki:${LOKI_VERSION:-2.8.2} + networks: [compose] + user: ":" + command: -config.file=/opt/loki/loki.yml + volumes: + - ./loki:/opt/loki + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestDockerCompose_run_template.golden b/test-infra/compose/testdata/TestDockerCompose_run_template.golden new file mode 100644 index 00000000..ec5f0f1a --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_run_template.golden @@ -0,0 +1,494 @@ +{ + "ComposeDir": "testdir", + "CharonImageTag": "latest", + "CharonEntrypoint": "", + "CharonCommand": "run", + "Nodes": [ + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node0/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node0" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node0/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"false\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node0/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node0" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node0" + } + ], + "Ports": [ + { + "External": 3600, + "Internal": 3600 + }, + { + "External": 3610, + "Internal": 3610 + }, + { + "External": 3620, + "Internal": 3620 + }, + { + "External": 3630, + "Internal": 3630 + } + ] + }, + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node1/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node1" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node1/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"false\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node1/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node1" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node1" + } + ], + "Ports": [ + { + "External": 13600, + "Internal": 3600 + }, + { + "External": 13610, + "Internal": 3610 + }, + { + "External": 13620, + "Internal": 3620 + }, + { + "External": 13630, + "Internal": 3630 + } + ] + }, + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node2/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node2" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node2/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node2/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node2" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node2" + } + ], + "Ports": [ + { + "External": 23600, + "Internal": 3600 + }, + { + "External": 23610, + "Internal": 3610 + }, + { + "External": 23620, + "Internal": 3620 + }, + { + "External": 23630, + "Internal": 3630 + } + ] + }, + { + "Image": "", + "Entrypoint": "", + "Command": "", + "EnvVars": [ + { + "Key": "private-key-file", + "Value": "/compose/node3/charon-enr-private-key" + }, + { + "Key": "monitoring-address", + "Value": "0.0.0.0:3620" + }, + { + "Key": "p2p-external-hostname", + "Value": "node3" + }, + { + "Key": "p2p-tcp-address", + "Value": "0.0.0.0:3610" + }, + { + "Key": "p2p-relays", + "Value": "http://relay:3640" + }, + { + "Key": "log-level", + "Value": "debug" + }, + { + "Key": "log-color", + "Value": "force" + }, + { + "Key": "feature-set", + "Value": "alpha" + }, + { + "Key": "lock-file", + "Value": "/compose/node3/cluster-lock.json" + }, + { + "Key": "validator-api-address", + "Value": "0.0.0.0:3600" + }, + { + "Key": "beacon-node-endpoints", + "Value": "" + }, + { + "Key": "simnet-beacon_mock", + "Value": "\"true\"" + }, + { + "Key": "simnet-validator-mock", + "Value": "\"false\"" + }, + { + "Key": "simnet-slot-duration", + "Value": "1s" + }, + { + "Key": "simnet-validator-keys-dir", + "Value": "/compose/node3/validator_keys" + }, + { + "Key": "simnet-beacon-mock-fuzz", + "Value": "\"false\"" + }, + { + "Key": "synthetic-block-proposals", + "Value": "\"true\"" + }, + { + "Key": "builder-api", + "Value": "\"false\"" + }, + { + "Key": "otlp-address", + "Value": "tempo:4317" + }, + { + "Key": "otlp-service-name", + "Value": "node3" + }, + { + "Key": "loki-addresses", + "Value": "http://loki:3100/loki/api/v1/push" + }, + { + "Key": "loki-service", + "Value": "node3" + } + ], + "Ports": [ + { + "External": 33600, + "Internal": 3600 + }, + { + "External": 33610, + "Internal": 3610 + }, + { + "External": 33620, + "Internal": 3620 + }, + { + "External": 33630, + "Internal": 3630 + } + ] + } + ], + "VCs": [ + { + "Label": "lighthouse", + "Image": "", + "Build": "lighthouse", + "Command": "", + "Ports": null + }, + { + "Label": "lighthouse", + "Image": "", + "Build": "lighthouse", + "Command": "", + "Ports": null + }, + { + "Label": "", + "Image": "", + "Build": "", + "Command": "", + "Ports": null + }, + { + "Label": "lighthouse", + "Image": "", + "Build": "lighthouse", + "Command": "", + "Ports": null + } + ], + "Relay": true, + "Monitoring": true, + "Alerting": true, + "MonitoringPorts": true +} \ No newline at end of file diff --git a/test-infra/compose/testdata/TestDockerCompose_run_yml.golden b/test-infra/compose/testdata/TestDockerCompose_run_yml.golden new file mode 100644 index 00000000..d642c188 --- /dev/null +++ b/test-infra/compose/testdata/TestDockerCompose_run_yml.golden @@ -0,0 +1,257 @@ +x-node-base: &node-base + image: obolnetwork/charon:latest + command: run + networks: [compose] + volumes: [testdir:/compose] + depends_on: [relay] + +services: + node0: + <<: *node-base + container_name: node0 + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node0/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node0 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node0/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "false" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node0/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node0 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node0 + + ports: + - "3600:3600" + + - "3610:3610" + + - "3620:3620" + + - "3630:3630" + + node1: + <<: *node-base + container_name: node1 + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node1/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node1 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node1/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "false" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node1/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node1 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node1 + + ports: + - "13600:3600" + + - "13610:3610" + + - "13620:3620" + + - "13630:3630" + + node2: + <<: *node-base + container_name: node2 + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node2/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node2 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node2/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "true" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node2/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node2 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node2 + + ports: + - "23600:3600" + + - "23610:3610" + + - "23620:3620" + + - "23630:3630" + + node3: + <<: *node-base + container_name: node3 + + environment: + CHARON_PRIVATE_KEY_FILE: /compose/node3/charon-enr-private-key + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_P2P_EXTERNAL_HOSTNAME: node3 + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_RELAYS: http://relay:3640 + CHARON_LOG_LEVEL: debug + CHARON_LOG_COLOR: force + CHARON_FEATURE_SET: alpha + CHARON_LOCK_FILE: /compose/node3/cluster-lock.json + CHARON_VALIDATOR_API_ADDRESS: 0.0.0.0:3600 + CHARON_BEACON_NODE_ENDPOINTS: + CHARON_SIMNET_BEACON_MOCK: "true" + CHARON_SIMNET_VALIDATOR_MOCK: "false" + CHARON_SIMNET_SLOT_DURATION: 1s + CHARON_SIMNET_VALIDATOR_KEYS_DIR: /compose/node3/validator_keys + CHARON_SIMNET_BEACON_MOCK_FUZZ: "false" + CHARON_SYNTHETIC_BLOCK_PROPOSALS: "true" + CHARON_BUILDER_API: "false" + CHARON_OTLP_ADDRESS: tempo:4317 + CHARON_OTLP_SERVICE_NAME: node3 + CHARON_LOKI_ADDRESSES: http://loki:3100/loki/api/v1/push + CHARON_LOKI_SERVICE: node3 + + ports: + - "33600:3600" + + - "33610:3610" + + - "33620:3620" + + - "33630:3630" + + relay: + <<: *node-base + container_name: relay + command: relay + depends_on: [] + environment: + CHARON_HTTP_ADDRESS: 0.0.0.0:3640 + CHARON_MONITORING_ADDRESS: 0.0.0.0:3620 + CHARON_DATA_DIR: /compose/relay + CHARON_P2P_RELAYS: "" + CHARON_P2P_EXTERNAL_HOSTNAME: relay + CHARON_P2P_TCP_ADDRESS: 0.0.0.0:3610 + CHARON_P2P_UDP_ADDRESS: 0.0.0.0:3630 + CHARON_P2P_ADVERTISE_PRIVATE_ADDRESSES: "true" + CHARON_LOKI_ADDRESS: http://loki:3100/loki/api/v1/push + + vc0-lighthouse: + container_name: vc0-lighthouse + build: lighthouse + networks: [compose] + depends_on: [node0] + environment: + NODE: node0 + volumes: + - .:/compose + + vc1-lighthouse: + container_name: vc1-lighthouse + build: lighthouse + networks: [compose] + depends_on: [node1] + environment: + NODE: node1 + volumes: + - .:/compose + + vc3-lighthouse: + container_name: vc3-lighthouse + build: lighthouse + networks: [compose] + depends_on: [node3] + environment: + NODE: node3 + volumes: + - .:/compose + + curl: + container_name: curl + # Can be used to curl services; e.g. docker compose exec curl curl http://prometheus:9090/api/v1/rules\?type\=alert + image: curlimages/curl:latest + command: sleep 1d + networks: [compose] + + prometheus: + container_name: prometheus + image: prom/prometheus:${PROMETHEUS_VERSION:-v2.50.1} + ports: + - "9090:9090" + networks: [compose] + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - ./prometheus/rules.yml:/etc/prometheus/rules.yml + + + + grafana: + container_name: grafana + image: grafana/grafana:${GRAFANA_VERSION:-10.4.2} + ports: + - "3000:3000" + networks: [compose] + volumes: + - ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml + - ./grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/datasource.yml + - ./grafana/notifiers.yml:/etc/grafana/provisioning/notifiers/notifiers.yml + - ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro + - ./grafana/dash_charon_overview.json:/etc/dashboards/dash_charon_overview.json + - ./grafana/dash_duty_details.json:/etc/dashboards/dash_duty_details.json + - ./grafana/dash_alerts.json:/etc/dashboards/dash_alerts.json + + tempo: + container_name: tempo + image: grafana/tempo:${TEMPO_VERSION:-2.7.1} + networks: [compose] + user: ":" + command: -config.file=/opt/tempo/tempo.yaml + volumes: + - ./tempo:/opt/tempo + + loki: + container_name: loki + image: grafana/loki:${LOKI_VERSION:-2.8.2} + networks: [compose] + user: ":" + command: -config.file=/opt/loki/loki.yml + volumes: + - ./loki:/opt/loki + + +networks: + compose: diff --git a/test-infra/compose/testdata/TestNewDefaultConfig.golden b/test-infra/compose/testdata/TestNewDefaultConfig.golden new file mode 100644 index 00000000..648bf028 --- /dev/null +++ b/test-infra/compose/testdata/TestNewDefaultConfig.golden @@ -0,0 +1,32 @@ +{ + "version": "obol/charon/compose/1.0.0", + "step": "new", + "num_nodes": 4, + "threshold": 3, + "num_validators": 1, + "image_tag": "latest", + "build_local": false, + "node_impls": [ + "charon" + ], + "keygen_impl": "", + "pluto_image_tag": "local", + "key_gen": "create", + "split_keys_dir": "", + "beacon_nodes": "mock", + "external_relay": "", + "validator_clients": [ + "lighthouse", + "lighthouse", + "mock" + ], + "feature_set": "alpha", + "disable_monitoring_ports": false, + "insecure_keys": false, + "slot_duration": 1000000000, + "beacon-fuzz": false, + "p2p-fuzz": false, + "synthetic_block_proposals": true, + "monitoring": true, + "builder_api": false +} \ No newline at end of file From b42647e4849a7a2b32fb4b971a0ffce639fef3ba Mon Sep 17 00:00:00 2001 From: Quang Le Date: Mon, 13 Jul 2026 16:25:25 +0700 Subject: [PATCH 2/3] test(smoke): gate smoke tests on firing alerts via generated rules and warmup --- .gitignore | 5 +- test-infra/compose/README.md | 91 +++++--- test-infra/compose/alert.go | 145 +++++++++--- test-infra/compose/alert_internal_test.go | 65 ++++++ test-infra/compose/config.go | 72 +++++- test-infra/compose/define.go | 209 +++++++++++++++++- test-infra/compose/lock.go | 4 + test-infra/compose/rules_internal_test.go | 148 +++++++++++++ test-infra/compose/smoke/smoke_test.go | 157 +++++++++---- test-infra/compose/static/lighthouse/run.sh | 9 +- .../compose/static/prometheus/rules.yml | 44 ---- 11 files changed, 795 insertions(+), 154 deletions(-) create mode 100644 test-infra/compose/alert_internal_test.go create mode 100644 test-infra/compose/rules_internal_test.go delete mode 100644 test-infra/compose/static/prometheus/rules.yml diff --git a/.gitignore b/.gitignore index 304cfad0..d046f5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,7 @@ test-infra/sszfixtures/sszfixtures .claude/worktrees/ .claude/scheduled_tasks.lock -test-cluster \ No newline at end of file +test-cluster + +# Smoke-test docker-compose logs (go test -log-dir) +test-infra/compose/**/*.log diff --git a/test-infra/compose/README.md b/test-infra/compose/README.md index ae06caff..e3773f7a 100644 --- a/test-infra/compose/README.md +++ b/test-infra/compose/README.md @@ -2,7 +2,7 @@ > Run, test, and debug a developer-focussed insecure local pluto/charon cluster using docker compose -This is a port of charon's [`testutil/compose`](https://github.com/ObolNetwork/charon/tree/main/testutil/compose) +This is a port of charon's `[testutil/compose](https://github.com/ObolNetwork/charon/tree/main/testutil/compose)` (pinned at `v1.7.1`, the pluto parity reference) with one extra axis: each node in the cluster can run either **charon** or **pluto**, so clusters of N charon + M pluto nodes can be composed for cross-implementation testing. @@ -12,12 +12,14 @@ can be created and run. The aim is for developers to be able to debug features a functionality of clusters on their local machines. The `compose` command should be executed in sequential steps: - 1. `compose new`: Creates a new config.json that defines what will be composed. - 2. `compose define`: Creates a docker-compose.yml that executes `create dkg` if keygen==dkg. - 3. `compose lock`: Creates a docker-compose.yml that executes `create cluster` or `dkg`. - 4. `compose run`: Creates a docker-compose.yml that executes `run`. + +1. `compose new`: Creates a new config.json that defines what will be composed. +2. `compose define`: Creates a docker-compose.yml that executes `create dkg` if keygen==dkg. +3. `compose lock`: Creates a docker-compose.yml that executes `create cluster` or `dkg`. +4. `compose run`: Creates a docker-compose.yml that executes `run`. The `compose` command also includes some convenience functions. + - `compose clean`: Cleans the compose directory of existing files. - `compose auto`: Runs `compose define && compose lock && compose run`. @@ -34,24 +36,26 @@ compose new --nodes=4 --node-impls=charon,charon,pluto,pluto # 2 charon + 2 plu ``` - Charon nodes run `obolnetwork/charon:{tag}` (default `latest`, or `local` built from - `CHARON_REPO` with `--build-local`). +`CHARON_REPO` with `--build-local`). - Pluto nodes run `pluto:{tag}` (default `local`), built automatically from the repo - root `Dockerfile` during the define step. This requires the `PLUTO_REPO` env var - pointing at the pluto repo. +root `Dockerfile` during the define step. This requires the `PLUTO_REPO` env var +pointing at the pluto repo. - `--keygen-impl` selects which implementation runs the single-container keygen steps - (`create cluster` / `create dkg`); it defaults to node0's implementation. +(`create cluster` / `create dkg`); it defaults to node0's implementation. - The relay always runs the charon node-base image. Pluto accepts `CHARON_*` env vars and charon-compatible flags by design (CLI parity), so the generated docker-compose.yml services are identical for both implementations -apart from the image. +apart from the image. All node roles are supported for both implementations: keygen +(`create cluster`, `create dkg`, `dkg`) and `run`. -Note: scenarios where pluto nodes `run` require `pluto run` to be implemented; keygen -steps (`create cluster`, `create dkg`, `dkg`) work today. +Implementation names are validated when configs are written and loaded; anything +other than `charon` or `pluto` is rejected. ## Usage Examples Install the `compose` binary: + ``` # From inside the pluto repo go install ./test-infra/compose/compose @@ -60,6 +64,7 @@ go install ./test-infra/compose/compose ``` Create a pluto compose workspace folder: + ``` cd /tmp mkdir pluto-compose @@ -67,19 +72,32 @@ cd pluto-compose ``` Create the default cluster: + ``` export PLUTO_REPO=/path/to/pluto compose clean && compose new --node-impls=pluto && compose define && compose lock && compose run ``` Monitor the cluster via `grafana`: + ``` open http://localhost:3000/d/charon_overview_dashboard/charon-overview # Open Grafana simnet dashboard ``` -Creating a DKG based cluster that uses a locally built charon binary: +Creating a DKG based all-pluto cluster: + ``` -compose new --keygen=dkg --build-local +export PLUTO_REPO=/path/to/pluto +compose new --keygen=dkg --node-impls=pluto +compose auto +``` + +Creating a mixed cluster (2 charon + 2 pluto, forcing both implementations +to participate in every duty at the default 3-of-4 threshold): + +``` +export PLUTO_REPO=/path/to/pluto +compose new --nodes=4 --node-impls=charon,charon,pluto,pluto compose auto ``` @@ -87,28 +105,45 @@ compose auto `smoke/smoke_test.go` mirrors charon's compose smoke tests: each scenario generates and runs a full cluster with a mock beacon node (simnet), while a Prometheus container -evaluates the alert rules in `static/prometheus/rules.yml`. A scenario fails if any -alert fires within the test timeout. +evaluates the generated alert rules (see `writeAlertRules` in `define.go`). A scenario +fails if any alert fires. + +Alert semantics: collection starts once Prometheus answers its rules API. For the +next 60 seconds (the warmup window) exactly three known cold-start transients are +tolerated and must self-resolve — `Error Log Rate` (one consensus-timeout error per +node at the first epoch boundary, before the validator mock submits duties), +`Warn Log Rate` (charon's app-start warning burst), and `Broadcast Duty Rate` (no +duties broadcast before the p2p mesh forms). Any other alert fires the scenario +immediately, warmup or not, and so does anything still firing after the warmup. ``` cd test-infra/compose -# Charon-only scenario matrix (pulls obolnetwork/charon:v1.7.1): -go test ./smoke -v -integration -run 'TestSmoke/default_alpha' +# Pluto scenarios only (builds pluto:local from PLUTO_REPO; relay and +# pluto_keygen_create runtime nodes pull obolnetwork/charon:v1.7.1): +PLUTO_REPO=$(git rev-parse --show-toplevel) go test ./smoke -v -integration -timeout=35m \ + -run 'TestSmoke/(pluto_keygen_create|all_pluto|mixed_2_charon_2_pluto|pluto_dkg)$' -# Include pluto scenarios that work today (pluto keygen + charon runtime): -PLUTO_REPO=$(git rev-parse --show-toplevel) go test ./smoke -v -integration - -# Full matrix including scenarios running pluto nodes (requires `pluto run` support): -PLUTO_REPO=$(git rev-parse --show-toplevel) go test ./smoke -v -integration -pluto-run +# Full matrix (pluto + charon-only scenarios): +PLUTO_REPO=$(git rev-parse --show-toplevel) go test ./smoke -v -integration -timeout=35m # Keep docker-compose logs per scenario: -go test ./smoke -v -integration -log-dir=. +go test ./smoke -v -integration -timeout=35m -log-dir=. ``` -Scenarios running pluto nodes (`all_pluto`, `mixed_2_charon_2_pluto`, `pluto_dkg`) skip -until enabled with `-pluto-run`; flip the flag on (and later, the default) once -`pluto run` lands. +Scenarios that involve pluto (`pluto_keygen_create`, `all_pluto`, +`mixed_2_charon_2_pluto`, `pluto_dkg`) skip when the `PLUTO_REPO` env var is unset; +everything else always runs. `-timeout=35m` covers the full matrix (each scenario is +bounded by its own 2–3 minute alert window plus image builds); the Go default of 10m +is not enough. + +All smoke scenarios run the mock validator client. Real VCs cannot pass the alert +gate against charon v1.7.1's beaconmock: it reports `head_slot: "1"` from +`/eth/v1/node/syncing`, so e.g. lighthouse permanently treats the beacon node as +unsynced and performs no duties, starving the cluster below its signing threshold. +(Upstream charon runs lighthouse in these scenarios but its alert collector matches +a Prometheus state that never occurs, so nothing was ever gated.) The real-VC +services still work for manual clusters via `compose new --validator-types`. ## Versioning @@ -116,4 +151,4 @@ Charon is pinned to the pluto parity reference (`v1.7.1`): both the Go library i `go.mod` and the docker image tag used by smoke tests. The two `replace` directives in `go.mod` are copied from charon's own `go.mod` (Go does not propagate a dependency's replaces) and must be kept in sync when bumping charon. Bump deliberately alongside the -parity target, not to track charon main. +parity target, not to track charon main. \ No newline at end of file diff --git a/test-infra/compose/alert.go b/test-infra/compose/alert.go index f8cb62ec..349e750f 100644 --- a/test-infra/compose/alert.go +++ b/test-infra/compose/alert.go @@ -16,40 +16,76 @@ import ( const alertsPolled = "alerts_polled" +// alertWarmup is the window after Prometheus first answers the rules API in +// which the known cold-start transients (startupTransientRules) may fire; they +// must resolve before it ends. Alerts outside that allowlist fail immediately, +// warmup or not. Callers must give the alert context comfortably more than +// this (the smoke suite uses 2m timeouts). +const alertWarmup = time.Second * 60 + +// startupTransientRules names the alert rules that fire on any healthy +// cluster while it boots and self-resolve within the warmup window (all rule +// expressions are windowed, so a transient ages out): +// - Error Log Rate: the first epoch-boundary proposer consensus fails on +// every node (the validatormock delays 2 slots before submitting duties, +// so no randao exists yet), logging one consensus-timeout ERROR each. +// - Warn Log Rate: charon's app-start warning burst (insecure relay URL, +// empty QUIC address, beacon version parse) exceeds the 30s-window +// threshold once at boot. +// - Broadcast Duty Rate: nodes are scraped before the p2p mesh forms and +// the first duties broadcast, so the injected absent-series zero fires. +// +// Anything else firing during warmup (e.g. Pluto Down, validator API error +// rates) is a real failure and is reported immediately. +var startupTransientRules = map[string]bool{ + errorRateRule: true, + warnRateRule: true, + broadcastRule: true, +} + +// activeAlert is a firing alert: the rule name that produced it and its +// rendered description. +type activeAlert struct { + Rule string + Description string +} + // startAlertCollector starts a goroutine that polls prometheus alerts until the context is closed and returns // a channel on which the received alert descriptions will be sent. func startAlertCollector(ctx context.Context, dir string) chan string { resp := make(chan string, 100) go func() { - var ( - success bool - dedup = make(map[string]bool) - ) - defer close(resp) const iterSleep = time.Second * 2 - // Time required to wait for prometheus container to start. - time.Sleep(time.Second * 10) + // Wait for Prometheus to answer instead of sleeping blindly; the + // warmup window is anchored to readiness so slow container starts do + // not eat into it. + readyAt, ok := awaitPrometheusReady(ctx, dir, iterSleep) + if !ok { + return // Context closed: Auto reports "alerts couldn't be polled". + } - for ; ctx.Err() == nil; time.Sleep(iterSleep) { // Sleep for iterSleep before next iteration. - //nolint:revive // tls not required for testing. - cmd := exec.CommandContext(ctx, "docker", "compose", "exec", "-T", "curl", "curl", "-s", "http://prometheus:9090/api/v1/rules?type=alert") - cmd.Dir = dir + log.Info(ctx, "Prometheus ready, collecting alerts", + z.Str("warmup", alertWarmup.String())) - out, err := cmd.CombinedOutput() + resp <- alertsPolled // Push initial "fake alert" so logic can fail if no alerts polled. + + warmupEnd := readyAt.Add(alertWarmup) + + var ( + reported = make(map[string]bool) + ignored = make(map[string]bool) + ) + + for ; ctx.Err() == nil; time.Sleep(iterSleep) { // Sleep for iterSleep before next iteration. + alerts, err := queryAlerts(ctx, dir) if ctx.Err() != nil { return } else if err != nil { - log.Error(ctx, "Exec curl alerts", err, z.Str("out", string(out))) - continue - } - - var alerts promAlerts - if err := json.Unmarshal(bytes.TrimSpace(out), &alerts); err != nil { - resp <- errors.Wrap(err, "unmarshal alerts", z.Str("out", string(out))).Error() + log.Error(ctx, "Poll prometheus alerts", err) continue } @@ -58,21 +94,27 @@ func startAlertCollector(ctx context.Context, dir string) chan string { continue } - if !success { - resp <- alertsPolled // Push initial "fake alert" so logic can fail is not alerts polled. - - success = true - } + inWarmup := time.Now().Before(warmupEnd) for _, active := range getActiveAlerts(alerts) { - if dedup[active] { + if inWarmup && startupTransientRules[active.Rule] { + if !ignored[active.Description] { + ignored[active.Description] = true + log.Info(ctx, "Ignoring known cold-start transient during warmup", + z.Str("alert", active.Description)) + } + + continue // Still fails if firing after warmup, see below. + } + + if reported[active.Description] { continue } - dedup[active] = true - log.Info(ctx, "Detected new alert", z.Str("alert", active)) + reported[active.Description] = true + log.Info(ctx, "Detected new alert", z.Str("alert", active.Description)) - resp <- active + resp <- active.Description } } }() @@ -80,8 +122,46 @@ func startAlertCollector(ctx context.Context, dir string) chan string { return resp } -func getActiveAlerts(alerts promAlerts) []string { - var resp []string +// awaitPrometheusReady polls the prometheus rules API until it answers +// successfully, returning the readiness time. Returns false if the context +// closes first. +func awaitPrometheusReady(ctx context.Context, dir string, interval time.Duration) (time.Time, bool) { + log.Info(ctx, "Waiting for prometheus to answer the rules API") + + for ctx.Err() == nil { + alerts, err := queryAlerts(ctx, dir) + if err == nil && alerts.Status == "success" { + return time.Now(), true + } + + time.Sleep(interval) + } + + return time.Time{}, false +} + +// queryAlerts fetches and parses the prometheus alert rules via the curl +// container. +func queryAlerts(ctx context.Context, dir string) (promAlerts, error) { + //nolint:revive // tls not required for testing. + cmd := exec.CommandContext(ctx, "docker", "compose", "exec", "-T", "curl", "curl", "-s", "http://prometheus:9090/api/v1/rules?type=alert") + cmd.Dir = dir + + out, err := cmd.CombinedOutput() + if err != nil { + return promAlerts{}, errors.Wrap(err, "exec curl alerts", z.Str("out", string(out))) + } + + var alerts promAlerts + if err := json.Unmarshal(bytes.TrimSpace(out), &alerts); err != nil { + return promAlerts{}, errors.Wrap(err, "unmarshal alerts", z.Str("out", string(out))) + } + + return alerts, nil +} + +func getActiveAlerts(alerts promAlerts) []activeAlert { + var resp []activeAlert for _, group := range alerts.Data.Groups { for _, rule := range group.Rules { @@ -93,7 +173,10 @@ func getActiveAlerts(alerts promAlerts) []string { continue } - resp = append(resp, alert.Annotations.Description) + resp = append(resp, activeAlert{ + Rule: rule.Name, + Description: alert.Annotations.Description, + }) } } } diff --git a/test-infra/compose/alert_internal_test.go b/test-infra/compose/alert_internal_test.go new file mode 100644 index 00000000..caa7947e --- /dev/null +++ b/test-infra/compose/alert_internal_test.go @@ -0,0 +1,65 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestGetActiveAlertsFiringOnly asserts only firing alerts are reported: +// pending and inactive states (and charon's never-occurring "active") are +// ignored. +func TestGetActiveAlertsFiringOnly(t *testing.T) { + payload := `{ + "status": "success", + "data": { + "groups": [ + { + "name": "pluto", + "rules": [ + { + "name": "Error Log Rate", + "alerts": [ + {"state": "firing", "annotations": {"description": "node0 has a high error rate"}}, + {"state": "pending", "annotations": {"description": "node1 has a high error rate"}} + ] + }, + { + "name": "Pluto Down", + "alerts": [ + {"state": "inactive", "annotations": {"description": "node2 is down"}}, + {"state": "active", "annotations": {"description": "node3 is down"}} + ] + } + ] + } + ] + } + }` + + var alerts promAlerts + require.NoError(t, json.Unmarshal([]byte(payload), &alerts)) + + active := getActiveAlerts(alerts) + require.Equal(t, []activeAlert{{ + Rule: "Error Log Rate", + Description: "node0 has a high error rate", + }}, active) +} + +// TestStartupTransientRulesScoped pins the warmup allowlist: only the three +// proven cold-start transients may fire during warmup; scrape and API error +// alerts always fail. +func TestStartupTransientRulesScoped(t *testing.T) { + require.True(t, startupTransientRules["Error Log Rate"]) + require.True(t, startupTransientRules["Warn Log Rate"]) + require.True(t, startupTransientRules["Broadcast Duty Rate"]) + + require.False(t, startupTransientRules["Pluto Down"]) + require.False(t, startupTransientRules["Validator API Error Rate"]) + require.False(t, startupTransientRules["Proxy API Error Rate"]) + require.Len(t, startupTransientRules, 3) +} diff --git a/test-infra/compose/config.go b/test-infra/compose/config.go index 4cda0072..ab5fa6b2 100644 --- a/test-infra/compose/config.go +++ b/test-infra/compose/config.go @@ -3,8 +3,12 @@ package compose import ( + "fmt" "strings" "time" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/z" ) const ( @@ -150,6 +154,63 @@ type Config struct { // BuilderAPI enables the builder API for the compose cluster. BuilderAPI bool `json:"builder_api"` + + // AlertExcludeJobs exempts prometheus jobs (nodes) from the per-node + // behavioral alert rules (log rates, validator API rates, broadcast + // liveness) — the "Pluto Down" scrape check still applies. Used by smoke + // scenarios that deliberately degrade a node (e.g. 1_of_4_down isolates + // node0 from the p2p network): the degraded node is expected to log + // errors and stop broadcasting, while the rest of the cluster must stay + // clean. + AlertExcludeJobs []string `json:"alert_exclude_jobs,omitempty"` + + // AlertWarnExcludeTopics appends log topics to the Warn Log Rate + // exclusion list on top of the built-in charon mock noise (see + // writeAlertRules). Scenario-scoped escape hatch: e.g. mixed + // charon/pluto clusters exclude "sched" because charon's infosync warns + // each epoch until pluto serves /charon/priority/2.0.0 (#402B). + AlertWarnExcludeTopics []string `json:"alert_warn_exclude_topics,omitempty"` + + // AlertDisableRules drops entire alert rules (by name, see + // alertRuleNames) from the generated rules. Last-resort scenario knob + // for cluster-wide degradation that per-job exclusion cannot express: + // e.g. 1_of_3_down disables the error-rate gates because every third + // epoch-boundary proposer duty is round-1-led by the downed node and + // charon v1.7.1 cannot recover it (linear-timer bug #4537 plus the + // 1s-slot proposer deadline), so the HEALTHY nodes log the collateral + // consensus timeouts. + AlertDisableRules []string `json:"alert_disable_rules,omitempty"` +} + +// Validate rejects configs with unknown implementation names. It runs on +// every config write and load so a typo (e.g. --node-impls=plutoo) fails +// fast instead of silently selecting the charon image. +func (c Config) Validate() error { + validImpl := func(impl NodeImpl) bool { + return impl == ImplCharon || impl == ImplPluto + } + + for i, impl := range c.NodeImpls { + if !validImpl(impl) { + return errors.New("unknown node implementation; must be charon or pluto", + z.Str("impl", string(impl)), z.Int("index", i)) + } + } + + // Empty defaults to node0's implementation. + if c.KeyGenImpl != "" && !validImpl(c.KeyGenImpl) { + return errors.New("unknown keygen implementation; must be charon or pluto", + z.Str("impl", string(c.KeyGenImpl))) + } + + for _, rule := range c.AlertDisableRules { + if !alertRuleNames[rule] { + return errors.New("unknown alert rule name in alert_disable_rules", + z.Str("rule", rule)) + } + } + + return nil } // VCStrings returns the VCs field as a slice of strings. @@ -182,11 +243,16 @@ func (c Config) KeygenImpl() NodeImpl { // ImplImage returns the full docker image reference for the provided implementation. func (c Config) ImplImage(impl NodeImpl) string { - if impl == ImplPluto { + switch impl { + case ImplPluto: return plutoImage + ":" + c.PlutoImageTag + case ImplCharon: + return charonImage + ":" + c.ImageTag + default: + // Impls are validated on config write and load (Validate); reaching + // here means a code path bypassed that boundary. + panic(fmt.Sprintf("bug: unvalidated node implementation %q", impl)) } - - return charonImage + ":" + c.ImageTag } // ImageOverride returns the per-node image override for the provided implementation, diff --git a/test-infra/compose/define.go b/test-infra/compose/define.go index 36573219..8ffa7df8 100644 --- a/test-infra/compose/define.go +++ b/test-infra/compose/define.go @@ -188,6 +188,10 @@ func Define(ctx context.Context, dir string, conf Config) (TmplData, error) { return TmplData{}, err } + if err := writeAlertRules(dir, conf); err != nil { + return TmplData{}, err + } + log.Info(ctx, "Creating docker-compose.yml") log.Info(ctx, "Create cluster definition: docker compose up") @@ -281,9 +285,19 @@ func BuildLocalPluto(ctx context.Context) error { log.Info(ctx, "Building `pluto:local` docker container", z.Str("repo", repo)) + args := []string{"build", "-t", "pluto:local"} + + // Bake the git hash into the image: peers exchange it over peerinfo and + // warn about an empty/unparseable hash ("Invalid peer git hash"). + if hash, err := gitCommitHashShort(ctx, repo); err == nil { + args = append(args, "--build-arg", "GIT_COMMIT_HASH_SHORT="+hash) + } + + args = append(args, ".") + var out bytes.Buffer // Only log output if there is an error. - cmd := exec.CommandContext(ctx, "docker", "build", "-t", "pluto:local", ".") + cmd := exec.CommandContext(ctx, "docker", args...) cmd.Stdout = &out cmd.Stderr = &out cmd.Dir = repo @@ -295,6 +309,19 @@ func BuildLocalPluto(ctx context.Context) error { return nil } +// gitCommitHashShort returns the repo's short (7 char) commit hash. +func gitCommitHashShort(ctx context.Context, repo string) (string, error) { + cmd := exec.CommandContext(ctx, "git", "rev-parse", "--short=7", "HEAD") + cmd.Dir = repo + + out, err := cmd.Output() + if err != nil { + return "", errors.Wrap(err, "git rev-parse") + } + + return strings.TrimSpace(string(out)), nil +} + // copyStaticFolders copies the embedded static folders to the compose dir. func copyStaticFolders(dir string) error { const staticRoot = "static" @@ -371,6 +398,10 @@ rule_files: - /etc/prometheus/rules.yml `) + if err := os.MkdirAll(path.Join(dir, "prometheus"), 0o755); err != nil { + return errors.Wrap(err, "mkdir prometheus") + } + err := os.WriteFile(path.Join(dir, "prometheus", "prometheus.yml"), []byte(b.String()), 0o644) //nolint:gosec if err != nil { return errors.Wrap(err, "write prometheus.yml") @@ -379,6 +410,178 @@ rule_files: return nil } +// Canonical alert rule names: generated by writeAlertRules, validated +// against Config.AlertDisableRules, and referenced by the alert collector's +// warmup allowlist. +const ( + plutoDownRule = "Pluto Down" + errorRateRule = "Error Log Rate" + warnRateRule = "Warn Log Rate" + vapiRateRule = "Validator API Error Rate" + proxyRateRule = "Proxy API Error Rate" + broadcastRule = "Broadcast Duty Rate" +) + +// alertRuleNames is the set of valid rule names for Config.AlertDisableRules. +var alertRuleNames = map[string]bool{ + plutoDownRule: true, + errorRateRule: true, + warnRateRule: true, + vapiRateRule: true, + proxyRateRule: true, + broadcastRule: true, +} + +// writeAlertRules writes the prometheus alert rules evaluated by the smoke +// tests. Rules are generated (not static) because the expressions depend on +// config: scenarios that deliberately degrade a node exempt its job via +// conf.AlertExcludeJobs, mixed-impl scenarios extend the warn-topic +// exclusions via conf.AlertWarnExcludeTopics, and cluster-wide degradations +// drop whole rules via conf.AlertDisableRules. +// +// Charon's "Outstanding Duty Rate" rule (core_bcast_broadcast_total - +// core_scheduler_duty_total > 50) is deliberately not ported: a node cannot +// broadcast a duty more often than it is scheduled, and the two counters +// only share a subset of duty label values (the rest drop out of the vector +// match), so the expression can never exceed zero — the rule is dead +// upstream too. +func writeAlertRules(dir string, conf Config) error { + // Exclusion matcher for per-node behavioral rules; empty when no node is + // exempted. "Pluto Down" (up == 0) is never exempted: a degraded node + // must still be scrapable. + var jobExcl string + if len(conf.AlertExcludeJobs) > 0 { + jobExcl = fmt.Sprintf(`job!~"%s"`, strings.Join(conf.AlertExcludeJobs, "|")) + } + + // sel renders a PromQL label-matcher block from the non-empty matchers. + sel := func(matchers ...string) string { + var parts []string + for _, m := range matchers { + if m != "" { + parts = append(parts, m) + } + } + + if len(parts) == 0 { + return "" + } + + return "{" + strings.Join(parts, ",") + "}" + } + + // Warn Log Rate always excludes charon v1.7.1 topics that warn + // structurally in any healthy simnet cluster (verified in the all-charon + // `dkg` scenario): + // - vmock: the in-process validatormock schedules DutyBuilderRegistration + // every epoch (~16 duties per epoch start) with no handler, so every + // VCMock node warns "Duty failed: unexpected duty" in bursts. + // - tracker: the beaconmock never includes broadcast duties on-chain, so + // every successful proposal epoch warns "Broadcasted block/attestation + // never included on-chain" (the better the cluster works, the more it + // warns). + // Both are mock artifacts, not node behavior; all other warn topics stay + // gated unless a scenario opts out via AlertWarnExcludeTopics. + warnTopics := strings.Join(append([]string{"vmock", "tracker"}, conf.AlertWarnExcludeTopics...), "|") + + // The broadcast-liveness expression must fail when a node exposes NO + // core_bcast_broadcast_total series at all: the counter is created on + // first broadcast, so a node that never broadcasts has no series and a + // plain `increase(...) < 0.5` can never fire for it. Inject a 0 for + // every scraped node job (`0 * up`) so absent series alert too. Summed + // per job because the per-duty sync_message series legitimately pauses 6 + // of every 8 epochs (simnet sync-committee membership window). Scoped to + // node jobs: the relay never broadcasts duties. + bcastSel := sel(`job=~"node[0-9]+"`, jobExcl) + + errorSel := sel(jobExcl) + warnSel := sel(fmt.Sprintf(`topic!~"%s"`, warnTopics), jobExcl) + vapiSel := sel(`endpoint!="proxy"`, jobExcl) + proxySel := sel(`endpoint="proxy"`, jobExcl) + + // Blocks keyed by rule name so conf.AlertDisableRules can drop whole + // rules; the names double as the collector's warmup allowlist keys. + ruleBlocks := []struct { + name string + block string + }{ + {plutoDownRule, ` - alert: Pluto Down + expr: up == 0 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} is down" +`}, + // Windowed instead of charon's absolute app_log_error_total > 0: a + // fresh simnet cluster loses the first epoch-boundary proposer + // consensus (vmock 2-slot startup delay -> no randao yet), logging + // exactly one consensus timeout ERROR per node on charon and pluto + // alike. An absolute counter gate can never recover from that + // cold-start artifact; a 30s window plus the collector warmup + // (compose/alert.go) gates steady-state errors only. + {errorRateRule, fmt.Sprintf(` - alert: Error Log Rate + expr: increase(app_log_error_total%s[30s]) > 0 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} has a high error rate" +`, errorSel)}, + {warnRateRule, fmt.Sprintf(` - alert: Warn Log Rate + expr: increase(app_log_warn_total%s[30s]) > 2 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} has a high warning rate" +`, warnSel)}, + {vapiRateRule, fmt.Sprintf(` - alert: Validator API Error Rate + expr: increase(core_validatorapi_request_error_total%s[30s]) > 1 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} validator API a high error rate" +`, vapiSel)}, + {proxyRateRule, fmt.Sprintf(` - alert: Proxy API Error Rate + expr: increase(core_validatorapi_request_error_total%s[30s]) > 5 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} proxy API a high error rate" +`, proxySel)}, + {broadcastRule, fmt.Sprintf(` - alert: Broadcast Duty Rate + expr: (sum by (job) (increase(core_bcast_broadcast_total%[1]s[30s])) or on (job) max by (job) (0 * up%[1]s)) < 0.5 + for: 15s + annotations: + description: "Pluto {{ $labels.job }} is not broadcasting enough duties" +`, bcastSel)}, + } + + disabled := make(map[string]bool) + for _, rule := range conf.AlertDisableRules { + disabled[rule] = true + } + + var b strings.Builder + + b.WriteString("groups:\n- name: pluto\n rules:\n") + + for _, rule := range ruleBlocks { + if disabled[rule.name] { + continue + } + + b.WriteString(rule.block) + b.WriteString("\n") + } + + rules := strings.TrimSuffix(b.String(), "\n") + + if err := os.MkdirAll(path.Join(dir, "prometheus"), 0o755); err != nil { + return errors.Wrap(err, "mkdir prometheus") + } + + err := os.WriteFile(path.Join(dir, "prometheus", "rules.yml"), []byte(rules), 0o644) //nolint:gosec + if err != nil { + return errors.Wrap(err, "write rules.yml") + } + + return nil +} + // keyGenFunc can be overridden in tests for deterministic p2pkeys. var keyGenFunc = func() (*k1.PrivateKey, error) { privkey, err := k1.GeneratePrivateKey() @@ -412,6 +615,10 @@ func nodeFile(dir string, i int, file string) string { // WriteConfig writes the config as yaml to disk. func WriteConfig(dir string, conf Config) error { + if err := conf.Validate(); err != nil { + return err + } + b, err := json.MarshalIndent(conf, "", " ") if err != nil { return errors.Wrap(err, "marshal config") diff --git a/test-infra/compose/lock.go b/test-infra/compose/lock.go index f35a52ee..e50076ca 100644 --- a/test-infra/compose/lock.go +++ b/test-infra/compose/lock.go @@ -180,5 +180,9 @@ func LoadConfig(dir string) (Config, error) { return Config{}, errors.Wrap(err, "unmarshal Config") } + if err := resp.Validate(); err != nil { + return Config{}, err + } + return resp, nil } diff --git a/test-infra/compose/rules_internal_test.go b/test-infra/compose/rules_internal_test.go new file mode 100644 index 00000000..08b4c263 --- /dev/null +++ b/test-infra/compose/rules_internal_test.go @@ -0,0 +1,148 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "os" + "path" + "strconv" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestWritePrometheusConfigScrapesAllNodes asserts the generated scrape +// config covers every configured node plus the relay, so the `up == 0` and +// injected-zero broadcast alerts can see all of them. +func TestWritePrometheusConfigScrapesAllNodes(t *testing.T) { + dir := t.TempDir() + + conf := NewDefaultConfig() + conf.NumNodes = 10 + + require.NoError(t, writePrometheusConfig(dir, conf)) + + b, err := os.ReadFile(path.Join(dir, "prometheus", "prometheus.yml")) + require.NoError(t, err) + + content := string(b) + require.Contains(t, content, "- targets: [ 'relay:3620' ]") + + for i := range conf.NumNodes { + require.Contains(t, content, "job_name: 'node"+strconv.Itoa(i)+"'") + require.Contains(t, content, "- targets: ['node"+strconv.Itoa(i)+":3620']") + } + + require.NotContains(t, content, "node10", "must not scrape beyond NumNodes") +} + +// TestWriteAlertRulesBroadcastCoversMissingSeries asserts the broadcast +// liveness expression injects a zero for scraped node jobs with no +// core_bcast_broadcast_total series, so a node that never broadcasts (the +// counter is only created on first broadcast) fails instead of silently +// passing. +func TestWriteAlertRulesBroadcastCoversMissingSeries(t *testing.T) { + content := writeRules(t, NewDefaultConfig()) + + require.Contains(t, content, + `expr: (sum by (job) (increase(core_bcast_broadcast_total{job=~"node[0-9]+"}[30s])) or on (job) max by (job) (0 * up{job=~"node[0-9]+"})) < 0.5`) +} + +// TestWriteAlertRulesExcludesDegradedJobs asserts AlertExcludeJobs exempts a +// node from every behavioral rule while "Pluto Down" keeps watching it. +func TestWriteAlertRulesExcludesDegradedJobs(t *testing.T) { + conf := NewDefaultConfig() + conf.AlertExcludeJobs = []string{"node0"} + + content := writeRules(t, conf) + + require.Contains(t, content, `increase(app_log_error_total{job!~"node0"}[30s]) > 0`) + require.Contains(t, content, `increase(app_log_warn_total{topic!~"vmock|tracker",job!~"node0"}[30s]) > 2`) + require.Contains(t, content, `increase(core_validatorapi_request_error_total{endpoint!="proxy",job!~"node0"}[30s]) > 1`) + require.Contains(t, content, `increase(core_validatorapi_request_error_total{endpoint="proxy",job!~"node0"}[30s]) > 5`) + require.Contains(t, content, + `(sum by (job) (increase(core_bcast_broadcast_total{job=~"node[0-9]+",job!~"node0"}[30s])) or on (job) max by (job) (0 * up{job=~"node[0-9]+",job!~"node0"})) < 0.5`) + + // The scrape-liveness rule must never carry exclusions. + require.Contains(t, content, "expr: up == 0") +} + +// TestWriteAlertRulesWarnTopicExtension asserts scenario-scoped warn-topic +// exclusions append to the built-in mock-noise list. +func TestWriteAlertRulesWarnTopicExtension(t *testing.T) { + conf := NewDefaultConfig() + conf.AlertWarnExcludeTopics = []string{"sched"} + + content := writeRules(t, conf) + require.Contains(t, content, `increase(app_log_warn_total{topic!~"vmock|tracker|sched"}[30s]) > 2`) +} + +// TestWriteAlertRulesDropsOutstandingDuty pins the removal of charon's dead +// "Outstanding Duty Rate" rule (broadcast counts can never exceed scheduled +// counts, so the expression could never fire). +func TestWriteAlertRulesDropsOutstandingDuty(t *testing.T) { + content := writeRules(t, NewDefaultConfig()) + require.NotContains(t, content, "Outstanding Duty") + require.NotContains(t, content, "core_scheduler_duty_total") +} + +// TestWriteAlertRulesDisableRules asserts AlertDisableRules drops exactly the +// named rules and validation rejects unknown names. +func TestWriteAlertRulesDisableRules(t *testing.T) { + conf := NewDefaultConfig() + conf.AlertDisableRules = []string{"Error Log Rate", "Validator API Error Rate"} + + content := writeRules(t, conf) + require.NotContains(t, content, "Error Log Rate") + require.NotContains(t, content, `endpoint!="proxy"`) + // The remaining gates stay. + require.Contains(t, content, "Pluto Down") + require.Contains(t, content, "Warn Log Rate") + require.Contains(t, content, "Proxy API Error Rate") + require.Contains(t, content, "Broadcast Duty Rate") + + conf = NewDefaultConfig() + conf.AlertDisableRules = []string{"No Such Rule"} + require.ErrorContains(t, WriteConfig(t.TempDir(), conf), "unknown alert rule name") +} + +// TestConfigValidateRejectsUnknownImpl asserts unknown implementation names +// fail on write and on load instead of silently running the charon image. +func TestConfigValidateRejectsUnknownImpl(t *testing.T) { + conf := NewDefaultConfig() + conf.NodeImpls = []NodeImpl{ImplCharon, "geth"} + require.ErrorContains(t, WriteConfig(t.TempDir(), conf), "unknown node implementation") + + conf = NewDefaultConfig() + conf.KeyGenImpl = "plutoo" + require.ErrorContains(t, WriteConfig(t.TempDir(), conf), "unknown keygen implementation") + + // Loading a hand-edited config with a bad impl fails too. + dir := t.TempDir() + badJSON := `{"version":"obol/charon/compose/1.0.0","node_impls":["geth"]}` + require.NoError(t, os.WriteFile(path.Join(dir, "config.json"), []byte(badJSON), 0o644)) + _, err := LoadConfig(dir) + require.ErrorContains(t, err, "unknown node implementation") + + // The happy path still validates. + conf = NewDefaultConfig() + conf.NodeImpls = []NodeImpl{ImplCharon, ImplPluto} + conf.KeyGenImpl = ImplPluto + dir = t.TempDir() + require.NoError(t, WriteConfig(dir, conf)) + _, err = LoadConfig(dir) + require.NoError(t, err) +} + +// writeRules writes alert rules for conf into a temp dir and returns them. +func writeRules(t *testing.T, conf Config) string { + t.Helper() + + dir := t.TempDir() + require.NoError(t, writeAlertRules(dir, conf)) + + b, err := os.ReadFile(path.Join(dir, "prometheus", "rules.yml")) + require.NoError(t, err) + + return string(b) +} diff --git a/test-infra/compose/smoke/smoke_test.go b/test-infra/compose/smoke/smoke_test.go index c5059435..43196fca 100644 --- a/test-infra/compose/smoke/smoke_test.go +++ b/test-infra/compose/smoke/smoke_test.go @@ -22,7 +22,6 @@ import ( var ( integration = flag.Bool("integration", false, "Enable docker based integration test") - plutoRun = flag.Bool("pluto-run", false, "Enable scenarios that run pluto nodes. Requires `pluto run` support and the PLUTO_REPO env var.") sudoPerms = flag.Bool("sudo-perms", false, "Enables changing all compose artefacts file permissions using sudo.") logDir = flag.String("log-dir", "", "Specifies the directory to store test docker-compose logs. Empty defaults to stdout.") ) @@ -30,23 +29,49 @@ var ( // charonImageTag pins the charon reference version pluto is ported from. const charonImageTag = "v1.7.1" -func TestSmoke(t *testing.T) { - if !*integration { - t.Skip("Skipping smoke integration test") - } +// defaultTimeout bounds one scenario's alert collection: Prometheus readiness +// (~10s) + the 60s cold-start warmup (compose/alert.go) + steady-state +// polling time beyond it. +const defaultTimeout = 2 * time.Minute - const defaultTimeout = time.Minute - - tests := []struct { - Name string - ConfigFunc func(*compose.Config) - RunTmplFunc func(*compose.TmplData) - DefineTmplFunc func(*compose.TmplData) - PrintYML bool - Timeout time.Duration - RequirePluto bool // Scenario needs the pluto docker image (PLUTO_REPO env var). - RequirePlutoRun bool // Scenario runs pluto nodes, which requires `pluto run` support (-pluto-run flag). - }{ +// smokeBaseConfig returns the config every scenario starts from. +// +// All scenarios run the mock validator client: charon v1.7.1's beaconmock +// hardcodes `head_slot: "1"` in /eth/v1/node/syncing, so a real VC (e.g. +// lighthouse) permanently considers the beacon node unsynced and performs no +// duties — the cluster then never reaches the signing threshold and the +// broadcast/error alerts fire by design. Upstream charon runs lighthouse VCs +// in these scenarios but never noticed because its alert gate matches a +// state ("active") that Prometheus never reports. The real-VC compose +// services remain available for manual runs via `compose new +// --validator-types`. +func smokeBaseConfig() compose.Config { + conf := compose.NewDefaultConfig() + conf.Monitoring = false + conf.DisableMonitoringPorts = true + conf.ImageTag = charonImageTag + conf.InsecureKeys = true + conf.VCs = []compose.VCType{compose.VCMock} + + return conf +} + +// smokeScenario defines one smoke matrix entry. +type smokeScenario struct { + Name string + ConfigFunc func(*compose.Config) + RunTmplFunc func(*compose.TmplData) + DefineTmplFunc func(*compose.TmplData) + PrintYML bool + Timeout time.Duration + RequirePluto bool // Scenario needs the pluto docker image (PLUTO_REPO env var). +} + +// smokeScenarios returns the full scenario matrix. Every scenario runs when +// -integration is set; the only skip condition is a pluto scenario without +// the PLUTO_REPO env var. +func smokeScenarios() []smokeScenario { + return []smokeScenario{ { Name: "default_alpha", PrintYML: true, @@ -75,7 +100,6 @@ func TestSmoke(t *testing.T) { Name: "dkg", ConfigFunc: func(conf *compose.Config) { conf.KeyGen = compose.KeyGenDKG - conf.VCs = []compose.VCType{compose.VCMock} }, }, { @@ -85,14 +109,21 @@ func TestSmoke(t *testing.T) { conf.Threshold = 7 conf.NumValidators = 100 conf.KeyGen = compose.KeyGenCreate - conf.VCs = []compose.VCType{compose.VCMock} conf.SlotDuration = time.Second * 6 conf.SyntheticBlockProposals = false }, - Timeout: time.Minute * 2, + Timeout: time.Minute * 3, }, { + // node0 keeps default p2p flags (public relays) so it runs but + // cannot reach the cluster: expected to log errors and stop + // broadcasting, hence exempted from the per-node behavioral + // alerts (not from "Pluto Down"). The other three nodes must + // stay clean. Name: "1_of_4_down", + ConfigFunc: func(conf *compose.Config) { + conf.AlertExcludeJobs = []string{"node0"} + }, RunTmplFunc: func(data *compose.TmplData) { node0 := data.Nodes[0] for i := range len(node0.EnvVars) { @@ -103,10 +134,23 @@ func TestSmoke(t *testing.T) { }, }, { + // Unlike 1_of_4_down, the error gates are disabled cluster-wide: + // with 3 nodes the epoch-boundary proposer's round-1 leader + // rotates ((slot+type+round)%nodes, epoch slots are 0 mod 16), + // so every third proposer duty is led by the downed node0 and + // charon v1.7.1 cannot recover it — the linear round timer's + // post-round-1 timeouts are nanoseconds (upstream bug #4537) and + // the 1s-slot proposer deadline (~0.4s) expires regardless — so + // the HEALTHY nodes log the collateral consensus timeouts and + // failing vmock proposal requests. Broadcast liveness, warn + // rates, and scrape health stay gated. (4-node clusters dodge + // this: 16 % 4 == 0 keeps the round-1 leader fixed off node0.) Name: "1_of_3_down", ConfigFunc: func(conf *compose.Config) { conf.NumNodes = 3 conf.Threshold = 2 + conf.AlertExcludeJobs = []string{"node0"} + conf.AlertDisableRules = []string{"Error Log Rate", "Validator API Error Rate"} }, RunTmplFunc: func(data *compose.TmplData) { node0 := data.Nodes[0] @@ -131,69 +175,65 @@ func TestSmoke(t *testing.T) { ConfigFunc: func(conf *compose.Config) { conf.KeyGen = compose.KeyGenCreate conf.KeyGenImpl = compose.ImplPluto - conf.VCs = []compose.VCType{compose.VCMock} }, }, { - Name: "all_pluto", - RequirePluto: true, - RequirePlutoRun: true, + Name: "all_pluto", + RequirePluto: true, ConfigFunc: func(conf *compose.Config) { conf.KeyGen = compose.KeyGenCreate conf.NodeImpls = []compose.NodeImpl{compose.ImplPluto} - conf.VCs = []compose.VCType{compose.VCMock} // `pluto run` fails fast on --synthetic-block-proposals. conf.SyntheticBlockProposals = false }, }, { // Threshold 3 of 4 forces both implementations to participate in every duty. - Name: "mixed_2_charon_2_pluto", - RequirePluto: true, - RequirePlutoRun: true, + Name: "mixed_2_charon_2_pluto", + RequirePluto: true, ConfigFunc: func(conf *compose.Config) { conf.KeyGen = compose.KeyGenCreate conf.NodeImpls = []compose.NodeImpl{ compose.ImplCharon, compose.ImplCharon, compose.ImplPluto, compose.ImplPluto, } - conf.VCs = []compose.VCType{compose.VCMock} // `pluto run` fails fast on --synthetic-block-proposals. conf.SyntheticBlockProposals = false + // Charon triggers infosync (/charon/priority/2.0.0) every + // epoch; pluto does not serve the protocol yet (#402B), so + // charon nodes warn "P2P sending failing" under topic=sched + // twice per epoch. Exempt that topic in mixed clusters until + // the protocol lands; drop this with #402B. + conf.AlertWarnExcludeTopics = []string{"sched"} }, }, { - Name: "pluto_dkg", - RequirePluto: true, - RequirePlutoRun: true, + Name: "pluto_dkg", + RequirePluto: true, ConfigFunc: func(conf *compose.Config) { conf.KeyGen = compose.KeyGenDKG conf.NodeImpls = []compose.NodeImpl{compose.ImplPluto} - conf.VCs = []compose.VCType{compose.VCMock} // `pluto run` fails fast on --synthetic-block-proposals. conf.SyntheticBlockProposals = false }, }, } +} + +func TestSmoke(t *testing.T) { + if !*integration { + t.Skip("Skipping smoke integration test") + } - for _, test := range tests { + for _, test := range smokeScenarios() { t.Run(test.Name, func(t *testing.T) { if test.RequirePluto && os.Getenv("PLUTO_REPO") == "" { t.Skip("Skipping pluto scenario since PLUTO_REPO env var is not set") } - if test.RequirePlutoRun && !*plutoRun { - t.Skip("Skipping scenario running pluto nodes; enable with -pluto-run once `pluto run` is supported") - } - dir := t.TempDir() - conf := compose.NewDefaultConfig() - conf.Monitoring = false - conf.DisableMonitoringPorts = true - conf.ImageTag = charonImageTag - - conf.InsecureKeys = true + conf := smokeBaseConfig() if test.ConfigFunc != nil { test.ConfigFunc(&conf) } @@ -224,3 +264,32 @@ func TestSmoke(t *testing.T) { }) } } + +// TestScenarioMatrix guards the scenario table invariants without docker: +// unique names, valid configs, and the RequirePluto gate matching the impls a +// scenario actually uses — a pluto scenario without the gate would fail on +// missing PLUTO_REPO (or silently run a stale pluto:local image), and a +// charon-only scenario with the gate would skip for no reason. +func TestScenarioMatrix(t *testing.T) { + seen := make(map[string]bool) + + for _, test := range smokeScenarios() { + require.NotEmpty(t, test.Name) + require.False(t, seen[test.Name], "duplicate scenario name: %s", test.Name) + seen[test.Name] = true + + conf := smokeBaseConfig() + if test.ConfigFunc != nil { + test.ConfigFunc(&conf) + } + + require.Equal(t, test.RequirePluto, conf.UsesPluto(), + "RequirePluto must match the implementations scenario %q uses", test.Name) + + // Every scenario config must survive the write/load validation boundary. + dir := t.TempDir() + require.NoError(t, compose.WriteConfig(dir, conf)) + _, err := compose.LoadConfig(dir) + require.NoError(t, err) + } +} diff --git a/test-infra/compose/static/lighthouse/run.sh b/test-infra/compose/static/lighthouse/run.sh index 192a2730..9f32ed40 100755 --- a/test-infra/compose/static/lighthouse/run.sh +++ b/test-infra/compose/static/lighthouse/run.sh @@ -14,9 +14,14 @@ echo "0" > /tmp/testnet/deposit_contract_block.txt for f in /compose/"${NODE}"/validator_keys/keystore-*.json; do echo "Importing key ${f}" - cat "$(echo "${f}" | sed 's/json/txt/')" | lighthouse account validator import \ + # --password-file instead of charon's stdin pipe: lighthouse v7 prompts on + # the tty and dies with "Error reading from tty" in a docker container, + # leaving the VC with zero validators. + lighthouse account validator import \ --testnet-dir "/tmp/testnet" \ - --keystore "${f}" + --keystore "${f}" \ + --password-file "$(echo "${f}" | sed 's/json/txt/')" \ + --reuse-password done diff --git a/test-infra/compose/static/prometheus/rules.yml b/test-infra/compose/static/prometheus/rules.yml deleted file mode 100644 index 583c784a..00000000 --- a/test-infra/compose/static/prometheus/rules.yml +++ /dev/null @@ -1,44 +0,0 @@ -groups: -- name: pluto - rules: - - alert: Pluto Down - expr: up == 0 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} is down" - - - alert: Error Log Rate - expr: app_log_error_total > 0 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} has a high error rate" - - - alert: Warn Log Rate - expr: increase(app_log_warn_total[30s]) > 2 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} has a high warning rate" - - - alert: Validator API Error Rate - expr: increase(core_validatorapi_request_error_total{endpoint!="proxy"}[30s]) > 1 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} validator API a high error rate" - - - alert: Proxy API Error Rate - expr: increase(core_validatorapi_request_error_total{endpoint="proxy"}[30s]) > 5 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} proxy API a high error rate" - - - alert: Broadcast Duty Rate - expr: increase(core_bcast_broadcast_total[30s]) < 0.5 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} is not broadcasting enough duties" - - - alert: Outstanding Duty Rate - expr: core_bcast_broadcast_total - core_scheduler_duty_total > 50 - for: 15s - annotations: - description: "Pluto {{ $labels.job }} has too many outstanding duties" From add04711d7ab97ea27f701aeb2c4bda974673087 Mon Sep 17 00:00:00 2001 From: Quang Le Date: Fri, 24 Jul 2026 17:42:53 +0700 Subject: [PATCH 3/3] fix: remove compose cli --- test-infra/compose/README.md | 135 ++++++--------- test-infra/compose/auto.go | 2 +- test-infra/compose/compose/main.go | 217 ------------------------- test-infra/compose/smoke/smoke_test.go | 12 +- 4 files changed, 64 insertions(+), 302 deletions(-) delete mode 100644 test-infra/compose/compose/main.go diff --git a/test-infra/compose/README.md b/test-infra/compose/README.md index e3773f7a..a6286ab9 100644 --- a/test-infra/compose/README.md +++ b/test-infra/compose/README.md @@ -1,106 +1,46 @@ # Pluto Compose -> Run, test, and debug a developer-focussed insecure local pluto/charon cluster using docker compose +> A docker-compose test harness for standing up insecure local pluto/charon clusters, used by the smoke integration tests. -This is a port of charon's `[testutil/compose](https://github.com/ObolNetwork/charon/tree/main/testutil/compose)` -(pinned at `v1.7.1`, the pluto parity reference) with one extra axis: each node in the -cluster can run either **charon** or **pluto**, so clusters of N charon + M pluto nodes +This is adapted from charon's [testutil/compose](https://github.com/ObolNetwork/charon/tree/main/testutil/compose) +(pinned at `v1.7.1`, the pluto parity reference) and extended with one extra axis: each node +in the cluster can run either **charon** or **pluto**, so clusters of N charon + M pluto nodes can be composed for cross-implementation testing. -Compose is a tool that generates `docker-compose.yml` files such that different clusters -can be created and run. The aim is for developers to be able to debug features and check -functionality of clusters on their local machines. +The harness generates `docker-compose.yml` files that stand up a full cluster (keygen + +run) against a mock beacon node. It is driven programmatically by the integration tests +under `smoke/` — there is no standalone CLI. Cluster generation happens in +three stages, exposed as package functions and pinned by the golden tests in `testdata/`: -The `compose` command should be executed in sequential steps: +1. **define** (`Define`): writes a `docker-compose.yml` that runs `create dkg` when keygen==dkg. +2. **lock** (`Lock`): writes a `docker-compose.yml` that runs `create cluster` or `dkg`. +3. **run** (`Run`): writes a `docker-compose.yml` that runs the cluster. -1. `compose new`: Creates a new config.json that defines what will be composed. -2. `compose define`: Creates a docker-compose.yml that executes `create dkg` if keygen==dkg. -3. `compose lock`: Creates a docker-compose.yml that executes `create cluster` or `dkg`. -4. `compose run`: Creates a docker-compose.yml that executes `run`. - -The `compose` command also includes some convenience functions. - -- `compose clean`: Cleans the compose directory of existing files. -- `compose auto`: Runs `compose define && compose lock && compose run`. - -Note that compose automatically runs `docker compose up` at the end of each command. This can be disabled via `--up=false`. +`Auto` (see `auto.go`) chains define → lock → run and runs `docker compose up`; it is what +the tests call after writing a config with `WriteConfig`. ## Node implementations -Node implementations are assigned round-robin from `--node-impls` (like validator types): +Each node runs either charon or pluto, assigned round-robin from a scenario's `NodeImpls` +config (empty defaults to all charon): -``` -compose new --node-impls=charon # all charon (default) -compose new --node-impls=pluto # all pluto -compose new --nodes=4 --node-impls=charon,charon,pluto,pluto # 2 charon + 2 pluto -``` - -- Charon nodes run `obolnetwork/charon:{tag}` (default `latest`, or `local` built from -`CHARON_REPO` with `--build-local`). +- Charon nodes run `obolnetwork/charon:{tag}` (smoke pins `v1.7.1`; the default config +uses `latest`). Set the tag to `local` to build from `CHARON_REPO`. - Pluto nodes run `pluto:{tag}` (default `local`), built automatically from the repo root `Dockerfile` during the define step. This requires the `PLUTO_REPO` env var pointing at the pluto repo. -- `--keygen-impl` selects which implementation runs the single-container keygen steps +- `KeyGenImpl` selects which implementation runs the single-container keygen steps (`create cluster` / `create dkg`); it defaults to node0's implementation. - The relay always runs the charon node-base image. -Pluto accepts `CHARON_*` env vars and charon-compatible flags by design (CLI parity), +Pluto accepts `CHARON_`* env vars and charon-compatible flags by design (CLI parity), so the generated docker-compose.yml services are identical for both implementations apart from the image. All node roles are supported for both implementations: keygen -(`create cluster`, `create dkg`, `dkg`) and `run`. +(`create cluster`, `create dkg`, `dkg`) and run. Implementation names are validated when configs are written and loaded; anything other than `charon` or `pluto` is rejected. -## Usage Examples - -Install the `compose` binary: - -``` -# From inside the pluto repo -go install ./test-infra/compose/compose - -# If `which compose` fails, then fix your environment: `export PATH=$PATH:$(go env GOPATH)/bin`. Or see https://go.dev/doc/gopath_code -``` - -Create a pluto compose workspace folder: - -``` -cd /tmp -mkdir pluto-compose -cd pluto-compose -``` - -Create the default cluster: - -``` -export PLUTO_REPO=/path/to/pluto -compose clean && compose new --node-impls=pluto && compose define && compose lock && compose run -``` - -Monitor the cluster via `grafana`: - -``` -open http://localhost:3000/d/charon_overview_dashboard/charon-overview # Open Grafana simnet dashboard -``` - -Creating a DKG based all-pluto cluster: - -``` -export PLUTO_REPO=/path/to/pluto -compose new --keygen=dkg --node-impls=pluto -compose auto -``` - -Creating a mixed cluster (2 charon + 2 pluto, forcing both implementations -to participate in every duty at the default 3-of-4 threshold): - -``` -export PLUTO_REPO=/path/to/pluto -compose new --nodes=4 --node-impls=charon,charon,pluto,pluto -compose auto -``` - ## Smoke tests `smoke/smoke_test.go` mirrors charon's compose smoke tests: each scenario generates @@ -116,6 +56,10 @@ node at the first epoch boundary, before the validator mock submits duties), duties broadcast before the p2p mesh forms). Any other alert fires the scenario immediately, warmup or not, and so does anything still firing after the warmup. +Prerequisites: a running Docker daemon and Go. The first run builds `pluto:local` +from `PLUTO_REPO` (a few minutes) and pulls `obolnetwork/charon:v1.7.1` — both +happen automatically, no manual build or `go install` needed. + ``` cd test-infra/compose @@ -142,8 +86,35 @@ gate against charon v1.7.1's beaconmock: it reports `head_slot: "1"` from `/eth/v1/node/syncing`, so e.g. lighthouse permanently treats the beacon node as unsynced and performs no duties, starving the cluster below its signing threshold. (Upstream charon runs lighthouse in these scenarios but its alert collector matches -a Prometheus state that never occurs, so nothing was ever gated.) The real-VC -services still work for manual clusters via `compose new --validator-types`. +a Prometheus state that never occurs, so nothing was ever gated.) The real-VC compose +service definitions remain in the harness (`static/`), but the tests always run the +mock VC. + +### Alert criteria vs. charon + +Adapted from charon's `testutil/compose` alert rules, but the gate is corrected and the +criteria calibrated to actually fire: charon's collector matches Prometheus alert state +`"active"`, which is never emitted (only `inactive` / `pending` / `firing`), so upstream +nothing is ever gated. This harness matches `"firing"`, so several rules necessarily differ: + +| Rule | Charon v1.7.1 | Pluto | Change & why | +|------|---------------|-------|--------------| +| `Pluto Down` | `up == 0` | `up == 0` | identical | +| `Validator API Error Rate` | `increase(…{endpoint!="proxy"}[30s]) > 1` | same | identical | +| `Proxy API Error Rate` | `increase(…{endpoint="proxy"}[30s]) > 5` | same | identical | +| `Warn Log Rate` | `increase(app_log_warn_total[30s]) > 2` | same + `{topic!~"vmock\|tracker"}` | exclude charon mock-noise topics (vmock has no builder-registration handler; the beacon mock never includes broadcasts on-chain) | +| `Error Log Rate` | `app_log_error_total > 0` | `increase(app_log_error_total[30s]) > 0` | windowed — an absolute counter can't recover from the inherent cold-start consensus timeout (mock-VC startup delay → no randao); a window + warmup can | +| `Broadcast Duty Rate` | `increase(core_bcast_broadcast_total[30s]) < 0.5` | `(sum by (job) (increase(…{job=~"node[0-9]+"}[30s])) or on (job) max by (job) (0 * up)) < 0.5` | per-node sum + absent-series fallback, so a node emitting *no* broadcast series fails (charon's per-series form missed it) | +| `Outstanding Duty Rate` | `core_bcast_broadcast_total − core_scheduler_duty_total > 50` | *removed* | dead rule — a duty is broadcast at most as often as scheduled, so it can never be positive | +| _gate (alert state)_ | `"active"` — never emitted | `"firing"` + readiness wait + 60s warmup allowlist | charon's gate is vacuous; pluto's enforces | + +Scenarios that intentionally degrade the cluster tune the gate via config, not the code: + +| Config knob | Effect | Used by | +|-------------|--------|---------| +| `AlertExcludeJobs` | exempt a node from the per-node rules (never from `Pluto Down`) | `1_of_4_down`, `1_of_3_down` | +| `AlertWarnExcludeTopics` | extra warn-topic exclusions | `mixed_2_charon_2_pluto` (excludes `sched` until pluto serves infosync) | +| `AlertDisableRules` | drop an entire rule | `1_of_3_down` (disables the error-rate gates — a downed round-1 leader makes every third proposer duty unrecoverable on the mock) | ## Versioning diff --git a/test-infra/compose/auto.go b/test-infra/compose/auto.go index d0f29df0..3799fbed 100644 --- a/test-infra/compose/auto.go +++ b/test-infra/compose/auto.go @@ -267,7 +267,7 @@ func NewRunnerFunc(topic string, dir string, up bool, runFunc RunFunc, conf, err := LoadConfig(dir) if errors.Is(err, fs.ErrNotExist) { - return TmplData{}, errors.New("compose config.json not found; maybe try `compose new` first", z.Str("dir", dir)) + return TmplData{}, errors.New("compose config.json not found; write one with WriteConfig or New first", z.Str("dir", dir)) } else if err != nil { return TmplData{}, err } diff --git a/test-infra/compose/compose/main.go b/test-infra/compose/compose/main.go deleted file mode 100644 index 304fc8ff..00000000 --- a/test-infra/compose/compose/main.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 - -// Command compose provides a tool to run, test, debug local charon clusters -// using docker compose. -// -// It consists of multiple steps: -// - compose new: Creates a new config.json that defines what will be composed. -// - compose define: Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg. -// - compose lock: Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`. -// - compose run: Creates a docker-compose.yml that executes `charon run`. -package main - -import ( - "context" - "os/signal" - "syscall" - "time" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - "github.com/NethermindEth/pluto/test-infra/compose" - "github.com/obolnetwork/charon/app/log" - "github.com/obolnetwork/charon/cluster" -) - -func main() { - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer cancel() - - cobra.CheckErr(newRootCmd().ExecuteContext(ctx)) -} - -func newRootCmd() *cobra.Command { - root := &cobra.Command{ - Use: "compose", - Short: "Charon Compose - Run, test, and debug a developer-focussed insecure local charon cluster using docker compose", - } - - root.AddCommand(newNewCmd()) - root.AddCommand(newCleanCmd()) - root.AddCommand(newBuildLocalCmd()) - root.AddCommand(newAutoCmd()) - root.AddCommand(newDockerCmd( - "define", - "Creates a docker-compose.yml that executes `charon create dkg` if keygen==dkg", - compose.Define, - )) - root.AddCommand(newDockerCmd( - "lock", - "Creates a docker-compose.yml that executes `charon create cluster` or `charon dkg`", - compose.Lock, - )) - root.AddCommand(newDockerCmd( - "run", - "Creates a docker-compose.yml that executes `charon run`", - compose.Run, - )) - - return root -} - -// newDockerCmd returns a cobra command that generates docker-compose.yml files and executes it. -func newDockerCmd(use string, short string, runFunc compose.RunFunc) *cobra.Command { - cmd := &cobra.Command{ - Use: use, - Short: short, - } - - up := addUpFlag(cmd.Flags()) - dir := addDirFlag(cmd.Flags()) - cmd.RunE = func(cmd *cobra.Command, _ []string) error { - _, err := compose.NewRunnerFunc(use, *dir, *up, runFunc)(cmd.Context()) - if err != nil { - log.Error(cmd.Context(), "Fatal error", err) - } - - return err - } - - return cmd -} - -func newAutoCmd() *cobra.Command { - var conf compose.AutoConfig - - cmd := &cobra.Command{ - Use: "auto", - Short: "Convenience function that runs `compose define && compose lock && compose run`", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { //nolint:revive // keep args variable name for clarity - err := compose.Auto(cmd.Context(), conf) - if err != nil { - log.Error(cmd.Context(), "auto command fatal error", err) - return err - } - - return nil - }, - } - - cmd.Flags().StringVar(&conf.Dir, "compose-dir", ".", "Directory to use for compose artifacts") - cmd.Flags().DurationVar(&conf.AlertTimeout, "alert-timeout", 0, "Timeout to collect alerts before shutdown. Zero disables timeout.") - cmd.Flags().BoolVar(&conf.SudoPerms, "sudo-perms", false, "Enables changing all compose artefacts file permissions using sudo.") - cmd.Flags().BoolVar(&conf.PrintYML, "print-yml", false, "Print generated docker-compose.yml files.") - - return cmd -} - -func newBuildLocalCmd() *cobra.Command { - return &cobra.Command{ - Use: "build-local", - Short: "Builds the obolnetwork/charon:local docker container from the local source code. Note this requires the CHARON_REPO env var.", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { //nolint:revive // keep args variable name for clarity - return compose.BuildLocal(cmd.Context()) - }, - } -} - -func newNewCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "new", - Short: "Creates a new config.json that defines what will be composed", - } - - conf := compose.NewDefaultConfig() - - dir := addDirFlag(cmd.Flags()) - keygen := cmd.Flags().String("keygen", string(conf.KeyGen), "Key generation process: create, split, dkg") - buildLocal := cmd.Flags().Bool("build-local", conf.BuildLocal, "Enables building a local charon container from source. Note this requires the CHARON_REPO env var.") - beaconNode := cmd.Flags().String("beacon-nodes", conf.BeaconNodes, "Beacon node URL endpoints or 'mock' for simnet.") - extRelay := cmd.Flags().String("external-relay", "", "Optional external relay HTTP url.") - splitKeys := cmd.Flags().String("split-keys-dir", conf.SplitKeysDir, "Directory containing keys to split for keygen==create, or empty not to split.") - featureSet := cmd.Flags().String("feature-set", conf.FeatureSet, "Minimum feature set to enable: alpha, beta, stable") - numVals := cmd.Flags().Int("num-validators", conf.NumValidators, "Number of distributed validators.") - vcTypes := cmd.Flags().StringSlice("validator-types", conf.VCStrings(), "Validator types to include.") - nodes := cmd.Flags().Int("nodes", conf.NumNodes, "Number of charon nodes in the cluster.") - insecureKeys := cmd.Flags().Bool("insecure-keys", conf.InsecureKeys, "To generate keys quickly.") - slotDuration := cmd.Flags().Duration("simnet-slot-duration", time.Second, "Configures slot duration in simnet beacon mock.") - beaconFuzz := cmd.Flags().Bool("beacon-fuzz", false, "Configures simnet beaconmock to return fuzzed responses.") - p2pFuzz := cmd.Flags().Bool("p2p-fuzz", false, "Configures charon p2p network to return fuzzed responses of one of the nodes in the cluster.") - nodeImpls := cmd.Flags().StringSlice("node-impls", nil, "Node implementations (charon or pluto) assigned round-robin to nodes; e.g. charon,pluto for alternating. Empty defaults to all charon.") - keygenImpl := cmd.Flags().String("keygen-impl", string(conf.KeyGenImpl), "Implementation running single-container keygen steps: charon or pluto. Empty defaults to node0's implementation.") - plutoImageTag := cmd.Flags().String("pluto-image-tag", conf.PlutoImageTag, "Pluto docker image tag: pluto:{tag}. Tag 'local' is built from source, which requires the PLUTO_REPO env var.") - - cmd.RunE = func(cmd *cobra.Command, _ []string) error { - conf.KeyGen = compose.KeyGen(*keygen) - conf.BuildLocal = *buildLocal - conf.BeaconNodes = *beaconNode - conf.SplitKeysDir = *splitKeys - conf.FeatureSet = *featureSet - conf.ExternalRelay = *extRelay - conf.NumValidators = *numVals - conf.NumNodes = *nodes - conf.Threshold = cluster.Threshold(conf.NumNodes) - conf.InsecureKeys = *insecureKeys - conf.SlotDuration = *slotDuration - conf.BeaconFuzz = *beaconFuzz - conf.P2PFuzz = *p2pFuzz - conf.KeyGenImpl = compose.NodeImpl(*keygenImpl) - conf.PlutoImageTag = *plutoImageTag - - if len(*nodeImpls) > 0 { - var impls []compose.NodeImpl - for _, impl := range *nodeImpls { - impls = append(impls, compose.NodeImpl(impl)) - } - - conf.NodeImpls = impls - } - - if conf.BuildLocal { - conf.ImageTag = "local" - } - - var vcs []compose.VCType - for _, vc := range *vcTypes { - vcs = append(vcs, compose.VCType(vc)) - } - - conf.VCs = vcs - - ctx := log.WithTopic(cmd.Context(), "new") - if err := compose.New(ctx, *dir, conf); err != nil { - log.Error(ctx, "Fatal error", err) - return err - } - - return nil - } - - return cmd -} - -func newCleanCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "clean", - Short: "Convenience function that cleans the compose directory", - } - - dir := addDirFlag(cmd.Flags()) - - cmd.RunE = func(cmd *cobra.Command, _ []string) error { - return compose.Clean(cmd.Context(), *dir) - } - - return cmd -} - -func addDirFlag(flags *pflag.FlagSet) *string { - return flags.String("compose-dir", ".", "Directory to use for compose artifacts") -} - -func addUpFlag(flags *pflag.FlagSet) *bool { - return flags.Bool("up", true, "Execute `docker compose up` when compose command completes") -} diff --git a/test-infra/compose/smoke/smoke_test.go b/test-infra/compose/smoke/smoke_test.go index 43196fca..c8574448 100644 --- a/test-infra/compose/smoke/smoke_test.go +++ b/test-infra/compose/smoke/smoke_test.go @@ -43,8 +43,8 @@ const defaultTimeout = 2 * time.Minute // broadcast/error alerts fire by design. Upstream charon runs lighthouse VCs // in these scenarios but never noticed because its alert gate matches a // state ("active") that Prometheus never reports. The real-VC compose -// services remain available for manual runs via `compose new -// --validator-types`. +// service definitions remain in the harness (`static/`), but the tests +// always run the mock VC. func smokeBaseConfig() compose.Config { conf := compose.NewDefaultConfig() conf.Monitoring = false @@ -53,6 +53,14 @@ func smokeBaseConfig() compose.Config { conf.InsecureKeys = true conf.VCs = []compose.VCType{compose.VCMock} + // Route the cluster through an external relay (e.g. the public + // https://0.relay.obol.tech) instead of the local relay container. The + // local relay service still runs so its prometheus scrape target stays + // up; nothing dials it. + if url := os.Getenv("SMOKE_EXTERNAL_RELAY"); url != "" { + conf.ExternalRelay = url + } + return conf }