Skip to content

Commit 927882a

Browse files
committed
Implement a command for load testing with predefined JSON-RPC API calls
1 parent bbeb971 commit 927882a

2 files changed

Lines changed: 273 additions & 0 deletions

File tree

cmd/load_test/cmd.go

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
package load_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"math/big"
10+
"math/rand"
11+
"net/http"
12+
"os"
13+
"os/signal"
14+
"syscall"
15+
16+
"github.com/ethereum/go-ethereum/common/hexutil"
17+
"github.com/ethereum/go-ethereum/core/types"
18+
"github.com/rs/zerolog/log"
19+
"github.com/spf13/cobra"
20+
"golang.org/x/sync/errgroup"
21+
)
22+
23+
var Cmd = &cobra.Command{
24+
Use: "load_test",
25+
Short: "Simulates API traffic on an EVM Gateway node for load testing purposes",
26+
RunE: func(command *cobra.Command, _ []string) error {
27+
ctx, cancel := context.WithCancel(command.Context())
28+
defer cancel()
29+
30+
g := errgroup.Group{}
31+
for range workers {
32+
g.Go(func() error {
33+
for ctx.Err() == nil {
34+
err := simulateTraffic()
35+
if err != nil {
36+
fmt.Println("Error: ", err)
37+
}
38+
}
39+
40+
return nil
41+
})
42+
}
43+
44+
osSig := make(chan os.Signal, 1)
45+
signal.Notify(osSig, syscall.SIGINT, syscall.SIGTERM)
46+
47+
// wait for command to exit or for a shutdown signal
48+
<-osSig
49+
log.Info().Msg("OS Signal to shutdown received, shutting down")
50+
cancel()
51+
52+
return nil
53+
},
54+
}
55+
56+
func simulateTraffic() error {
57+
rpcClient := &RpcClient{url: evmGatewayHost}
58+
59+
blockNumber, err := rpcClient.EthBlockNumber()
60+
if err != nil {
61+
return fmt.Errorf("failed to call `eth_blockNumber`: %w", err)
62+
}
63+
fmt.Println("Block Number: ", blockNumber)
64+
65+
block, err := rpcClient.EthGetBlockByNumber(blockNumber)
66+
if err != nil {
67+
return fmt.Errorf("failed to call `eth_getBlockByNumber`: %w", err)
68+
}
69+
fmt.Println("Block: ", block)
70+
71+
blockReceipts, err := rpcClient.EthGetBlockReceipts(blockNumber)
72+
if err != nil {
73+
return fmt.Errorf("failed to call `eth_getBlockReceipts`: %w", err)
74+
}
75+
fmt.Println("Block Receipts: ", blockReceipts)
76+
77+
if len(blockReceipts) > 0 {
78+
receipt := blockReceipts[rand.Intn(len(blockReceipts))]
79+
if val, ok := receipt["transactionHash"]; ok {
80+
txHash := val.(string)
81+
receipt, err := rpcClient.EthGetReceipt(txHash)
82+
if err != nil {
83+
return fmt.Errorf("failed to call `eth_getTransactionReceipt`: %w", err)
84+
}
85+
fmt.Println("Receipt: ", receipt)
86+
87+
txTrace, err := rpcClient.DebugTraceTransaction(txHash)
88+
if err != nil {
89+
return fmt.Errorf("failed to call `debug_TraceTransaction`: %w", err)
90+
}
91+
fmt.Println("Tx Trace: ", txTrace)
92+
}
93+
94+
if val, ok := receipt["from"]; ok {
95+
from := val.(string)
96+
balance, err := rpcClient.EthGetBalance(from)
97+
if err != nil {
98+
return fmt.Errorf("failed to call `eth_getBalance`: %w", err)
99+
}
100+
fmt.Println("Balance: ", balance)
101+
}
102+
}
103+
104+
blockTraces, err := rpcClient.DebugTraceBlockByNumber(blockNumber)
105+
if err != nil {
106+
return fmt.Errorf("failed to call `debug_traceBlockByNumber`: %w", err)
107+
}
108+
fmt.Println("Block Traces: ", blockTraces)
109+
110+
callResult, err := rpcClient.EthCall()
111+
if err != nil {
112+
return fmt.Errorf("failed to call `eth_call`: %w", err)
113+
}
114+
fmt.Println("Call Result: ", callResult)
115+
116+
return nil
117+
}
118+
119+
type RpcResult struct {
120+
Result json.RawMessage `json:"result"`
121+
Error any `json:"error"`
122+
}
123+
124+
type RpcClient struct {
125+
url string
126+
}
127+
128+
func (r *RpcClient) request(method string, params string) (json.RawMessage, error) {
129+
requestURL := fmt.Sprintf(`{"jsonrpc":"2.0","id":0,"method":"%s","params":%s}`, method, params)
130+
body := bytes.NewReader([]byte(requestURL))
131+
req, err := http.NewRequest(http.MethodPost, r.url, body)
132+
if err != nil {
133+
return nil, err
134+
}
135+
req.Header.Set("content-type", "application/json")
136+
req.Header.Set("accept-encoding", "identity")
137+
138+
res, err := http.DefaultClient.Do(req)
139+
if err != nil {
140+
return nil, err
141+
}
142+
143+
content, err := io.ReadAll(res.Body)
144+
if err != nil {
145+
return nil, err
146+
}
147+
148+
var resp RpcResult
149+
err = json.Unmarshal(content, &resp)
150+
if err != nil {
151+
return nil, err
152+
}
153+
if resp.Error != nil {
154+
return nil, fmt.Errorf("%s", resp.Error)
155+
}
156+
157+
return resp.Result, nil
158+
}
159+
160+
func (r *RpcClient) EthBlockNumber() (string, error) {
161+
rpcRes, err := r.request("eth_blockNumber", "[]")
162+
if err != nil {
163+
return "", err
164+
}
165+
166+
var blockNumber string
167+
err = json.Unmarshal(rpcRes, &blockNumber)
168+
if err != nil {
169+
return "", err
170+
}
171+
172+
return blockNumber, nil
173+
}
174+
175+
func (r *RpcClient) EthGetBlockByNumber(blockNumber string) (string, error) {
176+
rpcRes, err := r.request("eth_getBlockByNumber", fmt.Sprintf(`["%s",true]`, blockNumber))
177+
if err != nil {
178+
return "", err
179+
}
180+
181+
return string(rpcRes), nil
182+
}
183+
184+
func (r *RpcClient) EthGetBlockReceipts(blockNumber string) ([]map[string]any, error) {
185+
rpcRes, err := r.request("eth_getBlockReceipts", fmt.Sprintf(`["%s"]`, blockNumber))
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
var blockReceipts []map[string]any
191+
err = json.Unmarshal(rpcRes, &blockReceipts)
192+
if err != nil {
193+
return nil, err
194+
}
195+
196+
return blockReceipts, nil
197+
}
198+
199+
func (r *RpcClient) DebugTraceBlockByNumber(blockNumber string) (string, error) {
200+
tracerConfig := `{"tracer":"callTracer","tracerConfig":{"withLog":true,"onlyTopCall":false}}`
201+
rpcRes, err := r.request("debug_traceBlockByNumber", fmt.Sprintf(`["%s",%s]`, blockNumber, tracerConfig))
202+
if err != nil {
203+
return "", err
204+
}
205+
206+
return string(rpcRes), nil
207+
}
208+
209+
func (r *RpcClient) DebugTraceTransaction(txHash string) (string, error) {
210+
tracerConfig := `{"tracer":"callTracer","tracerConfig":{"withLog":true,"onlyTopCall":false}}`
211+
rpcRes, err := r.request("debug_traceTransaction", fmt.Sprintf(`["%s",%s]`, txHash, tracerConfig))
212+
if err != nil {
213+
return "", err
214+
}
215+
216+
return string(rpcRes), nil
217+
}
218+
219+
func (r *RpcClient) EthGetBalance(address string) (*big.Int, error) {
220+
balanceRes, err := r.request("eth_getBalance", fmt.Sprintf(`["%s", "latest"]`, address))
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
var balance hexutil.Big
226+
err = json.Unmarshal(balanceRes, &balance)
227+
if err != nil {
228+
return nil, err
229+
}
230+
231+
return balance.ToInt(), nil
232+
}
233+
234+
func (r *RpcClient) EthGetReceipt(hash string) (*types.Receipt, error) {
235+
rpcRes, err := r.request("eth_getTransactionReceipt", fmt.Sprintf(`["%s"]`, hash))
236+
if err != nil {
237+
return nil, err
238+
}
239+
240+
var rcp types.Receipt
241+
err = json.Unmarshal(rpcRes, &rcp)
242+
if err != nil {
243+
return nil, err
244+
}
245+
246+
return &rcp, nil
247+
}
248+
249+
func (r *RpcClient) EthCall() (string, error) {
250+
callParams := `{"from":"0x980DbdE8EC2cFebFA46660778afC1cc182EaEADa","to":"0xf19fD4347CafAdd409a9fA090b3AD068272035a1","gas":"0x3CFA39","value":"0x0","input":"0x5148e0ba00000000000000000000000000000000000000000000006c6b935b8bbd400000000000000000000000000000000000000000000000000000000000000000037800000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000"}`
251+
rpcRes, err := r.request("eth_call", fmt.Sprintf(`[%s,"latest"]`, callParams))
252+
if err != nil {
253+
return "", err
254+
}
255+
256+
return string(rpcRes), nil
257+
}
258+
259+
var (
260+
evmGatewayHost string
261+
workers int
262+
)
263+
264+
func init() {
265+
Cmd.Flags().StringVar(&evmGatewayHost, "evm-gw-host", "http://127.0.0.1:8545", "EVM Gateway host against which to run the load test")
266+
Cmd.Flags().IntVar(&workers, "workers", 10, "Number of workers to use for concurrent JSON-RPC requests")
267+
268+
if err := Cmd.MarkFlagRequired("evm-gw-host"); err != nil {
269+
panic(err)
270+
}
271+
}

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/onflow/flow-evm-gateway/cmd/blocks"
77
"github.com/onflow/flow-evm-gateway/cmd/export"
8+
"github.com/onflow/flow-evm-gateway/cmd/load_test"
89
"github.com/onflow/flow-evm-gateway/cmd/run"
910
"github.com/onflow/flow-evm-gateway/cmd/version"
1011
"github.com/rs/zerolog/log"
@@ -28,6 +29,7 @@ func main() {
2829
rootCmd.AddCommand(export.Cmd)
2930
rootCmd.AddCommand(blocks.Cmd)
3031
rootCmd.AddCommand(run.Cmd)
32+
rootCmd.AddCommand(load_test.Cmd)
3133

3234
Execute()
3335
}

0 commit comments

Comments
 (0)