Skip to content
115 changes: 95 additions & 20 deletions evmd/cmd/evmd/cmd/testnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bufio"
"encoding/json"
"fmt"
"strconv"
"strings"

"net"
"os"
Expand Down Expand Up @@ -32,7 +34,7 @@ import (
srvconfig "github.com/cosmos/cosmos-sdk/server/config"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
"github.com/cosmos/cosmos-sdk/testutil"
"github.com/cosmos/cosmos-sdk/testutil/network"
sdknetwork "github.com/cosmos/cosmos-sdk/testutil/network"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
Expand All @@ -47,6 +49,7 @@ import (
cosmosevmhd "github.com/cosmos/evm/crypto/hd"
cosmosevmkeyring "github.com/cosmos/evm/crypto/keyring"
"github.com/cosmos/evm/evmd"
customnetwork "github.com/cosmos/evm/evmd/tests/network"
cosmosevmserverconfig "github.com/cosmos/evm/server/config"
evmnetwork "github.com/cosmos/evm/testutil/integration/evm/network"
evmtypes "github.com/cosmos/evm/x/vm/types"
Expand All @@ -66,6 +69,7 @@ var (
flagPrintMnemonic = "print-mnemonic"
flagSingleHost = "single-host"
flagCommitTimeout = "commit-timeout"
flagValidatorPowers = "validator-powers"
unsafeStartValidatorFn UnsafeStartValidatorCmdCreator
)

Expand All @@ -92,20 +96,22 @@ type initArgs struct {
startingIPAddress string
singleMachine bool
useDocker bool
validatorPowers []int64
}

type startArgs struct {
algo string
apiAddress string
chainID string
enableLogging bool
grpcAddress string
minGasPrices string
numValidators int
outputDir string
printMnemonic bool
rpcAddress string
timeoutCommit time.Duration
algo string
apiAddress string
chainID string
enableLogging bool
grpcAddress string
minGasPrices string
numValidators int
outputDir string
printMnemonic bool
rpcAddress string
timeoutCommit time.Duration
validatorPowers []int64
}

func addTestnetFlagsToCmd(cmd *cobra.Command) {
Expand All @@ -114,6 +120,7 @@ func addTestnetFlagsToCmd(cmd *cobra.Command) {
cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created")
cmd.Flags().String(server.FlagMinGasPrices, fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)")
cmd.Flags().String(flags.FlagKeyType, string(cosmosevmhd.EthSecp256k1Type), "Key signing algorithm to generate keys for")
cmd.Flags().String(flagValidatorPowers, "", "Comma-separated list of validator powers (e.g. '100,200,150'). If not specified, all validators have equal power of 100.")

// support old flags name for backwards compatibility
cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
Expand Down Expand Up @@ -193,6 +200,12 @@ Example:
}
args.algo, _ = cmd.Flags().GetString(flags.FlagKeyType)

validatorPowersStr, _ := cmd.Flags().GetString(flagValidatorPowers)
args.validatorPowers, err = parseValidatorPowers(validatorPowersStr, args.numValidators)
if err != nil {
return err
}

return initTestnetFiles(clientCtx, cmd, config, mbm, genBalIterator, args)
},
}
Expand Down Expand Up @@ -234,6 +247,13 @@ Example:
args.grpcAddress, _ = cmd.Flags().GetString(flagGRPCAddress)
args.printMnemonic, _ = cmd.Flags().GetBool(flagPrintMnemonic)

validatorPowersStr, _ := cmd.Flags().GetString(flagValidatorPowers)
var err error
args.validatorPowers, err = parseValidatorPowers(validatorPowersStr, args.numValidators)
if err != nil {
return err
}

return startTestnet(cmd, args)
},
}
Expand All @@ -249,6 +269,53 @@ Example:

const nodeDirPerm = 0o755

// parseValidatorPowers parses a comma-separated list of validator powers from a string.
// If the string is empty, returns a slice of default powers (100) for each validator.
// If fewer powers are specified than numValidators, fills remaining with the last specified power.
func parseValidatorPowers(powersStr string, numValidators int) ([]int64, error) {
powers := make([]int64, numValidators)
if powersStr == "" {
for i := 0; i < numValidators; i++ {
powers[i] = 100
}
return powers, nil
}

powerStrs := strings.Split(powersStr, ",")
parsedPowers := make([]int64, 0, len(powerStrs))

for _, powerStr := range powerStrs {
powerStr = strings.TrimSpace(powerStr)
if powerStr == "" {
continue
}

power, err := strconv.ParseInt(powerStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid validator power '%s': %w", powerStr, err)
}
if power <= 0 {
return nil, fmt.Errorf("validator power must be positive, got %d", power)
}
parsedPowers = append(parsedPowers, power)
}

if len(parsedPowers) == 0 {
return nil, fmt.Errorf("no valid validator powers specified")
}

for i := 0; i < numValidators; i++ {
if i < len(parsedPowers) {
powers[i] = parsedPowers[i]
} else {
// use the last specified power for remaining validators
powers[i] = parsedPowers[len(parsedPowers)-1]
}
}

return powers, nil
}

