diff --git a/CHANGELOG.md b/CHANGELOG.md index e637abc14..7963de6c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ ### IMPROVEMENTS +- [\#708](https://github.com/cosmos/evm/pull/708) Add configurable testnet validator powers - [\#698](https://github.com/cosmos/evm/pull/698) Expose mempool configuration flags and move mempool configuration in app.go to helper - [\#538](https://github.com/cosmos/evm/pull/538) Optimize `eth_estimateGas` gRPC path: short-circuit plain transfers, add optimistic gas bound based on `MaxUsedGas`. - [\#513](https://github.com/cosmos/evm/pull/513) Replace `TestEncodingConfig` with production `EncodingConfig` in encoding package to remove test dependencies from production code. diff --git a/evmd/cmd/evmd/cmd/testnet.go b/evmd/cmd/evmd/cmd/testnet.go index 5c6d018d2..9497c4cbf 100644 --- a/evmd/cmd/evmd/cmd/testnet.go +++ b/evmd/cmd/evmd/cmd/testnet.go @@ -4,7 +4,6 @@ import ( "bufio" "encoding/json" "fmt" - "net" "os" "path/filepath" @@ -32,7 +31,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" @@ -47,6 +46,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" @@ -66,6 +66,7 @@ var ( flagPrintMnemonic = "print-mnemonic" flagSingleHost = "single-host" flagCommitTimeout = "commit-timeout" + flagValidatorPowers = "validator-powers" unsafeStartValidatorFn UnsafeStartValidatorCmdCreator ) @@ -92,20 +93,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) { @@ -114,6 +117,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().IntSlice(flagValidatorPowers, []int{}, "Comma-separated list of validator powers (e.g. '100,200,150'). If not specified, all validators have equal power of 100. Last value is repeated for remaining validators.") // support old flags name for backwards compatibility cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { @@ -193,6 +197,12 @@ Example: } args.algo, _ = cmd.Flags().GetString(flags.FlagKeyType) + validatorPowers, _ := cmd.Flags().GetIntSlice(flagValidatorPowers) + args.validatorPowers, err = parseValidatorPowers(validatorPowers, args.numValidators) + if err != nil { + return err + } + return initTestnetFiles(clientCtx, cmd, config, mbm, genBalIterator, args) }, } @@ -234,6 +244,13 @@ Example: args.grpcAddress, _ = cmd.Flags().GetString(flagGRPCAddress) args.printMnemonic, _ = cmd.Flags().GetBool(flagPrintMnemonic) + validatorPowers, _ := cmd.Flags().GetIntSlice(flagValidatorPowers) + var err error + args.validatorPowers, err = parseValidatorPowers(validatorPowers, args.numValidators) + if err != nil { + return err + } + return startTestnet(cmd, args) }, } @@ -249,6 +266,36 @@ Example: const nodeDirPerm = 0o755 +// parseValidatorPowers processes validator powers from an int slice. +// If the slice 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(powers []int, numValidators int) ([]int64, error) { + result := make([]int64, numValidators) + if len(powers) == 0 { + for i := 0; i < numValidators; i++ { + result[i] = 100 + } + return result, nil + } + + for _, power := range powers { + if power <= 0 { + return nil, fmt.Errorf("validator power must be positive, got %d", power) + } + } + + for i := 0; i < numValidators; i++ { + if i < len(powers) { + result[i] = int64(powers[i]) + } else { + // use the last specified power for remaining validators + result[i] = int64(powers[len(powers)-1]) + } + } + + return result, nil +} + // initTestnetFiles initializes testnet files for a testnet to be run in a separate process func initTestnetFiles( clientCtx client.Context, @@ -407,7 +454,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], @@ -642,7 +689,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 @@ -652,12 +699,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) { @@ -666,7 +721,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 } @@ -684,7 +739,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)) @@ -699,7 +754,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(), @@ -709,7 +764,7 @@ func NewTestNetworkFixture() network.TestFixture { ) } - return network.TestFixture{ + return sdknetwork.TestFixture{ AppConstructor: appCtr, GenesisState: app.DefaultGenesis(), EncodingConfig: moduletestutil.TestEncodingConfig{ diff --git a/evmd/cmd/evmd/cmd/testnet_test.go b/evmd/cmd/evmd/cmd/testnet_test.go new file mode 100644 index 000000000..0b0abe032 --- /dev/null +++ b/evmd/cmd/evmd/cmd/testnet_test.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseValidatorPowers(t *testing.T) { + tests := []struct { + name string + powers []int + numValidators int + want []int64 + wantErr bool + }{ + { + name: "empty slice - use defaults", + powers: []int{}, + numValidators: 4, + want: []int64{100, 100, 100, 100}, + wantErr: false, + }, + { + name: "nil slice - use defaults", + powers: nil, + numValidators: 4, + want: []int64{100, 100, 100, 100}, + wantErr: false, + }, + { + name: "exact number of powers", + powers: []int{100, 200, 150, 300}, + numValidators: 4, + want: []int64{100, 200, 150, 300}, + wantErr: false, + }, + { + name: "fewer powers than validators", + powers: []int{100, 200}, + numValidators: 5, + want: []int64{100, 200, 200, 200, 200}, + wantErr: false, + }, + { + name: "single power for all validators", + powers: []int{500}, + numValidators: 3, + want: []int64{500, 500, 500}, + wantErr: false, + }, + { + name: "more powers than validators", + powers: []int{100, 200, 300, 400}, + numValidators: 2, + want: []int64{100, 200}, + wantErr: false, + }, + { + name: "invalid power - negative", + powers: []int{100, -200, 300}, + numValidators: 3, + want: nil, + wantErr: true, + }, + { + name: "invalid power - zero", + powers: []int{100, 0, 300}, + numValidators: 3, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseValidatorPowers(tt.powers, tt.numValidators) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/evmd/tests/network/network.go b/evmd/tests/network/network.go index 8a61762d9..40c00875d 100644 --- a/evmd/tests/network/network.go +++ b/evmd/tests/network/network.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/cosmos/evm/utils" "net/http" "net/url" "os" @@ -18,6 +17,8 @@ import ( "testing" "time" + "github.com/cosmos/evm/utils" + "github.com/ethereum/go-ethereum/ethclient" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -71,32 +72,33 @@ type AppConstructor = func(val Validator) servertypes.Application // Config defines the necessary configuration used to bootstrap and start an // in-process local testing network. type Config struct { - KeyringOptions []keyring.Option // keyring configuration options - Codec codec.Codec - LegacyAmino *codec.LegacyAmino // TODO: Remove! - InterfaceRegistry codectypes.InterfaceRegistry - TxConfig client.TxConfig - AccountRetriever client.AccountRetriever - AppConstructor AppConstructor // the ABCI application constructor - GenesisState evmtestutil.GenesisState // custom gensis state to provide - TimeoutCommit time.Duration // the consensus commitment timeout - AccountTokens math.Int // the amount of unique validator tokens (e.g. 1000node0) - StakingTokens math.Int // the amount of tokens each validator has available to stake - BondedTokens math.Int // the amount of tokens each validator stakes - NumValidators int // the total number of validators to create and bond - ChainID string // the network chain-id - EVMChainID uint64 - BondDenom string // the staking bond denomination - MinGasPrices string // the minimum gas prices each validator will accept - PruningStrategy string // the pruning strategy each validator will have - SigningAlgo string // signing algorithm for keys - RPCAddress string // RPC listen address (including port) - JSONRPCAddress string // JSON-RPC listen address (including port) - APIAddress string // REST API listen address (including port) - GRPCAddress string // GRPC server listen address (including port) - EnableCMTLogging bool // enable CometBFT logging to STDOUT - CleanupDir bool // remove base temporary directory during cleanup - PrintMnemonic bool // print the mnemonic of first validator as log output for testing + KeyringOptions []keyring.Option // keyring configuration options + Codec codec.Codec + LegacyAmino *codec.LegacyAmino // TODO: Remove! + InterfaceRegistry codectypes.InterfaceRegistry + TxConfig client.TxConfig + AccountRetriever client.AccountRetriever + AppConstructor AppConstructor // the ABCI application constructor + GenesisState evmtestutil.GenesisState // custom gensis state to provide + TimeoutCommit time.Duration // the consensus commitment timeout + AccountTokens math.Int // the amount of unique validator tokens (e.g. 1000node0) + StakingTokens math.Int // the amount of tokens each validator has available to stake + BondedTokens math.Int // the amount of tokens each validator stakes (used if BondedTokensPerValidator is nil) + BondedTokensPerValidator []math.Int // optional per-validator bonded tokens (overrides BondedTokens if set) + NumValidators int // the total number of validators to create and bond + ChainID string // the network chain-id + EVMChainID uint64 + BondDenom string // the staking bond denomination + MinGasPrices string // the minimum gas prices each validator will accept + PruningStrategy string // the pruning strategy each validator will have + SigningAlgo string // signing algorithm for keys + RPCAddress string // RPC listen address (including port) + JSONRPCAddress string // JSON-RPC listen address (including port) + APIAddress string // REST API listen address (including port) + GRPCAddress string // GRPC server listen address (including port) + EnableCMTLogging bool // enable CometBFT logging to STDOUT + CleanupDir bool // remove base temporary directory during cleanup + PrintMnemonic bool // print the mnemonic of first validator as log output for testing } // DefaultConfig returns a sane default configuration suitable for nearly all @@ -424,10 +426,21 @@ func New(l Logger, baseDir string, cfg Config) (*Network, error) { return nil, err } + // determine validator bonded tokens + bondedTokens := cfg.BondedTokens + if len(cfg.BondedTokensPerValidator) > 0 { + if i < len(cfg.BondedTokensPerValidator) { + bondedTokens = cfg.BondedTokensPerValidator[i] + } else { + // use last value if not enough entries + bondedTokens = cfg.BondedTokensPerValidator[len(cfg.BondedTokensPerValidator)-1] + } + } + createValMsg, err := stakingtypes.NewMsgCreateValidator( sdk.ValAddress(addr).String(), valPubKeys[i], - sdk.NewCoin(cfg.BondDenom, cfg.BondedTokens), + sdk.NewCoin(cfg.BondDenom, bondedTokens), stakingtypes.NewDescription(nodeDirName, "", "", "", ""), stakingtypes.NewCommissionRates(commission, math.LegacyOneDec(), math.LegacyOneDec()), math.OneInt(),