From c74f46fdac9441898aef3fd7bd5b72caa2c1715c Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 7 May 2026 12:48:32 +0800 Subject: [PATCH 1/2] add EVM stress workload tooling --- .gitignore | 1 + scripts/evm_stress.sh | 125 ++++++++++++++++++++++++++++++ scripts/evm_stress/main.go | 154 +++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100755 scripts/evm_stress.sh create mode 100644 scripts/evm_stress/main.go diff --git a/.gitignore b/.gitignore index 9ba0cfe438..4098f30577 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ release/ .DS_Store build/ cache/ +evm_stress *.iml # Local .terraform directories diff --git a/scripts/evm_stress.sh b/scripts/evm_stress.sh new file mode 100755 index 0000000000..dd8695c5ba --- /dev/null +++ b/scripts/evm_stress.sh @@ -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" diff --git a/scripts/evm_stress/main.go b/scripts/evm_stress/main.go new file mode 100644 index 0000000000..aec30cc670 --- /dev/null +++ b/scripts/evm_stress/main.go @@ -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) + } +} + +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) +} From 93fda38202bc2b113069cf41b7539333d4c0e66e Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 12 May 2026 11:07:17 +0800 Subject: [PATCH 2/2] fix gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4098f30577..03aedfc3b1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ release/ .DS_Store build/ cache/ -evm_stress +./evm_stress *.iml # Local .terraform directories