From 923248b4b7a5fa63dd9c4269a6603bbdddafe969 Mon Sep 17 00:00:00 2001 From: auricom <27022259+auricom@users.noreply.github.com> Date: Wed, 27 May 2026 15:28:34 +0200 Subject: [PATCH 01/15] feat: benchmarks container image --- apps/benchmarks/Dockerfile | 37 +++++++++++ apps/benchmarks/README.md | 55 ++++++++++++++++ apps/benchmarks/crontab | 2 + apps/benchmarks/matrices/baseline.json | 87 ++++++++++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 apps/benchmarks/Dockerfile create mode 100644 apps/benchmarks/README.md create mode 100644 apps/benchmarks/crontab create mode 100644 apps/benchmarks/matrices/baseline.json diff --git a/apps/benchmarks/Dockerfile b/apps/benchmarks/Dockerfile new file mode 100644 index 0000000000..5f4a113aca --- /dev/null +++ b/apps/benchmarks/Dockerfile @@ -0,0 +1,37 @@ +FROM golang:1.26-alpine AS build-env + +WORKDIR /src + +# TODO +# COPY go.mod go.sum ./ +# RUN go mod download + +# COPY . . + +# WORKDIR /src/apps/evm +# RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o evm . + +FROM alpine:3.22.2 + +ARG SUPERCRONIC_VERSION=0.2.46 +ARG TARGETARCH + +ENV TZ=UTC + +#hadolint ignore=DL3018 +RUN apk --no-cache add ca-certificates curl tzdata + +RUN wget -q "https://github.com/aptible/supercronic/releases/download/v${SUPERCRONIC_VERSION}/supercronic-linux-${TARGETARCH}" \ + -O /usr/local/bin/supercronic && \ + chmod +x /usr/local/bin/supercronic + +WORKDIR /root + +# TODO +# COPY --from=build-env /src/apps/benchmarks/benchmarks /usr/bin/benchmarks +COPY apps/benchmarks/entrypoint.sh /usr/bin/entrypoint.sh +COPY apps/benchmarks/crontab /etc/crontab +COPY apps/benchmarks/matrices/baseline.json /root/baseline.json +RUN chmod +x /usr/bin/entrypoint.sh + +ENTRYPOINT ["/usr/bin/entrypoint.sh"] diff --git a/apps/benchmarks/README.md b/apps/benchmarks/README.md new file mode 100644 index 0000000000..a13d3acc27 --- /dev/null +++ b/apps/benchmarks/README.md @@ -0,0 +1,55 @@ +# benchmarks + +A containerised benchmark runner that executes `ev-benchmark` on an hourly cron schedule via [supercronic](https://github.com/aptible/supercronic). + +## Environment Variables + +The following environment variables **must** be provided at runtime: + +| Variable | Required | Description | Example | +|---|---|---|---| +| `BENCH_TRACE_QUERY_URL` | No | Base URL of the OTLP trace query service | `http://:10428` | +| `BENCH_PRIVATE_KEY` | Yes | Private key used to sign benchmark transactions | *(secret)* | +| `BENCH_ETH_RPC_URL` | Yes | Ethereum JSON-RPC endpoint | `http://:8545` | + +## Schedule + +The benchmark runs **once per hour** (`@hourly`). The cron schedule is defined in [`crontab`](./crontab) and executed by supercronic inside the container. +You can supercharche the `/etc/crontab` to customize the job execution. + +## Matrices + +Benchmarks are driven by a JSON matrix file that lists which tests to run and the environment variables for each one. The default matrix is [`matrices/baseline.json`](./matrices/baseline.json) and contains the following tests: `TestGasBurner`, `TestStatePressure`, `TestMixedWorkload`, `TestDeFiSimulation`, and `TestERC20Throughput`. + +You can supercharge the test suite by supplying your own matrix file. Each entry specifies a `test_name`, an optional `timeout`, and an `env` map of benchmark knobs: + +```json +{ + "entries": [ + { + "test_name": "TestGasBurner", + "timeout": "15m", + "env": { + "BENCH_BLOCK_TIME": "100ms", + "BENCH_NUM_SPAMMERS": "8", + "BENCH_COUNT_PER_SPAMMER": "10000" + } + } + ] +} +``` + +Mount your custom matrix into the container and point `ev-benchmark` at it: + +```sh +docker run \ + -v /path/to/your/matrix.json:/root/matrix.json \ + -e BENCH_MATRIX_FILE=/root/matrix.json \ + benchmarks +``` + +## Build + +```sh +docker build -f apps/benchmarking/Dockerfile -t benchmarking . +``` diff --git a/apps/benchmarks/crontab b/apps/benchmarks/crontab new file mode 100644 index 0000000000..43ff986d17 --- /dev/null +++ b/apps/benchmarks/crontab @@ -0,0 +1,2 @@ +# crontab +@hourly benchmarks /root/baseline.json diff --git a/apps/benchmarks/matrices/baseline.json b/apps/benchmarks/matrices/baseline.json new file mode 100644 index 0000000000..3974b84dd7 --- /dev/null +++ b/apps/benchmarks/matrices/baseline.json @@ -0,0 +1,87 @@ +{ + "entries": [ + { + "test_name": "TestGasBurner", + "timeout": "15m", + "env": { + "BENCH_BLOCK_TIME": "100ms", + "BENCH_SLOT_DURATION": "100ms", + "BENCH_GAS_LIMIT": "0x1C9C380", + "BENCH_SCRAPE_INTERVAL": "25ms", + "BENCH_NUM_SPAMMERS": "4", + "BENCH_COUNT_PER_SPAMMER": "6000", + "BENCH_THROUGHPUT": "60", + "BENCH_WARMUP_TXS": "200", + "BENCH_GAS_UNITS_TO_BURN": "1000000", + "BENCH_MAX_WALLETS": "200", + "BENCH_WAIT_TIMEOUT": "10m" + } + }, + { + "test_name": "TestStatePressure", + "timeout": "15m", + "env": { + "BENCH_BLOCK_TIME": "100ms", + "BENCH_SLOT_DURATION": "100ms", + "BENCH_GAS_LIMIT": "0x1C9C380", + "BENCH_SCRAPE_INTERVAL": "25ms", + "BENCH_NUM_SPAMMERS": "3", + "BENCH_COUNT_PER_SPAMMER": "4000", + "BENCH_THROUGHPUT": "40", + "BENCH_WARMUP_TXS": "200", + "BENCH_GAS_UNITS_TO_BURN": "1000000", + "BENCH_MAX_WALLETS": "150", + "BENCH_WAIT_TIMEOUT": "10m" + } + }, + { + "test_name": "TestMixedWorkload", + "timeout": "20m", + "env": { + "BENCH_BLOCK_TIME": "100ms", + "BENCH_SLOT_DURATION": "100ms", + "BENCH_GAS_LIMIT": "0x1C9C380", + "BENCH_SCRAPE_INTERVAL": "25ms", + "BENCH_NUM_SPAMMERS": "4", + "BENCH_COUNT_PER_SPAMMER": "5000", + "BENCH_THROUGHPUT": "26", + "BENCH_WARMUP_TXS": "500", + "BENCH_GAS_UNITS_TO_BURN": "1000000", + "BENCH_MAX_WALLETS": "150", + "BENCH_WAIT_TIMEOUT": "15m" + } + }, + { + "test_name": "TestDeFiSimulation", + "timeout": "20m", + "env": { + "BENCH_BLOCK_TIME": "100ms", + "BENCH_SLOT_DURATION": "100ms", + "BENCH_GAS_LIMIT": "0x1C9C380", + "BENCH_SCRAPE_INTERVAL": "25ms", + "BENCH_NUM_SPAMMERS": "3", + "BENCH_COUNT_PER_SPAMMER": "30000", + "BENCH_THROUGHPUT": "100", + "BENCH_WARMUP_TXS": "800", + "BENCH_MAX_WALLETS": "150", + "BENCH_WAIT_TIMEOUT": "15m" + } + }, + { + "test_name": "TestERC20Throughput", + "timeout": "15m", + "env": { + "BENCH_BLOCK_TIME": "100ms", + "BENCH_SLOT_DURATION": "100ms", + "BENCH_GAS_LIMIT": "0x1C9C380", + "BENCH_SCRAPE_INTERVAL": "25ms", + "BENCH_NUM_SPAMMERS": "4", + "BENCH_COUNT_PER_SPAMMER": "12000", + "BENCH_THROUGHPUT": "115", + "BENCH_WARMUP_TXS": "300", + "BENCH_MAX_WALLETS": "200", + "BENCH_WAIT_TIMEOUT": "10m" + } + } + ] +} From dcc24e84c377837cd9eb1c7ec5150e6b87651a40 Mon Sep 17 00:00:00 2001 From: auricom <27022259+auricom@users.noreply.github.com> Date: Wed, 27 May 2026 15:32:43 +0200 Subject: [PATCH 02/15] fix: entrypoint --- apps/benchmarks/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/benchmarks/Dockerfile b/apps/benchmarks/Dockerfile index 5f4a113aca..da3c13a986 100644 --- a/apps/benchmarks/Dockerfile +++ b/apps/benchmarks/Dockerfile @@ -32,6 +32,5 @@ WORKDIR /root COPY apps/benchmarks/entrypoint.sh /usr/bin/entrypoint.sh COPY apps/benchmarks/crontab /etc/crontab COPY apps/benchmarks/matrices/baseline.json /root/baseline.json -RUN chmod +x /usr/bin/entrypoint.sh -ENTRYPOINT ["/usr/bin/entrypoint.sh"] +CMD ["supercronic", "/etc/crontab"] From 7a00d39e4a2fdb21b2169edeeff3954ca0df469f Mon Sep 17 00:00:00 2001 From: auricom <27022259+auricom@users.noreply.github.com> Date: Wed, 27 May 2026 15:43:56 +0200 Subject: [PATCH 03/15] fix --- apps/benchmarks/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/benchmarks/README.md b/apps/benchmarks/README.md index a13d3acc27..b6fd0e667a 100644 --- a/apps/benchmarks/README.md +++ b/apps/benchmarks/README.md @@ -39,7 +39,7 @@ You can supercharge the test suite by supplying your own matrix file. Each entry } ``` -Mount your custom matrix into the container and point `ev-benchmark` at it: +Mount your custom matrix into the container and point `benchmarks` binary at it: ```sh docker run \ @@ -51,5 +51,5 @@ docker run \ ## Build ```sh -docker build -f apps/benchmarking/Dockerfile -t benchmarking . +docker build -f apps/benchmarks/Dockerfile -t benchmarks . ``` From cd8f880f02726f78e86aba8060e95f4c7f0dcdae Mon Sep 17 00:00:00 2001 From: auricom <27022259+auricom@users.noreply.github.com> Date: Thu, 28 May 2026 09:20:10 +0200 Subject: [PATCH 04/15] Fix typo in README for benchmark runner command --- apps/benchmarks/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/benchmarks/README.md b/apps/benchmarks/README.md index b6fd0e667a..f302265988 100644 --- a/apps/benchmarks/README.md +++ b/apps/benchmarks/README.md @@ -1,6 +1,6 @@ # benchmarks -A containerised benchmark runner that executes `ev-benchmark` on an hourly cron schedule via [supercronic](https://github.com/aptible/supercronic). +A containerised benchmark runner that executes `ev-benchmarks` on an hourly cron schedule via [supercronic](https://github.com/aptible/supercronic). ## Environment Variables @@ -15,7 +15,7 @@ The following environment variables **must** be provided at runtime: ## Schedule The benchmark runs **once per hour** (`@hourly`). The cron schedule is defined in [`crontab`](./crontab) and executed by supercronic inside the container. -You can supercharche the `/etc/crontab` to customize the job execution. +You can supercharge the `/etc/crontab` to customize the job execution. ## Matrices @@ -39,13 +39,13 @@ You can supercharge the test suite by supplying your own matrix file. Each entry } ``` -Mount your custom matrix into the container and point `benchmarks` binary at it: +Mount your custom matrix into the container and point `ev-benchmarks` binary at it: ```sh docker run \ -v /path/to/your/matrix.json:/root/matrix.json \ -e BENCH_MATRIX_FILE=/root/matrix.json \ - benchmarks + ev-benchmarks ``` ## Build From 08c8a36980cc41b5054d92529d0a0d4cd686416f Mon Sep 17 00:00:00 2001 From: auricom <27022259+auricom@users.noreply.github.com> Date: Thu, 28 May 2026 09:20:34 +0200 Subject: [PATCH 05/15] Update Dockerfile to copy ev-benchmarks --- apps/benchmarks/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/benchmarks/Dockerfile b/apps/benchmarks/Dockerfile index da3c13a986..67d2ec5f29 100644 --- a/apps/benchmarks/Dockerfile +++ b/apps/benchmarks/Dockerfile @@ -28,7 +28,7 @@ RUN wget -q "https://github.com/aptible/supercronic/releases/download/v${SUPERCR WORKDIR /root # TODO -# COPY --from=build-env /src/apps/benchmarks/benchmarks /usr/bin/benchmarks +# COPY --from=build-env /src/apps/benchmarks/ev-benchmarks /usr/bin/ev-benchmarks COPY apps/benchmarks/entrypoint.sh /usr/bin/entrypoint.sh COPY apps/benchmarks/crontab /etc/crontab COPY apps/benchmarks/matrices/baseline.json /root/baseline.json From a60bf87cde761e0a3c6aefd9ef9cea16b2210128 Mon Sep 17 00:00:00 2001 From: auricom <27022259+auricom@users.noreply.github.com> Date: Thu, 28 May 2026 09:20:46 +0200 Subject: [PATCH 06/15] Update crontab --- apps/benchmarks/crontab | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/benchmarks/crontab b/apps/benchmarks/crontab index 43ff986d17..5b14197ab9 100644 --- a/apps/benchmarks/crontab +++ b/apps/benchmarks/crontab @@ -1,2 +1,2 @@ # crontab -@hourly benchmarks /root/baseline.json +@hourly ev-benchmarks /root/baseline.json From 57b02f27a84c0dbd0ff43c8852d7b92cc48f8794 Mon Sep 17 00:00:00 2001 From: chatton Date: Thu, 28 May 2026 11:56:38 +0100 Subject: [PATCH 07/15] feat(benchmarks): add ev-benchmarks cobra CLI for stress testing via spamoor Standalone Go binary that orchestrates spamoor-daemon via HTTP API to drive sustained and burst transaction load against ev-reth. Structured as a cobra CLI with subcommands: check (connectivity), regular (~1M tx/day), burst (500K probabilistic), and run (custom matrix). Includes Dockerfile, docker-compose, baseline/burst matrices, crontab for supercronic scheduling, just targets for build and smoke testing. --- .just/bench.just | 71 +++ apps/benchmarks/.gitignore | 1 + apps/benchmarks/Dockerfile | 18 +- apps/benchmarks/README.md | 115 +++- apps/benchmarks/cmd/burst.go | 25 + apps/benchmarks/cmd/check.go | 25 + apps/benchmarks/cmd/regular.go | 25 + apps/benchmarks/cmd/root.go | 28 + apps/benchmarks/cmd/run.go | 17 + apps/benchmarks/crontab | 6 +- apps/benchmarks/docker-compose.yml | 28 + apps/benchmarks/go.mod | 159 ++++++ apps/benchmarks/go.sum | 698 +++++++++++++++++++++++++ apps/benchmarks/internal/config.go | 48 ++ apps/benchmarks/internal/matrix.go | 103 ++++ apps/benchmarks/internal/runner.go | 224 ++++++++ apps/benchmarks/main.go | 21 + apps/benchmarks/matrices/baseline.json | 83 +-- apps/benchmarks/matrices/burst.json | 19 + justfile | 1 + 20 files changed, 1600 insertions(+), 115 deletions(-) create mode 100644 .just/bench.just create mode 100644 apps/benchmarks/.gitignore create mode 100644 apps/benchmarks/cmd/burst.go create mode 100644 apps/benchmarks/cmd/check.go create mode 100644 apps/benchmarks/cmd/regular.go create mode 100644 apps/benchmarks/cmd/root.go create mode 100644 apps/benchmarks/cmd/run.go create mode 100644 apps/benchmarks/docker-compose.yml create mode 100644 apps/benchmarks/go.mod create mode 100644 apps/benchmarks/go.sum create mode 100644 apps/benchmarks/internal/config.go create mode 100644 apps/benchmarks/internal/matrix.go create mode 100644 apps/benchmarks/internal/runner.go create mode 100644 apps/benchmarks/main.go create mode 100644 apps/benchmarks/matrices/burst.json diff --git a/.just/bench.just b/.just/bench.just new file mode 100644 index 0000000000..b1ba3e6137 --- /dev/null +++ b/.just/bench.just @@ -0,0 +1,71 @@ +# Build ev-benchmarks binary +[group('bench')] +build-benchmarks: + @echo "--> Building ev-benchmarks" + @mkdir -p {{ build_dir }} + @cd apps/benchmarks && go build -o {{ build_dir }}/ev-benchmarks . + @echo " Check the binary with: {{ build_dir }}/ev-benchmarks" + +# Build ev-benchmarks Docker image +[group('bench')] +docker-build-benchmarks: + @echo "--> Building ev-benchmarks Docker image" + @docker build -f apps/benchmarks/Dockerfile -t ev-benchmarks:dev . + @echo "--> Docker image built: ev-benchmarks:dev" + +# Smoke test: start spamoor + benchmarks, verify txs are submitted, then tear down. +# Requires BENCH_PRIVATE_KEY and BENCH_ETH_RPC_URL env vars. +[group('bench')] +bench-smoke: + #!/usr/bin/env bash + set -euo pipefail + + : "${BENCH_PRIVATE_KEY:?Set BENCH_PRIVATE_KEY}" + : "${BENCH_ETH_RPC_URL:?Set BENCH_ETH_RPC_URL}" + + export BENCH_PRIVATE_KEY BENCH_ETH_RPC_URL + + echo "--> Building ev-benchmarks binary" + cd apps/benchmarks && go build -o ../../{{ build_dir }}/ev-benchmarks . && cd ../.. + + echo "--> Starting spamoor-daemon via docker compose" + docker compose -f apps/benchmarks/docker-compose.yml up -d spamoor-daemon + trap 'echo "--> Tearing down"; docker compose -f apps/benchmarks/docker-compose.yml down' EXIT + + echo "--> Waiting for spamoor-daemon to be healthy" + for i in $(seq 1 30); do + if curl -sf http://localhost:8080/metrics > /dev/null 2>&1; then + echo " spamoor-daemon ready" + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: spamoor-daemon did not become healthy" + exit 1 + fi + sleep 1 + done + + echo "--> Running smoke matrix (small tx count)" + cat > /tmp/bench-smoke.json <<'EOF' + { + "entries": [ + { + "test_name": "SmokeEOA", + "scenario": "eoatx", + "timeout": "5m", + "env": { + "BENCH_NUM_SPAMMERS": "1", + "BENCH_COUNT_PER_SPAMMER": "10", + "BENCH_THROUGHPUT": "10", + "BENCH_MAX_PENDING": "100", + "BENCH_MAX_WALLETS": "10", + "BENCH_BASE_FEE": "20", + "BENCH_TIP_FEE": "2" + } + } + ] + } + EOF + + {{ build_dir }}/ev-benchmarks run --spamoor-url=http://localhost:8080 /tmp/bench-smoke.json + echo "--> Smoke test passed: transactions submitted successfully" diff --git a/apps/benchmarks/.gitignore b/apps/benchmarks/.gitignore new file mode 100644 index 0000000000..e6952c55fa --- /dev/null +++ b/apps/benchmarks/.gitignore @@ -0,0 +1 @@ +ev-benchmarks diff --git a/apps/benchmarks/Dockerfile b/apps/benchmarks/Dockerfile index 67d2ec5f29..eb1f177a91 100644 --- a/apps/benchmarks/Dockerfile +++ b/apps/benchmarks/Dockerfile @@ -1,15 +1,12 @@ -FROM golang:1.26-alpine AS build-env +FROM golang:1.25-alpine AS build-env WORKDIR /src -# TODO -# COPY go.mod go.sum ./ -# RUN go mod download +COPY apps/benchmarks/go.mod apps/benchmarks/go.sum ./ +RUN go mod download -# COPY . . - -# WORKDIR /src/apps/evm -# RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -o evm . +COPY apps/benchmarks/ . +RUN CGO_ENABLED=0 GOOS=linux go build -o ev-benchmarks . FROM alpine:3.22.2 @@ -27,10 +24,9 @@ RUN wget -q "https://github.com/aptible/supercronic/releases/download/v${SUPERCR WORKDIR /root -# TODO -# COPY --from=build-env /src/apps/benchmarks/ev-benchmarks /usr/bin/ev-benchmarks -COPY apps/benchmarks/entrypoint.sh /usr/bin/entrypoint.sh +COPY --from=build-env /src/ev-benchmarks /usr/bin/ev-benchmarks COPY apps/benchmarks/crontab /etc/crontab COPY apps/benchmarks/matrices/baseline.json /root/baseline.json +COPY apps/benchmarks/matrices/burst.json /root/burst.json CMD ["supercronic", "/etc/crontab"] diff --git a/apps/benchmarks/README.md b/apps/benchmarks/README.md index f302265988..9807f25af6 100644 --- a/apps/benchmarks/README.md +++ b/apps/benchmarks/README.md @@ -1,55 +1,118 @@ # benchmarks -A containerised benchmark runner that executes `ev-benchmarks` on an hourly cron schedule via [supercronic](https://github.com/aptible/supercronic). +Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon](https://github.com/ethpandaops/spamoor) sidecar via HTTP API. Runs on a cron schedule via [supercronic](https://github.com/aptible/supercronic). -## Environment Variables +## Architecture -The following environment variables **must** be provided at runtime: +``` +ev-benchmarks (this binary) --> spamoor-daemon --> ev-reth RPC + | | + reads matrix JSON manages wallets, + creates/polls spammers signs & sends txs +``` -| Variable | Required | Description | Example | -|---|---|---|---| -| `BENCH_TRACE_QUERY_URL` | No | Base URL of the OTLP trace query service | `http://:10428` | -| `BENCH_PRIVATE_KEY` | Yes | Private key used to sign benchmark transactions | *(secret)* | -| `BENCH_ETH_RPC_URL` | Yes | Ethereum JSON-RPC endpoint | `http://:8545` | +- **spamoor-daemon** needs: a funded private key + ev-reth RPC URL +- **ev-benchmarks** needs: spamoor-daemon API URL + a matrix JSON file -## Schedule +## Commands + +``` +ev-benchmarks check # send 1 tx to verify spamoor → ev-reth connectivity +ev-benchmarks regular # sustained ~1M tx/day (baseline matrix) +ev-benchmarks burst # probabilistic 500K tx burst (~15%/invocation) +ev-benchmarks run # custom matrix file +``` + +Global flag: `--spamoor-url` (or `BENCH_SPAMOOR_URL` env, default `http://spamoor-daemon:8080`) + +## Quick Start + +### 1. Start spamoor-daemon + +```sh +docker run -d --name spamoor -p 8080:8080 \ + ethpandaops/spamoor:latest /app/spamoor-daemon \ + --privkey= \ + --rpchost=http://:8545 \ + --port=8080 --startup-delay=0 +``` + +### 2. Run benchmarks + +```sh +# build +cd apps/benchmarks && go build -o ev-benchmarks . -The benchmark runs **once per hour** (`@hourly`). The cron schedule is defined in [`crontab`](./crontab) and executed by supercronic inside the container. -You can supercharge the `/etc/crontab` to customize the job execution. +# run +./ev-benchmarks regular --spamoor-url=http://localhost:8080 +``` + +### Docker Compose + +Spins up both spamoor-daemon and benchmarks together: + +```sh +export BENCH_PRIVATE_KEY= +export BENCH_ETH_RPC_URL=http://:8545 +docker compose -f apps/benchmarks/docker-compose.yml up +``` -## Matrices +### Smoke Test -Benchmarks are driven by a JSON matrix file that lists which tests to run and the environment variables for each one. The default matrix is [`matrices/baseline.json`](./matrices/baseline.json) and contains the following tests: `TestGasBurner`, `TestStatePressure`, `TestMixedWorkload`, `TestDeFiSimulation`, and `TestERC20Throughput`. +```sh +export BENCH_PRIVATE_KEY= +export BENCH_ETH_RPC_URL=http://:8545 +just bench-smoke +``` -You can supercharge the test suite by supplying your own matrix file. Each entry specifies a `test_name`, an optional `timeout`, and an `env` map of benchmark knobs: +## Matrix Format + +Each entry specifies a spamoor scenario, tx counts, and optional probability: ```json { "entries": [ { - "test_name": "TestGasBurner", + "test_name": "EOATransfer", + "scenario": "eoatx", "timeout": "15m", + "probability": 1.0, "env": { - "BENCH_BLOCK_TIME": "100ms", - "BENCH_NUM_SPAMMERS": "8", - "BENCH_COUNT_PER_SPAMMER": "10000" + "BENCH_NUM_SPAMMERS": "4", + "BENCH_COUNT_PER_SPAMMER": "10500", + "BENCH_THROUGHPUT": "200", + "BENCH_MAX_PENDING": "50000", + "BENCH_MAX_WALLETS": "200", + "BENCH_BASE_FEE": "20", + "BENCH_TIP_FEE": "2" } } ] } ``` -Mount your custom matrix into the container and point `ev-benchmarks` binary at it: +| Field | Description | +|---|---| +| `scenario` | spamoor scenario name (`eoatx`, `gasburnertx`, `erc20tx`, `uniswap-swaps`, etc.) | +| `probability` | 0.0–1.0, chance of running per invocation (omit = always run) | +| `timeout` | max duration per entry (default `15m`) | -```sh -docker run \ - -v /path/to/your/matrix.json:/root/matrix.json \ - -e BENCH_MATRIX_FILE=/root/matrix.json \ - ev-benchmarks -``` +## Schedule + +Supercronic runs both matrices `@hourly`: +- `regular` — 4 × 10,500 = 42K txs/run → ~1M/day +- `burst` — 10 × 50,000 = 500K txs, 15% chance → ~3-4 bursts/day ## Build ```sh -docker build -f apps/benchmarks/Dockerfile -t benchmarks . +# binary +cd apps/benchmarks && go build -o ev-benchmarks . + +# docker image +docker build -f apps/benchmarks/Dockerfile -t ev-benchmarks:dev . + +# via just +just build-benchmarks +just docker-build-benchmarks ``` diff --git a/apps/benchmarks/cmd/burst.go b/apps/benchmarks/cmd/burst.go new file mode 100644 index 0000000000..a2b3186f71 --- /dev/null +++ b/apps/benchmarks/cmd/burst.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/spf13/cobra" +) + +const defaultBurstPath = "/root/burst.json" + +func newBurstCmd() *cobra.Command { + var matrixPath string + + cmd := &cobra.Command{ + Use: "burst", + Short: "run probabilistic burst load (500K tx, ~15% chance per invocation)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return internal.ExecuteMatrix(matrixPath, resolveSpamoorURL()) + }, + } + + cmd.Flags().StringVar(&matrixPath, "matrix", defaultBurstPath, "path to burst matrix JSON") + + return cmd +} diff --git a/apps/benchmarks/cmd/check.go b/apps/benchmarks/cmd/check.go new file mode 100644 index 0000000000..6e7ae3d53c --- /dev/null +++ b/apps/benchmarks/cmd/check.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "time" + + "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/spf13/cobra" +) + +func newCheckCmd() *cobra.Command { + var timeout time.Duration + + cmd := &cobra.Command{ + Use: "check", + Short: "verify connectivity by sending a single eoatx through spamoor to ev-reth", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return internal.RunCheck(resolveSpamoorURL(), timeout) + }, + } + + cmd.Flags().DurationVar(&timeout, "timeout", 60*time.Second, "max time to wait for tx") + + return cmd +} diff --git a/apps/benchmarks/cmd/regular.go b/apps/benchmarks/cmd/regular.go new file mode 100644 index 0000000000..89e315cded --- /dev/null +++ b/apps/benchmarks/cmd/regular.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/spf13/cobra" +) + +const defaultBaselinePath = "/root/baseline.json" + +func newRegularCmd() *cobra.Command { + var matrixPath string + + cmd := &cobra.Command{ + Use: "regular", + Short: "run sustained load from baseline matrix (~1M tx/day at @hourly)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return internal.ExecuteMatrix(matrixPath, resolveSpamoorURL()) + }, + } + + cmd.Flags().StringVar(&matrixPath, "matrix", defaultBaselinePath, "path to baseline matrix JSON") + + return cmd +} diff --git a/apps/benchmarks/cmd/root.go b/apps/benchmarks/cmd/root.go new file mode 100644 index 0000000000..3658393faf --- /dev/null +++ b/apps/benchmarks/cmd/root.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/spf13/cobra" +) + +var spamoorFlag string + +func NewRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "ev-benchmarks", + Short: "benchmark runner for ev-node stress testing via spamoor", + } + + rootCmd.PersistentFlags().StringVar(&spamoorFlag, "spamoor-url", "", "spamoor-daemon API URL (env: BENCH_SPAMOOR_URL)") + + rootCmd.AddCommand(newRunCmd(), newRegularCmd(), newBurstCmd(), newCheckCmd()) + + return rootCmd +} + +func resolveSpamoorURL() string { + if spamoorFlag != "" { + return spamoorFlag + } + return internal.SpamoorURL() +} diff --git a/apps/benchmarks/cmd/run.go b/apps/benchmarks/cmd/run.go new file mode 100644 index 0000000000..e63ba2e56d --- /dev/null +++ b/apps/benchmarks/cmd/run.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/spf13/cobra" +) + +func newRunCmd() *cobra.Command { + return &cobra.Command{ + Use: "run ", + Short: "run benchmarks from a matrix JSON file", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return internal.ExecuteMatrix(args[0], resolveSpamoorURL()) + }, + } +} diff --git a/apps/benchmarks/crontab b/apps/benchmarks/crontab index 5b14197ab9..e47f498a6f 100644 --- a/apps/benchmarks/crontab +++ b/apps/benchmarks/crontab @@ -1,2 +1,4 @@ -# crontab -@hourly ev-benchmarks /root/baseline.json +# sustained ~1M tx/day (4 × 10,500 = 42,000 per run × 24 = ~1,008,000/day) +@hourly ev-benchmarks regular +# burst 500K tx at random (~15% chance per hour ≈ 3-4 bursts/day) +@hourly ev-benchmarks burst diff --git a/apps/benchmarks/docker-compose.yml b/apps/benchmarks/docker-compose.yml new file mode 100644 index 0000000000..c340233318 --- /dev/null +++ b/apps/benchmarks/docker-compose.yml @@ -0,0 +1,28 @@ +name: "ev-benchmarks" + +services: + spamoor-daemon: + image: ethpandaops/spamoor:latest + entrypoint: /app/spamoor-daemon + command: + - --privkey=${BENCH_PRIVATE_KEY:?BENCH_PRIVATE_KEY is required} + - --rpchost=${BENCH_ETH_RPC_URL:?BENCH_ETH_RPC_URL is required} + - --port=8080 + - --startup-delay=0 + ports: + - "8080:8080" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/metrics"] + interval: 2s + timeout: 2s + retries: 10 + + benchmarks: + build: + context: ../.. + dockerfile: apps/benchmarks/Dockerfile + depends_on: + spamoor-daemon: + condition: service_healthy + environment: + - BENCH_SPAMOOR_URL=http://spamoor-daemon:8080 diff --git a/apps/benchmarks/go.mod b/apps/benchmarks/go.mod new file mode 100644 index 0000000000..6bad727248 --- /dev/null +++ b/apps/benchmarks/go.mod @@ -0,0 +1,159 @@ +module github.com/evstack/ev-node/apps/benchmarks + +go 1.25.8 + +require ( + github.com/celestiaorg/tastora v0.20.0 + github.com/prometheus/client_model v0.6.2 + github.com/spf13/cobra v1.9.1 +) + +require ( + cosmossdk.io/api v0.7.6 // indirect + cosmossdk.io/collections v0.4.0 // indirect + cosmossdk.io/core v0.11.1 // indirect + cosmossdk.io/depinject v1.1.0 // indirect + cosmossdk.io/errors v1.0.2 // indirect + cosmossdk.io/log v1.6.0 // indirect + cosmossdk.io/math v1.5.1 // indirect + cosmossdk.io/store v1.1.2 // indirect + cosmossdk.io/x/tx v0.13.8 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.2 // indirect + github.com/DataDog/zstd v1.5.6 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/speakeasy v0.2.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/celestiaorg/go-square/v3 v3.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect + github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect + github.com/cockroachdb/pebble v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect + github.com/cometbft/cometbft v0.38.17 // indirect + github.com/cometbft/cometbft-db v1.0.4 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/cosmos/btcutil v1.0.5 // indirect + github.com/cosmos/cosmos-db v1.1.1 // indirect + github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect + github.com/cosmos/cosmos-sdk v0.50.12 // indirect + github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/gogoproto v1.7.0 // indirect + github.com/cosmos/iavl v1.2.6 // indirect + github.com/cosmos/ics23/go v0.11.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.15.0 // indirect + github.com/danieljoos/wincred v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dgraph-io/badger/v4 v4.5.1 // indirect + github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.7.0 // indirect + github.com/emicklei/dot v1.6.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/getsentry/sentry-go v0.31.1 // indirect + github.com/go-kit/kit v0.13.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/flatbuffers v25.1.24+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/linxGnu/grocksdb v1.9.8 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // 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.0 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.20.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tendermint/go-amino v0.16.0 // indirect + github.com/tidwall/btree v1.7.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v1.0.0 // indirect + go.etcd.io/bbolt v1.4.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.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.3 // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.2 // indirect + pgregory.net/rapid v1.2.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/apps/benchmarks/go.sum b/apps/benchmarks/go.sum new file mode 100644 index 0000000000..c3f71b0c09 --- /dev/null +++ b/apps/benchmarks/go.sum @@ -0,0 +1,698 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= +cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= +cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= +cosmossdk.io/collections v0.4.0/go.mod h1:oa5lUING2dP+gdDquow+QjlF45eL1t4TJDypgGd+tv0= +cosmossdk.io/core v0.11.1 h1:h9WfBey7NAiFfIcUhDVNS503I2P2HdZLebJlUIs8LPA= +cosmossdk.io/core v0.11.1/go.mod h1:OJzxcdC+RPrgGF8NJZR2uoQr56tc7gfBKhiKeDO7hH0= +cosmossdk.io/depinject v1.1.0 h1:wLan7LG35VM7Yo6ov0jId3RHWCGRhe8E8bsuARorl5E= +cosmossdk.io/depinject v1.1.0/go.mod h1:kkI5H9jCGHeKeYWXTqYdruogYrEeWvBQCw1Pj4/eCFI= +cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo= +cosmossdk.io/errors v1.0.2/go.mod h1:0rjgiHkftRYPj//3DrD6y8hcm40HcPv/dR4R/4efr0k= +cosmossdk.io/log v1.6.0 h1:SJIOmJ059wi1piyRgNRXKXhlDXGqnB5eQwhcZKv2tOk= +cosmossdk.io/log v1.6.0/go.mod h1:5cXXBvfBkR2/BcXmosdCSLXllvgSjphrrDVdfVRmBGM= +cosmossdk.io/math v1.5.1 h1:c6zo52nBRlqOeSIIQrn/zbxwcNwhaLjTMRn6e4vD7uc= +cosmossdk.io/math v1.5.1/go.mod h1:ToembcWID/wR94cucsMD+2gq6xrlBBOfWcGwC7ZdwZA= +cosmossdk.io/store v1.1.2 h1:3HOZG8+CuThREKv6cn3WSohAc6yccxO3hLzwK6rBC7o= +cosmossdk.io/store v1.1.2/go.mod h1:60rAGzTHevGm592kFhiUVkNC9w7gooSEn5iUBPzHQ6A= +cosmossdk.io/x/tx v0.13.8 h1:dQwC8jMe7awx/edi1HPPZ40AjHnsix6KSO/jbKMUYKk= +cosmossdk.io/x/tx v0.13.8/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q= +github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= +github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= +github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/celestiaorg/go-square/v3 v3.0.0 h1:ivLUUuVr7SpkvPiPLPvuH8/Vrm+iw9X3QL8gX25j+Sk= +github.com/celestiaorg/go-square/v3 v3.0.0/go.mod h1:IROQinUbHNyFWW1J5idanVLOSLHaK1wWwVmuVSfiCVo= +github.com/celestiaorg/tastora v0.20.0 h1:J5ytjPfvH5JJjGJNpKRCTjVH4Bu9z29HGHkqbt2El0M= +github.com/celestiaorg/tastora v0.20.0/go.mod h1:kCxob6KWSGYGPwbgKdphi3mjwKVI6iMZQXbBMYYMo3w= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +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.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/cometbft/cometbft v0.38.17 h1:FkrQNbAjiFqXydeAO81FUzriL4Bz0abYxN/eOHrQGOk= +github.com/cometbft/cometbft v0.38.17/go.mod h1:5l0SkgeLRXi6bBfQuevXjKqML1jjfJJlvI1Ulp02/o4= +github.com/cometbft/cometbft-db v1.0.4 h1:cezb8yx/ZWcF124wqUtAFjAuDksS1y1yXedvtprUFxs= +github.com/cometbft/cometbft-db v1.0.4/go.mod h1:M+BtHAGU2XLrpUxo3Nn1nOCcnVCiLM9yx5OuT0u5SCA= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= +github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= +github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-sdk v0.50.12 h1:WizeD4K74737Gq46/f9fq+WjyZ1cP/1bXwVR3dvyp0g= +github.com/cosmos/cosmos-sdk v0.50.12/go.mod h1:hrWEFMU1eoXqLJeE6VVESpJDQH67FS1nnMrQIjO2daw= +github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= +github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= +github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= +github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= +github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= +github.com/cosmos/iavl v1.2.6 h1:Hs3LndJbkIB+rEvToKJFXZvKo6Vy0Ex1SJ54hhtioIs= +github.com/cosmos/iavl v1.2.6/go.mod h1:GiM43q0pB+uG53mLxLDzimxM9l/5N9UuSY3/D0huuVw= +github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= +github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= +github.com/cosmos/ledger-cosmos-go v0.15.0 h1:xmizkkEX19tyFLVL6PPMQNg21Jc9W9/bpbwxMDdtxXg= +github.com/cosmos/ledger-cosmos-go v0.15.0/go.mod h1:KJqW5U4/MMl8ICPO4WPjIAyC4TfYRnr28d9N9bBUKWc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= +github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= +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/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/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= +github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +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/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= +github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +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.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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/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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= +github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.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 v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v25.1.24+incompatible h1:4wPqL3K7GzBd1CwyhSd3usxLKOaJN/AC6puCca6Jm7o= +github.com/google/flatbuffers v25.1.24+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= +github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +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/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= +github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= +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-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/linxGnu/grocksdb v1.9.8 h1:vOIKv9/+HKiqJAElJIEYv3ZLcihRxyP7Suu/Mu8Dxjs= +github.com/linxGnu/grocksdb v1.9.8/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= +github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +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/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= +github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +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/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +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/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= +github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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.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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= +github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v1.0.0 h1:BvNoksIyRqyQTW78rIZP9A44WwAminKiomQa7jXp9EI= +github.com/zondax/ledger-go v1.0.0/go.mod h1:HpgkgFh3Jkwi9iYLDATdyRxc8CxqxcywsFj6QerWzvo= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +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.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +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-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/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-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/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-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 h1:7ei4lp52gK1uSejlA8AZl5AJjeLUOHBQscRQZUgAcu0= +google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20/go.mod h1:ZdbssH/1SOVnjnDlXzxDHK2MCidiqXtbYccJNzNYPEE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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/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.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/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/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/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/apps/benchmarks/internal/config.go b/apps/benchmarks/internal/config.go new file mode 100644 index 0000000000..16b98b1d4f --- /dev/null +++ b/apps/benchmarks/internal/config.go @@ -0,0 +1,48 @@ +package internal + +import ( + "os" + "strconv" +) + +const DefaultSpamoorURL = "http://spamoor-daemon:8080" + +func SpamoorURL() string { + if v := os.Getenv("BENCH_SPAMOOR_URL"); v != "" { + return v + } + return DefaultSpamoorURL +} + +var envMapping = map[string]string{ + "BENCH_COUNT_PER_SPAMMER": "total_count", + "BENCH_THROUGHPUT": "throughput", + "BENCH_MAX_PENDING": "max_pending", + "BENCH_MAX_WALLETS": "max_wallets", + "BENCH_GAS_UNITS_TO_BURN": "gas_units_to_burn", + "BENCH_BASE_FEE": "base_fee", + "BENCH_TIP_FEE": "tip_fee", + "BENCH_REBROADCAST": "rebroadcast", +} + +func BuildScenarioConfig(env map[string]string) map[string]any { + cfg := map[string]any{ + "refill_amount": "500000000000000000000", + "refill_balance": "200000000000000000000", + "refill_interval": 300, + } + + for envKey, cfgKey := range envMapping { + val, ok := env[envKey] + if !ok { + continue + } + if n, err := strconv.Atoi(val); err == nil { + cfg[cfgKey] = n + } else { + cfg[cfgKey] = val + } + } + + return cfg +} diff --git a/apps/benchmarks/internal/matrix.go b/apps/benchmarks/internal/matrix.go new file mode 100644 index 0000000000..f6ccfc7bb5 --- /dev/null +++ b/apps/benchmarks/internal/matrix.go @@ -0,0 +1,103 @@ +package internal + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" +) + +var validScenarios = map[string]bool{ + spamoor.ScenarioEOATX: true, + spamoor.ScenarioERC20TX: true, + spamoor.ScenarioERC721TX: true, + spamoor.ScenarioERC1155TX: true, + spamoor.ScenarioCallTX: true, + spamoor.ScenarioDeployTX: true, + spamoor.ScenarioDeployDestruct: true, + spamoor.ScenarioSetCodeTX: true, + spamoor.ScenarioUniswapSwaps: true, + spamoor.ScenarioBlobs: true, + spamoor.ScenarioBlobAverage: true, + spamoor.ScenarioBlobReplacements: true, + spamoor.ScenarioBlobConflicting: true, + spamoor.ScenarioBlobCombined: true, + spamoor.ScenarioGasBurnerTX: true, + spamoor.ScenarioStorageSpam: true, + spamoor.ScenarioGeasTX: true, + spamoor.ScenarioXenToken: true, + spamoor.ScenarioTaskRunner: true, +} + +type Matrix struct { + Entries []Entry `json:"entries"` +} + +type Entry struct { + TestName string `json:"test_name"` + Scenario string `json:"scenario"` + Timeout string `json:"timeout"` + Probability *float64 `json:"probability,omitempty"` + Env map[string]string `json:"env"` + NumSpammers int `json:"-"` + CountPerSpammer int `json:"-"` +} + +func LoadMatrix(path string) (*Matrix, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read matrix file: %w", err) + } + var m Matrix + if err := json.Unmarshal(data, &m); err != nil { + return nil, fmt.Errorf("parse matrix JSON: %w", err) + } + if len(m.Entries) == 0 { + return nil, fmt.Errorf("matrix has no entries") + } + for i := range m.Entries { + if err := m.Entries[i].validate(); err != nil { + return nil, fmt.Errorf("entry %d (%s): %w", i, m.Entries[i].TestName, err) + } + } + return &m, nil +} + +func (e *Entry) validate() error { + if e.Scenario == "" { + return fmt.Errorf("missing scenario") + } + if !validScenarios[e.Scenario] { + return fmt.Errorf("unknown scenario %q", e.Scenario) + } + + e.NumSpammers = envInt(e.Env, "BENCH_NUM_SPAMMERS", 1) + if e.NumSpammers <= 0 { + return fmt.Errorf("BENCH_NUM_SPAMMERS must be > 0") + } + + e.CountPerSpammer = envInt(e.Env, "BENCH_COUNT_PER_SPAMMER", 0) + if e.CountPerSpammer <= 0 { + return fmt.Errorf("BENCH_COUNT_PER_SPAMMER must be > 0") + } + + if e.Probability != nil && (*e.Probability < 0 || *e.Probability > 1) { + return fmt.Errorf("probability must be between 0.0 and 1.0, got %f", *e.Probability) + } + + return nil +} + +func envInt(env map[string]string, key string, fallback int) int { + v, ok := env[key] + if !ok { + return fallback + } + n, err := strconv.Atoi(v) + if err != nil { + return fallback + } + return n +} diff --git a/apps/benchmarks/internal/runner.go b/apps/benchmarks/internal/runner.go new file mode 100644 index 0000000000..174d6ef2e1 --- /dev/null +++ b/apps/benchmarks/internal/runner.go @@ -0,0 +1,224 @@ +package internal + +import ( + "context" + "fmt" + "log" + "math/rand/v2" + "time" + + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" + dto "github.com/prometheus/client_model/go" +) + +func ExecuteMatrix(matrixPath, spamoorAddr string) error { + matrix, err := LoadMatrix(matrixPath) + if err != nil { + return fmt.Errorf("load matrix: %w", err) + } + + api := spamoor.NewAPI(spamoorAddr) + log.Printf("spamoor API: %s", spamoorAddr) + log.Printf("loaded %d matrix entries from %s", len(matrix.Entries), matrixPath) + + var failures []string + for i, entry := range matrix.Entries { + log.Printf("--- [%d/%d] %s ---", i+1, len(matrix.Entries), entry.TestName) + + if entry.Probability != nil { + roll := rand.Float64() + if roll >= *entry.Probability { + log.Printf("[%s] skipped (probability=%.2f, roll=%.4f)", entry.TestName, *entry.Probability, roll) + continue + } + log.Printf("[%s] triggered (probability=%.2f, roll=%.4f)", entry.TestName, *entry.Probability, roll) + } + + timeout := 15 * time.Minute + if entry.Timeout != "" { + parsed, pErr := time.ParseDuration(entry.Timeout) + if pErr != nil { + log.Printf("warning: invalid timeout %q, using default %s", entry.Timeout, timeout) + } else { + timeout = parsed + } + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + if err := runEntry(ctx, api, entry); err != nil { + log.Printf("FAIL [%s]: %v", entry.TestName, err) + failures = append(failures, entry.TestName) + } + cancel() + } + + log.Printf("=== completed: %d/%d succeeded ===", len(matrix.Entries)-len(failures), len(matrix.Entries)) + if len(failures) > 0 { + return fmt.Errorf("failed entries: %v", failures) + } + return nil +} + +func RunCheck(spamoorAddr string, timeout time.Duration) error { + api := spamoor.NewAPI(spamoorAddr) + + log.Printf("checking connectivity via %s", spamoorAddr) + + if _, err := api.ListSpammers(); err != nil { + return fmt.Errorf("cannot reach spamoor at %s: %w", spamoorAddr, err) + } + log.Printf("spamoor reachable") + + if err := deleteAllSpammers(api); err != nil { + return fmt.Errorf("cleanup: %w", err) + } + defer func() { _ = deleteAllSpammers(api) }() + + cfg := map[string]any{ + "total_count": 1, + "throughput": 1, + "max_pending": 10, + "max_wallets": 1, + "base_fee": 20, + "tip_fee": 2, + "refill_amount": "500000000000000000000", + "refill_balance": "200000000000000000000", + "refill_interval": 300, + } + + id, err := api.CreateSpammer("check-eoatx", spamoor.ScenarioEOATX, cfg, true) + if err != nil { + return fmt.Errorf("create spammer: %w", err) + } + log.Printf("created check spammer (id=%d)", id) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + sent, failed, err := waitForSpamoorDone(ctx, api, 1) + if err != nil { + return fmt.Errorf("tx not confirmed: %w", err) + } + + if failed > 0 { + return fmt.Errorf("tx failed (sent=%.0f failed=%.0f)", sent, failed) + } + + log.Printf("check passed: 1 tx sent successfully") + return nil +} + +func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { + totalCount := entry.NumSpammers * entry.CountPerSpammer + + log.Printf("[%s] scenario=%s spammers=%d count_per=%d total=%d", + entry.TestName, entry.Scenario, entry.NumSpammers, entry.CountPerSpammer, totalCount) + + if err := deleteAllSpammers(api); err != nil { + return fmt.Errorf("delete stale spammers: %w", err) + } + defer func() { + if err := deleteAllSpammers(api); err != nil { + log.Printf("[%s] warning: cleanup failed: %v", entry.TestName, err) + } + }() + + scenarioCfg := BuildScenarioConfig(entry.Env) + + var spammerIDs []int + for i := range entry.NumSpammers { + name := fmt.Sprintf("bench-%s-%d", entry.TestName, i) + id, err := api.CreateSpammer(name, entry.Scenario, scenarioCfg, true) + if err != nil { + return fmt.Errorf("create spammer %s: %w", name, err) + } + spammerIDs = append(spammerIDs, id) + log.Printf("[%s] created spammer %s (id=%d)", entry.TestName, name, id) + } + + for _, id := range spammerIDs { + sp, err := api.GetSpammer(id) + if err != nil { + return fmt.Errorf("get spammer %d: %w", id, err) + } + if sp.Status == 0 { + return fmt.Errorf("spammer %d (%s) failed to start (status=0)", id, sp.Name) + } + } + + start := time.Now() + sent, failed, err := waitForSpamoorDone(ctx, api, totalCount) + elapsed := time.Since(start) + + if err != nil { + log.Printf("[%s] ERROR: %v (sent=%.0f failed=%.0f elapsed=%s)", + entry.TestName, err, sent, failed, elapsed.Round(time.Second)) + return err + } + + log.Printf("[%s] DONE: sent=%.0f failed=%.0f elapsed=%s avg_rate=%.1f tx/s", + entry.TestName, sent, failed, elapsed.Round(time.Second), sent/elapsed.Seconds()) + + return nil +} + +func deleteAllSpammers(api *spamoor.API) error { + existing, err := api.ListSpammers() + if err != nil { + return fmt.Errorf("list spammers: %w", err) + } + for _, sp := range existing { + if err := api.DeleteSpammer(sp.ID); err != nil { + return fmt.Errorf("delete spammer %d: %w", sp.ID, err) + } + } + return nil +} + +func waitForSpamoorDone(ctx context.Context, api *spamoor.API, targetCount int) (sent, failed float64, err error) { + const pollInterval = 2 * time.Second + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + start := time.Now() + var prevSent float64 + + for { + select { + case <-ctx.Done(): + return sent, failed, fmt.Errorf("timed out waiting for %d txs (sent %.0f): %w", targetCount, sent, ctx.Err()) + case <-ticker.C: + metrics, mErr := api.GetMetrics() + if mErr != nil { + log.Printf("warning: failed to get metrics: %v", mErr) + continue + } + sent = sumCounter(metrics["spamoor_transactions_sent_total"]) + failed = sumCounter(metrics["spamoor_transactions_failed_total"]) + + delta := sent - prevSent + rate := delta / pollInterval.Seconds() + elapsed := time.Since(start).Round(time.Second) + log.Printf(" progress: %.0f/%d sent (%.0f tx/s instant, %.1f tx/s avg, %.0f failed) [%s]", + sent, targetCount, rate, sent/time.Since(start).Seconds(), failed, elapsed) + prevSent = sent + + if sent >= float64(targetCount) { + return sent, failed, nil + } + } + } +} + +func sumCounter(f *dto.MetricFamily) float64 { + if f == nil || f.GetType() != dto.MetricType_COUNTER { + return 0 + } + var sum float64 + for _, m := range f.GetMetric() { + if m.GetCounter() != nil && m.GetCounter().Value != nil { + sum += m.GetCounter().GetValue() + } + } + return sum +} diff --git a/apps/benchmarks/main.go b/apps/benchmarks/main.go new file mode 100644 index 0000000000..f0f95ec4ba --- /dev/null +++ b/apps/benchmarks/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/evstack/ev-node/apps/benchmarks/cmd" +) + +func main() { + if err := cmd.NewRootCmd().Execute(); err != nil { + os.Exit(1) + } +} + +func init() { + log.SetFlags(log.Ldate | log.Ltime | log.LUTC) + log.SetOutput(os.Stdout) + fmt.Fprintln(os.Stderr, "ev-benchmarks starting") +} diff --git a/apps/benchmarks/matrices/baseline.json b/apps/benchmarks/matrices/baseline.json index 3974b84dd7..9cfda961ee 100644 --- a/apps/benchmarks/matrices/baseline.json +++ b/apps/benchmarks/matrices/baseline.json @@ -1,86 +1,17 @@ { "entries": [ { - "test_name": "TestGasBurner", + "test_name": "EOATransfer", + "scenario": "eoatx", "timeout": "15m", "env": { - "BENCH_BLOCK_TIME": "100ms", - "BENCH_SLOT_DURATION": "100ms", - "BENCH_GAS_LIMIT": "0x1C9C380", - "BENCH_SCRAPE_INTERVAL": "25ms", "BENCH_NUM_SPAMMERS": "4", - "BENCH_COUNT_PER_SPAMMER": "6000", - "BENCH_THROUGHPUT": "60", - "BENCH_WARMUP_TXS": "200", - "BENCH_GAS_UNITS_TO_BURN": "1000000", + "BENCH_COUNT_PER_SPAMMER": "10500", + "BENCH_THROUGHPUT": "200", + "BENCH_MAX_PENDING": "50000", "BENCH_MAX_WALLETS": "200", - "BENCH_WAIT_TIMEOUT": "10m" - } - }, - { - "test_name": "TestStatePressure", - "timeout": "15m", - "env": { - "BENCH_BLOCK_TIME": "100ms", - "BENCH_SLOT_DURATION": "100ms", - "BENCH_GAS_LIMIT": "0x1C9C380", - "BENCH_SCRAPE_INTERVAL": "25ms", - "BENCH_NUM_SPAMMERS": "3", - "BENCH_COUNT_PER_SPAMMER": "4000", - "BENCH_THROUGHPUT": "40", - "BENCH_WARMUP_TXS": "200", - "BENCH_GAS_UNITS_TO_BURN": "1000000", - "BENCH_MAX_WALLETS": "150", - "BENCH_WAIT_TIMEOUT": "10m" - } - }, - { - "test_name": "TestMixedWorkload", - "timeout": "20m", - "env": { - "BENCH_BLOCK_TIME": "100ms", - "BENCH_SLOT_DURATION": "100ms", - "BENCH_GAS_LIMIT": "0x1C9C380", - "BENCH_SCRAPE_INTERVAL": "25ms", - "BENCH_NUM_SPAMMERS": "4", - "BENCH_COUNT_PER_SPAMMER": "5000", - "BENCH_THROUGHPUT": "26", - "BENCH_WARMUP_TXS": "500", - "BENCH_GAS_UNITS_TO_BURN": "1000000", - "BENCH_MAX_WALLETS": "150", - "BENCH_WAIT_TIMEOUT": "15m" - } - }, - { - "test_name": "TestDeFiSimulation", - "timeout": "20m", - "env": { - "BENCH_BLOCK_TIME": "100ms", - "BENCH_SLOT_DURATION": "100ms", - "BENCH_GAS_LIMIT": "0x1C9C380", - "BENCH_SCRAPE_INTERVAL": "25ms", - "BENCH_NUM_SPAMMERS": "3", - "BENCH_COUNT_PER_SPAMMER": "30000", - "BENCH_THROUGHPUT": "100", - "BENCH_WARMUP_TXS": "800", - "BENCH_MAX_WALLETS": "150", - "BENCH_WAIT_TIMEOUT": "15m" - } - }, - { - "test_name": "TestERC20Throughput", - "timeout": "15m", - "env": { - "BENCH_BLOCK_TIME": "100ms", - "BENCH_SLOT_DURATION": "100ms", - "BENCH_GAS_LIMIT": "0x1C9C380", - "BENCH_SCRAPE_INTERVAL": "25ms", - "BENCH_NUM_SPAMMERS": "4", - "BENCH_COUNT_PER_SPAMMER": "12000", - "BENCH_THROUGHPUT": "115", - "BENCH_WARMUP_TXS": "300", - "BENCH_MAX_WALLETS": "200", - "BENCH_WAIT_TIMEOUT": "10m" + "BENCH_BASE_FEE": "20", + "BENCH_TIP_FEE": "2" } } ] diff --git a/apps/benchmarks/matrices/burst.json b/apps/benchmarks/matrices/burst.json new file mode 100644 index 0000000000..d4c68c0f8f --- /dev/null +++ b/apps/benchmarks/matrices/burst.json @@ -0,0 +1,19 @@ +{ + "entries": [ + { + "test_name": "EOATransferBurst", + "scenario": "eoatx", + "timeout": "30m", + "probability": 0.15, + "env": { + "BENCH_NUM_SPAMMERS": "10", + "BENCH_COUNT_PER_SPAMMER": "50000", + "BENCH_THROUGHPUT": "500", + "BENCH_MAX_PENDING": "50000", + "BENCH_MAX_WALLETS": "500", + "BENCH_BASE_FEE": "20", + "BENCH_TIP_FEE": "2" + } + } + ] +} diff --git a/justfile b/justfile index 4259dcc461..6636b213e5 100644 --- a/justfile +++ b/justfile @@ -18,6 +18,7 @@ import '.just/lint.just' import '.just/codegen.just' import '.just/run.just' import '.just/tools.just' +import '.just/bench.just' # List available recipes when running `just` with no args default: From d39de67dc0ba947a0b0b5580390443539ebc2d1c Mon Sep 17 00:00:00 2001 From: chatton Date: Thu, 28 May 2026 16:12:12 +0100 Subject: [PATCH 08/15] feat(benchmarks): replace supercronic with in-process scheduler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace separate regular/burst subcommands invoked by supercronic with a single `start` command that runs an infinite scheduling loop. Regular workloads fire on a ticker, bursts at random times within rolling 24h windows. All timing controlled via CLI flags with env fallbacks. - remove supercronic from Dockerfile - delete regular.go, burst.go, crontab - add start.go with scheduler loop - plumb context for graceful shutdown - unify ExecuteMatrix/ExecuteMatrixWithOverrides via matrixOpts - bump fees (BASE_FEE 20→500, TIP_FEE 2→50) - remove healthcheck from docker-compose, rely on WaitForSync --- .just/bench.just | 6 +- apps/benchmarks/.gitignore | 1 - apps/benchmarks/Dockerfile | 10 +- apps/benchmarks/README.md | 49 ++++--- apps/benchmarks/cmd/burst.go | 25 ---- apps/benchmarks/cmd/check.go | 2 +- apps/benchmarks/cmd/regular.go | 25 ---- apps/benchmarks/cmd/root.go | 3 +- apps/benchmarks/cmd/run.go | 3 +- apps/benchmarks/cmd/start.go | 174 +++++++++++++++++++++++++ apps/benchmarks/crontab | 4 - apps/benchmarks/docker-compose.yml | 18 +-- apps/benchmarks/internal/config.go | 4 + apps/benchmarks/internal/matrix.go | 4 + apps/benchmarks/internal/runner.go | 150 ++++++++++++++++++--- apps/benchmarks/matrices/baseline.json | 4 +- apps/benchmarks/matrices/burst.json | 4 +- 17 files changed, 371 insertions(+), 115 deletions(-) delete mode 100644 apps/benchmarks/.gitignore delete mode 100644 apps/benchmarks/cmd/burst.go delete mode 100644 apps/benchmarks/cmd/regular.go create mode 100644 apps/benchmarks/cmd/start.go delete mode 100644 apps/benchmarks/crontab diff --git a/.just/bench.just b/.just/bench.just index b1ba3e6137..1ef0a42097 100644 --- a/.just/bench.just +++ b/.just/bench.just @@ -26,7 +26,7 @@ bench-smoke: export BENCH_PRIVATE_KEY BENCH_ETH_RPC_URL echo "--> Building ev-benchmarks binary" - cd apps/benchmarks && go build -o ../../{{ build_dir }}/ev-benchmarks . && cd ../.. + (cd apps/benchmarks && go build -o {{ build_dir }}/ev-benchmarks .) echo "--> Starting spamoor-daemon via docker compose" docker compose -f apps/benchmarks/docker-compose.yml up -d spamoor-daemon @@ -59,8 +59,8 @@ bench-smoke: "BENCH_THROUGHPUT": "10", "BENCH_MAX_PENDING": "100", "BENCH_MAX_WALLETS": "10", - "BENCH_BASE_FEE": "20", - "BENCH_TIP_FEE": "2" + "BENCH_BASE_FEE": "500", + "BENCH_TIP_FEE": "50" } } ] diff --git a/apps/benchmarks/.gitignore b/apps/benchmarks/.gitignore deleted file mode 100644 index e6952c55fa..0000000000 --- a/apps/benchmarks/.gitignore +++ /dev/null @@ -1 +0,0 @@ -ev-benchmarks diff --git a/apps/benchmarks/Dockerfile b/apps/benchmarks/Dockerfile index eb1f177a91..4ec437f428 100644 --- a/apps/benchmarks/Dockerfile +++ b/apps/benchmarks/Dockerfile @@ -10,23 +10,15 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o ev-benchmarks . FROM alpine:3.22.2 -ARG SUPERCRONIC_VERSION=0.2.46 -ARG TARGETARCH - ENV TZ=UTC #hadolint ignore=DL3018 RUN apk --no-cache add ca-certificates curl tzdata -RUN wget -q "https://github.com/aptible/supercronic/releases/download/v${SUPERCRONIC_VERSION}/supercronic-linux-${TARGETARCH}" \ - -O /usr/local/bin/supercronic && \ - chmod +x /usr/local/bin/supercronic - WORKDIR /root COPY --from=build-env /src/ev-benchmarks /usr/bin/ev-benchmarks -COPY apps/benchmarks/crontab /etc/crontab COPY apps/benchmarks/matrices/baseline.json /root/baseline.json COPY apps/benchmarks/matrices/burst.json /root/burst.json -CMD ["supercronic", "/etc/crontab"] +CMD ["ev-benchmarks", "start"] diff --git a/apps/benchmarks/README.md b/apps/benchmarks/README.md index 9807f25af6..dea4faffa8 100644 --- a/apps/benchmarks/README.md +++ b/apps/benchmarks/README.md @@ -1,6 +1,6 @@ # benchmarks -Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon](https://github.com/ethpandaops/spamoor) sidecar via HTTP API. Runs on a cron schedule via [supercronic](https://github.com/aptible/supercronic). +Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon](https://github.com/ethpandaops/spamoor) sidecar via HTTP API. Runs an in-process scheduler with configurable regular and burst workloads. ## Architecture @@ -12,19 +12,35 @@ ev-benchmarks (this binary) --> spamoor-daemon --> ev-reth RPC ``` - **spamoor-daemon** needs: a funded private key + ev-reth RPC URL -- **ev-benchmarks** needs: spamoor-daemon API URL + a matrix JSON file +- **ev-benchmarks** needs: spamoor-daemon API URL + matrix JSON files ## Commands ``` +ev-benchmarks start # run continuous scheduler (regular + burst) ev-benchmarks check # send 1 tx to verify spamoor → ev-reth connectivity -ev-benchmarks regular # sustained ~1M tx/day (baseline matrix) -ev-benchmarks burst # probabilistic 500K tx burst (~15%/invocation) -ev-benchmarks run # custom matrix file +ev-benchmarks run # one-shot: run a custom matrix file ``` +### start flags + +| Flag | Env | Default | Description | +|------|-----|---------|-------------| +| `--tx-per-day` | `BENCH_TX_PER_DAY` | `1000000` | sustained txs/day | +| `--interval` | `BENCH_INTERVAL` | `1h` | regular workload frequency | +| `--burst-tx-count` | `BENCH_BURST_TX_COUNT` | `500000` | txs per burst | +| `--burst-per-day` | `BENCH_BURST_PER_DAY` | `2` | bursts per day, randomly spaced | +| `--regular-matrix` | `BENCH_REGULAR_MATRIX` | `/root/baseline.json` | path to regular matrix JSON | +| `--burst-matrix` | `BENCH_BURST_MATRIX` | `/root/burst.json` | path to burst matrix JSON | + Global flag: `--spamoor-url` (or `BENCH_SPAMOOR_URL` env, default `http://spamoor-daemon:8080`) +### Scheduling + +- **Regular**: fires immediately at startup, then repeats at `--interval`. Per-run tx count = `tx-per-day / (24h / interval)`. Overrides each matrix entry's `BENCH_COUNT_PER_SPAMMER`. +- **Burst**: at startup + each midnight UTC, generates N random times across the day. Each burst overrides `BENCH_COUNT_PER_SPAMMER` = `burst-tx-count / NumSpammers`. +- **Serialization**: a mutex prevents concurrent spamoor access. If burst fires during regular (or vice versa), it waits for the lock. + ## Quick Start ### 1. Start spamoor-daemon @@ -43,8 +59,16 @@ docker run -d --name spamoor -p 8080:8080 \ # build cd apps/benchmarks && go build -o ev-benchmarks . -# run -./ev-benchmarks regular --spamoor-url=http://localhost:8080 +# run with defaults (~1M tx/day, 2 bursts/day) +./ev-benchmarks start --spamoor-url=http://localhost:8080 + +# custom config +./ev-benchmarks start \ + --spamoor-url=http://localhost:8080 \ + --tx-per-day=500000 \ + --interval=30m \ + --burst-tx-count=100000 \ + --burst-per-day=4 ``` ### Docker Compose @@ -76,15 +100,14 @@ Each entry specifies a spamoor scenario, tx counts, and optional probability: "test_name": "EOATransfer", "scenario": "eoatx", "timeout": "15m", - "probability": 1.0, "env": { "BENCH_NUM_SPAMMERS": "4", "BENCH_COUNT_PER_SPAMMER": "10500", "BENCH_THROUGHPUT": "200", "BENCH_MAX_PENDING": "50000", "BENCH_MAX_WALLETS": "200", - "BENCH_BASE_FEE": "20", - "BENCH_TIP_FEE": "2" + "BENCH_BASE_FEE": "500", + "BENCH_TIP_FEE": "50" } } ] @@ -97,11 +120,7 @@ Each entry specifies a spamoor scenario, tx counts, and optional probability: | `probability` | 0.0–1.0, chance of running per invocation (omit = always run) | | `timeout` | max duration per entry (default `15m`) | -## Schedule - -Supercronic runs both matrices `@hourly`: -- `regular` — 4 × 10,500 = 42K txs/run → ~1M/day -- `burst` — 10 × 50,000 = 500K txs, 15% chance → ~3-4 bursts/day +When using `start`, the `BENCH_COUNT_PER_SPAMMER` value in the matrix is overridden by the computed per-run count. The matrix value is still used by the `run` command. ## Build diff --git a/apps/benchmarks/cmd/burst.go b/apps/benchmarks/cmd/burst.go deleted file mode 100644 index a2b3186f71..0000000000 --- a/apps/benchmarks/cmd/burst.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/evstack/ev-node/apps/benchmarks/internal" - "github.com/spf13/cobra" -) - -const defaultBurstPath = "/root/burst.json" - -func newBurstCmd() *cobra.Command { - var matrixPath string - - cmd := &cobra.Command{ - Use: "burst", - Short: "run probabilistic burst load (500K tx, ~15% chance per invocation)", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return internal.ExecuteMatrix(matrixPath, resolveSpamoorURL()) - }, - } - - cmd.Flags().StringVar(&matrixPath, "matrix", defaultBurstPath, "path to burst matrix JSON") - - return cmd -} diff --git a/apps/benchmarks/cmd/check.go b/apps/benchmarks/cmd/check.go index 6e7ae3d53c..a29ed132ce 100644 --- a/apps/benchmarks/cmd/check.go +++ b/apps/benchmarks/cmd/check.go @@ -15,7 +15,7 @@ func newCheckCmd() *cobra.Command { Short: "verify connectivity by sending a single eoatx through spamoor to ev-reth", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return internal.RunCheck(resolveSpamoorURL(), timeout) + return internal.RunCheck(cmd.Context(), resolveSpamoorURL(), timeout) }, } diff --git a/apps/benchmarks/cmd/regular.go b/apps/benchmarks/cmd/regular.go deleted file mode 100644 index 89e315cded..0000000000 --- a/apps/benchmarks/cmd/regular.go +++ /dev/null @@ -1,25 +0,0 @@ -package cmd - -import ( - "github.com/evstack/ev-node/apps/benchmarks/internal" - "github.com/spf13/cobra" -) - -const defaultBaselinePath = "/root/baseline.json" - -func newRegularCmd() *cobra.Command { - var matrixPath string - - cmd := &cobra.Command{ - Use: "regular", - Short: "run sustained load from baseline matrix (~1M tx/day at @hourly)", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - return internal.ExecuteMatrix(matrixPath, resolveSpamoorURL()) - }, - } - - cmd.Flags().StringVar(&matrixPath, "matrix", defaultBaselinePath, "path to baseline matrix JSON") - - return cmd -} diff --git a/apps/benchmarks/cmd/root.go b/apps/benchmarks/cmd/root.go index 3658393faf..0929831c9d 100644 --- a/apps/benchmarks/cmd/root.go +++ b/apps/benchmarks/cmd/root.go @@ -7,6 +7,7 @@ import ( var spamoorFlag string +// NewRootCmd returns the top-level cobra command for ev-benchmarks. func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ Use: "ev-benchmarks", @@ -15,7 +16,7 @@ func NewRootCmd() *cobra.Command { rootCmd.PersistentFlags().StringVar(&spamoorFlag, "spamoor-url", "", "spamoor-daemon API URL (env: BENCH_SPAMOOR_URL)") - rootCmd.AddCommand(newRunCmd(), newRegularCmd(), newBurstCmd(), newCheckCmd()) + rootCmd.AddCommand(newRunCmd(), newStartCmd(), newCheckCmd()) return rootCmd } diff --git a/apps/benchmarks/cmd/run.go b/apps/benchmarks/cmd/run.go index e63ba2e56d..abebeb7174 100644 --- a/apps/benchmarks/cmd/run.go +++ b/apps/benchmarks/cmd/run.go @@ -1,6 +1,7 @@ package cmd import ( + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" "github.com/evstack/ev-node/apps/benchmarks/internal" "github.com/spf13/cobra" ) @@ -11,7 +12,7 @@ func newRunCmd() *cobra.Command { Short: "run benchmarks from a matrix JSON file", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return internal.ExecuteMatrix(args[0], resolveSpamoorURL()) + return internal.ExecuteMatrix(cmd.Context(), args[0], spamoor.NewAPI(resolveSpamoorURL())) }, } } diff --git a/apps/benchmarks/cmd/start.go b/apps/benchmarks/cmd/start.go new file mode 100644 index 0000000000..a9186aefb4 --- /dev/null +++ b/apps/benchmarks/cmd/start.go @@ -0,0 +1,174 @@ +package cmd + +import ( + "context" + "log" + "math/rand/v2" + "os" + "os/signal" + "strconv" + "sync" + "syscall" + "time" + + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" + "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/spf13/cobra" +) + +const ( + defaultBaselinePath = "/root/baseline.json" + defaultBurstPath = "/root/burst.json" + burstWindow = 24 * time.Hour +) + +type startConfig struct { + txPerDay int + interval time.Duration + burstTxCount int + burstPerDay int + regularMatrix string + burstMatrix string +} + +func newStartCmd() *cobra.Command { + cfg := startConfig{} + + cmd := &cobra.Command{ + Use: "start", + Short: "run continuous benchmark scheduler (regular + burst workloads)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runScheduler(cmd.Context(), cfg) + }, + } + + cmd.Flags().IntVar(&cfg.txPerDay, "tx-per-day", envIntOr("BENCH_TX_PER_DAY", 1000000), "sustained txs/day") + cmd.Flags().DurationVar(&cfg.interval, "interval", envDurationOr("BENCH_INTERVAL", time.Hour), "regular workload frequency") + cmd.Flags().IntVar(&cfg.burstTxCount, "burst-tx-count", envIntOr("BENCH_BURST_TX_COUNT", 500000), "txs per burst") + cmd.Flags().IntVar(&cfg.burstPerDay, "burst-per-day", envIntOr("BENCH_BURST_PER_DAY", 2), "bursts per day, randomly spaced") + cmd.Flags().StringVar(&cfg.regularMatrix, "regular-matrix", envStringOr("BENCH_REGULAR_MATRIX", defaultBaselinePath), "path to regular matrix JSON") + cmd.Flags().StringVar(&cfg.burstMatrix, "burst-matrix", envStringOr("BENCH_BURST_MATRIX", defaultBurstPath), "path to burst matrix JSON") + + return cmd +} + +func runScheduler(parent context.Context, cfg startConfig) error { + ctx, cancel := signal.NotifyContext(parent, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + spamoorAddr := resolveSpamoorURL() + api := spamoor.NewAPI(spamoorAddr) + + runsPerDay := float64(24*time.Hour) / float64(cfg.interval) + regularTxPerRun := int(float64(cfg.txPerDay) / runsPerDay) + + log.Printf("scheduler config: tx-per-day=%d interval=%s regular-tx-per-run=%d burst-tx-count=%d burst-per-day=%d", + cfg.txPerDay, cfg.interval, regularTxPerRun, cfg.burstTxCount, cfg.burstPerDay) + log.Printf("regular-matrix=%s burst-matrix=%s spamoor=%s", cfg.regularMatrix, cfg.burstMatrix, spamoorAddr) + + if err := internal.WaitForSync(ctx, api); err != nil { + return err + } + + var mu sync.Mutex + runWorkload := func(label, matrixPath string, txCount int) { + mu.Lock() + defer mu.Unlock() + log.Printf("==> %s workload starting (%d tx)", label, txCount) + if err := internal.ExecuteMatrixWithOverrides(ctx, matrixPath, api, txCount); err != nil { + log.Printf("%s workload error: %v", label, err) + } + } + + // fire regular immediately + go runWorkload("regular", cfg.regularMatrix, regularTxPerRun) + + ticker := time.NewTicker(cfg.interval) + defer ticker.Stop() + + // burst: single timer, reschedule after each fire, reset count every 24h. + burstsRemaining := cfg.burstPerDay + burstTimer := nextBurstTimer(burstsRemaining, burstWindow) + resetTimer := time.NewTimer(burstWindow) + defer resetTimer.Stop() + + for { + select { + case <-ctx.Done(): + log.Printf("shutting down...") + burstTimer.Stop() + mu.Lock() + log.Printf("cleaning up spammers") + if err := internal.DeleteAllSpammers(api); err != nil { + log.Printf("warning: shutdown cleanup failed: %v", err) + } + mu.Unlock() + return nil + + case <-ticker.C: + go runWorkload("regular", cfg.regularMatrix, regularTxPerRun) + + case <-burstTimer.C: + go runWorkload("burst", cfg.burstMatrix, cfg.burstTxCount) + burstsRemaining-- + burstTimer = nextBurstTimer(burstsRemaining, burstWindow) + + case <-resetTimer.C: + log.Printf("24h elapsed - resetting burst count") + burstTimer.Stop() + burstsRemaining = cfg.burstPerDay + burstTimer = nextBurstTimer(burstsRemaining, burstWindow) + resetTimer.Reset(burstWindow) + } + } +} + +// nextBurstTimer returns a timer for the next burst. If no bursts remain, +// returns a stopped timer (channel never fires). +func nextBurstTimer(remaining int, window time.Duration) *time.Timer { + if remaining <= 0 { + t := time.NewTimer(0) + t.Stop() + // drain channel in case it fired before Stop + select { + case <-t.C: + default: + } + return t + } + delay := time.Duration(rand.Int64N(int64(window) / int64(remaining))) + log.Printf("next burst in %s (%d remaining)", delay.Round(time.Second), remaining) + return time.NewTimer(delay) +} + +func envIntOr(key string, fallback int) int { + v := os.Getenv(key) + if v == "" { + return fallback + } + n, err := strconv.Atoi(v) + if err != nil { + return fallback + } + return n +} + +func envDurationOr(key string, fallback time.Duration) time.Duration { + v := os.Getenv(key) + if v == "" { + return fallback + } + d, err := time.ParseDuration(v) + if err != nil { + return fallback + } + return d +} + +func envStringOr(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/apps/benchmarks/crontab b/apps/benchmarks/crontab deleted file mode 100644 index e47f498a6f..0000000000 --- a/apps/benchmarks/crontab +++ /dev/null @@ -1,4 +0,0 @@ -# sustained ~1M tx/day (4 × 10,500 = 42,000 per run × 24 = ~1,008,000/day) -@hourly ev-benchmarks regular -# burst 500K tx at random (~15% chance per hour ≈ 3-4 bursts/day) -@hourly ev-benchmarks burst diff --git a/apps/benchmarks/docker-compose.yml b/apps/benchmarks/docker-compose.yml index c340233318..a15b9e89d5 100644 --- a/apps/benchmarks/docker-compose.yml +++ b/apps/benchmarks/docker-compose.yml @@ -11,18 +11,18 @@ services: - --startup-delay=0 ports: - "8080:8080" - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/metrics"] - interval: 2s - timeout: 2s - retries: 10 benchmarks: build: context: ../.. dockerfile: apps/benchmarks/Dockerfile + command: + - ev-benchmarks + - start + - --spamoor-url=http://spamoor-daemon:8080 + - --tx-per-day=1000000 + - --interval=1h + - --burst-tx-count=500000 + - --burst-per-day=2 depends_on: - spamoor-daemon: - condition: service_healthy - environment: - - BENCH_SPAMOOR_URL=http://spamoor-daemon:8080 + - spamoor-daemon diff --git a/apps/benchmarks/internal/config.go b/apps/benchmarks/internal/config.go index 16b98b1d4f..c044659094 100644 --- a/apps/benchmarks/internal/config.go +++ b/apps/benchmarks/internal/config.go @@ -7,6 +7,8 @@ import ( const DefaultSpamoorURL = "http://spamoor-daemon:8080" +// SpamoorURL returns the spamoor-daemon API URL from BENCH_SPAMOOR_URL, +// falling back to DefaultSpamoorURL. func SpamoorURL() string { if v := os.Getenv("BENCH_SPAMOOR_URL"); v != "" { return v @@ -25,6 +27,8 @@ var envMapping = map[string]string{ "BENCH_REBROADCAST": "rebroadcast", } +// BuildScenarioConfig translates BENCH_* env vars from a matrix entry into +// the spamoor scenario config map expected by the spamoor API. func BuildScenarioConfig(env map[string]string) map[string]any { cfg := map[string]any{ "refill_amount": "500000000000000000000", diff --git a/apps/benchmarks/internal/matrix.go b/apps/benchmarks/internal/matrix.go index f6ccfc7bb5..5e9841305f 100644 --- a/apps/benchmarks/internal/matrix.go +++ b/apps/benchmarks/internal/matrix.go @@ -31,10 +31,13 @@ var validScenarios = map[string]bool{ spamoor.ScenarioTaskRunner: true, } +// Matrix is the top-level structure of a benchmark matrix JSON file. type Matrix struct { Entries []Entry `json:"entries"` } +// Entry is a single benchmark scenario in a matrix file. NumSpammers and +// CountPerSpammer are derived from Env during validation. type Entry struct { TestName string `json:"test_name"` Scenario string `json:"scenario"` @@ -45,6 +48,7 @@ type Entry struct { CountPerSpammer int `json:"-"` } +// LoadMatrix reads and validates a matrix JSON file from disk. func LoadMatrix(path string) (*Matrix, error) { data, err := os.ReadFile(path) if err != nil { diff --git a/apps/benchmarks/internal/runner.go b/apps/benchmarks/internal/runner.go index 174d6ef2e1..2b43004511 100644 --- a/apps/benchmarks/internal/runner.go +++ b/apps/benchmarks/internal/runner.go @@ -2,30 +2,70 @@ package internal import ( "context" + "encoding/json" "fmt" "log" "math/rand/v2" + "net/http" "time" "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" dto "github.com/prometheus/client_model/go" ) -func ExecuteMatrix(matrixPath, spamoorAddr string) error { +type matrixOpts struct { + totalTxTarget int + allowProbability bool + waitForSync bool +} + +// ExecuteMatrixWithOverrides runs a matrix with BENCH_COUNT_PER_SPAMMER +// overridden to totalTxTarget / NumSpammers per entry. Skips probability +// filtering and sync waiting (caller is responsible for sync). +func ExecuteMatrixWithOverrides(ctx context.Context, matrixPath string, api *spamoor.API, totalTxTarget int) error { + return executeMatrix(ctx, matrixPath, api, matrixOpts{ + totalTxTarget: totalTxTarget, + }) +} + +// ExecuteMatrix runs all entries in a matrix file as-is, with probability +// filtering and an initial WaitForSync call. Used by the one-shot `run` command. +func ExecuteMatrix(ctx context.Context, matrixPath string, api *spamoor.API) error { + return executeMatrix(ctx, matrixPath, api, matrixOpts{ + allowProbability: true, + waitForSync: true, + }) +} + +func executeMatrix(ctx context.Context, matrixPath string, api *spamoor.API, opts matrixOpts) error { matrix, err := LoadMatrix(matrixPath) if err != nil { return fmt.Errorf("load matrix: %w", err) } - api := spamoor.NewAPI(spamoorAddr) - log.Printf("spamoor API: %s", spamoorAddr) - log.Printf("loaded %d matrix entries from %s", len(matrix.Entries), matrixPath) + log.Printf("spamoor API: %s", api.BaseURL) + if opts.totalTxTarget > 0 { + log.Printf("loaded %d matrix entries from %s (totalTxTarget=%d)", len(matrix.Entries), matrixPath, opts.totalTxTarget) + } else { + log.Printf("loaded %d matrix entries from %s", len(matrix.Entries), matrixPath) + } + + if opts.waitForSync { + if err := WaitForSync(ctx, api); err != nil { + return fmt.Errorf("waiting for spamoor sync: %w", err) + } + } var failures []string for i, entry := range matrix.Entries { + if err := ctx.Err(); err != nil { + log.Printf("cancelled before entry %d/%d", i+1, len(matrix.Entries)) + return err + } + log.Printf("--- [%d/%d] %s ---", i+1, len(matrix.Entries), entry.TestName) - if entry.Probability != nil { + if opts.allowProbability && entry.Probability != nil { roll := rand.Float64() if roll >= *entry.Probability { log.Printf("[%s] skipped (probability=%.2f, roll=%.4f)", entry.TestName, *entry.Probability, roll) @@ -34,6 +74,15 @@ func ExecuteMatrix(matrixPath, spamoorAddr string) error { log.Printf("[%s] triggered (probability=%.2f, roll=%.4f)", entry.TestName, *entry.Probability, roll) } + if opts.totalTxTarget > 0 { + countPerSpammer := opts.totalTxTarget / entry.NumSpammers + if countPerSpammer < 1 { + countPerSpammer = 1 + } + entry.Env["BENCH_COUNT_PER_SPAMMER"] = fmt.Sprintf("%d", countPerSpammer) + entry.CountPerSpammer = countPerSpammer + } + timeout := 15 * time.Minute if entry.Timeout != "" { parsed, pErr := time.ParseDuration(entry.Timeout) @@ -44,8 +93,8 @@ func ExecuteMatrix(matrixPath, spamoorAddr string) error { } } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - if err := runEntry(ctx, api, entry); err != nil { + entryCtx, cancel := context.WithTimeout(ctx, timeout) + if err := runEntry(entryCtx, api, entry); err != nil { log.Printf("FAIL [%s]: %v", entry.TestName, err) failures = append(failures, entry.TestName) } @@ -59,7 +108,8 @@ func ExecuteMatrix(matrixPath, spamoorAddr string) error { return nil } -func RunCheck(spamoorAddr string, timeout time.Duration) error { +// RunCheck verifies connectivity by sending a single eoatx through spamoor. +func RunCheck(ctx context.Context, spamoorAddr string, timeout time.Duration) error { api := spamoor.NewAPI(spamoorAddr) log.Printf("checking connectivity via %s", spamoorAddr) @@ -69,18 +119,22 @@ func RunCheck(spamoorAddr string, timeout time.Duration) error { } log.Printf("spamoor reachable") - if err := deleteAllSpammers(api); err != nil { + if err := WaitForSync(ctx, api); err != nil { + return fmt.Errorf("waiting for spamoor sync: %w", err) + } + + if err := DeleteAllSpammers(api); err != nil { return fmt.Errorf("cleanup: %w", err) } - defer func() { _ = deleteAllSpammers(api) }() + defer func() { _ = DeleteAllSpammers(api) }() cfg := map[string]any{ "total_count": 1, "throughput": 1, "max_pending": 10, "max_wallets": 1, - "base_fee": 20, - "tip_fee": 2, + "base_fee": 500, + "tip_fee": 50, "refill_amount": "500000000000000000000", "refill_balance": "200000000000000000000", "refill_interval": 300, @@ -92,10 +146,10 @@ func RunCheck(spamoorAddr string, timeout time.Duration) error { } log.Printf("created check spammer (id=%d)", id) - ctx, cancel := context.WithTimeout(context.Background(), timeout) + checkCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - sent, failed, err := waitForSpamoorDone(ctx, api, 1) + sent, failed, err := waitForSpamoorDone(checkCtx, api, 1) if err != nil { return fmt.Errorf("tx not confirmed: %w", err) } @@ -114,11 +168,11 @@ func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { log.Printf("[%s] scenario=%s spammers=%d count_per=%d total=%d", entry.TestName, entry.Scenario, entry.NumSpammers, entry.CountPerSpammer, totalCount) - if err := deleteAllSpammers(api); err != nil { + if err := DeleteAllSpammers(api); err != nil { return fmt.Errorf("delete stale spammers: %w", err) } defer func() { - if err := deleteAllSpammers(api); err != nil { + if err := DeleteAllSpammers(api); err != nil { log.Printf("[%s] warning: cleanup failed: %v", entry.TestName, err) } }() @@ -162,7 +216,8 @@ func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { return nil } -func deleteAllSpammers(api *spamoor.API) error { +// DeleteAllSpammers removes all active spammers from the spamoor daemon. +func DeleteAllSpammers(api *spamoor.API) error { existing, err := api.ListSpammers() if err != nil { return fmt.Errorf("list spammers: %w", err) @@ -210,6 +265,67 @@ func waitForSpamoorDone(ctx context.Context, api *spamoor.API, targetCount int) } } +type spamoorClient struct { + BlockHeight uint64 `json:"block_height"` + Ready bool `json:"ready"` +} + +// WaitForSync polls spamoor until its block processing rate stabilizes, +// indicating it has caught up to the chain tip. Returns immediately if +// ctx is cancelled. +func WaitForSync(ctx context.Context, api *spamoor.API) error { + log.Printf("waiting for spamoor to sync to chain tip...") + + const syncThreshold uint64 = 10 + const pollInterval = 3 * time.Second + + var lastHeight uint64 + + for { + height, err := getSpamoorHeight(api) + if err != nil { + log.Printf("warning: %v", err) + } else { + if lastHeight > 0 && height > 0 { + delta := height - lastHeight + if delta < syncThreshold { + log.Printf("spamoor synced at block %d (delta=%d in %s)", height, delta, pollInterval) + return nil + } + log.Printf("syncing... block %d (+%d in %s)", height, delta, pollInterval) + } else { + log.Printf("syncing... block %d", height) + } + lastHeight = height + } + + timer := time.NewTimer(pollInterval) + select { + case <-ctx.Done(): + timer.Stop() + return fmt.Errorf("cancelled waiting for sync: %w", ctx.Err()) + case <-timer.C: + } + } +} + +func getSpamoorHeight(api *spamoor.API) (uint64, error) { + resp, err := http.Get(fmt.Sprintf("%s/api/clients", api.BaseURL)) + if err != nil { + return 0, fmt.Errorf("get clients: %w", err) + } + defer resp.Body.Close() + + var clients []spamoorClient + if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil { + return 0, fmt.Errorf("decode clients: %w", err) + } + if len(clients) == 0 { + return 0, fmt.Errorf("no RPC clients configured") + } + return clients[0].BlockHeight, nil +} + func sumCounter(f *dto.MetricFamily) float64 { if f == nil || f.GetType() != dto.MetricType_COUNTER { return 0 diff --git a/apps/benchmarks/matrices/baseline.json b/apps/benchmarks/matrices/baseline.json index 9cfda961ee..d7f22ac351 100644 --- a/apps/benchmarks/matrices/baseline.json +++ b/apps/benchmarks/matrices/baseline.json @@ -10,8 +10,8 @@ "BENCH_THROUGHPUT": "200", "BENCH_MAX_PENDING": "50000", "BENCH_MAX_WALLETS": "200", - "BENCH_BASE_FEE": "20", - "BENCH_TIP_FEE": "2" + "BENCH_BASE_FEE": "500", + "BENCH_TIP_FEE": "50" } } ] diff --git a/apps/benchmarks/matrices/burst.json b/apps/benchmarks/matrices/burst.json index d4c68c0f8f..b82d848246 100644 --- a/apps/benchmarks/matrices/burst.json +++ b/apps/benchmarks/matrices/burst.json @@ -11,8 +11,8 @@ "BENCH_THROUGHPUT": "500", "BENCH_MAX_PENDING": "50000", "BENCH_MAX_WALLETS": "500", - "BENCH_BASE_FEE": "20", - "BENCH_TIP_FEE": "2" + "BENCH_BASE_FEE": "500", + "BENCH_TIP_FEE": "50" } } ] From 6a92190fa35c087873d0fa3252418af01ae5eed2 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 2 Jun 2026 09:45:44 +0100 Subject: [PATCH 09/15] feat(loadgen): rename benchmarks app and add tests --- .github/workflows/ci.yml | 50 +++- .just/bench.just | 26 +- .just/build.just | 2 + apps/benchmarks/Dockerfile | 24 -- apps/loadgen/Dockerfile | 24 ++ apps/{benchmarks => loadgen}/README.md | 22 +- apps/{benchmarks => loadgen}/cmd/check.go | 2 +- apps/{benchmarks => loadgen}/cmd/root.go | 6 +- apps/{benchmarks => loadgen}/cmd/run.go | 5 +- apps/{benchmarks => loadgen}/cmd/start.go | 26 +- apps/loadgen/cmd/start_test.go | 40 ++++ .../docker-compose.yml | 6 +- apps/{benchmarks => loadgen}/go.mod | 4 +- apps/{benchmarks => loadgen}/go.sum | 0 apps/loadgen/internal/client.go | 45 ++++ .../internal/config.go | 0 apps/loadgen/internal/config_test.go | 36 +++ .../internal/matrix.go | 0 apps/loadgen/internal/matrix_test.go | 120 ++++++++++ .../internal/runner.go | 89 ++++--- apps/loadgen/internal/runner_test.go | 226 ++++++++++++++++++ apps/{benchmarks => loadgen}/main.go | 4 +- .../matrices/baseline.json | 0 .../matrices/burst.json | 0 24 files changed, 655 insertions(+), 102 deletions(-) delete mode 100644 apps/benchmarks/Dockerfile create mode 100644 apps/loadgen/Dockerfile rename apps/{benchmarks => loadgen}/README.md (85%) rename apps/{benchmarks => loadgen}/cmd/check.go (90%) rename apps/{benchmarks => loadgen}/cmd/root.go (77%) rename apps/{benchmarks => loadgen}/cmd/run.go (56%) rename apps/{benchmarks => loadgen}/cmd/start.go (89%) create mode 100644 apps/loadgen/cmd/start_test.go rename apps/{benchmarks => loadgen}/docker-compose.yml (87%) rename apps/{benchmarks => loadgen}/go.mod (98%) rename apps/{benchmarks => loadgen}/go.sum (100%) create mode 100644 apps/loadgen/internal/client.go rename apps/{benchmarks => loadgen}/internal/config.go (100%) create mode 100644 apps/loadgen/internal/config_test.go rename apps/{benchmarks => loadgen}/internal/matrix.go (100%) create mode 100644 apps/loadgen/internal/matrix_test.go rename apps/{benchmarks => loadgen}/internal/runner.go (76%) create mode 100644 apps/loadgen/internal/runner_test.go rename apps/{benchmarks => loadgen}/main.go (68%) rename apps/{benchmarks => loadgen}/matrices/baseline.json (100%) rename apps/{benchmarks => loadgen}/matrices/burst.json (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72c620f4e8..667b54e874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: pull-requests: read outputs: code: ${{ steps.filter.outputs.code }} + loadgen: ${{ steps.filter.outputs.loadgen }} steps: - uses: actions/checkout@v6.0.2 - uses: dorny/paths-filter@v4 @@ -28,6 +29,12 @@ jobs: - '!docs/**' - '!**/*.md' - '!.github/workflows/docs_*.yml' + loadgen: + - 'apps/loadgen/**' + - '.just/bench.just' + - '.just/build.just' + - '.github/workflows/ci.yml' + - '.github/workflows/docker-build-push.yml' determine-image-tag: name: Determine Image Tag @@ -60,6 +67,40 @@ jobs: echo "tag=$TAG" >> $GITHUB_OUTPUT + determine-docker-apps: + name: Determine Docker Apps + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + apps: ${{ steps.set-apps.outputs.apps }} + steps: + - name: Set docker app matrix + id: set-apps + run: | + APPS='[ + {"name": "ev-node-evm", "dockerfile": "apps/evm/Dockerfile"}, + {"name": "ev-node-grpc", "dockerfile": "apps/grpc/Dockerfile"}, + {"name": "ev-node-testapp", "dockerfile": "apps/testapp/Dockerfile"} + ]' + + if [ "${{ github.event_name }}" = "push" ] || [ "${{ needs.changes.outputs.loadgen }}" = "true" ]; then + APPS='[ + {"name": "ev-node-evm", "dockerfile": "apps/evm/Dockerfile"}, + {"name": "ev-node-grpc", "dockerfile": "apps/grpc/Dockerfile"}, + {"name": "ev-node-loadgen", "dockerfile": "apps/loadgen/Dockerfile"}, + {"name": "ev-node-testapp", "dockerfile": "apps/testapp/Dockerfile"} + ]' + fi + + { + echo "apps<> "$GITHUB_OUTPUT" + lint: needs: changes if: needs.changes.outputs.code == 'true' @@ -68,7 +109,7 @@ jobs: uses: ./.github/workflows/lint.yml docker: - needs: [determine-image-tag, changes] + needs: [determine-image-tag, determine-docker-apps, changes] if: needs.changes.outputs.code == 'true' uses: ./.github/workflows/docker-build-push.yml secrets: inherit @@ -77,12 +118,7 @@ jobs: packages: write with: image-tag: ${{ needs.determine-image-tag.outputs.tag }} - apps: | - [ - {"name": "ev-node-evm", "dockerfile": "apps/evm/Dockerfile"}, - {"name": "ev-node-grpc", "dockerfile": "apps/grpc/Dockerfile"}, - {"name": "ev-node-testapp", "dockerfile": "apps/testapp/Dockerfile"} - ] + apps: ${{ needs.determine-docker-apps.outputs.apps }} test: needs: changes diff --git a/.just/bench.just b/.just/bench.just index 1ef0a42097..6195b5a408 100644 --- a/.just/bench.just +++ b/.just/bench.just @@ -1,17 +1,17 @@ -# Build ev-benchmarks binary +# Build ev-loadgen binary [group('bench')] build-benchmarks: - @echo "--> Building ev-benchmarks" + @echo "--> Building ev-loadgen" @mkdir -p {{ build_dir }} - @cd apps/benchmarks && go build -o {{ build_dir }}/ev-benchmarks . - @echo " Check the binary with: {{ build_dir }}/ev-benchmarks" + @cd apps/loadgen && go build -o {{ build_dir }}/ev-loadgen . + @echo " Check the binary with: {{ build_dir }}/ev-loadgen" -# Build ev-benchmarks Docker image +# Build ev-loadgen Docker image [group('bench')] docker-build-benchmarks: - @echo "--> Building ev-benchmarks Docker image" - @docker build -f apps/benchmarks/Dockerfile -t ev-benchmarks:dev . - @echo "--> Docker image built: ev-benchmarks:dev" + @echo "--> Building ev-loadgen Docker image" + @docker build -f apps/loadgen/Dockerfile -t ev-loadgen:dev . + @echo "--> Docker image built: ev-loadgen:dev" # Smoke test: start spamoor + benchmarks, verify txs are submitted, then tear down. # Requires BENCH_PRIVATE_KEY and BENCH_ETH_RPC_URL env vars. @@ -25,12 +25,12 @@ bench-smoke: export BENCH_PRIVATE_KEY BENCH_ETH_RPC_URL - echo "--> Building ev-benchmarks binary" - (cd apps/benchmarks && go build -o {{ build_dir }}/ev-benchmarks .) + echo "--> Building ev-loadgen binary" + (cd apps/loadgen && go build -o {{ build_dir }}/ev-loadgen .) echo "--> Starting spamoor-daemon via docker compose" - docker compose -f apps/benchmarks/docker-compose.yml up -d spamoor-daemon - trap 'echo "--> Tearing down"; docker compose -f apps/benchmarks/docker-compose.yml down' EXIT + docker compose -f apps/loadgen/docker-compose.yml up -d spamoor-daemon + trap 'echo "--> Tearing down"; docker compose -f apps/loadgen/docker-compose.yml down' EXIT echo "--> Waiting for spamoor-daemon to be healthy" for i in $(seq 1 30); do @@ -67,5 +67,5 @@ bench-smoke: } EOF - {{ build_dir }}/ev-benchmarks run --spamoor-url=http://localhost:8080 /tmp/bench-smoke.json + {{ build_dir }}/ev-loadgen run --spamoor-url=http://localhost:8080 /tmp/bench-smoke.json echo "--> Smoke test passed: transactions submitted successfully" diff --git a/.just/build.just b/.just/build.just index e81fa948c0..36b8003ca9 100644 --- a/.just/build.just +++ b/.just/build.just @@ -28,6 +28,8 @@ build-all: @cd apps/evm && go build -ldflags "{{ ldflags }}" -o {{ build_dir }}/evm . @echo "--> Building grpc" @cd apps/grpc && go build -ldflags "{{ ldflags }}" -o {{ build_dir }}/evgrpc . + @echo "--> Building loadgen" + @cd apps/loadgen && go build -ldflags "{{ ldflags }}" -o {{ build_dir }}/ev-loadgen . @echo "--> Building local-da" @cd tools/local-da && go build -ldflags "{{ ldflags }}" -o {{ build_dir }}/local-da . @echo "--> All ev-node binaries built!" diff --git a/apps/benchmarks/Dockerfile b/apps/benchmarks/Dockerfile deleted file mode 100644 index 4ec437f428..0000000000 --- a/apps/benchmarks/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM golang:1.25-alpine AS build-env - -WORKDIR /src - -COPY apps/benchmarks/go.mod apps/benchmarks/go.sum ./ -RUN go mod download - -COPY apps/benchmarks/ . -RUN CGO_ENABLED=0 GOOS=linux go build -o ev-benchmarks . - -FROM alpine:3.22.2 - -ENV TZ=UTC - -#hadolint ignore=DL3018 -RUN apk --no-cache add ca-certificates curl tzdata - -WORKDIR /root - -COPY --from=build-env /src/ev-benchmarks /usr/bin/ev-benchmarks -COPY apps/benchmarks/matrices/baseline.json /root/baseline.json -COPY apps/benchmarks/matrices/burst.json /root/burst.json - -CMD ["ev-benchmarks", "start"] diff --git a/apps/loadgen/Dockerfile b/apps/loadgen/Dockerfile new file mode 100644 index 0000000000..8da8b5e213 --- /dev/null +++ b/apps/loadgen/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.25-alpine AS build-env + +WORKDIR /src + +COPY apps/loadgen/go.mod apps/loadgen/go.sum ./ +RUN go mod download + +COPY apps/loadgen/ . +RUN CGO_ENABLED=0 GOOS=linux go build -o ev-loadgen . + +FROM alpine:3.22.2 + +ENV TZ=UTC + +#hadolint ignore=DL3018 +RUN apk --no-cache add ca-certificates curl tzdata + +WORKDIR /root + +COPY --from=build-env /src/ev-loadgen /usr/bin/ev-loadgen +COPY apps/loadgen/matrices/baseline.json /root/baseline.json +COPY apps/loadgen/matrices/burst.json /root/burst.json + +CMD ["ev-loadgen", "start"] diff --git a/apps/benchmarks/README.md b/apps/loadgen/README.md similarity index 85% rename from apps/benchmarks/README.md rename to apps/loadgen/README.md index dea4faffa8..7633d16bae 100644 --- a/apps/benchmarks/README.md +++ b/apps/loadgen/README.md @@ -5,21 +5,21 @@ Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon ## Architecture ``` -ev-benchmarks (this binary) --> spamoor-daemon --> ev-reth RPC +ev-loadgen (this binary) --> spamoor-daemon --> ev-reth RPC | | reads matrix JSON manages wallets, creates/polls spammers signs & sends txs ``` - **spamoor-daemon** needs: a funded private key + ev-reth RPC URL -- **ev-benchmarks** needs: spamoor-daemon API URL + matrix JSON files +- **ev-loadgen** needs: spamoor-daemon API URL + matrix JSON files ## Commands ``` -ev-benchmarks start # run continuous scheduler (regular + burst) -ev-benchmarks check # send 1 tx to verify spamoor → ev-reth connectivity -ev-benchmarks run # one-shot: run a custom matrix file +ev-loadgen start # run continuous scheduler (regular + burst) +ev-loadgen check # send 1 tx to verify spamoor → ev-reth connectivity +ev-loadgen run # one-shot: run a custom matrix file ``` ### start flags @@ -57,13 +57,13 @@ docker run -d --name spamoor -p 8080:8080 \ ```sh # build -cd apps/benchmarks && go build -o ev-benchmarks . +cd apps/loadgen && go build -o ev-loadgen . # run with defaults (~1M tx/day, 2 bursts/day) -./ev-benchmarks start --spamoor-url=http://localhost:8080 +./ev-loadgen start --spamoor-url=http://localhost:8080 # custom config -./ev-benchmarks start \ +./ev-loadgen start \ --spamoor-url=http://localhost:8080 \ --tx-per-day=500000 \ --interval=30m \ @@ -78,7 +78,7 @@ Spins up both spamoor-daemon and benchmarks together: ```sh export BENCH_PRIVATE_KEY= export BENCH_ETH_RPC_URL=http://:8545 -docker compose -f apps/benchmarks/docker-compose.yml up +docker compose -f apps/loadgen/docker-compose.yml up ``` ### Smoke Test @@ -126,10 +126,10 @@ When using `start`, the `BENCH_COUNT_PER_SPAMMER` value in the matrix is overrid ```sh # binary -cd apps/benchmarks && go build -o ev-benchmarks . +cd apps/loadgen && go build -o ev-loadgen . # docker image -docker build -f apps/benchmarks/Dockerfile -t ev-benchmarks:dev . +docker build -f apps/loadgen/Dockerfile -t ev-loadgen:dev . # via just just build-benchmarks diff --git a/apps/benchmarks/cmd/check.go b/apps/loadgen/cmd/check.go similarity index 90% rename from apps/benchmarks/cmd/check.go rename to apps/loadgen/cmd/check.go index a29ed132ce..020a75da53 100644 --- a/apps/benchmarks/cmd/check.go +++ b/apps/loadgen/cmd/check.go @@ -3,7 +3,7 @@ package cmd import ( "time" - "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/evstack/ev-node/apps/loadgen/internal" "github.com/spf13/cobra" ) diff --git a/apps/benchmarks/cmd/root.go b/apps/loadgen/cmd/root.go similarity index 77% rename from apps/benchmarks/cmd/root.go rename to apps/loadgen/cmd/root.go index 0929831c9d..952d9f5a38 100644 --- a/apps/benchmarks/cmd/root.go +++ b/apps/loadgen/cmd/root.go @@ -1,16 +1,16 @@ package cmd import ( - "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/evstack/ev-node/apps/loadgen/internal" "github.com/spf13/cobra" ) var spamoorFlag string -// NewRootCmd returns the top-level cobra command for ev-benchmarks. +// NewRootCmd returns the top-level cobra command for ev-loadgen. func NewRootCmd() *cobra.Command { rootCmd := &cobra.Command{ - Use: "ev-benchmarks", + Use: "ev-loadgen", Short: "benchmark runner for ev-node stress testing via spamoor", } diff --git a/apps/benchmarks/cmd/run.go b/apps/loadgen/cmd/run.go similarity index 56% rename from apps/benchmarks/cmd/run.go rename to apps/loadgen/cmd/run.go index abebeb7174..d1436d609c 100644 --- a/apps/benchmarks/cmd/run.go +++ b/apps/loadgen/cmd/run.go @@ -1,8 +1,7 @@ package cmd import ( - "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" - "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/evstack/ev-node/apps/loadgen/internal" "github.com/spf13/cobra" ) @@ -12,7 +11,7 @@ func newRunCmd() *cobra.Command { Short: "run benchmarks from a matrix JSON file", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return internal.ExecuteMatrix(cmd.Context(), args[0], spamoor.NewAPI(resolveSpamoorURL())) + return internal.ExecuteMatrix(cmd.Context(), args[0], internal.NewSpamoorClient(resolveSpamoorURL())) }, } } diff --git a/apps/benchmarks/cmd/start.go b/apps/loadgen/cmd/start.go similarity index 89% rename from apps/benchmarks/cmd/start.go rename to apps/loadgen/cmd/start.go index a9186aefb4..04675e4a85 100644 --- a/apps/benchmarks/cmd/start.go +++ b/apps/loadgen/cmd/start.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "fmt" "log" "math/rand/v2" "os" @@ -11,8 +12,7 @@ import ( "syscall" "time" - "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" - "github.com/evstack/ev-node/apps/benchmarks/internal" + "github.com/evstack/ev-node/apps/loadgen/internal" "github.com/spf13/cobra" ) @@ -54,11 +54,15 @@ func newStartCmd() *cobra.Command { } func runScheduler(parent context.Context, cfg startConfig) error { + if err := validateStartConfig(cfg); err != nil { + return err + } + ctx, cancel := signal.NotifyContext(parent, syscall.SIGINT, syscall.SIGTERM) defer cancel() spamoorAddr := resolveSpamoorURL() - api := spamoor.NewAPI(spamoorAddr) + api := internal.NewSpamoorClient(spamoorAddr) runsPerDay := float64(24*time.Hour) / float64(cfg.interval) regularTxPerRun := int(float64(cfg.txPerDay) / runsPerDay) @@ -124,6 +128,22 @@ func runScheduler(parent context.Context, cfg startConfig) error { } } +func validateStartConfig(cfg startConfig) error { + if cfg.interval <= 0 { + return fmt.Errorf("interval must be > 0") + } + if cfg.txPerDay < 0 { + return fmt.Errorf("tx-per-day must be >= 0") + } + if cfg.burstTxCount < 0 { + return fmt.Errorf("burst-tx-count must be >= 0") + } + if cfg.burstPerDay < 0 { + return fmt.Errorf("burst-per-day must be >= 0") + } + return nil +} + // nextBurstTimer returns a timer for the next burst. If no bursts remain, // returns a stopped timer (channel never fires). func nextBurstTimer(remaining int, window time.Duration) *time.Timer { diff --git a/apps/loadgen/cmd/start_test.go b/apps/loadgen/cmd/start_test.go new file mode 100644 index 0000000000..e0bea51b17 --- /dev/null +++ b/apps/loadgen/cmd/start_test.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestValidateStartConfig(t *testing.T) { + t.Run("valid", func(t *testing.T) { + err := validateStartConfig(startConfig{ + txPerDay: 1, + interval: time.Hour, + burstTxCount: 1, + burstPerDay: 0, + }) + require.NoError(t, err) + }) + + t.Run("invalid interval", func(t *testing.T) { + err := validateStartConfig(startConfig{interval: 0}) + require.ErrorContains(t, err, "interval must be > 0") + }) + + t.Run("invalid tx per day", func(t *testing.T) { + err := validateStartConfig(startConfig{interval: time.Hour, txPerDay: -1}) + require.ErrorContains(t, err, "tx-per-day must be >= 0") + }) + + t.Run("invalid burst tx count", func(t *testing.T) { + err := validateStartConfig(startConfig{interval: time.Hour, burstTxCount: -1}) + require.ErrorContains(t, err, "burst-tx-count must be >= 0") + }) + + t.Run("invalid burst per day", func(t *testing.T) { + err := validateStartConfig(startConfig{interval: time.Hour, burstPerDay: -1}) + require.ErrorContains(t, err, "burst-per-day must be >= 0") + }) +} diff --git a/apps/benchmarks/docker-compose.yml b/apps/loadgen/docker-compose.yml similarity index 87% rename from apps/benchmarks/docker-compose.yml rename to apps/loadgen/docker-compose.yml index a15b9e89d5..8967ae2a5c 100644 --- a/apps/benchmarks/docker-compose.yml +++ b/apps/loadgen/docker-compose.yml @@ -1,4 +1,4 @@ -name: "ev-benchmarks" +name: "ev-loadgen" services: spamoor-daemon: @@ -15,9 +15,9 @@ services: benchmarks: build: context: ../.. - dockerfile: apps/benchmarks/Dockerfile + dockerfile: apps/loadgen/Dockerfile command: - - ev-benchmarks + - ev-loadgen - start - --spamoor-url=http://spamoor-daemon:8080 - --tx-per-day=1000000 diff --git a/apps/benchmarks/go.mod b/apps/loadgen/go.mod similarity index 98% rename from apps/benchmarks/go.mod rename to apps/loadgen/go.mod index 6bad727248..a2547bcab8 100644 --- a/apps/benchmarks/go.mod +++ b/apps/loadgen/go.mod @@ -1,4 +1,4 @@ -module github.com/evstack/ev-node/apps/benchmarks +module github.com/evstack/ev-node/apps/loadgen go 1.25.8 @@ -6,6 +6,7 @@ require ( github.com/celestiaorg/tastora v0.20.0 github.com/prometheus/client_model v0.6.2 github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.11.1 ) require ( @@ -121,7 +122,6 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/viper v1.20.0 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect diff --git a/apps/benchmarks/go.sum b/apps/loadgen/go.sum similarity index 100% rename from apps/benchmarks/go.sum rename to apps/loadgen/go.sum diff --git a/apps/loadgen/internal/client.go b/apps/loadgen/internal/client.go new file mode 100644 index 0000000000..561c91d5c7 --- /dev/null +++ b/apps/loadgen/internal/client.go @@ -0,0 +1,45 @@ +package internal + +import ( + dto "github.com/prometheus/client_model/go" + + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" +) + +// SpamoorClient captures the subset of spamoor-daemon operations used by loadgen. +type SpamoorClient interface { + URL() string + ListSpammers() ([]spamoor.Spammer, error) + DeleteSpammer(id int) error + CreateSpammer(name, scenario string, config any, start bool) (int, error) + GetSpammer(id int) (*spamoor.Spammer, error) + GetMetrics() (map[string]*dto.MetricFamily, error) + GetClients() ([]spamoor.Client, error) +} + +type spamoorAPIClient struct { + api *spamoor.API +} + +// NewSpamoorClient creates a SpamoorClient backed by the real spamoor HTTP API. +func NewSpamoorClient(baseURL string) SpamoorClient { + return spamoorAPIClient{api: spamoor.NewAPI(baseURL)} +} + +func (c spamoorAPIClient) URL() string { return c.api.BaseURL } + +func (c spamoorAPIClient) ListSpammers() ([]spamoor.Spammer, error) { return c.api.ListSpammers() } + +func (c spamoorAPIClient) DeleteSpammer(id int) error { return c.api.DeleteSpammer(id) } + +func (c spamoorAPIClient) CreateSpammer(name, scenario string, config any, start bool) (int, error) { + return c.api.CreateSpammer(name, scenario, config, start) +} + +func (c spamoorAPIClient) GetSpammer(id int) (*spamoor.Spammer, error) { return c.api.GetSpammer(id) } + +func (c spamoorAPIClient) GetMetrics() (map[string]*dto.MetricFamily, error) { + return c.api.GetMetrics() +} + +func (c spamoorAPIClient) GetClients() ([]spamoor.Client, error) { return c.api.GetClients() } diff --git a/apps/benchmarks/internal/config.go b/apps/loadgen/internal/config.go similarity index 100% rename from apps/benchmarks/internal/config.go rename to apps/loadgen/internal/config.go diff --git a/apps/loadgen/internal/config_test.go b/apps/loadgen/internal/config_test.go new file mode 100644 index 0000000000..11a44e4f15 --- /dev/null +++ b/apps/loadgen/internal/config_test.go @@ -0,0 +1,36 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSpamoorURL(t *testing.T) { + t.Run("default", func(t *testing.T) { + t.Setenv("BENCH_SPAMOOR_URL", "") + require.Equal(t, DefaultSpamoorURL, SpamoorURL()) + }) + + t.Run("env override", func(t *testing.T) { + t.Setenv("BENCH_SPAMOOR_URL", "http://localhost:9999") + require.Equal(t, "http://localhost:9999", SpamoorURL()) + }) +} + +func TestBuildScenarioConfig(t *testing.T) { + cfg := BuildScenarioConfig(map[string]string{ + "BENCH_COUNT_PER_SPAMMER": "42", + "BENCH_THROUGHPUT": "100", + "BENCH_REBROADCAST": "true", + "BENCH_BASE_FEE": "500", + }) + + require.Equal(t, "500000000000000000000", cfg["refill_amount"]) + require.Equal(t, "200000000000000000000", cfg["refill_balance"]) + require.Equal(t, 300, cfg["refill_interval"]) + require.Equal(t, 42, cfg["total_count"]) + require.Equal(t, 100, cfg["throughput"]) + require.Equal(t, 500, cfg["base_fee"]) + require.Equal(t, "true", cfg["rebroadcast"]) +} diff --git a/apps/benchmarks/internal/matrix.go b/apps/loadgen/internal/matrix.go similarity index 100% rename from apps/benchmarks/internal/matrix.go rename to apps/loadgen/internal/matrix.go diff --git a/apps/loadgen/internal/matrix_test.go b/apps/loadgen/internal/matrix_test.go new file mode 100644 index 0000000000..772970d30f --- /dev/null +++ b/apps/loadgen/internal/matrix_test.go @@ -0,0 +1,120 @@ +package internal + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadMatrix(t *testing.T) { + t.Run("success", func(t *testing.T) { + path := writeMatrixFile(t, `{ + "entries": [ + { + "test_name": "EOA", + "scenario": "eoatx", + "timeout": "2m", + "probability": 0.5, + "env": { + "BENCH_NUM_SPAMMERS": "2", + "BENCH_COUNT_PER_SPAMMER": "7" + } + } + ] + }`) + + matrix, err := LoadMatrix(path) + require.NoError(t, err) + require.Len(t, matrix.Entries, 1) + require.Equal(t, 2, matrix.Entries[0].NumSpammers) + require.Equal(t, 7, matrix.Entries[0].CountPerSpammer) + }) + + t.Run("empty entries", func(t *testing.T) { + path := writeMatrixFile(t, `{"entries":[]}`) + _, err := LoadMatrix(path) + require.ErrorContains(t, err, "matrix has no entries") + }) + + t.Run("invalid json", func(t *testing.T) { + path := writeMatrixFile(t, `{`) + _, err := LoadMatrix(path) + require.ErrorContains(t, err, "parse matrix JSON") + }) + + t.Run("invalid scenario", func(t *testing.T) { + path := writeMatrixFile(t, `{ + "entries": [ + { + "test_name": "Bad", + "scenario": "unknown", + "env": { + "BENCH_COUNT_PER_SPAMMER": "1" + } + } + ] + }`) + _, err := LoadMatrix(path) + require.ErrorContains(t, err, `unknown scenario "unknown"`) + }) + + t.Run("invalid spammer count", func(t *testing.T) { + path := writeMatrixFile(t, `{ + "entries": [ + { + "test_name": "Bad", + "scenario": "eoatx", + "env": { + "BENCH_NUM_SPAMMERS": "0", + "BENCH_COUNT_PER_SPAMMER": "1" + } + } + ] + }`) + _, err := LoadMatrix(path) + require.ErrorContains(t, err, "BENCH_NUM_SPAMMERS must be > 0") + }) + + t.Run("invalid tx count", func(t *testing.T) { + path := writeMatrixFile(t, `{ + "entries": [ + { + "test_name": "Bad", + "scenario": "eoatx", + "env": { + "BENCH_COUNT_PER_SPAMMER": "0" + } + } + ] + }`) + _, err := LoadMatrix(path) + require.ErrorContains(t, err, "BENCH_COUNT_PER_SPAMMER must be > 0") + }) + + t.Run("invalid probability", func(t *testing.T) { + path := writeMatrixFile(t, `{ + "entries": [ + { + "test_name": "Bad", + "scenario": "eoatx", + "probability": 1.5, + "env": { + "BENCH_COUNT_PER_SPAMMER": "1" + } + } + ] + }`) + _, err := LoadMatrix(path) + require.ErrorContains(t, err, "probability must be between 0.0 and 1.0") + }) +} + +func writeMatrixFile(t *testing.T, contents string) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "matrix.json") + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) + return path +} diff --git a/apps/benchmarks/internal/runner.go b/apps/loadgen/internal/runner.go similarity index 76% rename from apps/benchmarks/internal/runner.go rename to apps/loadgen/internal/runner.go index 2b43004511..deb81299b2 100644 --- a/apps/benchmarks/internal/runner.go +++ b/apps/loadgen/internal/runner.go @@ -2,11 +2,9 @@ package internal import ( "context" - "encoding/json" "fmt" "log" "math/rand/v2" - "net/http" "time" "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" @@ -22,7 +20,7 @@ type matrixOpts struct { // ExecuteMatrixWithOverrides runs a matrix with BENCH_COUNT_PER_SPAMMER // overridden to totalTxTarget / NumSpammers per entry. Skips probability // filtering and sync waiting (caller is responsible for sync). -func ExecuteMatrixWithOverrides(ctx context.Context, matrixPath string, api *spamoor.API, totalTxTarget int) error { +func ExecuteMatrixWithOverrides(ctx context.Context, matrixPath string, api SpamoorClient, totalTxTarget int) error { return executeMatrix(ctx, matrixPath, api, matrixOpts{ totalTxTarget: totalTxTarget, }) @@ -30,20 +28,20 @@ func ExecuteMatrixWithOverrides(ctx context.Context, matrixPath string, api *spa // ExecuteMatrix runs all entries in a matrix file as-is, with probability // filtering and an initial WaitForSync call. Used by the one-shot `run` command. -func ExecuteMatrix(ctx context.Context, matrixPath string, api *spamoor.API) error { +func ExecuteMatrix(ctx context.Context, matrixPath string, api SpamoorClient) error { return executeMatrix(ctx, matrixPath, api, matrixOpts{ allowProbability: true, waitForSync: true, }) } -func executeMatrix(ctx context.Context, matrixPath string, api *spamoor.API, opts matrixOpts) error { +func executeMatrix(ctx context.Context, matrixPath string, api SpamoorClient, opts matrixOpts) error { matrix, err := LoadMatrix(matrixPath) if err != nil { return fmt.Errorf("load matrix: %w", err) } - log.Printf("spamoor API: %s", api.BaseURL) + log.Printf("spamoor API: %s", api.URL()) if opts.totalTxTarget > 0 { log.Printf("loaded %d matrix entries from %s (totalTxTarget=%d)", len(matrix.Entries), matrixPath, opts.totalTxTarget) } else { @@ -110,8 +108,11 @@ func executeMatrix(ctx context.Context, matrixPath string, api *spamoor.API, opt // RunCheck verifies connectivity by sending a single eoatx through spamoor. func RunCheck(ctx context.Context, spamoorAddr string, timeout time.Duration) error { - api := spamoor.NewAPI(spamoorAddr) + return runCheck(ctx, NewSpamoorClient(spamoorAddr), timeout) +} +func runCheck(ctx context.Context, api SpamoorClient, timeout time.Duration) error { + spamoorAddr := api.URL() log.Printf("checking connectivity via %s", spamoorAddr) if _, err := api.ListSpammers(); err != nil { @@ -128,6 +129,11 @@ func RunCheck(ctx context.Context, spamoorAddr string, timeout time.Duration) er } defer func() { _ = DeleteAllSpammers(api) }() + baselineSent, baselineFailed, err := getSpamoorCounters(api) + if err != nil { + return fmt.Errorf("get baseline metrics: %w", err) + } + cfg := map[string]any{ "total_count": 1, "throughput": 1, @@ -149,7 +155,7 @@ func RunCheck(ctx context.Context, spamoorAddr string, timeout time.Duration) er checkCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - sent, failed, err := waitForSpamoorDone(checkCtx, api, 1) + sent, failed, err := waitForSpamoorDone(checkCtx, api, 1, baselineSent, baselineFailed) if err != nil { return fmt.Errorf("tx not confirmed: %w", err) } @@ -162,7 +168,13 @@ func RunCheck(ctx context.Context, spamoorAddr string, timeout time.Duration) er return nil } -func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { +type waitForDoneFunc func(context.Context, SpamoorClient, int, float64, float64) (float64, float64, error) + +func runEntry(ctx context.Context, api SpamoorClient, entry Entry) error { + return runEntryWithWait(ctx, api, entry, waitForSpamoorDone) +} + +func runEntryWithWait(ctx context.Context, api SpamoorClient, entry Entry, wait waitForDoneFunc) error { totalCount := entry.NumSpammers * entry.CountPerSpammer log.Printf("[%s] scenario=%s spammers=%d count_per=%d total=%d", @@ -177,6 +189,11 @@ func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { } }() + baselineSent, baselineFailed, err := getSpamoorCounters(api) + if err != nil { + return fmt.Errorf("get baseline metrics: %w", err) + } + scenarioCfg := BuildScenarioConfig(entry.Env) var spammerIDs []int @@ -201,7 +218,7 @@ func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { } start := time.Now() - sent, failed, err := waitForSpamoorDone(ctx, api, totalCount) + sent, failed, err := wait(ctx, api, totalCount, baselineSent, baselineFailed) elapsed := time.Since(start) if err != nil { @@ -217,7 +234,7 @@ func runEntry(ctx context.Context, api *spamoor.API, entry Entry) error { } // DeleteAllSpammers removes all active spammers from the spamoor daemon. -func DeleteAllSpammers(api *spamoor.API) error { +func DeleteAllSpammers(api SpamoorClient) error { existing, err := api.ListSpammers() if err != nil { return fmt.Errorf("list spammers: %w", err) @@ -230,8 +247,11 @@ func DeleteAllSpammers(api *spamoor.API) error { return nil } -func waitForSpamoorDone(ctx context.Context, api *spamoor.API, targetCount int) (sent, failed float64, err error) { - const pollInterval = 2 * time.Second +func waitForSpamoorDone(ctx context.Context, api SpamoorClient, targetCount int, baselineSent, baselineFailed float64) (sent, failed float64, err error) { + return waitForSpamoorDoneWithInterval(ctx, api, targetCount, baselineSent, baselineFailed, 2*time.Second) +} + +func waitForSpamoorDoneWithInterval(ctx context.Context, api SpamoorClient, targetCount int, baselineSent, baselineFailed float64, pollInterval time.Duration) (sent, failed float64, err error) { ticker := time.NewTicker(pollInterval) defer ticker.Stop() @@ -243,13 +263,20 @@ func waitForSpamoorDone(ctx context.Context, api *spamoor.API, targetCount int) case <-ctx.Done(): return sent, failed, fmt.Errorf("timed out waiting for %d txs (sent %.0f): %w", targetCount, sent, ctx.Err()) case <-ticker.C: - metrics, mErr := api.GetMetrics() + currentSent, currentFailed, mErr := getSpamoorCounters(api) if mErr != nil { log.Printf("warning: failed to get metrics: %v", mErr) continue } - sent = sumCounter(metrics["spamoor_transactions_sent_total"]) - failed = sumCounter(metrics["spamoor_transactions_failed_total"]) + + sent = currentSent - baselineSent + failed = currentFailed - baselineFailed + if sent < 0 { + sent = 0 + } + if failed < 0 { + failed = 0 + } delta := sent - prevSent rate := delta / pollInterval.Seconds() @@ -265,19 +292,27 @@ func waitForSpamoorDone(ctx context.Context, api *spamoor.API, targetCount int) } } -type spamoorClient struct { - BlockHeight uint64 `json:"block_height"` - Ready bool `json:"ready"` +func getSpamoorCounters(api SpamoorClient) (sent, failed float64, err error) { + metrics, err := api.GetMetrics() + if err != nil { + return 0, 0, err + } + return sumCounter(metrics["spamoor_transactions_sent_total"]), + sumCounter(metrics["spamoor_transactions_failed_total"]), + nil } // WaitForSync polls spamoor until its block processing rate stabilizes, // indicating it has caught up to the chain tip. Returns immediately if // ctx is cancelled. -func WaitForSync(ctx context.Context, api *spamoor.API) error { +func WaitForSync(ctx context.Context, api SpamoorClient) error { + return waitForSync(ctx, api, 3*time.Second) +} + +func waitForSync(ctx context.Context, api SpamoorClient, pollInterval time.Duration) error { log.Printf("waiting for spamoor to sync to chain tip...") const syncThreshold uint64 = 10 - const pollInterval = 3 * time.Second var lastHeight uint64 @@ -309,21 +344,15 @@ func WaitForSync(ctx context.Context, api *spamoor.API) error { } } -func getSpamoorHeight(api *spamoor.API) (uint64, error) { - resp, err := http.Get(fmt.Sprintf("%s/api/clients", api.BaseURL)) +func getSpamoorHeight(api SpamoorClient) (uint64, error) { + clients, err := api.GetClients() if err != nil { return 0, fmt.Errorf("get clients: %w", err) } - defer resp.Body.Close() - - var clients []spamoorClient - if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil { - return 0, fmt.Errorf("decode clients: %w", err) - } if len(clients) == 0 { return 0, fmt.Errorf("no RPC clients configured") } - return clients[0].BlockHeight, nil + return clients[0].Height, nil } func sumCounter(f *dto.MetricFamily) float64 { diff --git a/apps/loadgen/internal/runner_test.go b/apps/loadgen/internal/runner_test.go new file mode 100644 index 0000000000..bcf7dfa226 --- /dev/null +++ b/apps/loadgen/internal/runner_test.go @@ -0,0 +1,226 @@ +package internal + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" +) + +func TestRunEntryUsesBaselineCounters(t *testing.T) { + client := &fakeSpamoorClient{ + spammers: []spamoor.Spammer{{ID: 99}}, + createIDs: []int{11, 12}, + getSpammerByID: map[int]*spamoor.Spammer{ + 11: {ID: 11, Name: "bench-baseline-0", Status: 1}, + 12: {ID: 12, Name: "bench-baseline-1", Status: 1}, + }, + metricsSeq: []metricSnapshot{ + {sent: 100, failed: 7}, + }, + } + + var gotTarget int + var gotBaselineSent float64 + var gotBaselineFailed float64 + + err := runEntryWithWait(context.Background(), client, Entry{ + TestName: "baseline", + Scenario: spamoor.ScenarioEOATX, + Env: map[string]string{"BENCH_COUNT_PER_SPAMMER": "5"}, + NumSpammers: 2, + CountPerSpammer: 5, + }, func(ctx context.Context, api SpamoorClient, targetCount int, baselineSent, baselineFailed float64) (float64, float64, error) { + gotTarget = targetCount + gotBaselineSent = baselineSent + gotBaselineFailed = baselineFailed + return 10, 0, nil + }) + + require.NoError(t, err) + require.Equal(t, 10, gotTarget) + require.Equal(t, 100.0, gotBaselineSent) + require.Equal(t, 7.0, gotBaselineFailed) + require.Len(t, client.createCalls, 2) +} + +func TestRunEntryFailsWhenSpammerDoesNotStart(t *testing.T) { + client := &fakeSpamoorClient{ + createIDs: []int{11}, + getSpammerByID: map[int]*spamoor.Spammer{ + 11: {ID: 11, Name: "bench-fail-0", Status: 0}, + }, + metricsSeq: []metricSnapshot{ + {sent: 0, failed: 0}, + }, + } + + err := runEntryWithWait(context.Background(), client, Entry{ + TestName: "fail", + Scenario: spamoor.ScenarioEOATX, + Env: map[string]string{"BENCH_COUNT_PER_SPAMMER": "1"}, + NumSpammers: 1, + CountPerSpammer: 1, + }, func(ctx context.Context, api SpamoorClient, targetCount int, baselineSent, baselineFailed float64) (float64, float64, error) { + t.Fatal("wait function should not be called when spammer startup fails") + return 0, 0, nil + }) + + require.ErrorContains(t, err, "failed to start") +} + +func TestWaitForSpamoorDoneUsesDeltas(t *testing.T) { + client := &fakeSpamoorClient{ + metricsSeq: []metricSnapshot{ + {sent: 105, failed: 3}, + {sent: 108, failed: 4}, + }, + } + + sent, failed, err := waitForSpamoorDoneWithInterval(context.Background(), client, 8, 100, 2, time.Millisecond) + require.NoError(t, err) + require.Equal(t, 8.0, sent) + require.Equal(t, 2.0, failed) +} + +func TestWaitForSpamoorDoneHonorsContext(t *testing.T) { + client := &fakeSpamoorClient{ + metricsSeq: []metricSnapshot{ + {sent: 100, failed: 0}, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond) + defer cancel() + + _, _, err := waitForSpamoorDoneWithInterval(ctx, client, 2, 100, 0, 10*time.Millisecond) + require.ErrorContains(t, err, "timed out waiting for 2 txs") +} + +func TestWaitForSyncReturnsOnceHeightDeltaSettles(t *testing.T) { + client := &fakeSpamoorClient{ + clientsSeq: [][]spamoor.Client{ + {{Height: 100}}, + {{Height: 108}}, + }, + } + + err := waitForSync(context.Background(), client, time.Millisecond) + require.NoError(t, err) +} + +func TestWaitForSyncHonorsContext(t *testing.T) { + client := &fakeSpamoorClient{ + getClientsErr: errors.New("boom"), + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond) + defer cancel() + + err := waitForSync(ctx, client, 10*time.Millisecond) + require.ErrorContains(t, err, "cancelled waiting for sync") +} + +type metricSnapshot struct { + sent float64 + failed float64 +} + +type fakeSpamoorClient struct { + spammers []spamoor.Spammer + createIDs []int + createCalls []createCall + getSpammerByID map[int]*spamoor.Spammer + metricsSeq []metricSnapshot + metricsIndex int + clientsSeq [][]spamoor.Client + clientsIndex int + getClientsErr error +} + +type createCall struct { + name string + scenario string + config any + start bool +} + +func (f *fakeSpamoorClient) URL() string { return "http://spamoor.test" } + +func (f *fakeSpamoorClient) ListSpammers() ([]spamoor.Spammer, error) { + return append([]spamoor.Spammer(nil), f.spammers...), nil +} + +func (f *fakeSpamoorClient) DeleteSpammer(id int) error { + return nil +} + +func (f *fakeSpamoorClient) CreateSpammer(name, scenario string, config any, start bool) (int, error) { + f.createCalls = append(f.createCalls, createCall{name: name, scenario: scenario, config: config, start: start}) + if len(f.createIDs) == 0 { + return 0, fmt.Errorf("unexpected CreateSpammer call") + } + id := f.createIDs[0] + f.createIDs = f.createIDs[1:] + return id, nil +} + +func (f *fakeSpamoorClient) GetSpammer(id int) (*spamoor.Spammer, error) { + sp, ok := f.getSpammerByID[id] + if !ok { + return nil, fmt.Errorf("spammer %d not found", id) + } + return sp, nil +} + +func (f *fakeSpamoorClient) GetMetrics() (map[string]*dto.MetricFamily, error) { + snapshot := metricSnapshot{} + if len(f.metricsSeq) > 0 { + if f.metricsIndex >= len(f.metricsSeq) { + snapshot = f.metricsSeq[len(f.metricsSeq)-1] + } else { + snapshot = f.metricsSeq[f.metricsIndex] + f.metricsIndex++ + } + } + + return map[string]*dto.MetricFamily{ + "spamoor_transactions_sent_total": counterFamily("spamoor_transactions_sent_total", snapshot.sent), + "spamoor_transactions_failed_total": counterFamily("spamoor_transactions_failed_total", snapshot.failed), + }, nil +} + +func (f *fakeSpamoorClient) GetClients() ([]spamoor.Client, error) { + if f.getClientsErr != nil { + return nil, f.getClientsErr + } + if len(f.clientsSeq) == 0 { + return nil, nil + } + if f.clientsIndex >= len(f.clientsSeq) { + return f.clientsSeq[len(f.clientsSeq)-1], nil + } + clients := f.clientsSeq[f.clientsIndex] + f.clientsIndex++ + return clients, nil +} + +func counterFamily(name string, value float64) *dto.MetricFamily { + counterType := dto.MetricType_COUNTER + return &dto.MetricFamily{ + Name: &name, + Type: &counterType, + Metric: []*dto.Metric{ + { + Counter: &dto.Counter{ + Value: &value, + }, + }, + }, + } +} diff --git a/apps/benchmarks/main.go b/apps/loadgen/main.go similarity index 68% rename from apps/benchmarks/main.go rename to apps/loadgen/main.go index f0f95ec4ba..f1588ab027 100644 --- a/apps/benchmarks/main.go +++ b/apps/loadgen/main.go @@ -5,7 +5,7 @@ import ( "log" "os" - "github.com/evstack/ev-node/apps/benchmarks/cmd" + "github.com/evstack/ev-node/apps/loadgen/cmd" ) func main() { @@ -17,5 +17,5 @@ func main() { func init() { log.SetFlags(log.Ldate | log.Ltime | log.LUTC) log.SetOutput(os.Stdout) - fmt.Fprintln(os.Stderr, "ev-benchmarks starting") + fmt.Fprintln(os.Stderr, "ev-loadgen starting") } diff --git a/apps/benchmarks/matrices/baseline.json b/apps/loadgen/matrices/baseline.json similarity index 100% rename from apps/benchmarks/matrices/baseline.json rename to apps/loadgen/matrices/baseline.json diff --git a/apps/benchmarks/matrices/burst.json b/apps/loadgen/matrices/burst.json similarity index 100% rename from apps/benchmarks/matrices/burst.json rename to apps/loadgen/matrices/burst.json From 5d5fce0de07fa0249a56651a756bc8f8ba3ed5c4 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 2 Jun 2026 10:45:07 +0100 Subject: [PATCH 10/15] chore: simplify ci, use struct instead of raw json in tests --- .github/workflows/ci.yml | 51 ++----- .gitignore | 2 + .just/bench.just | 57 -------- apps/evm/go.mod | 2 +- apps/evm/go.sum | 4 +- apps/grpc/go.mod | 2 +- apps/grpc/go.sum | 4 +- apps/loadgen/README.md | 8 -- apps/loadgen/cmd/run.go | 2 +- apps/loadgen/cmd/start.go | 2 +- apps/loadgen/internal/matrix.go | 13 +- apps/loadgen/internal/matrix_test.go | 197 +++++++++++++++------------ apps/loadgen/internal/runner.go | 32 ++++- apps/testapp/go.mod | 2 +- apps/testapp/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- 17 files changed, 172 insertions(+), 216 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 667b54e874..e060b2bea6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ jobs: pull-requests: read outputs: code: ${{ steps.filter.outputs.code }} - loadgen: ${{ steps.filter.outputs.loadgen }} steps: - uses: actions/checkout@v6.0.2 - uses: dorny/paths-filter@v4 @@ -29,12 +28,6 @@ jobs: - '!docs/**' - '!**/*.md' - '!.github/workflows/docs_*.yml' - loadgen: - - 'apps/loadgen/**' - - '.just/bench.just' - - '.just/build.just' - - '.github/workflows/ci.yml' - - '.github/workflows/docker-build-push.yml' determine-image-tag: name: Determine Image Tag @@ -67,40 +60,6 @@ jobs: echo "tag=$TAG" >> $GITHUB_OUTPUT - determine-docker-apps: - name: Determine Docker Apps - needs: changes - if: needs.changes.outputs.code == 'true' - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - apps: ${{ steps.set-apps.outputs.apps }} - steps: - - name: Set docker app matrix - id: set-apps - run: | - APPS='[ - {"name": "ev-node-evm", "dockerfile": "apps/evm/Dockerfile"}, - {"name": "ev-node-grpc", "dockerfile": "apps/grpc/Dockerfile"}, - {"name": "ev-node-testapp", "dockerfile": "apps/testapp/Dockerfile"} - ]' - - if [ "${{ github.event_name }}" = "push" ] || [ "${{ needs.changes.outputs.loadgen }}" = "true" ]; then - APPS='[ - {"name": "ev-node-evm", "dockerfile": "apps/evm/Dockerfile"}, - {"name": "ev-node-grpc", "dockerfile": "apps/grpc/Dockerfile"}, - {"name": "ev-node-loadgen", "dockerfile": "apps/loadgen/Dockerfile"}, - {"name": "ev-node-testapp", "dockerfile": "apps/testapp/Dockerfile"} - ]' - fi - - { - echo "apps<> "$GITHUB_OUTPUT" - lint: needs: changes if: needs.changes.outputs.code == 'true' @@ -109,7 +68,7 @@ jobs: uses: ./.github/workflows/lint.yml docker: - needs: [determine-image-tag, determine-docker-apps, changes] + needs: [determine-image-tag, changes] if: needs.changes.outputs.code == 'true' uses: ./.github/workflows/docker-build-push.yml secrets: inherit @@ -118,7 +77,13 @@ jobs: packages: write with: image-tag: ${{ needs.determine-image-tag.outputs.tag }} - apps: ${{ needs.determine-docker-apps.outputs.apps }} + apps: | + [ + {"name": "ev-node-evm", "dockerfile": "apps/evm/Dockerfile"}, + {"name": "ev-node-grpc", "dockerfile": "apps/grpc/Dockerfile"}, + {"name": "ev-node-loadgen", "dockerfile": "apps/loadgen/Dockerfile"}, + {"name": "ev-node-testapp", "dockerfile": "apps/testapp/Dockerfile"} + ] test: needs: changes diff --git a/.gitignore b/.gitignore index 2904657ceb..99a635e276 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,11 @@ docs/.vitepress/cache *.log *.tgz .idea +.junie .temp .vite_opt_cache .vscode .gocache .gomodcache /.cache +*.diff diff --git a/.just/bench.just b/.just/bench.just index 6195b5a408..fe1060b97b 100644 --- a/.just/bench.just +++ b/.just/bench.just @@ -12,60 +12,3 @@ docker-build-benchmarks: @echo "--> Building ev-loadgen Docker image" @docker build -f apps/loadgen/Dockerfile -t ev-loadgen:dev . @echo "--> Docker image built: ev-loadgen:dev" - -# Smoke test: start spamoor + benchmarks, verify txs are submitted, then tear down. -# Requires BENCH_PRIVATE_KEY and BENCH_ETH_RPC_URL env vars. -[group('bench')] -bench-smoke: - #!/usr/bin/env bash - set -euo pipefail - - : "${BENCH_PRIVATE_KEY:?Set BENCH_PRIVATE_KEY}" - : "${BENCH_ETH_RPC_URL:?Set BENCH_ETH_RPC_URL}" - - export BENCH_PRIVATE_KEY BENCH_ETH_RPC_URL - - echo "--> Building ev-loadgen binary" - (cd apps/loadgen && go build -o {{ build_dir }}/ev-loadgen .) - - echo "--> Starting spamoor-daemon via docker compose" - docker compose -f apps/loadgen/docker-compose.yml up -d spamoor-daemon - trap 'echo "--> Tearing down"; docker compose -f apps/loadgen/docker-compose.yml down' EXIT - - echo "--> Waiting for spamoor-daemon to be healthy" - for i in $(seq 1 30); do - if curl -sf http://localhost:8080/metrics > /dev/null 2>&1; then - echo " spamoor-daemon ready" - break - fi - if [ "$i" -eq 30 ]; then - echo "ERROR: spamoor-daemon did not become healthy" - exit 1 - fi - sleep 1 - done - - echo "--> Running smoke matrix (small tx count)" - cat > /tmp/bench-smoke.json <<'EOF' - { - "entries": [ - { - "test_name": "SmokeEOA", - "scenario": "eoatx", - "timeout": "5m", - "env": { - "BENCH_NUM_SPAMMERS": "1", - "BENCH_COUNT_PER_SPAMMER": "10", - "BENCH_THROUGHPUT": "10", - "BENCH_MAX_PENDING": "100", - "BENCH_MAX_WALLETS": "10", - "BENCH_BASE_FEE": "500", - "BENCH_TIP_FEE": "50" - } - } - ] - } - EOF - - {{ build_dir }}/ev-loadgen run --spamoor-url=http://localhost:8080 /tmp/bench-smoke.json - echo "--> Smoke test passed: transactions submitted successfully" diff --git a/apps/evm/go.mod b/apps/evm/go.mod index 79ceff132c..8083500ff1 100644 --- a/apps/evm/go.mod +++ b/apps/evm/go.mod @@ -118,7 +118,7 @@ require ( github.com/ipld/go-ipld-prime v0.23.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect diff --git a/apps/evm/go.sum b/apps/evm/go.sum index 06dcdaa2fc..1b66467f34 100644 --- a/apps/evm/go.sum +++ b/apps/evm/go.sum @@ -480,8 +480,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/apps/grpc/go.mod b/apps/grpc/go.mod index e0b54cab06..45f1eed908 100644 --- a/apps/grpc/go.mod +++ b/apps/grpc/go.mod @@ -100,7 +100,7 @@ require ( github.com/ipld/go-ipld-prime v0.23.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect diff --git a/apps/grpc/go.sum b/apps/grpc/go.sum index 1aa14866c1..bbec9624a3 100644 --- a/apps/grpc/go.sum +++ b/apps/grpc/go.sum @@ -413,8 +413,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/apps/loadgen/README.md b/apps/loadgen/README.md index 7633d16bae..5157b9e0e4 100644 --- a/apps/loadgen/README.md +++ b/apps/loadgen/README.md @@ -81,14 +81,6 @@ export BENCH_ETH_RPC_URL=http://:8545 docker compose -f apps/loadgen/docker-compose.yml up ``` -### Smoke Test - -```sh -export BENCH_PRIVATE_KEY= -export BENCH_ETH_RPC_URL=http://:8545 -just bench-smoke -``` - ## Matrix Format Each entry specifies a spamoor scenario, tx counts, and optional probability: diff --git a/apps/loadgen/cmd/run.go b/apps/loadgen/cmd/run.go index d1436d609c..827b040337 100644 --- a/apps/loadgen/cmd/run.go +++ b/apps/loadgen/cmd/run.go @@ -11,7 +11,7 @@ func newRunCmd() *cobra.Command { Short: "run benchmarks from a matrix JSON file", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return internal.ExecuteMatrix(cmd.Context(), args[0], internal.NewSpamoorClient(resolveSpamoorURL())) + return internal.ExecuteMatrixFromFile(cmd.Context(), args[0], internal.NewSpamoorClient(resolveSpamoorURL())) }, } } diff --git a/apps/loadgen/cmd/start.go b/apps/loadgen/cmd/start.go index 04675e4a85..523808c128 100644 --- a/apps/loadgen/cmd/start.go +++ b/apps/loadgen/cmd/start.go @@ -80,7 +80,7 @@ func runScheduler(parent context.Context, cfg startConfig) error { mu.Lock() defer mu.Unlock() log.Printf("==> %s workload starting (%d tx)", label, txCount) - if err := internal.ExecuteMatrixWithOverrides(ctx, matrixPath, api, txCount); err != nil { + if err := internal.ExecuteMatrixWithOverridesFromFile(ctx, matrixPath, api, txCount); err != nil { log.Printf("%s workload error: %v", label, err) } } diff --git a/apps/loadgen/internal/matrix.go b/apps/loadgen/internal/matrix.go index 5e9841305f..eb15d33e81 100644 --- a/apps/loadgen/internal/matrix.go +++ b/apps/loadgen/internal/matrix.go @@ -58,15 +58,22 @@ func LoadMatrix(path string) (*Matrix, error) { if err := json.Unmarshal(data, &m); err != nil { return nil, fmt.Errorf("parse matrix JSON: %w", err) } + if err := validateMatrix(&m); err != nil { + return nil, err + } + return &m, nil +} + +func validateMatrix(m *Matrix) error { if len(m.Entries) == 0 { - return nil, fmt.Errorf("matrix has no entries") + return fmt.Errorf("matrix has no entries") } for i := range m.Entries { if err := m.Entries[i].validate(); err != nil { - return nil, fmt.Errorf("entry %d (%s): %w", i, m.Entries[i].TestName, err) + return fmt.Errorf("entry %d (%s): %w", i, m.Entries[i].TestName, err) } } - return &m, nil + return nil } func (e *Entry) validate() error { diff --git a/apps/loadgen/internal/matrix_test.go b/apps/loadgen/internal/matrix_test.go index 772970d30f..076a944ca0 100644 --- a/apps/loadgen/internal/matrix_test.go +++ b/apps/loadgen/internal/matrix_test.go @@ -1,120 +1,149 @@ package internal import ( + "encoding/json" "os" "path/filepath" "testing" + "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" "github.com/stretchr/testify/require" ) -func TestLoadMatrix(t *testing.T) { +func TestValidateMatrix(t *testing.T) { t.Run("success", func(t *testing.T) { - path := writeMatrixFile(t, `{ - "entries": [ - { - "test_name": "EOA", - "scenario": "eoatx", - "timeout": "2m", - "probability": 0.5, - "env": { - "BENCH_NUM_SPAMMERS": "2", - "BENCH_COUNT_PER_SPAMMER": "7" - } - } - ] - }`) - - matrix, err := LoadMatrix(path) - require.NoError(t, err) - require.Len(t, matrix.Entries, 1) - require.Equal(t, 2, matrix.Entries[0].NumSpammers) - require.Equal(t, 7, matrix.Entries[0].CountPerSpammer) + m := Matrix{ + Entries: []Entry{ + { + TestName: "EOA", + Scenario: spamoor.ScenarioEOATX, + Timeout: "2m", + Probability: func() *float64 { + v := 0.5 + return &v + }(), + Env: map[string]string{ + "BENCH_NUM_SPAMMERS": "2", + "BENCH_COUNT_PER_SPAMMER": "7", + }, + }, + }, + } + + require.NoError(t, validateMatrix(&m)) + require.Equal(t, 2, m.Entries[0].NumSpammers) + require.Equal(t, 7, m.Entries[0].CountPerSpammer) }) t.Run("empty entries", func(t *testing.T) { - path := writeMatrixFile(t, `{"entries":[]}`) - _, err := LoadMatrix(path) + err := validateMatrix(&Matrix{}) require.ErrorContains(t, err, "matrix has no entries") }) - t.Run("invalid json", func(t *testing.T) { - path := writeMatrixFile(t, `{`) - _, err := LoadMatrix(path) - require.ErrorContains(t, err, "parse matrix JSON") - }) - t.Run("invalid scenario", func(t *testing.T) { - path := writeMatrixFile(t, `{ - "entries": [ - { - "test_name": "Bad", - "scenario": "unknown", - "env": { - "BENCH_COUNT_PER_SPAMMER": "1" - } - } - ] - }`) - _, err := LoadMatrix(path) + m := Matrix{ + Entries: []Entry{ + { + TestName: "Bad", + Scenario: "unknown", + Env: map[string]string{ + "BENCH_COUNT_PER_SPAMMER": "1", + }, + }, + }, + } + + err := validateMatrix(&m) require.ErrorContains(t, err, `unknown scenario "unknown"`) }) t.Run("invalid spammer count", func(t *testing.T) { - path := writeMatrixFile(t, `{ - "entries": [ - { - "test_name": "Bad", - "scenario": "eoatx", - "env": { - "BENCH_NUM_SPAMMERS": "0", - "BENCH_COUNT_PER_SPAMMER": "1" - } - } - ] - }`) - _, err := LoadMatrix(path) + m := Matrix{ + Entries: []Entry{ + { + TestName: "Bad", + Scenario: spamoor.ScenarioEOATX, + Env: map[string]string{ + "BENCH_NUM_SPAMMERS": "0", + "BENCH_COUNT_PER_SPAMMER": "1", + }, + }, + }, + } + + err := validateMatrix(&m) require.ErrorContains(t, err, "BENCH_NUM_SPAMMERS must be > 0") }) t.Run("invalid tx count", func(t *testing.T) { - path := writeMatrixFile(t, `{ - "entries": [ - { - "test_name": "Bad", - "scenario": "eoatx", - "env": { - "BENCH_COUNT_PER_SPAMMER": "0" - } - } - ] - }`) - _, err := LoadMatrix(path) + m := Matrix{ + Entries: []Entry{ + { + TestName: "Bad", + Scenario: spamoor.ScenarioEOATX, + Env: map[string]string{ + "BENCH_COUNT_PER_SPAMMER": "0", + }, + }, + }, + } + + err := validateMatrix(&m) require.ErrorContains(t, err, "BENCH_COUNT_PER_SPAMMER must be > 0") }) t.Run("invalid probability", func(t *testing.T) { - path := writeMatrixFile(t, `{ - "entries": [ - { - "test_name": "Bad", - "scenario": "eoatx", - "probability": 1.5, - "env": { - "BENCH_COUNT_PER_SPAMMER": "1" - } - } - ] - }`) - _, err := LoadMatrix(path) + v := 1.5 + m := Matrix{ + Entries: []Entry{ + { + TestName: "Bad", + Scenario: spamoor.ScenarioEOATX, + Probability: &v, + Env: map[string]string{ + "BENCH_COUNT_PER_SPAMMER": "1", + }, + }, + }, + } + + err := validateMatrix(&m) require.ErrorContains(t, err, "probability must be between 0.0 and 1.0") }) } -func writeMatrixFile(t *testing.T, contents string) string { - t.Helper() +func TestLoadMatrix(t *testing.T) { + t.Run("success", func(t *testing.T) { + m := Matrix{ + Entries: []Entry{ + { + TestName: "EOA", + Scenario: spamoor.ScenarioEOATX, + Env: map[string]string{ + "BENCH_NUM_SPAMMERS": "2", + "BENCH_COUNT_PER_SPAMMER": "7", + }, + }, + }, + } + data, err := json.Marshal(m) + require.NoError(t, err) + + path := filepath.Join(t.TempDir(), "matrix.json") + require.NoError(t, os.WriteFile(path, data, 0o600)) + + loaded, err := LoadMatrix(path) + require.NoError(t, err) + require.Equal(t, m.Entries[0].TestName, loaded.Entries[0].TestName) + require.Equal(t, 2, loaded.Entries[0].NumSpammers) + require.Equal(t, 7, loaded.Entries[0].CountPerSpammer) + }) + + t.Run("invalid json", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "invalid.json") + require.NoError(t, os.WriteFile(path, []byte(`{`), 0o600)) - path := filepath.Join(t.TempDir(), "matrix.json") - require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) - return path + _, err := LoadMatrix(path) + require.ErrorContains(t, err, "parse matrix JSON") + }) } diff --git a/apps/loadgen/internal/runner.go b/apps/loadgen/internal/runner.go index deb81299b2..bfada51018 100644 --- a/apps/loadgen/internal/runner.go +++ b/apps/loadgen/internal/runner.go @@ -20,32 +20,50 @@ type matrixOpts struct { // ExecuteMatrixWithOverrides runs a matrix with BENCH_COUNT_PER_SPAMMER // overridden to totalTxTarget / NumSpammers per entry. Skips probability // filtering and sync waiting (caller is responsible for sync). -func ExecuteMatrixWithOverrides(ctx context.Context, matrixPath string, api SpamoorClient, totalTxTarget int) error { - return executeMatrix(ctx, matrixPath, api, matrixOpts{ +func ExecuteMatrixWithOverrides(ctx context.Context, matrix Matrix, api SpamoorClient, totalTxTarget int) error { + return executeMatrix(ctx, matrix, api, matrixOpts{ totalTxTarget: totalTxTarget, }) } // ExecuteMatrix runs all entries in a matrix file as-is, with probability // filtering and an initial WaitForSync call. Used by the one-shot `run` command. -func ExecuteMatrix(ctx context.Context, matrixPath string, api SpamoorClient) error { - return executeMatrix(ctx, matrixPath, api, matrixOpts{ +func ExecuteMatrix(ctx context.Context, matrix Matrix, api SpamoorClient) error { + return executeMatrix(ctx, matrix, api, matrixOpts{ allowProbability: true, waitForSync: true, }) } -func executeMatrix(ctx context.Context, matrixPath string, api SpamoorClient, opts matrixOpts) error { +// ExecuteMatrixFromFile loads a matrix from disk and executes it. +func ExecuteMatrixFromFile(ctx context.Context, matrixPath string, api SpamoorClient) error { matrix, err := LoadMatrix(matrixPath) if err != nil { return fmt.Errorf("load matrix: %w", err) } + return ExecuteMatrix(ctx, *matrix, api) +} + +// ExecuteMatrixWithOverridesFromFile loads a matrix from disk and executes it +// with per-run counts overridden. +func ExecuteMatrixWithOverridesFromFile(ctx context.Context, matrixPath string, api SpamoorClient, totalTxTarget int) error { + matrix, err := LoadMatrix(matrixPath) + if err != nil { + return fmt.Errorf("load matrix: %w", err) + } + return ExecuteMatrixWithOverrides(ctx, *matrix, api, totalTxTarget) +} + +func executeMatrix(ctx context.Context, matrix Matrix, api SpamoorClient, opts matrixOpts) error { + if err := validateMatrix(&matrix); err != nil { + return err + } log.Printf("spamoor API: %s", api.URL()) if opts.totalTxTarget > 0 { - log.Printf("loaded %d matrix entries from %s (totalTxTarget=%d)", len(matrix.Entries), matrixPath, opts.totalTxTarget) + log.Printf("loaded %d matrix entries (totalTxTarget=%d)", len(matrix.Entries), opts.totalTxTarget) } else { - log.Printf("loaded %d matrix entries from %s", len(matrix.Entries), matrixPath) + log.Printf("loaded %d matrix entries", len(matrix.Entries)) } if opts.waitForSync { diff --git a/apps/testapp/go.mod b/apps/testapp/go.mod index e429abf388..cff7f7fa54 100644 --- a/apps/testapp/go.mod +++ b/apps/testapp/go.mod @@ -97,7 +97,7 @@ require ( github.com/ipld/go-ipld-prime v0.23.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect diff --git a/apps/testapp/go.sum b/apps/testapp/go.sum index 1aa14866c1..bbec9624a3 100644 --- a/apps/testapp/go.sum +++ b/apps/testapp/go.sum @@ -413,8 +413,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/go.mod b/go.mod index 779acfcf8c..74762a1644 100644 --- a/go.mod +++ b/go.mod @@ -117,7 +117,7 @@ require ( github.com/ipld/go-ipld-prime v0.23.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/koron/go-ssdp v0.0.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect diff --git a/go.sum b/go.sum index 1aa14866c1..bbec9624a3 100644 --- a/go.sum +++ b/go.sum @@ -413,8 +413,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= From 8188e913690664c3e956396a5c6c2e8bfd8df906 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 2 Jun 2026 10:50:22 +0100 Subject: [PATCH 11/15] chore: rename bench -> loadgen --- .just/{bench.just => loadgen.just} | 8 ++++---- apps/loadgen/README.md | 10 +++++----- justfile | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) rename .just/{bench.just => loadgen.just} (84%) diff --git a/.just/bench.just b/.just/loadgen.just similarity index 84% rename from .just/bench.just rename to .just/loadgen.just index fe1060b97b..80a202ee3c 100644 --- a/.just/bench.just +++ b/.just/loadgen.just @@ -1,14 +1,14 @@ # Build ev-loadgen binary -[group('bench')] -build-benchmarks: +[group('loadgen')] +build-loadgen: @echo "--> Building ev-loadgen" @mkdir -p {{ build_dir }} @cd apps/loadgen && go build -o {{ build_dir }}/ev-loadgen . @echo " Check the binary with: {{ build_dir }}/ev-loadgen" # Build ev-loadgen Docker image -[group('bench')] -docker-build-benchmarks: +[group('loadgen')] +docker-build-loadgen: @echo "--> Building ev-loadgen Docker image" @docker build -f apps/loadgen/Dockerfile -t ev-loadgen:dev . @echo "--> Docker image built: ev-loadgen:dev" diff --git a/apps/loadgen/README.md b/apps/loadgen/README.md index 5157b9e0e4..67d760fb0f 100644 --- a/apps/loadgen/README.md +++ b/apps/loadgen/README.md @@ -1,4 +1,4 @@ -# benchmarks +# loadgen Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon](https://github.com/ethpandaops/spamoor) sidecar via HTTP API. Runs an in-process scheduler with configurable regular and burst workloads. @@ -53,7 +53,7 @@ docker run -d --name spamoor -p 8080:8080 \ --port=8080 --startup-delay=0 ``` -### 2. Run benchmarks +### 2. Run loadgen ```sh # build @@ -73,7 +73,7 @@ cd apps/loadgen && go build -o ev-loadgen . ### Docker Compose -Spins up both spamoor-daemon and benchmarks together: +Spins up both spamoor-daemon and loadgen together: ```sh export BENCH_PRIVATE_KEY= @@ -124,6 +124,6 @@ cd apps/loadgen && go build -o ev-loadgen . docker build -f apps/loadgen/Dockerfile -t ev-loadgen:dev . # via just -just build-benchmarks -just docker-build-benchmarks +just build-loadgen +just docker-build-loadgen ``` diff --git a/justfile b/justfile index 6636b213e5..ead942d5bd 100644 --- a/justfile +++ b/justfile @@ -18,7 +18,7 @@ import '.just/lint.just' import '.just/codegen.just' import '.just/run.just' import '.just/tools.just' -import '.just/bench.just' +import '.just/loadgen.just' # List available recipes when running `just` with no args default: From 1ece2735456be2d205b247cbae69dfb000183fc4 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 2 Jun 2026 11:36:13 +0100 Subject: [PATCH 12/15] chore: use correct block height --- apps/loadgen/cmd/flags_test.go | 31 +++++++++++++++++++ apps/loadgen/internal/client.go | 55 +++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 apps/loadgen/cmd/flags_test.go diff --git a/apps/loadgen/cmd/flags_test.go b/apps/loadgen/cmd/flags_test.go new file mode 100644 index 0000000000..18cf11f8e1 --- /dev/null +++ b/apps/loadgen/cmd/flags_test.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStartFlags(t *testing.T) { + root := NewRootCmd() + startCmd := newStartCmd() + root.AddCommand(startCmd) + + err := startCmd.ParseFlags([]string{"--regular-matrix", "custom.json"}) + require.NoError(t, err) + + // Since we can't easily access the cfg inside newStartCmd's closure from here + // without refactoring, we'll check if the flag is registered correctly. + flag := startCmd.Flags().Lookup("regular-matrix") + require.NotNil(t, flag) + require.Equal(t, "custom.json", flag.Value.String()) +} + +func TestRunArgs(t *testing.T) { + runCmd := newRunCmd() + err := runCmd.Args(runCmd, []string{"matrix.json"}) + require.NoError(t, err) + + err = runCmd.Args(runCmd, []string{}) + require.Error(t, err) +} diff --git a/apps/loadgen/internal/client.go b/apps/loadgen/internal/client.go index 561c91d5c7..0b3606bc5a 100644 --- a/apps/loadgen/internal/client.go +++ b/apps/loadgen/internal/client.go @@ -1,6 +1,12 @@ package internal import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" + dto "github.com/prometheus/client_model/go" "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" @@ -18,12 +24,16 @@ type SpamoorClient interface { } type spamoorAPIClient struct { - api *spamoor.API + api *spamoor.API + client *http.Client } // NewSpamoorClient creates a SpamoorClient backed by the real spamoor HTTP API. func NewSpamoorClient(baseURL string) SpamoorClient { - return spamoorAPIClient{api: spamoor.NewAPI(baseURL)} + return spamoorAPIClient{ + api: spamoor.NewAPI(baseURL), + client: &http.Client{Timeout: 2 * time.Second}, + } } func (c spamoorAPIClient) URL() string { return c.api.BaseURL } @@ -42,4 +52,43 @@ func (c spamoorAPIClient) GetMetrics() (map[string]*dto.MetricFamily, error) { return c.api.GetMetrics() } -func (c spamoorAPIClient) GetClients() ([]spamoor.Client, error) { return c.api.GetClients() } +// clientResponse matches the actual spamoor daemon JSON response where +// the block height field is "block_height". +type clientResponse struct { + Index int `json:"index"` + Name string `json:"name"` + URL string `json:"url"` + Groups []string `json:"groups"` + Enabled bool `json:"enabled"` + BlockHeight uint64 `json:"block_height"` +} + +// GetClients fetches clients from spamoor, correctly mapping "block_height" to Height. +func (c spamoorAPIClient) GetClients() ([]spamoor.Client, error) { + url := fmt.Sprintf("%s/api/clients", c.api.BaseURL) + resp, err := c.client.Get(url) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("get clients failed: %s", string(body)) + } + var raw []clientResponse + if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { + return nil, err + } + clients := make([]spamoor.Client, len(raw)) + for i, r := range raw { + clients[i] = spamoor.Client{ + Index: r.Index, + Name: r.Name, + URL: r.URL, + Groups: r.Groups, + Enabled: r.Enabled, + Height: r.BlockHeight, + } + } + return clients, nil +} From 7ad001859c133af09ad5582589a83ce6f3c3a2fd Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 2 Jun 2026 13:33:16 +0100 Subject: [PATCH 13/15] fix(loadgen): address PR review feedback - run container as non-root user - use ParseUint for numeric env values in scenario config - add language identifiers to fenced code blocks (MD040) - wrap bare errors with context in client.go - remove redundant root cmd in flags_test.go - validate timeout duration during matrix validation - remove startup banner from init() Co-Authored-By: Claude Opus 4.6 --- apps/loadgen/Dockerfile | 10 +++++++--- apps/loadgen/README.md | 8 ++++---- apps/loadgen/cmd/flags_test.go | 2 -- apps/loadgen/cmd/start.go | 4 ++-- apps/loadgen/internal/client.go | 4 ++-- apps/loadgen/internal/config.go | 2 +- apps/loadgen/internal/config_test.go | 6 +++--- apps/loadgen/internal/matrix.go | 7 +++++++ apps/loadgen/main.go | 2 -- 9 files changed, 26 insertions(+), 19 deletions(-) diff --git a/apps/loadgen/Dockerfile b/apps/loadgen/Dockerfile index 8da8b5e213..fbad9c91ad 100644 --- a/apps/loadgen/Dockerfile +++ b/apps/loadgen/Dockerfile @@ -15,10 +15,14 @@ ENV TZ=UTC #hadolint ignore=DL3018 RUN apk --no-cache add ca-certificates curl tzdata -WORKDIR /root +RUN addgroup -S ev && adduser -S ev -G ev + +WORKDIR /home/ev COPY --from=build-env /src/ev-loadgen /usr/bin/ev-loadgen -COPY apps/loadgen/matrices/baseline.json /root/baseline.json -COPY apps/loadgen/matrices/burst.json /root/burst.json +COPY apps/loadgen/matrices/baseline.json /home/ev/baseline.json +COPY apps/loadgen/matrices/burst.json /home/ev/burst.json + +USER ev CMD ["ev-loadgen", "start"] diff --git a/apps/loadgen/README.md b/apps/loadgen/README.md index 67d760fb0f..9f1ce0d029 100644 --- a/apps/loadgen/README.md +++ b/apps/loadgen/README.md @@ -4,7 +4,7 @@ Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon ## Architecture -``` +```text ev-loadgen (this binary) --> spamoor-daemon --> ev-reth RPC | | reads matrix JSON manages wallets, @@ -16,7 +16,7 @@ ev-loadgen (this binary) --> spamoor-daemon --> ev-reth RPC ## Commands -``` +```text ev-loadgen start # run continuous scheduler (regular + burst) ev-loadgen check # send 1 tx to verify spamoor → ev-reth connectivity ev-loadgen run # one-shot: run a custom matrix file @@ -30,8 +30,8 @@ ev-loadgen run # one-shot: run a custom matrix file | `--interval` | `BENCH_INTERVAL` | `1h` | regular workload frequency | | `--burst-tx-count` | `BENCH_BURST_TX_COUNT` | `500000` | txs per burst | | `--burst-per-day` | `BENCH_BURST_PER_DAY` | `2` | bursts per day, randomly spaced | -| `--regular-matrix` | `BENCH_REGULAR_MATRIX` | `/root/baseline.json` | path to regular matrix JSON | -| `--burst-matrix` | `BENCH_BURST_MATRIX` | `/root/burst.json` | path to burst matrix JSON | +| `--regular-matrix` | `BENCH_REGULAR_MATRIX` | `/home/ev/baseline.json` | path to regular matrix JSON | +| `--burst-matrix` | `BENCH_BURST_MATRIX` | `/home/ev/burst.json` | path to burst matrix JSON | Global flag: `--spamoor-url` (or `BENCH_SPAMOOR_URL` env, default `http://spamoor-daemon:8080`) diff --git a/apps/loadgen/cmd/flags_test.go b/apps/loadgen/cmd/flags_test.go index 18cf11f8e1..59b44ea07b 100644 --- a/apps/loadgen/cmd/flags_test.go +++ b/apps/loadgen/cmd/flags_test.go @@ -7,9 +7,7 @@ import ( ) func TestStartFlags(t *testing.T) { - root := NewRootCmd() startCmd := newStartCmd() - root.AddCommand(startCmd) err := startCmd.ParseFlags([]string{"--regular-matrix", "custom.json"}) require.NoError(t, err) diff --git a/apps/loadgen/cmd/start.go b/apps/loadgen/cmd/start.go index 523808c128..beaeafbd29 100644 --- a/apps/loadgen/cmd/start.go +++ b/apps/loadgen/cmd/start.go @@ -17,8 +17,8 @@ import ( ) const ( - defaultBaselinePath = "/root/baseline.json" - defaultBurstPath = "/root/burst.json" + defaultBaselinePath = "/home/ev/baseline.json" + defaultBurstPath = "/home/ev/burst.json" burstWindow = 24 * time.Hour ) diff --git a/apps/loadgen/internal/client.go b/apps/loadgen/internal/client.go index 0b3606bc5a..bcd10a3753 100644 --- a/apps/loadgen/internal/client.go +++ b/apps/loadgen/internal/client.go @@ -68,7 +68,7 @@ func (c spamoorAPIClient) GetClients() ([]spamoor.Client, error) { url := fmt.Sprintf("%s/api/clients", c.api.BaseURL) resp, err := c.client.Get(url) if err != nil { - return nil, err + return nil, fmt.Errorf("get clients: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { @@ -77,7 +77,7 @@ func (c spamoorAPIClient) GetClients() ([]spamoor.Client, error) { } var raw []clientResponse if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil { - return nil, err + return nil, fmt.Errorf("decode clients response: %w", err) } clients := make([]spamoor.Client, len(raw)) for i, r := range raw { diff --git a/apps/loadgen/internal/config.go b/apps/loadgen/internal/config.go index c044659094..43c9d6ed98 100644 --- a/apps/loadgen/internal/config.go +++ b/apps/loadgen/internal/config.go @@ -41,7 +41,7 @@ func BuildScenarioConfig(env map[string]string) map[string]any { if !ok { continue } - if n, err := strconv.Atoi(val); err == nil { + if n, err := strconv.ParseUint(val, 10, 64); err == nil { cfg[cfgKey] = n } else { cfg[cfgKey] = val diff --git a/apps/loadgen/internal/config_test.go b/apps/loadgen/internal/config_test.go index 11a44e4f15..dfb6995454 100644 --- a/apps/loadgen/internal/config_test.go +++ b/apps/loadgen/internal/config_test.go @@ -29,8 +29,8 @@ func TestBuildScenarioConfig(t *testing.T) { require.Equal(t, "500000000000000000000", cfg["refill_amount"]) require.Equal(t, "200000000000000000000", cfg["refill_balance"]) require.Equal(t, 300, cfg["refill_interval"]) - require.Equal(t, 42, cfg["total_count"]) - require.Equal(t, 100, cfg["throughput"]) - require.Equal(t, 500, cfg["base_fee"]) + require.Equal(t, uint64(42), cfg["total_count"]) + require.Equal(t, uint64(100), cfg["throughput"]) + require.Equal(t, uint64(500), cfg["base_fee"]) require.Equal(t, "true", cfg["rebroadcast"]) } diff --git a/apps/loadgen/internal/matrix.go b/apps/loadgen/internal/matrix.go index eb15d33e81..5307b51d42 100644 --- a/apps/loadgen/internal/matrix.go +++ b/apps/loadgen/internal/matrix.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "time" "github.com/celestiaorg/tastora/framework/docker/evstack/spamoor" ) @@ -98,6 +99,12 @@ func (e *Entry) validate() error { return fmt.Errorf("probability must be between 0.0 and 1.0, got %f", *e.Probability) } + if e.Timeout != "" { + if _, err := time.ParseDuration(e.Timeout); err != nil { + return fmt.Errorf("invalid timeout %q: %w", e.Timeout, err) + } + } + return nil } diff --git a/apps/loadgen/main.go b/apps/loadgen/main.go index f1588ab027..b9b22063c8 100644 --- a/apps/loadgen/main.go +++ b/apps/loadgen/main.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "log" "os" @@ -17,5 +16,4 @@ func main() { func init() { log.SetFlags(log.Ldate | log.Ltime | log.LUTC) log.SetOutput(os.Stdout) - fmt.Fprintln(os.Stderr, "ev-loadgen starting") } From 9f5d982e27d88107f43baa69200eadbcf301bb30 Mon Sep 17 00:00:00 2001 From: chatton Date: Tue, 2 Jun 2026 15:31:37 +0100 Subject: [PATCH 14/15] chore: ensure possibility for concurrent spammers if time window is short --- apps/loadgen/cmd/start.go | 6 ------ apps/loadgen/internal/runner.go | 26 ++++++++++++-------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/apps/loadgen/cmd/start.go b/apps/loadgen/cmd/start.go index beaeafbd29..59360fe61c 100644 --- a/apps/loadgen/cmd/start.go +++ b/apps/loadgen/cmd/start.go @@ -8,7 +8,6 @@ import ( "os" "os/signal" "strconv" - "sync" "syscall" "time" @@ -75,10 +74,7 @@ func runScheduler(parent context.Context, cfg startConfig) error { return err } - var mu sync.Mutex runWorkload := func(label, matrixPath string, txCount int) { - mu.Lock() - defer mu.Unlock() log.Printf("==> %s workload starting (%d tx)", label, txCount) if err := internal.ExecuteMatrixWithOverridesFromFile(ctx, matrixPath, api, txCount); err != nil { log.Printf("%s workload error: %v", label, err) @@ -102,12 +98,10 @@ func runScheduler(parent context.Context, cfg startConfig) error { case <-ctx.Done(): log.Printf("shutting down...") burstTimer.Stop() - mu.Lock() log.Printf("cleaning up spammers") if err := internal.DeleteAllSpammers(api); err != nil { log.Printf("warning: shutdown cleanup failed: %v", err) } - mu.Unlock() return nil case <-ticker.C: diff --git a/apps/loadgen/internal/runner.go b/apps/loadgen/internal/runner.go index bfada51018..cbc8d655cd 100644 --- a/apps/loadgen/internal/runner.go +++ b/apps/loadgen/internal/runner.go @@ -142,11 +142,6 @@ func runCheck(ctx context.Context, api SpamoorClient, timeout time.Duration) err return fmt.Errorf("waiting for spamoor sync: %w", err) } - if err := DeleteAllSpammers(api); err != nil { - return fmt.Errorf("cleanup: %w", err) - } - defer func() { _ = DeleteAllSpammers(api) }() - baselineSent, baselineFailed, err := getSpamoorCounters(api) if err != nil { return fmt.Errorf("get baseline metrics: %w", err) @@ -169,6 +164,11 @@ func runCheck(ctx context.Context, api SpamoorClient, timeout time.Duration) err return fmt.Errorf("create spammer: %w", err) } log.Printf("created check spammer (id=%d)", id) + defer func() { + if dErr := api.DeleteSpammer(id); dErr != nil { + log.Printf("warning: cleanup check spammer %d failed: %v", id, dErr) + } + }() checkCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -198,15 +198,6 @@ func runEntryWithWait(ctx context.Context, api SpamoorClient, entry Entry, wait log.Printf("[%s] scenario=%s spammers=%d count_per=%d total=%d", entry.TestName, entry.Scenario, entry.NumSpammers, entry.CountPerSpammer, totalCount) - if err := DeleteAllSpammers(api); err != nil { - return fmt.Errorf("delete stale spammers: %w", err) - } - defer func() { - if err := DeleteAllSpammers(api); err != nil { - log.Printf("[%s] warning: cleanup failed: %v", entry.TestName, err) - } - }() - baselineSent, baselineFailed, err := getSpamoorCounters(api) if err != nil { return fmt.Errorf("get baseline metrics: %w", err) @@ -224,6 +215,13 @@ func runEntryWithWait(ctx context.Context, api SpamoorClient, entry Entry, wait spammerIDs = append(spammerIDs, id) log.Printf("[%s] created spammer %s (id=%d)", entry.TestName, name, id) } + defer func() { + for _, id := range spammerIDs { + if err := api.DeleteSpammer(id); err != nil { + log.Printf("[%s] warning: cleanup spammer %d failed: %v", entry.TestName, id, err) + } + } + }() for _, id := range spammerIDs { sp, err := api.GetSpammer(id) From 32c010f294cfff7b79833494a777f9e47be75b73 Mon Sep 17 00:00:00 2001 From: chatton Date: Wed, 3 Jun 2026 11:03:54 +0100 Subject: [PATCH 15/15] fix(loadgen): fix burst scheduling window and shutdown races - use remaining time until next reset for burst timer instead of full 24h window - track workload goroutines with WaitGroup, wait before cleanup on shutdown Co-Authored-By: Claude Opus 4.6 --- apps/loadgen/cmd/start.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/loadgen/cmd/start.go b/apps/loadgen/cmd/start.go index 59360fe61c..a767c19158 100644 --- a/apps/loadgen/cmd/start.go +++ b/apps/loadgen/cmd/start.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "strconv" + "sync" "syscall" "time" @@ -74,7 +75,9 @@ func runScheduler(parent context.Context, cfg startConfig) error { return err } + var wg sync.WaitGroup runWorkload := func(label, matrixPath string, txCount int) { + defer wg.Done() log.Printf("==> %s workload starting (%d tx)", label, txCount) if err := internal.ExecuteMatrixWithOverridesFromFile(ctx, matrixPath, api, txCount); err != nil { log.Printf("%s workload error: %v", label, err) @@ -82,6 +85,7 @@ func runScheduler(parent context.Context, cfg startConfig) error { } // fire regular immediately + wg.Add(1) go runWorkload("regular", cfg.regularMatrix, regularTxPerRun) ticker := time.NewTicker(cfg.interval) @@ -89,15 +93,17 @@ func runScheduler(parent context.Context, cfg startConfig) error { // burst: single timer, reschedule after each fire, reset count every 24h. burstsRemaining := cfg.burstPerDay - burstTimer := nextBurstTimer(burstsRemaining, burstWindow) + nextReset := time.Now().Add(burstWindow) + burstTimer := nextBurstTimer(burstsRemaining, time.Until(nextReset)) resetTimer := time.NewTimer(burstWindow) defer resetTimer.Stop() for { select { case <-ctx.Done(): - log.Printf("shutting down...") + log.Printf("shutting down, waiting for in-flight workloads...") burstTimer.Stop() + wg.Wait() log.Printf("cleaning up spammers") if err := internal.DeleteAllSpammers(api); err != nil { log.Printf("warning: shutdown cleanup failed: %v", err) @@ -105,18 +111,21 @@ func runScheduler(parent context.Context, cfg startConfig) error { return nil case <-ticker.C: + wg.Add(1) go runWorkload("regular", cfg.regularMatrix, regularTxPerRun) case <-burstTimer.C: + wg.Add(1) go runWorkload("burst", cfg.burstMatrix, cfg.burstTxCount) burstsRemaining-- - burstTimer = nextBurstTimer(burstsRemaining, burstWindow) + burstTimer = nextBurstTimer(burstsRemaining, time.Until(nextReset)) case <-resetTimer.C: log.Printf("24h elapsed - resetting burst count") burstTimer.Stop() burstsRemaining = cfg.burstPerDay - burstTimer = nextBurstTimer(burstsRemaining, burstWindow) + nextReset = time.Now().Add(burstWindow) + burstTimer = nextBurstTimer(burstsRemaining, time.Until(nextReset)) resetTimer.Reset(burstWindow) } }