feat(tools): speedtest (#25555)

Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io>
This commit is contained in:
Tyler 2025-11-17 06:04:42 -08:00 committed by GitHub
parent 0d6228eaab
commit 94aebcd8ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 273 additions and 0 deletions

View File

@ -113,6 +113,7 @@ func initRootCmd(
confixcmd.ConfigCommand(),
pruning.Cmd(newApp, simapp.DefaultNodeHome),
snapshot.Cmd(newApp),
NewBankSpeedTest(),
)
server.AddCommandsWithStartCmdOptions(rootCmd, simapp.DefaultNodeHome, newApp, appExport, server.StartCmdOptions{

View File

@ -0,0 +1,105 @@
package cmd
import (
"math/rand"
"os"
"time"
dbm "github.com/cosmos/cosmos-db"
"github.com/spf13/cobra"
"cosmossdk.io/log"
"cosmossdk.io/simapp"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/tools/speedtest"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
func NewBankSpeedTest() *cobra.Command {
dir, err := os.MkdirTemp("", "bankspeedtest-*")
if err != nil {
panic(err)
}
db, err := dbm.NewDB("app", dbm.PebbleDBBackend, dir)
if err != nil {
panic(err)
}
chainID := "foo"
app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.NewAppOptionsWithFlagHome(dir), baseapp.SetChainID(chainID))
gen := generator{
app: app,
accounts: make([]accountInfo, 0),
}
cmd := speedtest.NewCmd(gen.createAccount, gen.generateTx, app, app.AppCodec(), app.DefaultGenesis(), chainID)
cmd.PostRunE = func(_ *cobra.Command, _ []string) error {
return os.RemoveAll(dir)
}
return cmd
}
type generator struct {
app *simapp.SimApp
accounts []accountInfo
}
type accountInfo struct {
privKey cryptotypes.PrivKey
address sdk.AccAddress
accNum uint64
seqNum uint64
}
func (g *generator) createAccount() (*authtypes.BaseAccount, sdk.Coins) {
privKey := secp256k1.GenPrivKey()
addr := sdk.AccAddress(privKey.PubKey().Address())
accNum := len(g.accounts)
baseAcc := authtypes.NewBaseAccount(addr, privKey.PubKey(), uint64(accNum), 0)
g.accounts = append(g.accounts, accountInfo{
privKey: privKey,
address: addr,
accNum: uint64(accNum),
seqNum: 0,
})
return baseAcc, sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1_000_000_000))
}
func (g *generator) generateTx() []byte {
senderIdx := r.Intn(len(g.accounts))
recipientIdx := (senderIdx + 1 + r.Intn(len(g.accounts)-1)) % len(g.accounts)
sender := g.accounts[senderIdx]
recipient := g.accounts[recipientIdx]
sendAmount := sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1))
msg := banktypes.NewMsgSend(sender.address, recipient.address, sendAmount)
txConfig := g.app.TxConfig()
// Build and sign transaction
tx, err := simtestutil.GenSignedMockTx(
r,
txConfig,
[]sdk.Msg{msg},
sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)),
simtestutil.DefaultGenTxGas,
g.app.ChainID(),
[]uint64{sender.accNum},
[]uint64{sender.seqNum},
sender.privKey,
)
if err != nil {
panic(err)
}
txBytes, err := txConfig.TxEncoder()(tx)
if err != nil {
panic(err)
}
g.accounts[senderIdx].seqNum++
return txBytes
}

View File