// initTestnetFiles initializes testnet files for a testnet to be run in a separate process
func initTestnetFiles(
clientCtx client.Context,
Expand Down Expand Up @@ -407,7 +474,7 @@ func initTestnetFiles(
genBalances = append(genBalances, bals...)
genAccounts = append(genAccounts, accs...)
}
valTokens := sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction)
valTokens := sdk.TokensFromConsensusPower(args.validatorPowers[i], sdk.DefaultPowerReduction)
createValMsg, err := stakingtypes.NewMsgCreateValidator(
sdk.ValAddress(addr).String(),
valPubKeys[i],
Expand Down Expand Up @@ -642,7 +709,7 @@ func writeFile(name string, dir string, contents []byte) error {

// startTestnet starts an in-process testnet
func startTestnet(cmd *cobra.Command, args startArgs) error {
networkConfig := network.DefaultConfig(NewTestNetworkFixture)
networkConfig := customnetwork.DefaultConfig()

// Default networkConfig.ChainID is random, and we should only override it if chainID provided
// is non-empty
Expand All @@ -652,12 +719,20 @@ func startTestnet(cmd *cobra.Command, args startArgs) error {
networkConfig.SigningAlgo = args.algo
networkConfig.MinGasPrices = args.minGasPrices
networkConfig.NumValidators = args.numValidators
networkConfig.EnableLogging = args.enableLogging
networkConfig.EnableCMTLogging = args.enableLogging
networkConfig.RPCAddress = args.rpcAddress
networkConfig.APIAddress = args.apiAddress
networkConfig.GRPCAddress = args.grpcAddress
networkConfig.PrintMnemonic = args.printMnemonic
networkLogger := network.NewCLILogger(cmd)

// Convert validator powers to bonded tokens
bondedTokensPerValidator := make([]math.Int, len(args.validatorPowers))
for i, power := range args.validatorPowers {
bondedTokensPerValidator[i] = sdk.TokensFromConsensusPower(power, sdk.DefaultPowerReduction)
}
networkConfig.BondedTokensPerValidator = bondedTokensPerValidator

networkLogger := customnetwork.NewCLILogger(cmd)

baseDir := fmt.Sprintf("%s/%s", args.outputDir, networkConfig.ChainID)
if _, err := os.Stat(baseDir); !os.IsNotExist(err) {
Expand All @@ -666,7 +741,7 @@ func startTestnet(cmd *cobra.Command, args startArgs) error {
networkConfig.ChainID, baseDir)
}

testnet, err := network.New(networkLogger, baseDir, networkConfig)
testnet, err := customnetwork.New(networkLogger, baseDir, networkConfig)
if err != nil {
return err
}
Expand All @@ -684,7 +759,7 @@ func startTestnet(cmd *cobra.Command, args startArgs) error {
}

// NewTestNetworkFixture returns a new evmd AppConstructor for network simulation tests
func NewTestNetworkFixture() network.TestFixture {
func NewTestNetworkFixture() sdknetwork.TestFixture {
dir, err := os.MkdirTemp("", "evm")
if err != nil {
panic(fmt.Sprintf("failed creating temporary directory: %v", err))
Expand All @@ -699,7 +774,7 @@ func NewTestNetworkFixture() network.TestFixture {
simtestutil.EmptyAppOptions{},
)

appCtr := func(val network.ValidatorI) servertypes.Application {
appCtr := func(val sdknetwork.ValidatorI) servertypes.Application {
return evmd.NewExampleApp(
log.NewNopLogger(),
dbm.NewMemDB(),
Expand All @@ -709,7 +784,7 @@ func NewTestNetworkFixture() network.TestFixture {
)
}

return network.TestFixture{
return sdknetwork.TestFixture{
AppConstructor: appCtr,
GenesisState: app.DefaultGenesis(),
EncodingConfig: moduletestutil.TestEncodingConfig{
Expand Down
100 changes: 100 additions & 0 deletions evmd/cmd/evmd/cmd/testnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestParseValidatorPowers(t *testing.T) {
tests := []struct {
name string
powersStr string
numValidators int
want []int64
wantErr bool
}{
{
name: "empty string - use defaults",
powersStr: "",
numValidators: 4,
want: []int64{100, 100, 100, 100},
wantErr: false,
},
{
name: "exact number of powers",
powersStr: "100,200,150,300",
numValidators: 4,
want: []int64{100, 200, 150, 300},
wantErr: false,
},
{
name: "fewer powers than validators",
powersStr: "100,200",
numValidators: 5,
want: []int64{100, 200, 200, 200, 200},
wantErr: false,
},
{
name: "single power for all validators",
powersStr: "500",
numValidators: 3,
want: []int64{500, 500, 500},
wantErr: false,
},
{
name: "more powers than validators",
powersStr: "100,200,300,400",
numValidators: 2,
want: []int64{100, 200},
wantErr: false,
},
{
name: "invalid power - not a number",
powersStr: "100,abc,300",
numValidators: 3,
want: nil,
wantErr: true,
},
{
name: "invalid power - negative",
powersStr: "100,-200,300",
numValidators: 3,
want: nil,
wantErr: true,
},
{
name: "invalid power - zero",
powersStr: "100,0,300",
numValidators: 3,
want: nil,
wantErr: true,
},
{
name: "with spaces",
powersStr: "100, 200, 300",
numValidators: 3,
want: []int64{100, 200, 300},
wantErr: false,
},
{
name: "trailing comma",
powersStr: "100,200,300,",
numValidators: 3,
want: []int64{100, 200, 300},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseValidatorPowers(tt.powersStr, tt.numValidators)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.want, got)
}
})
}
}
Loading
Loading