Skip to content

Commit 37cbc62

Browse files
committed
feat(load): add devnet support
1 parent e752db2 commit 37cbc62

File tree

3 files changed

+236
-33
lines changed

3 files changed

+236
-33
lines changed

tests/load/main/main.go

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55

66
import (
77
"context"
8+
"encoding/json"
89
"flag"
910
"fmt"
1011
"math/big"
@@ -38,6 +39,8 @@ const (
3839
var (
3940
flagVars *e2e.FlagVars
4041

42+
devnetConfigPath string
43+
4144
loadTimeoutArg time.Duration
4245
firewoodEnabledArg bool
4346
numWorkersArg int
@@ -48,6 +51,13 @@ func init() {
4851
e2e.WithDefaultNodeCount(defaultNodeCount),
4952
)
5053

54+
flag.StringVar(
55+
&devnetConfigPath,
56+
"devnet-config-path",
57+
"",
58+
"the file path for the devnet config",
59+
)
60+
5161
flag.DurationVar(
5262
&loadTimeoutArg,
5363
"load-timeout",
@@ -77,6 +87,65 @@ func main() {
7787

7888
require := require.New(tc)
7989

90+
ctx := tests.DefaultNotifyContext(0, tc.DeferCleanup)
91+
tc.SetDefaultContextParent(ctx)
92+
93+
registry := prometheus.NewRegistry()
94+
metricsServer, err := tests.NewPrometheusServer(registry)
95+
require.NoError(err)
96+
tc.DeferCleanup(func() {
97+
require.NoError(metricsServer.Stop())
98+
})
99+
100+
var workers []load.Worker
101+
if devnetConfigPath != "" {
102+
b, err := os.ReadFile(devnetConfigPath)
103+
require.NoError(err)
104+
105+
var devnetConfig load.DevnetConfig
106+
require.NoError(json.Unmarshal(b, &devnetConfig))
107+
108+
workers = load.ConnectNetwork(tc, metricsServer, devnetConfig)
109+
} else {
110+
workers = startNetwork(tc, metricsServer)
111+
}
112+
113+
chainID, err := workers[0].Client.ChainID(ctx)
114+
require.NoError(err)
115+
116+
tokenContract, err := newTokenContract(ctx, chainID, &workers[0], workers[1:])
117+
require.NoError(err)
118+
119+
randomTest, err := load.NewRandomTest(
120+
ctx,
121+
chainID,
122+
&workers[0],
123+
rand.NewSource(time.Now().UnixMilli()),
124+
tokenContract,
125+
)
126+
require.NoError(err)
127+
128+
generator, err := load.NewLoadGenerator(
129+
workers,
130+
chainID,
131+
metricsNamespace,
132+
registry,
133+
randomTest,
134+
)
135+
require.NoError(err)
136+
137+
log.Info("starting load generator")
138+
139+
generator.Run(ctx, log, loadTimeoutArg, testTimeout)
140+
}
141+
142+
// startNetwork starts a new network and returns a list of workers who can
143+
// interact with the network.
144+
//
145+
// Customization of the network and the number of workers can be set via flags.
146+
func startNetwork(tc tests.TestContext, metricsServer *tests.PrometheusServer) []load.Worker {
147+
require := require.New(tc)
148+
80149
numNodes, err := flagVars.NodeCount()
81150
require.NoError(err, "failed to get node count")
82151

@@ -97,17 +166,9 @@ func main() {
97166

98167
e2e.NewTestEnvironment(tc, flagVars, network)
99168

100-
ctx := tests.DefaultNotifyContext(0, tc.DeferCleanup)
101169
wsURIs, err := tmpnet.GetNodeWebsocketURIs(network.Nodes, blockchainID)
102170
require.NoError(err)
103171

104-
registry := prometheus.NewRegistry()
105-
metricsServer, err := tests.NewPrometheusServer(registry)
106-
require.NoError(err)
107-
tc.DeferCleanup(func() {
108-
require.NoError(metricsServer.Stop())
109-
})
110-
111172
monitoringConfigFilePath, err := tmpnet.WritePrometheusSDConfig("load-test", tmpnet.SDConfig{
112173
Targets: []string{metricsServer.Address()},
113174
Labels: network.GetMonitoringLabels(),
@@ -133,31 +194,7 @@ func main() {
133194
}
134195
}
135196

136-
chainID, err := workers[0].Client.ChainID(ctx)
137-
require.NoError(err)
138-
139-
tokenContract, err := newTokenContract(ctx, chainID, &workers[0], workers[1:])
140-
require.NoError(err)
141-
142-
randomTest, err := load.NewRandomTest(
143-
ctx,
144-
chainID,
145-
&workers[0],
146-
rand.NewSource(time.Now().UnixMilli()),
147-
tokenContract,
148-
)
149-
require.NoError(err)
150-
151-
generator, err := load.NewLoadGenerator(
152-
workers,
153-
chainID,
154-
metricsNamespace,
155-
registry,
156-
randomTest,
157-
)
158-
require.NoError(err)
159-
160-
generator.Run(ctx, log, loadTimeoutArg, testTimeout)
197+
return workers
161198
}
162199

163200
// newTokenContract deploys an instance of an ERC20 token and distributes the

tests/load/network.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package load
5+
6+
import (
7+
"os"
8+
9+
"github.com/ava-labs/libevm/ethclient"
10+
"github.com/google/uuid"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/ava-labs/avalanchego/tests"
14+
"github.com/ava-labs/avalanchego/tests/fixture/tmpnet"
15+
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
16+
)
17+
18+
// DevnetConfig holds the configuration for connecting to an existing devnet.
19+
// It specifies the private keys to use for load generation and the WebSocket
20+
// URIs of the nodes to connect to.
21+
type DevnetConfig struct {
22+
PrivateKeys []string `json:"private-keys"`
23+
NodeWsURIs []string `json:"node-ws-uris"`
24+
}
25+
26+
// ParseKeys converts the string representation of private keys into their
27+
// concrete types (*secp256k1.PrivateKeys). Returns an error if any key fails to parse.
28+
func (d DevnetConfig) ParseKeys() ([]*secp256k1.PrivateKey, error) {
29+
keys := make([]*secp256k1.PrivateKey, len(d.PrivateKeys))
30+
for i, pk := range d.PrivateKeys {
31+
key, err := secp256k1.ToPrivateKey([]byte(pk))
32+
if err != nil {
33+
return nil, err
34+
}
35+
keys[i] = key
36+
}
37+
38+
return keys, nil
39+
}
40+
41+
// ConnectNetwork connects to an existing network and returns a list of workers
42+
// that can interact with the network.
43+
//
44+
// The function also configures Prometheus monitoring for any client-side
45+
// metrics defined in metricsServer and registers a cleanup function to remove the
46+
// monitoring configuration file when the test completes.
47+
func ConnectNetwork(tc tests.TestContext, metricsServer *tests.PrometheusServer, devnetConfig DevnetConfig) []Worker {
48+
require := require.New(tc)
49+
50+
keys, err := devnetConfig.ParseKeys()
51+
require.NoError(err)
52+
53+
wsURIs := devnetConfig.NodeWsURIs
54+
workers := make([]Worker, len(keys))
55+
for i, key := range keys {
56+
wsURI := wsURIs[i%len(wsURIs)]
57+
client, err := ethclient.Dial(wsURI)
58+
require.NoError(err)
59+
60+
nonce, err := client.NonceAt(tc.DefaultContext(), key.EthAddress(), nil)
61+
require.NoError(err)
62+
63+
workers[i] = Worker{
64+
PrivKey: key.ToECDSA(),
65+
Client: client,
66+
Nonce: nonce,
67+
}
68+
}
69+
70+
// XXX: we could probably remove this metrics code from ConnectNetwork
71+
networkUUID := uuid.NewString()
72+
labels := map[string]string{
73+
"job": "load-test",
74+
"is_ephemeral_node": "false",
75+
"chain": "C",
76+
"networkUUID": networkUUID,
77+
}
78+
79+
monitoringConfigFilePath, err := tmpnet.WritePrometheusSDConfig("load-test", tmpnet.SDConfig{
80+
Targets: []string{metricsServer.Address()},
81+
Labels: labels,
82+
}, false)
83+
require.NoError(err, "failed to generate monitoring config file")
84+
85+
tc.DeferCleanup(func() {
86+
require.NoError(
87+
os.Remove(monitoringConfigFilePath),
88+
"failed †o remove monitoring config file",
89+
)
90+
})
91+
92+
return workers
93+
}

tests/load/network_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package load
5+
6+
import (
7+
"testing"
8+
9+
"github.com/onsi/ginkgo/v2"
10+
"github.com/prometheus/client_golang/prometheus"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/ava-labs/avalanchego/tests"
14+
"github.com/ava-labs/avalanchego/tests/fixture/e2e"
15+
"github.com/ava-labs/avalanchego/tests/fixture/tmpnet"
16+
)
17+
18+
const blockchainID = "C"
19+
20+
var flagVars *e2e.FlagVars
21+
22+
func TestDevnetConnection(t *testing.T) {
23+
ginkgo.RunSpecs(t, "devnet connection test suites")
24+
}
25+
26+
func init() {
27+
flagVars = e2e.RegisterFlags()
28+
}
29+
30+
var _ = ginkgo.Describe("[Devnet Connection]", func() {
31+
tc := e2e.NewTestContext()
32+
require := require.New(tc)
33+
34+
var devnetConfig DevnetConfig
35+
36+
ginkgo.BeforeEach(func() {
37+
numNodes, err := flagVars.NodeCount()
38+
require.NoError(err)
39+
40+
nodes := tmpnet.NewNodesOrPanic(numNodes)
41+
42+
keys, err := tmpnet.NewPrivateKeys(numNodes)
43+
require.NoError(err)
44+
45+
network := &tmpnet.Network{
46+
Nodes: nodes,
47+
PreFundedKeys: keys,
48+
}
49+
50+
e2e.NewTestEnvironment(tc, flagVars, network)
51+
52+
wsURIs, err := tmpnet.GetNodeWebsocketURIs(network.Nodes, blockchainID)
53+
require.NoError(err)
54+
55+
pks := make([]string, len(keys))
56+
for i, key := range keys {
57+
pks[i] = string(key.Bytes())
58+
}
59+
60+
devnetConfig = DevnetConfig{
61+
NodeWsURIs: wsURIs,
62+
PrivateKeys: pks,
63+
}
64+
})
65+
66+
ginkgo.It("can connect to an existing network", func() {
67+
registry := prometheus.NewRegistry()
68+
metricsServer, err := tests.NewPrometheusServer(registry)
69+
require.NoError(err)
70+
71+
_ = ConnectNetwork(tc, metricsServer, devnetConfig)
72+
})
73+
})

0 commit comments

Comments
 (0)