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 new file mode 100644 index 00000000..a6286ab9 --- /dev/null +++ b/test-infra/compose/README.md @@ -0,0 +1,125 @@ +# Pluto Compose + +> A docker-compose test harness for standing up insecure local pluto/charon clusters, used by the smoke integration tests. + +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. + +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/`: + +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. + +`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 + +Each node runs either charon or pluto, assigned round-robin from a scenario's `NodeImpls` +config (empty defaults to all charon): + +- 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. +- `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), +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. + +Implementation names are validated when configs are written and loaded; anything +other than `charon` or `pluto` is rejected. + +## 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 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. + +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 + +# 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)$' + +# 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 -timeout=35m -log-dir=. +``` + +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 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 + +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. \ No newline at end of file diff --git a/test-infra/compose/alert.go b/test-infra/compose/alert.go new file mode 100644 index 00000000..349e750f --- /dev/null +++ b/test-infra/compose/alert.go @@ -0,0 +1,204 @@ +// 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" + +// 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() { + defer close(resp) + + const iterSleep = time.Second * 2 + + // 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". + } + + log.Info(ctx, "Prometheus ready, collecting alerts", + z.Str("warmup", alertWarmup.String())) + + 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, "Poll prometheus alerts", err) + continue + } + + if alerts.Status != "success" { + resp <- "non success status from prometheus alerts: " + alerts.Status + continue + } + + inWarmup := time.Now().Before(warmupEnd) + + for _, active := range getActiveAlerts(alerts) { + 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 + } + + reported[active.Description] = true + log.Info(ctx, "Detected new alert", z.Str("alert", active.Description)) + + resp <- active.Description + } + } + }() + + return resp +} + +// 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 { + 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, activeAlert{ + Rule: rule.Name, + Description: 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/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/auto.go b/test-infra/compose/auto.go new file mode 100644 index 00000000..3799fbed --- /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; write one with WriteConfig or 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_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..ab5fa6b2 --- /dev/null +++ b/test-infra/compose/config.go @@ -0,0 +1,331 @@ +// Copyright © 2022-2025 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1 + +package compose + +import ( + "fmt" + "strings" + "time" + + "github.com/obolnetwork/charon/app/errors" + "github.com/obolnetwork/charon/app/z" +) + +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"` + + // 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. +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 { + 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)) + } +} + +// 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..8ffa7df8 --- /dev/null +++ b/test-infra/compose/define.go @@ -0,0 +1,633 @@ +// 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 + } + + 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") + + 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)) + + 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", args...) + 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 +} + +// 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" + + 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 +`) + + 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") + } + + 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() + 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 { + if err := conf.Validate(); err != nil { + return err + } + + 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..e50076ca --- /dev/null +++ b/test-infra/compose/lock.go @@ -0,0 +1,188 @@ +// 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") + } + + if err := resp.Validate(); err != nil { + return Config{}, err + } + + 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/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/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..c8574448 --- /dev/null +++ b/test-infra/compose/smoke/smoke_test.go @@ -0,0 +1,303 @@ +// 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") + 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" + +// 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 + +// 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 +// 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 + conf.DisableMonitoringPorts = true + conf.ImageTag = charonImageTag + 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 +} + +// 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, + 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 + }, + }, + { + Name: "very_large", + ConfigFunc: func(conf *compose.Config) { + conf.NumNodes = 10 + conf.Threshold = 7 + conf.NumValidators = 100 + conf.KeyGen = compose.KeyGenCreate + conf.SlotDuration = time.Second * 6 + conf.SyntheticBlockProposals = false + }, + 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) { + 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 + } + } + }, + }, + { + // 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] + 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 + }, + }, + { + Name: "all_pluto", + RequirePluto: true, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.NodeImpls = []compose.NodeImpl{compose.ImplPluto} + // `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, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenCreate + conf.NodeImpls = []compose.NodeImpl{ + compose.ImplCharon, compose.ImplCharon, + compose.ImplPluto, compose.ImplPluto, + } + // `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, + ConfigFunc: func(conf *compose.Config) { + conf.KeyGen = compose.KeyGenDKG + conf.NodeImpls = []compose.NodeImpl{compose.ImplPluto} + // `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 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") + } + + dir := t.TempDir() + + conf := smokeBaseConfig() + 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) + }) + } +} + +// 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/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..9f32ed40 --- /dev/null +++ b/test-infra/compose/static/lighthouse/run.sh @@ -0,0 +1,32 @@ +#!/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}" + # --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}" \ + --password-file "$(echo "${f}" | sed 's/json/txt/')" \ + --reuse-password +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/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