@ -0,0 +1,167 @@
package speedtest
import (
"encoding/json"
"fmt"
"math"
"time"
"github.com/cometbft/cometbft/abci/types"
cmtjson "github.com/cometbft/cometbft/libs/json"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/codec"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
type AccountCreator func() (*authtypes.BaseAccount, sdk.Coins)
type GenerateTx func() []byte
var (
numAccounts = 10_000
numTxsPerBlock = 4_000
numBlocksToRun = 100
blockMaxGas = math.MaxInt64
blockMaxBytes = math.MaxInt64
verifyTxs = false
)
// NewCmd returns a command that will run an execution test on your application.
// Balances and accounts are automatically added to the chain's state via AccountCreator.
func NewCmd(
createAccount AccountCreator,
generateTx GenerateTx,
app servertypes.ABCI,
cdc codec.Codec,
genState map[string]json.RawMessage,
chainID string,
) *cobra.Command {
cmd := &cobra.Command{
Use: "speedtest",
Short: "execution speedtest",
Long: "speedtest is a tool for measuring raw execution TPS of your application",
Example: "speedtest --accounts 20000 --txs 2000 --blocks 10 --block-max-gas 1000000000 --block-max-bytes 1000000000 --verify-txs",
RunE: func(cmd *cobra.Command, args []string) error {
accounts := make([]simtestutil.GenesisAccount, 0, numAccounts)
balances := make([]banktypes.Balance, 0, numAccounts)
for range numAccounts {
account, balance := createAccount()
genesisAcc := simtestutil.GenesisAccount{
GenesisAccount: account,
Coins: balance,
}
accounts = append(accounts, genesisAcc)
balances = append(balances, banktypes.Balance{
Address: account.Address,
Coins: balance,
})
}
vals, err := simtestutil.CreateRandomValidatorSet()
if err != nil {
return err
}
genAccs := make([]authtypes.GenesisAccount, 0, len(accounts))
for _, acc := range accounts {
genAccs = append(genAccs, acc.GenesisAccount)
}
genesisState, err := simtestutil.GenesisStateWithValSet(cdc, genState, vals, genAccs, balances...)
if err != nil {
return err
}
// init chain must be called to stop deliverState from being nil
stateBytes, err := cmtjson.MarshalIndent(genesisState, "", " ")
if err != nil {
return err
}
cp := simtestutil.DefaultConsensusParams
cp.Block.MaxGas = int64(blockMaxGas)
cp.Block.MaxBytes = int64(blockMaxBytes)
_, err = app.InitChain(&types.RequestInitChain{
ChainId: chainID,
Validators: []types.ValidatorUpdate{},
ConsensusParams: cp,
AppStateBytes: stateBytes,
})
if err != nil {
return fmt.Errorf("failed to InitChain: %w", err)
}
// commit genesis changes
_, err = app.FinalizeBlock(&types.RequestFinalizeBlock{
Height: 1,
NextValidatorsHash: vals.Hash(),
})
if err != nil {
return fmt.Errorf("failed to finalize genesis block: %w", err)
}
blocks := make([][][]byte, 0, numBlocksToRun)
for range numBlocksToRun {
block := make([][]byte, 0, numBlocksToRun)
for range numTxsPerBlock {
tx := generateTx()
block = append(block, tx)
}
blocks = append(blocks, block)
}
elapsed, err := runBlocks(blocks, app, vals.Proposer.Address, verifyTxs)
if err != nil {
return fmt.Errorf("failed to run blocks: %w", err)
}
cmd.Printf("Finished %d blocks in %s\n", numBlocksToRun, elapsed)
numTxs := numBlocksToRun * numTxsPerBlock
tps := float64(numTxs) / elapsed.Seconds()
cmd.Printf("TPS: %f", tps)
return nil
},
}
cmd.Flags().IntVar(&numAccounts, "accounts", numAccounts, "number of accounts")
cmd.Flags().IntVar(&numTxsPerBlock, "txs", numTxsPerBlock, "number of txs")
cmd.Flags().IntVar(&numBlocksToRun, "blocks", numBlocksToRun, "number of blocks")
cmd.Flags().BoolVar(&verifyTxs, "verify-txs", verifyTxs, "verify txs passed. this will loop over all tx results and ensure the code == 0.")
cmd.Flags().IntVar(&blockMaxGas, "block-max-gas", blockMaxGas, "block max gas")
cmd.Flags().IntVar(&blockMaxBytes, "block-max-bytes", blockMaxBytes, "block max bytes")
return cmd
}
func runBlocks(blocks [][][]byte, app servertypes.ABCI, proposer []byte, verify bool) (time.Duration, error) {
start := time.Now()
height := int64(1)
for blockNum, txs := range blocks {
res, err := app.FinalizeBlock(&types.RequestFinalizeBlock{
Height: height,
Txs: txs,
Time: time.Now(),
ProposerAddress: proposer,
})
if err != nil {
return 0, fmt.Errorf("failed to finalize block #%d: %w", blockNum, err)
}
if verify {
for _, result := range res.TxResults {
if result.Code != 0 {
return 0, fmt.Errorf("tx failed in block %d: code=%d codespace=%s", blockNum, result.Code, result.Codespace)
}
}
}
_, err = app.Commit()
if err != nil {
return 0, fmt.Errorf("failed to commit block #%d: %w", blockNum, err)
}
height++
}
end := time.Since(start)
return end, nil
}