Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ jobs:
[
{"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"}
]

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ docs/.vitepress/cache
*.log
*.tgz
.idea
.junie
.temp
.vite_opt_cache
.vscode
.gocache
.gomodcache
/.cache
*.diff
2 changes: 2 additions & 0 deletions .just/build.just
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
Expand Down
14 changes: 14 additions & 0 deletions .just/loadgen.just
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Build ev-loadgen binary
[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('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"
2 changes: 1 addition & 1 deletion apps/evm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/evm/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion apps/grpc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/grpc/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
28 changes: 28 additions & 0 deletions apps/loadgen/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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

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 /home/ev/baseline.json
COPY apps/loadgen/matrices/burst.json /home/ev/burst.json

USER ev

CMD ["ev-loadgen", "start"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
157 changes: 157 additions & 0 deletions apps/loadgen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# loadgen

Standalone load generator for ev-node stress testing. Talks to a [spamoor-daemon](https://github.com/ethpandaops/spamoor) sidecar via HTTP API.

## Architecture

```text
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-loadgen** needs: spamoor-daemon API URL + matrix JSON files

## Modes

### Daemon mode (`start`)

Runs a continuous scheduler with regular and burst workloads. Designed for long-running deployments.

```sh
ev-loadgen start --spamoor-url=http://localhost:8080
```

Regular workloads fire immediately at startup, then repeat at `--interval`. Per-run tx count = `tx-per-day / (24h / interval)`, overriding each matrix entry's `BENCH_COUNT_PER_SPAMMER`.

Bursts are randomly spaced throughout a rolling 24h window. Set `--burst-per-day=0` (the default) to disable bursts entirely.

#### 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` | `0` | bursts per day, randomly spaced (0 = disabled) |
| `--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 |

### CLI mode (one-shot commands)

#### `run` — execute a matrix file

Runs all entries from a matrix JSON file with probability filtering and sync waiting, then exits.

```sh
ev-loadgen run matrices/baseline.json --spamoor-url=http://localhost:8080
```

#### `burst` — trigger a single burst

Fires one burst workload immediately and exits.

```sh
ev-loadgen burst --spamoor-url=http://localhost:8080
```

| Flag | Env | Default | Description |
|------|-----|---------|-------------|
| `--tx-count` | `BENCH_BURST_TX_COUNT` | `500000` | total transactions for the burst |
| `--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`)

## Quick Start

### 1. Start spamoor-daemon

```sh
docker run -d --name spamoor -p 8080:8080 \
ethpandaops/spamoor:latest /app/spamoor-daemon \
--privkey=<funded-private-key> \
--rpchost=http://<ev-reth-host>:8545 \
--port=8080 --startup-delay=0
```

### 2. Run loadgen

```sh
# build
cd apps/loadgen && go build -o ev-loadgen .

# one-shot matrix run
./ev-loadgen run matrices/baseline.json --spamoor-url=http://localhost:8080

# continuous daemon (~1M tx/day, no bursts)
./ev-loadgen start --spamoor-url=http://localhost:8080

# continuous daemon with bursts
./ev-loadgen start \
--spamoor-url=http://localhost:8080 \
--tx-per-day=500000 \
--interval=30m \
--burst-tx-count=100000 \
--burst-per-day=4
```

### Docker Compose

Spins up both spamoor-daemon and loadgen together:

```sh
export BENCH_PRIVATE_KEY=<funded-private-key>
export BENCH_ETH_RPC_URL=http://<ev-reth-host>:8545
docker compose -f apps/loadgen/docker-compose.yml up
```

## Matrix Format

Each entry specifies a spamoor scenario, tx counts, and optional probability:

```json
{
"entries": [
{
"test_name": "EOATransfer",
"scenario": "eoatx",
"timeout": "15m",
"env": {
"BENCH_NUM_SPAMMERS": "4",
"BENCH_COUNT_PER_SPAMMER": "10500",
"BENCH_THROUGHPUT": "200",
"BENCH_MAX_PENDING": "50000",
"BENCH_MAX_WALLETS": "200",
"BENCH_BASE_FEE": "500",
"BENCH_TIP_FEE": "50"
}
}
]
}
```

| 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`) |

When using `start` or `burst`, `BENCH_COUNT_PER_SPAMMER` is overridden by the computed per-run count. The matrix value is used as-is by `run`.

## Build

```sh
# binary
cd apps/loadgen && go build -o ev-loadgen .

# docker image
docker build -f apps/loadgen/Dockerfile -t ev-loadgen:dev .

# via just
just build-loadgen
just docker-build-loadgen
```
37 changes: 37 additions & 0 deletions apps/loadgen/cmd/burst.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cmd

import (
"log"

"github.com/evstack/ev-node/apps/loadgen/internal/runner"
"github.com/evstack/ev-node/apps/loadgen/internal/spamoor"
"github.com/spf13/cobra"
)

func newBurstCmd() *cobra.Command {
var (
txCount int
matrixPath string
)

cmd := &cobra.Command{
Use: "burst",
Short: "trigger a single burst workload immediately",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
api := spamoor.NewClient(resolveSpamoorURL())

if err := runner.WaitForSync(cmd.Context(), api); err != nil {
return err
}

log.Printf("==> burst workload starting (%d tx)", txCount)
return runner.ExecuteMatrixWithOverridesFromFile(cmd.Context(), matrixPath, api, txCount)
},
}

cmd.Flags().IntVar(&txCount, "tx-count", envIntOr("BENCH_BURST_TX_COUNT", 500000), "total transactions for the burst")
cmd.Flags().StringVar(&matrixPath, "burst-matrix", envStringOr("BENCH_BURST_MATRIX", defaultBurstPath), "path to burst matrix JSON")

return cmd
}
29 changes: 29 additions & 0 deletions apps/loadgen/cmd/flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestStartFlags(t *testing.T) {
startCmd := newStartCmd()

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)
}
29 changes: 29 additions & 0 deletions apps/loadgen/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"github.com/evstack/ev-node/apps/loadgen/internal/spamoor"
"github.com/spf13/cobra"
)

var spamoorFlag string

// NewRootCmd returns the top-level cobra command for ev-loadgen.
func NewRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "ev-loadgen",
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(), newStartCmd(), newBurstCmd())

return rootCmd
}

func resolveSpamoorURL() string {
if spamoorFlag != "" {
return spamoorFlag
}
return spamoor.URL()
}
18 changes: 18 additions & 0 deletions apps/loadgen/cmd/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cmd

import (
"github.com/evstack/ev-node/apps/loadgen/internal/runner"
"github.com/evstack/ev-node/apps/loadgen/internal/spamoor"
"github.com/spf13/cobra"
)

func newRunCmd() *cobra.Command {
return &cobra.Command{
Use: "run <matrix.json>",
Short: "run benchmarks from a matrix JSON file",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runner.ExecuteMatrixFromFile(cmd.Context(), args[0], spamoor.NewClient(resolveSpamoorURL()))
},
}
}
Loading
Loading