Skip to content

add warp tests for Coreth #1000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
53 changes: 51 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ jobs:
repository: ${{ github.event.inputs.avalanchegoRepo }}
ref: ${{ github.event.inputs.avalanchegoBranch }}
path: avalanchego
token: ${{ secrets.AVALANCHE_PAT }}
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
Expand Down Expand Up @@ -72,7 +71,6 @@ jobs:
repository: ${{ github.event.inputs.avalanchegoRepo }}
ref: ${{ github.event.inputs.avalanchegoBranch }}
path: avalanchego
token: ${{ secrets.AVALANCHE_PAT }}
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
Expand Down Expand Up @@ -128,10 +126,61 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Build AvalancheGo and update Coreth dependency
run: ./scripts/build_avalanchego_with_coreth.sh
- name: Run e2e tests
uses: ava-labs/avalanchego/.github/actions/run-monitored-tmpnet-cmd@1a40195dc447a7b3b0303b03cd0af8e3a9389635
with:
run: ./scripts/tests.e2e.sh
run_env: AVALANCHEGO_CLONE_PATH=avalanchego
prometheus_username: ${{ secrets.PROMETHEUS_ID || '' }}
prometheus_password: ${{ secrets.PROMETHEUS_PASSWORD || '' }}
loki_username: ${{ secrets.LOKI_ID || '' }}
loki_password: ${{ secrets.LOKI_PASSWORD || '' }}
e2e_warp:
name: e2e warp tests
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: check out ${{ github.event.inputs.avalanchegoRepo }} ${{ github.event.inputs.avalanchegoBranch }}
if: ${{ github.event_name == 'workflow_dispatch' }}
uses: actions/checkout@v4
with:
repository: ${{ github.event.inputs.avalanchegoRepo }}
ref: ${{ github.event.inputs.avalanchegoBranch }}
path: avalanchego
- name: Move AvalancheGo
if: ${{ github.event_name == 'workflow_dispatch' }}
run: mv avalanchego /tmp/e2e/warp/avalanchego
- name: Build AvalancheGo and update Coreth dependency
run: ./scripts/build_avalanchego_with_coreth.sh
env:
AVALANCHEGO_CLONE_PATH: /tmp/e2e/warp/avalanchego
- name: Clone Subnet-EVM
uses: actions/checkout@v4
with:
repository: ava-labs/subnet-evm
ref: master
path: subnet-evm
- name: Move Subnet-EVM
run: mv subnet-evm /tmp/e2e/warp/subnet-evm
- name: Build Subnet-EVM
run: |
cd /tmp/e2e/warp/subnet-evm
./scripts/build.sh
- name: Run Warp E2E Tests
uses: ava-labs/avalanchego/.github/actions/run-monitored-tmpnet-cmd@1a40195dc447a7b3b0303b03cd0af8e3a9389635
with:
run: ./scripts/run_ginkgo_warp.sh
run_env: AVALANCHEGO_BUILD_PATH=/tmp/e2e/warp/avalanchego/build
artifact_prefix: warp
prometheus_username: ${{ secrets.PROMETHEUS_ID || '' }}
prometheus_password: ${{ secrets.PROMETHEUS_PASSWORD || '' }}
loki_username: ${{ secrets.LOKI_ID || '' }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ build/
avalanchego

.direnv

cmd/simulator/.simulator/*
cmd/simulator/simulator
13 changes: 13 additions & 0 deletions bin/ginkgo
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash

set -euo pipefail

# Ensure the go command is run from the root of the repository so that its go.mod file is used
REPO_ROOT=$(
cd "$(dirname "${BASH_SOURCE[0]}")"
cd .. && pwd
)
cd "${REPO_ROOT}"

# If an explicit version is not specified, go run uses the ginkgo version from go.mod
go run github.com/onsi/ginkgo/v2/ginkgo "${@}"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027
62 changes: 62 additions & 0 deletions cmd/simulator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Load Simulator

When building developing your own blockchain using `coreth`, you may want to analyze how your fee parameterization behaves and/or how many resources your VM uses under different load patterns. For this reason, we developed `cmd/simulator`. `cmd/simulator` lets you drive arbitrary load across any number of [endpoints] with a user-specified `keys` directory (insecure) `timeout`, `workers`, `max-fee-cap`, and `max-tip-cap`.

## Building the Load Simulator

To build the load simulator, navigate to the base of the simulator directory:

```bash
cd $GOPATH/src/github.com/ava-labs/coreth/cmd/simulator
```

Build the simulator:

```bash
go build -o ./simulator main/*.go
```

To confirm that you built successfully, run the simulator and print the version:

```bash
./simulator --version
```

This should give the following output:

```
v0.1.0
```

To run the load simulator, you must first start an EVM based network. The load simulator works on both the C-Chain and Subnet-EVM, so we will start a single node network and run the load simulator on the C-Chain.

To start a single node network, follow the instructions from the AvalancheGo [README](https://github.com/ava-labs/avalanchego#building-avalanchego) to build from source.

Once you've built AvalancheGo, open the AvalancheGo directory in a separate terminal window and run a single node non-staking network with the following command:

```bash
./build/avalanchego --sybil-protection-enabled=false --network-id=local
```

WARNING:

The `--sybil-protection-enabled=false` flag is only suitable for local testing. Disabling staking serves two functions explicitly for testing purposes:

1. Ignore stake weight on the P-Chain and count each connected peer as having a stake weight of 1
2. Automatically opts in to validate every Subnet

Once you have AvalancheGo running locally, it will be running an HTTP Server on the default port `9650`. This means that the RPC Endpoint for the C-Chain will be http://127.0.0.1:9650/ext/bc/C/rpc and ws://127.0.0.1:9650/ext/bc/C/ws for WebSocket connections.

Now, we can run the simulator command to simulate some load on the local C-Chain for 30s:

```bash
./simulator --timeout=1m --workers=1 --max-fee-cap=300 --max-tip-cap=10 --txs-per-worker=50
```

## Command Line Flags

To see all of the command line flag options, run

```bash
./simulator --help
```
129 changes: 129 additions & 0 deletions cmd/simulator/config/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package config

import (
"errors"
"fmt"
"strings"
"time"

"github.com/spf13/pflag"
"github.com/spf13/viper"
)

const Version = "v0.1.1"

const (
ConfigFilePathKey = "config-file"
LogLevelKey = "log-level"
EndpointsKey = "endpoints"
MaxFeeCapKey = "max-fee-cap"
MaxTipCapKey = "max-tip-cap"
WorkersKey = "workers"
TxsPerWorkerKey = "txs-per-worker"
KeyDirKey = "key-dir"
VersionKey = "version"
TimeoutKey = "timeout"
BatchSizeKey = "batch-size"
MetricsPortKey = "metrics-port"
MetricsOutputKey = "metrics-output"
)

var (
ErrNoEndpoints = errors.New("must specify at least one endpoint")
ErrNoWorkers = errors.New("must specify non-zero number of workers")
ErrNoTxs = errors.New("must specify non-zero number of txs-per-worker")
)

type Config struct {
Endpoints []string `json:"endpoints"`
MaxFeeCap int64 `json:"max-fee-cap"`
MaxTipCap int64 `json:"max-tip-cap"`
Workers int `json:"workers"`
TxsPerWorker uint64 `json:"txs-per-worker"`
KeyDir string `json:"key-dir"`
Timeout time.Duration `json:"timeout"`
BatchSize uint64 `json:"batch-size"`
MetricsPort uint64 `json:"metrics-port"`
MetricsOutput string `json:"metrics-output"`
}

func BuildConfig(v *viper.Viper) (Config, error) {
c := Config{
Endpoints: v.GetStringSlice(EndpointsKey),
MaxFeeCap: v.GetInt64(MaxFeeCapKey),
MaxTipCap: v.GetInt64(MaxTipCapKey),
Workers: v.GetInt(WorkersKey),
TxsPerWorker: v.GetUint64(TxsPerWorkerKey),
KeyDir: v.GetString(KeyDirKey),
Timeout: v.GetDuration(TimeoutKey),
BatchSize: v.GetUint64(BatchSizeKey),
MetricsPort: v.GetUint64(MetricsPortKey),
MetricsOutput: v.GetString(MetricsOutputKey),
}
if len(c.Endpoints) == 0 {
return c, ErrNoEndpoints
}
if c.Workers == 0 {
return c, ErrNoWorkers
}
if c.TxsPerWorker == 0 {
return c, ErrNoTxs
}
// Note: it's technically valid for the fee/tip cap to be 0, but cannot
// be less than 0.
if c.MaxFeeCap < 0 {
return c, fmt.Errorf("invalid max fee cap %d < 0", c.MaxFeeCap)
}
if c.MaxTipCap < 0 {
return c, fmt.Errorf("invalid max tip cap %d <= 0", c.MaxTipCap)
}
return c, nil
}

func BuildViper(fs *pflag.FlagSet, args []string) (*viper.Viper, error) {
if err := fs.Parse(args); err != nil {
return nil, err
}

v := viper.New()
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.SetEnvPrefix("evm_simulator")
if err := v.BindPFlags(fs); err != nil {
return nil, err
}

if v.IsSet(ConfigFilePathKey) {
v.SetConfigFile(v.GetString(ConfigFilePathKey))
if err := v.ReadInConfig(); err != nil {
return nil, err
}
}
return v, nil
}

// BuildFlagSet returns a complete set of flags for simulator
func BuildFlagSet() *pflag.FlagSet {
fs := pflag.NewFlagSet("simulator", pflag.ContinueOnError)
addSimulatorFlags(fs)
return fs
}

func addSimulatorFlags(fs *pflag.FlagSet) {
fs.Bool(VersionKey, false, "Print the version and exit")
fs.String(ConfigFilePathKey, "", "Specify the config path to use to load a YAML config for the simulator")
fs.StringSlice(EndpointsKey, []string{"ws://127.0.0.1:9650/ext/bc/C/ws"}, "Specify a comma separated list of RPC Websocket Endpoints (minimum of 1 endpoint)")
fs.Int64(MaxFeeCapKey, 50, "Specify the maximum fee cap to use for transactions denominated in GWei (must be > 0)")
fs.Int64(MaxTipCapKey, 1, "Specify the max tip cap for transactions denominated in GWei (must be >= 0)")
fs.Uint64(TxsPerWorkerKey, 100, "Specify the number of transactions to create per worker (must be > 0)")
fs.Int(WorkersKey, 1, "Specify the number of workers to create for the simulator (must be > 0)")
fs.String(KeyDirKey, ".simulator/keys", "Specify the directory to save private keys in (INSECURE: only use for testing)")
fs.Duration(TimeoutKey, 5*time.Minute, "Specify the timeout for the simulator to complete (0 indicates no timeout)")
fs.String(LogLevelKey, "info", "Specify the log level to use in the simulator")
fs.Uint64(BatchSizeKey, 100, "Specify the batchsize for the worker to issue and confirm txs")
fs.Uint64(MetricsPortKey, 8082, "Specify the port to use for the metrics server")
fs.String(MetricsOutputKey, "", "Specify the file to write metrics in json format, or empty to write to stdout (defaults to stdout)")
}
85 changes: 85 additions & 0 deletions cmd/simulator/key/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package key

import (
"context"
"crypto/ecdsa"
"fmt"
"os"
"path/filepath"

"github.com/ava-labs/libevm/common"
ethcrypto "github.com/ava-labs/libevm/crypto"
)

type Key struct {
PrivKey *ecdsa.PrivateKey
Address common.Address
}

func CreateKey(pk *ecdsa.PrivateKey) *Key {
return &Key{pk, ethcrypto.PubkeyToAddress(pk.PublicKey)}
}

// Load attempts to open a [Key] stored at [file].
func Load(file string) (*Key, error) {
pk, err := ethcrypto.LoadECDSA(file)
if err != nil {
return nil, fmt.Errorf("problem loading private key from %s: %w", file, err)
}
return CreateKey(pk), nil
}

// LoadAll loads all keys in [dir].
func LoadAll(ctx context.Context, dir string) ([]*Key, error) {
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("unable to create %s: %w", dir, err)
}

return nil, nil
}

var files []string

err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}

files = append(files, path)
return nil
})
if err != nil {
return nil, fmt.Errorf("could not walk %s: %w", dir, err)
}

ks := make([]*Key, len(files))
for i, file := range files {
k, err := Load(file)
if err != nil {
return nil, fmt.Errorf("could not load key at %s: %w", file, err)
}

ks[i] = k
}
return ks, nil
}

// Save persists a [Key] to [dir] (where the filename is the hex-encoded
// address).
func (k *Key) Save(dir string) error {
fp := filepath.Join(dir, k.Address.Hex())
return ethcrypto.SaveECDSA(fp, k.PrivKey)
}

// Generate creates a new [Key] and returns it.
func Generate() (*Key, error) {
pk, err := ethcrypto.GenerateKey()
if err != nil {
return nil, fmt.Errorf("%w: cannot generate key", err)
}
return CreateKey(pk), nil
}
Loading
Loading