diff --git a/simapp/simd/cmd/commands.go b/simapp/simd/cmd/commands.go index a38d398a1a66..606a58239919 100644 --- a/simapp/simd/cmd/commands.go +++ b/simapp/simd/cmd/commands.go @@ -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{ diff --git a/simapp/simd/cmd/speedtest.go b/simapp/simd/cmd/speedtest.go new file mode 100644 index 000000000000..4f515785ae7c --- /dev/null +++ b/simapp/simd/cmd/speedtest.go @@ -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 +} diff --git a/tools/speedtest/speedtest.go b/tools/speedtest/speedtest.go new file mode 100644 index 000000000000..8a67a50dc15f --- /dev/null +++ b/tools/speedtest/speedtest.go @@ -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 +}