// Copyright 2021 Evmos Foundation // This file is part of Evmos' Ethermint library. // // The Ethermint library is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // The Ethermint library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the Ethermint library. If not, see https://github.com/evmos/ethermint/blob/main/LICENSE package client // DONTCOVER import ( "bufio" "encoding/json" "fmt" "net" "os" "path/filepath" "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" tmconfig "github.com/tendermint/tendermint/config" tmrand "github.com/tendermint/tendermint/libs/rand" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/crypto/keyring" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdkserver "github.com/cosmos/cosmos-sdk/server" srvconfig "github.com/cosmos/cosmos-sdk/server/config" "github.com/cosmos/cosmos-sdk/testutil" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" mintypes "github.com/cosmos/cosmos-sdk/x/mint/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/evmos/ethermint/crypto/hd" "github.com/evmos/ethermint/server/config" srvflags "github.com/evmos/ethermint/server/flags" ethermint "github.com/evmos/ethermint/types" evmtypes "github.com/evmos/ethermint/x/evm/types" "github.com/evmos/ethermint/testutil/network" ) var ( flagNodeDirPrefix = "node-dir-prefix" flagNumValidators = "v" flagOutputDir = "output-dir" flagNodeDaemonHome = "node-daemon-home" flagStartingIPAddress = "starting-ip-address" flagEnableLogging = "enable-logging" flagRPCAddress = "rpc.address" flagAPIAddress = "api.address" flagPrintMnemonic = "print-mnemonic" ) type initArgs struct { algo string chainID string keyringBackend string minGasPrices string nodeDaemonHome string nodeDirPrefix string numValidators int outputDir string startingIPAddress string } type startArgs struct { algo string apiAddress string chainID string grpcAddress string minGasPrices string outputDir string rpcAddress string jsonrpcAddress string numValidators int enableLogging bool printMnemonic bool } func addTestnetFlagsToCmd(cmd *cobra.Command) { cmd.Flags().Int(flagNumValidators, 4, "Number of validators to initialize the testnet with") cmd.Flags().StringP(flagOutputDir, "o", "./.testnets", "Directory to store initialization data for the testnet") cmd.Flags().String(flags.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") cmd.Flags().String(sdkserver.FlagMinGasPrices, fmt.Sprintf("0.000006%s", ethermint.AttoPhoton), "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.FlagKeyAlgorithm, string(hd.EthSecp256k1Type), "Key signing algorithm to generate keys for") } // NewTestnetCmd creates a root testnet command with subcommands to run an in-process testnet or initialize // validator configuration files for running a multi-validator testnet in a separate process func NewTestnetCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator) *cobra.Command { testnetCmd := &cobra.Command{ Use: "testnet", Short: "subcommands for starting or configuring local testnets", DisableFlagParsing: true, SuggestionsMinimumDistance: 2, RunE: client.ValidateCmd, } testnetCmd.AddCommand(testnetStartCmd()) testnetCmd.AddCommand(testnetInitFilesCmd(mbm, genBalIterator)) return testnetCmd } // get cmd to initialize all files for tendermint testnet and application func testnetInitFilesCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator) *cobra.Command { cmd := &cobra.Command{ Use: "init-files", Short: "Initialize config directories & files for a multi-validator testnet running locally via separate processes (e.g. Docker Compose or similar)", //nolint:lll Long: `init-files will setup "v" number of directories and populate each with necessary files (private validator, genesis, config, etc.) for running "v" validator nodes. Booting up a network with these validator folders is intended to be used with Docker Compose, or a similar setup where each node has a manually configurable IP address. Note, strict routability for addresses is turned off in the config file. Example: evmosd testnet init-files --v 4 --output-dir ./.testnets --starting-ip-address 192.168.10.2 `, RunE: func(cmd *cobra.Command, _ []string) error { clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { return err } serverCtx := sdkserver.GetServerContextFromCmd(cmd) args := initArgs{} args.outputDir, _ = cmd.Flags().GetString(flagOutputDir) args.keyringBackend, _ = cmd.Flags().GetString(flags.FlagKeyringBackend) args.chainID, _ = cmd.Flags().GetString(flags.FlagChainID) args.minGasPrices, _ = cmd.Flags().GetString(sdkserver.FlagMinGasPrices) args.nodeDirPrefix, _ = cmd.Flags().GetString(flagNodeDirPrefix) args.nodeDaemonHome, _ = cmd.Flags().GetString(flagNodeDaemonHome) args.startingIPAddress, _ = cmd.Flags().GetString(flagStartingIPAddress) args.numValidators, _ = cmd.Flags().GetInt(flagNumValidators) args.algo, _ = cmd.Flags().GetString(flags.FlagKeyAlgorithm) return initTestnetFiles(clientCtx, cmd, serverCtx.Config, mbm, genBalIterator, args) }, } addTestnetFlagsToCmd(cmd) cmd.Flags().String(flagNodeDirPrefix, "node", "Prefix the directory name for each node with (node results in node0, node1, ...)") cmd.Flags().String(flagNodeDaemonHome, "evmosd", "Home directory of the node's daemon configuration") cmd.Flags().String(flagStartingIPAddress, "192.168.0.1", "Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:46656, ID1@192.168.0.2:46656, ...)") cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") return cmd } // get cmd to start multi validator in-process testnet func testnetStartCmd() *cobra.Command { cmd := &cobra.Command{ Use: "start", Short: "Launch an in-process multi-validator testnet", Long: `testnet will launch an in-process multi-validator testnet, and generate "v" directories, populated with necessary validator configuration files (private validator, genesis, config, etc.). Example: evmosd testnet --v 4 --output-dir ./.testnets `, RunE: func(cmd *cobra.Command, _ []string) error { args := startArgs{} args.outputDir, _ = cmd.Flags().GetString(flagOutputDir) args.chainID, _ = cmd.Flags().GetString(flags.FlagChainID) args.minGasPrices, _ = cmd.Flags().GetString(sdkserver.FlagMinGasPrices) args.numValidators, _ = cmd.Flags().GetInt(flagNumValidators) args.algo, _ = cmd.Flags().GetString(flags.FlagKeyAlgorithm) args.enableLogging, _ = cmd.Flags().GetBool(flagEnableLogging) args.rpcAddress, _ = cmd.Flags().GetString(flagRPCAddress) args.apiAddress, _ = cmd.Flags().GetString(flagAPIAddress) args.grpcAddress, _ = cmd.Flags().GetString(srvflags.GRPCAddress) args.jsonrpcAddress, _ = cmd.Flags().GetString(srvflags.JSONRPCAddress) args.printMnemonic, _ = cmd.Flags().GetBool(flagPrintMnemonic) return startTestnet(cmd, args) }, } addTestnetFlagsToCmd(cmd) cmd.Flags().Bool(flagEnableLogging, false, "Enable INFO logging of tendermint validator nodes") cmd.Flags().String(flagRPCAddress, "tcp://0.0.0.0:26657", "the RPC address to listen on") cmd.Flags().String(flagAPIAddress, "tcp://0.0.0.0:1317", "the address to listen on for REST API") cmd.Flags().String(srvflags.GRPCAddress, config.DefaultGRPCAddress, "the gRPC server address to listen on") cmd.Flags().String(srvflags.JSONRPCAddress, config.DefaultJSONRPCAddress, "the JSON-RPC server address to listen on") cmd.Flags().Bool(flagPrintMnemonic, true, "print mnemonic of first validator to stdout for manual testing") return cmd } const nodeDirPerm = 0o755 // initTestnetFiles initializes testnet files for a testnet to be run in a separate process func initTestnetFiles( clientCtx client.Context, cmd *cobra.Command, nodeConfig *tmconfig.Config, mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator, args initArgs, ) error { if args.chainID == "" { args.chainID = fmt.Sprintf("ethermint_%d-1", tmrand.Int63n(9999999999999)+1) } nodeIDs := make([]string, args.numValidators) valPubKeys := make([]cryptotypes.PubKey, args.numValidators) appConfig := config.DefaultConfig() appConfig.MinGasPrices = args.minGasPrices appConfig.API.Enable = true appConfig.Telemetry.Enabled = true appConfig.Telemetry.PrometheusRetentionTime = 60 appConfig.Telemetry.EnableHostnameLabel = false appConfig.Telemetry.GlobalLabels = [][]string{{"chain_id", args.chainID}} var ( genAccounts []authtypes.GenesisAccount genBalances []banktypes.Balance genFiles []string ) inBuf := bufio.NewReader(cmd.InOrStdin()) // generate private keys, node IDs, and initial transactions for i := 0; i < args.numValidators; i++ { nodeDirName := fmt.Sprintf("%s%d", args.nodeDirPrefix, i) nodeDir := filepath.Join(args.outputDir, nodeDirName, args.nodeDaemonHome) gentxsDir := filepath.Join(args.outputDir, "gentxs") nodeConfig.SetRoot(nodeDir) nodeConfig.RPC.ListenAddress = "tcp://0.0.0.0:26657" if err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm); err != nil { _ = os.RemoveAll(args.outputDir) return err } nodeConfig.Moniker = nodeDirName ip, err := getIP(i, args.startingIPAddress) if err != nil { _ = os.RemoveAll(args.outputDir) return err } nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(nodeConfig) if err != nil { _ = os.RemoveAll(args.outputDir) return err } memo := fmt.Sprintf("%s@%s:26656", nodeIDs[i], ip) genFiles = append(genFiles, nodeConfig.GenesisFile()) kb, err := keyring.New(sdk.KeyringServiceName(), args.keyringBackend, nodeDir, inBuf, clientCtx.Codec, hd.EthSecp256k1Option()) if err != nil { return err } keyringAlgos, _ := kb.SupportedAlgorithms() algo, err := keyring.NewSigningAlgoFromString(args.algo, keyringAlgos) if err != nil { return err } addr, secret, err := testutil.GenerateSaveCoinKey(kb, nodeDirName, "", true, algo) if err != nil { _ = os.RemoveAll(args.outputDir) return err } info := map[string]string{"secret": secret} cliPrint, err := json.Marshal(info) if err != nil { return err } // save private key seed words if err := network.WriteFile(fmt.Sprintf("%v.json", "key_seed"), nodeDir, cliPrint); err != nil { return err } accStakingTokens := sdk.TokensFromConsensusPower(5000, ethermint.PowerReduction) coins := sdk.Coins{ sdk.NewCoin(ethermint.AttoPhoton, accStakingTokens), } genBalances = append(genBalances, banktypes.Balance{Address: addr.String(), Coins: coins.Sort()}) genAccounts = append(genAccounts, ðermint.EthAccount{ BaseAccount: authtypes.NewBaseAccount(addr, nil, 0, 0), CodeHash: common.BytesToHash(evmtypes.EmptyCodeHash).Hex(), }) valTokens := sdk.TokensFromConsensusPower(100, ethermint.PowerReduction) createValMsg, err := stakingtypes.NewMsgCreateValidator( sdk.ValAddress(addr), valPubKeys[i], sdk.NewCoin(ethermint.AttoPhoton, valTokens), stakingtypes.NewDescription(nodeDirName, "", "", "", ""), stakingtypes.NewCommissionRates(sdk.OneDec(), sdk.OneDec(), sdk.OneDec()), sdk.OneInt(), ) if err != nil { return err } txBuilder := clientCtx.TxConfig.NewTxBuilder() if err := txBuilder.SetMsgs(createValMsg); err != nil { return err } txBuilder.SetMemo(memo) txFactory := tx.Factory{} txFactory = txFactory. WithChainID(args.chainID). WithMemo(memo). WithKeybase(kb). WithTxConfig(clientCtx.TxConfig) if err := tx.Sign(txFactory, nodeDirName, txBuilder, true); err != nil { return err } txBz, err := clientCtx.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) if err != nil { return err } if err := network.WriteFile(fmt.Sprintf("%v.json", nodeDirName), gentxsDir, txBz); err != nil { return err } customAppTemplate, customAppConfig := config.AppConfig(ethermint.AttoPhoton) srvconfig.SetConfigTemplate(customAppTemplate) if err := sdkserver.InterceptConfigsPreRunHandler(cmd, customAppTemplate, customAppConfig, tmconfig.DefaultConfig()); err != nil { return err } srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config/app.toml"), appConfig) } if err := initGenFiles(clientCtx, mbm, args.chainID, ethermint.AttoPhoton, genAccounts, genBalances, genFiles, args.numValidators); err != nil { return err } err := collectGenFiles( clientCtx, nodeConfig, args.chainID, nodeIDs, valPubKeys, args.numValidators, args.outputDir, args.nodeDirPrefix, args.nodeDaemonHome, genBalIterator, ) if err != nil { return err } cmd.PrintErrf("Successfully initialized %d node directories\n", args.numValidators) return nil } func initGenFiles( clientCtx client.Context, mbm module.BasicManager, chainID, coinDenom string, genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, genFiles []string, numValidators int, ) error { appGenState := mbm.DefaultGenesis(clientCtx.Codec) // set the accounts in the genesis state var authGenState authtypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[authtypes.ModuleName], &authGenState) accounts, err := authtypes.PackAccounts(genAccounts) if err != nil { return err } authGenState.Accounts = accounts appGenState[authtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&authGenState) // set the balances in the genesis state var bankGenState banktypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[banktypes.ModuleName], &bankGenState) bankGenState.Balances = genBalances appGenState[banktypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&bankGenState) var stakingGenState stakingtypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[stakingtypes.ModuleName], &stakingGenState) stakingGenState.Params.BondDenom = coinDenom appGenState[stakingtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&stakingGenState) var govGenState govv1.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[govtypes.ModuleName], &govGenState) govGenState.DepositParams.MinDeposit[0].Denom = coinDenom appGenState[govtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&govGenState) var mintGenState mintypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[mintypes.ModuleName], &mintGenState) mintGenState.Params.MintDenom = coinDenom appGenState[mintypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&mintGenState) var crisisGenState crisistypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[crisistypes.ModuleName], &crisisGenState) crisisGenState.ConstantFee.Denom = coinDenom appGenState[crisistypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&crisisGenState) var evmGenState evmtypes.GenesisState clientCtx.Codec.MustUnmarshalJSON(appGenState[evmtypes.ModuleName], &evmGenState) evmGenState.Params.EvmDenom = coinDenom appGenState[evmtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&evmGenState) appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ") if err != nil { return err } genDoc := types.GenesisDoc{ ChainID: chainID, AppState: appGenStateJSON, Validators: nil, } // generate empty genesis files for each validator and save for i := 0; i < numValidators; i++ { if err := genDoc.SaveAs(genFiles[i]); err != nil { return err } } return nil } func collectGenFiles( clientCtx client.Context, nodeConfig *tmconfig.Config, chainID string, nodeIDs []string, valPubKeys []cryptotypes.PubKey, numValidators int, outputDir, nodeDirPrefix, nodeDaemonHome string, genBalIterator banktypes.GenesisBalancesIterator, ) error { var appState json.RawMessage genTime := tmtime.Now() for i := 0; i < numValidators; i++ { nodeDirName := fmt.Sprintf("%s%d", nodeDirPrefix, i) nodeDir := filepath.Join(outputDir, nodeDirName, nodeDaemonHome) gentxsDir := filepath.Join(outputDir, "gentxs") nodeConfig.Moniker = nodeDirName nodeConfig.SetRoot(nodeDir) nodeID, valPubKey := nodeIDs[i], valPubKeys[i] initCfg := genutiltypes.NewInitConfig(chainID, gentxsDir, nodeID, valPubKey) genDoc, err := types.GenesisDocFromFile(nodeConfig.GenesisFile()) if err != nil { return err } nodeAppState, err := genutil.GenAppStateFromConfig(clientCtx.Codec, clientCtx.TxConfig, nodeConfig, initCfg, *genDoc, genBalIterator) if err != nil { return err } if appState == nil { // set the canonical application state (they should not differ) appState = nodeAppState } genFile := nodeConfig.GenesisFile() // overwrite each validator's genesis file to have a canonical genesis time if err := genutil.ExportGenesisFileWithTime(genFile, chainID, nil, appState, genTime); err != nil { return err } } return nil } func getIP(i int, startingIPAddr string) (ip string, err error) { if len(startingIPAddr) == 0 { ip, err = sdkserver.ExternalIP() if err != nil { return "", err } return ip, nil } return calculateIP(startingIPAddr, i) } func calculateIP(ip string, i int) (string, error) { ipv4 := net.ParseIP(ip).To4() if ipv4 == nil { return "", fmt.Errorf("%v: non ipv4 address", ip) } for j := 0; j < i; j++ { ipv4[3]++ } return ipv4.String(), nil } // startTestnet starts an in-process testnet func startTestnet(cmd *cobra.Command, args startArgs) error { networkConfig := network.DefaultConfig() // Default networkConfig.ChainID is random, and we should only override it if chainID provided // is non-empty if args.chainID != "" { networkConfig.ChainID = args.chainID } networkConfig.SigningAlgo = args.algo networkConfig.MinGasPrices = args.minGasPrices networkConfig.NumValidators = args.numValidators networkConfig.EnableTMLogging = args.enableLogging networkConfig.RPCAddress = args.rpcAddress networkConfig.APIAddress = args.apiAddress networkConfig.GRPCAddress = args.grpcAddress networkConfig.JSONRPCAddress = args.jsonrpcAddress networkConfig.PrintMnemonic = args.printMnemonic networkLogger := network.NewCLILogger(cmd) baseDir := fmt.Sprintf("%s/%s", args.outputDir, networkConfig.ChainID) if _, err := os.Stat(baseDir); !os.IsNotExist(err) { return fmt.Errorf( "testnests directory already exists for chain-id '%s': %s, please remove or select a new --chain-id", networkConfig.ChainID, baseDir) } testnet, err := network.New(networkLogger, baseDir, networkConfig) if err != nil { return err } _, err = testnet.WaitForHeight(1) if err != nil { return err } cmd.Println("press the Enter Key to terminate") _, err = fmt.Scanln() // wait for Enter Key if err != nil { return err } testnet.Cleanup() return nil }