-
Notifications
You must be signed in to change notification settings - Fork 878
Add EVM stress workload tooling #3404
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ release/ | |
| .DS_Store | ||
| build/ | ||
| cache/ | ||
| ./evm_stress | ||
| *.iml | ||
|
|
||
| # Local .terraform directories | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| #!/usr/bin/env bash | ||
| # Spin up a local seid node, flood it with EVM transfers from N accounts to one | ||
| # recipient, and print only branch-specific logs + block time. | ||
| # | ||
| # Usage: ./scripts/evm_stress.sh | ||
| # Run from the repo root. | ||
| set -euo pipefail | ||
|
|
||
| REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | ||
| SEID="$HOME/go/bin/seid" | ||
| LOG_FILE="/tmp/seid_stress.log" | ||
|
|
||
| # Genesis balance per sender account. 10^12 usei ≈ unlimited for any test run. | ||
| SENDER_GENESIS_FUNDS="1000000000000" | ||
|
|
||
| cd "$REPO_ROOT" | ||
|
|
||
| cleanup() { | ||
| echo "" | ||
| echo "==> shutting down..." | ||
| [ -n "${SEID_PID:-}" ] && kill "$SEID_PID" 2>/dev/null || true | ||
| # Kill the entire process group so tail and grep children are also terminated. | ||
| [ -n "${LOG_PID:-}" ] && kill -- -"$LOG_PID" 2>/dev/null || true | ||
| } | ||
| trap cleanup EXIT INT TERM | ||
|
|
||
| # Kill any tail processes from previous runs that are still watching this log | ||
| # file. tail -F re-opens the file by name when it is truncated, so a stale | ||
| # tail would re-read the new run's log and emit duplicate lines. | ||
| pkill -f "tail.*${LOG_FILE}" 2>/dev/null || true | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 1. Init chain (no start) | ||
| # --------------------------------------------------------------------------- | ||
| echo "==> initializing chain..." | ||
| NO_RUN=1 ./scripts/initialize_local_chain.sh | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 2. Bulk-add sender accounts to genesis via direct JSON patch. | ||
| # go run -dump-sei-addrs generates all bech32 addresses; Python patches | ||
| # genesis.json in one pass (same technique as populate_genesis_accounts.py). | ||
| # --------------------------------------------------------------------------- | ||
| cat > /tmp/evm_stress_patch_genesis.py << 'PYEOF' | ||
| import json, sys | ||
|
|
||
| genesis_path = sys.argv[1] | ||
| amount = sys.argv[2] | ||
| denom = "usei" | ||
|
|
||
| addrs = [l.strip() for l in sys.stdin if l.strip()] | ||
|
|
||
| with open(genesis_path) as f: | ||
| g = json.load(f) | ||
|
|
||
| for addr in addrs: | ||
| g["app_state"]["auth"]["accounts"].append({ | ||
| "@type": "/cosmos.auth.v1beta1.BaseAccount", | ||
| "address": addr, | ||
| "pub_key": None, | ||
| "account_number": "0", | ||
| "sequence": "0", | ||
| }) | ||
| g["app_state"]["bank"]["balances"].append({ | ||
| "address": addr, | ||
| "coins": [{"denom": denom, "amount": amount}], | ||
| }) | ||
|
|
||
| with open(genesis_path, "w") as f: | ||
| json.dump(g, f, separators=(",", ":")) | ||
|
|
||
| print(f"Added {len(addrs)} accounts to genesis", file=sys.stderr) | ||
| PYEOF | ||
|
|
||
| echo "==> patching genesis with sender accounts..." | ||
| go run "$REPO_ROOT/scripts/evm_stress/main.go" -dump-sei-addrs \ | ||
| | python3 /tmp/evm_stress_patch_genesis.py \ | ||
| "$HOME/.sei/config/genesis.json" "$SENDER_GENESIS_FUNDS" | ||
| echo "==> genesis patched" | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 3. Start seid, capturing all output to log file | ||
| # --------------------------------------------------------------------------- | ||
| echo "==> starting seid (logs -> $LOG_FILE)..." | ||
| mkdir -p /tmp/race | ||
| GORACE="log_path=/tmp/race/seid_race" \ | ||
| "$SEID" start --trace --chain-id sei-chain > "$LOG_FILE" 2>&1 & | ||
| SEID_PID=$! | ||
| echo "==> seid PID: $SEID_PID" | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 4. Tail log file, printing only branch-specific messages | ||
| # - "occ scheduler key conflicts" from sei-cosmos/tasks/scheduler.go | ||
| # - "execution block time" from x/evm/keeper/abci.go | ||
| # --------------------------------------------------------------------------- | ||
| ( | ||
| tail -F "$LOG_FILE" 2>/dev/null \ | ||
| | grep --line-buffered -E \ | ||
| '"occ scheduler key conflicts"|"execution block time"' | ||
| ) & | ||
| LOG_PID=$! | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 5. Wait for the EVM RPC to accept connections | ||
| # --------------------------------------------------------------------------- | ||
| echo "==> waiting for EVM RPC at http://127.0.0.1:8545..." | ||
| for i in $(seq 1 60); do | ||
| if curl -sf -X POST http://127.0.0.1:8545 \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ | ||
| > /dev/null 2>&1; then | ||
| echo "==> EVM RPC ready (after ${i}s)" | ||
| break | ||
| fi | ||
| if [ "$i" -eq 60 ]; then | ||
| echo "ERROR: EVM RPC not ready after 60s" >&2 | ||
| exit 1 | ||
| fi | ||
| sleep 1 | ||
| done | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 6. Run the Go load tester | ||
| # --------------------------------------------------------------------------- | ||
| echo "==> starting EVM transfer stress test (target 500 TPS, 50k unique senders)..." | ||
| go run "$REPO_ROOT/scripts/evm_stress/main.go" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "crypto/ecdsa" | ||
| "flag" | ||
| "fmt" | ||
| "math/big" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/ethereum/go-ethereum/common" | ||
| "github.com/ethereum/go-ethereum/core/types" | ||
| "github.com/ethereum/go-ethereum/crypto" | ||
| "github.com/ethereum/go-ethereum/ethclient" | ||
|
|
||
| seibech32 "github.com/sei-protocol/sei-chain/sei-cosmos/types/bech32" | ||
| ) | ||
|
|
||
| const ( | ||
| evmRPC = "http://127.0.0.1:8545" | ||
| chainID = 713714 // default EVM chain ID for "sei-chain" | ||
| targetTPS = 500 | ||
| numWorkers = 250 | ||
|
|
||
| // Total unique sender accounts pre-funded in genesis. Every tx sent by | ||
| // the stress test has a distinct sender with nonce=0. At targetTPS, the | ||
| // pool lasts totalAccounts/targetTPS seconds. | ||
| totalAccounts = 50_000 | ||
| ) | ||
|
|
||
| var ( | ||
| bigChainID = big.NewInt(chainID) | ||
| signer = types.NewLondonSigner(bigChainID) | ||
| maxFee = big.NewInt(1_000_000_000_000) // 1000 gwei | ||
| priorityFee = big.NewInt(1_000_000_000) // 1 gwei | ||
| txValue = big.NewInt(1_000_000_000_001) // 10^12+1 wei: touches both usei balance and wei remainder | ||
| ) | ||
|
|
||
| // nextKey returns a unique deterministic private key for the given index. | ||
| func nextKey(idx uint64) *ecdsa.PrivateKey { | ||
| seed := make([]byte, 32) | ||
| // use upper 8 bytes for the index so seed is never all-zero | ||
| seed[0] = 0x01 | ||
| for i := 0; i < 8; i++ { | ||
| seed[1+i] = byte(idx >> (56 - 8*i)) | ||
| } | ||
| key, err := crypto.ToECDSA(seed) | ||
| if err != nil { | ||
| panic(fmt.Sprintf("bad key seed %d: %v", idx, err)) | ||
| } | ||
| return key | ||
| } | ||
|
|
||
| func keyAddr(key *ecdsa.PrivateKey) common.Address { | ||
| return crypto.PubkeyToAddress(key.PublicKey) | ||
| } | ||
|
|
||
| func evmToSei(addr common.Address) string { | ||
| s, err := seibech32.ConvertAndEncode("sei", addr.Bytes()) | ||
| if err != nil { | ||
| panic(fmt.Sprintf("bech32 encode: %v", err)) | ||
| } | ||
| return s | ||
| } | ||
|
|
||
| func signTx(tx *types.Transaction, key *ecdsa.PrivateKey) *types.Transaction { | ||
| signed, err := types.SignTx(tx, signer, key) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
| return signed | ||
| } | ||
|
|
||
| func transfer(nonce uint64, to common.Address, key *ecdsa.PrivateKey) *types.Transaction { | ||
| return signTx(types.NewTx(&types.DynamicFeeTx{ | ||
| ChainID: bigChainID, | ||
| Nonce: nonce, | ||
| GasTipCap: priorityFee, | ||
| GasFeeCap: maxFee, | ||
| Gas: 21_000, | ||
| To: &to, | ||
| Value: txValue, | ||
| }), key) | ||
| } | ||
|
|
||
| func waitForBalance(ctx context.Context, client *ethclient.Client, addr common.Address) { | ||
| fmt.Printf("waiting for %s to have balance...\n", addr.Hex()) | ||
| for { | ||
| bal, err := client.BalanceAt(ctx, addr, nil) | ||
| if err == nil && bal.Sign() > 0 { | ||
| fmt.Printf(" %s: %s wei\n", addr.Hex(), bal.String()) | ||
| return | ||
| } | ||
| time.Sleep(300 * time.Millisecond) | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Infinite loop in
|
||
|
|
||
| func main() { | ||
| dumpSeiAddrs := flag.Bool("dump-sei-addrs", false, "print sender sei bech32 addresses for genesis funding and exit") | ||
| flag.Parse() | ||
|
|
||
| // Key 0 = recipient; keys 1..totalAccounts = one-time genesis-funded senders. | ||
| recipient := keyAddr(nextKey(0)) | ||
|
|
||
| if *dumpSeiAddrs { | ||
| for i := uint64(1); i <= totalAccounts; i++ { | ||
| fmt.Println(evmToSei(keyAddr(nextKey(i)))) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| ctx := context.Background() | ||
| client, err := ethclient.Dial(evmRPC) | ||
| if err != nil { | ||
| panic(fmt.Sprintf("dial %s: %v", evmRPC, err)) | ||
| } | ||
| defer client.Close() | ||
|
|
||
| fmt.Printf("recipient: %s\n", recipient.Hex()) | ||
|
|
||
| // Wait for genesis accounts to have balance — confirms the node is live. | ||
| waitForBalance(ctx, client, keyAddr(nextKey(1))) | ||
|
|
||
| // Pre-fill the work queue. Each key is used for exactly one tx (nonce=0). | ||
| funded := make(chan *ecdsa.PrivateKey, totalAccounts) | ||
| for i := uint64(1); i <= totalAccounts; i++ { | ||
| funded <- nextKey(i) | ||
| } | ||
| close(funded) | ||
|
|
||
| // Shared rate limiter across all workers: one tick per tx slot. | ||
| ticker := time.NewTicker(time.Second / time.Duration(targetTPS)) | ||
| defer ticker.Stop() | ||
|
|
||
| fmt.Printf("starting %d workers, %d unique senders, target %d TPS\n", | ||
| numWorkers, totalAccounts, targetTPS) | ||
|
|
||
| var wg sync.WaitGroup | ||
| for range numWorkers { | ||
| wg.Add(1) | ||
| go func() { | ||
| defer wg.Done() | ||
| for key := range funded { | ||
| <-ticker.C | ||
| tx := transfer(0, recipient, key) | ||
| _ = client.SendTransaction(ctx, tx) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| wg.Wait() | ||
| fmt.Printf("all %d accounts exhausted\n", totalAccounts) | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleanup fails to kill log-tailing child processes
Medium Severity
kill -- -"$LOG_PID"attempts to send a signal to the process group identified by$LOG_PID, but in a non-interactive shell script (noset -m), background subshells do not become process group leaders — they inherit the parent's PGID. So there is no process group with PGID equal to$LOG_PID, thekillsilently fails (masked by2>/dev/null || true), and thetail/grepchildren are left orphaned. The comment explicitly states the intent to "kill the entire process group so tail and grep children are also terminated," but this doesn't happen.Reviewed by Cursor Bugbot for commit 93fda38. Configure here.