diff --git a/.gitignore b/.gitignore index 0194bd172..3b42d5053 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ contracts/@openzeppelin/* evmd/build/ build/ + +.testnets diff --git a/Makefile b/Makefile index 9a75a01ca..9c40c92a9 100644 --- a/Makefile +++ b/Makefile @@ -349,3 +349,25 @@ contracts-compile: contracts-add: @echo "Adding a new smart contract to be compiled..." @python3 ./scripts/compile_smart_contracts/compile_smart_contracts.py --add $(CONTRACT) + +############################################################################### +### Localnet ### +############################################################################### + +localnet-build-env: + $(MAKE) -C contrib/images evmd-env + +localnet-build-nodes: + $(DOCKER) run --rm -v $(CURDIR)/.testnets:/data cosmos/evmd \ + testnet init-files --v 4 -o /data --starting-ip-address 192.168.10.2 --keyring-backend=test --chain-id=local-4221 --use-docker=true + docker compose up -d + +localnet-stop: + docker compose down + +# localnet-start will run a 4-node testnet locally. The nodes are +# based off the docker images in: ./contrib/images/simd-env +localnet-start: localnet-stop localnet-build-env localnet-build-nodes + + +.PHONY: localnet-start localnet-stop localnet-build-env localnet-build-nodes \ No newline at end of file diff --git a/contrib/images/Makefile b/contrib/images/Makefile new file mode 100644 index 000000000..84601fa91 --- /dev/null +++ b/contrib/images/Makefile @@ -0,0 +1,10 @@ +all: evmd-env + +evmd-env: evmd-rmi + docker build --tag cosmos/evmd -f evmd-env/Dockerfile \ + $(shell git rev-parse --show-toplevel) + +evmd-rmi: + docker rmi cosmos/evmd 2>/dev/null; true + +.PHONY: all evmd-env evmd-rmi diff --git a/contrib/images/evmd-env/Dockerfile b/contrib/images/evmd-env/Dockerfile new file mode 100644 index 000000000..689469fc5 --- /dev/null +++ b/contrib/images/evmd-env/Dockerfile @@ -0,0 +1,42 @@ +# Info on how to use this docker image can be found in DOCKER_README.md +ARG IMG_TAG=latest + +# Compile the evmd binary +FROM golang:1.24-alpine AS evmd-builder +WORKDIR /work +ENV PACKAGES="curl build-base git bash file linux-headers eudev-dev" +RUN apk add --no-cache $PACKAGES + +COPY go.mod go.sum* ./ +RUN go mod download + +COPY . . +RUN LEDGER_ENABLED=false COSMOS_BUILD_OPTIONS="staticlink" BUILD_TAGS=muslc make build +RUN echo "Checking binary linkage..." \ + && file /work/build/evmd \ + && (file /work/build/evmd | grep -q "statically linked" || echo "Warning: Binary may not be statically linked") + +FROM alpine:$IMG_TAG AS run +RUN apk add --no-cache build-base jq bash curl +RUN addgroup -g 1025 nonroot +RUN adduser -D nonroot -u 1025 -G nonroot + +# Set up the runtime environment +EXPOSE 26656 26657 1317 9090 +STOPSIGNAL SIGTERM +VOLUME /evmd +WORKDIR /evmd + +# Copy the wrapper script and binary to expected locations +COPY contrib/images/evmd-env/wrapper.sh /usr/bin/wrapper.sh +COPY --from=evmd-builder /work/build/evmd /evmd/ +COPY --from=evmd-builder /work/build/evmd /usr/local/bin/ + +# Set proper ownership and permissions before switching to nonroot user +RUN chown nonroot:nonroot /usr/bin/wrapper.sh && chmod +x /usr/bin/wrapper.sh +RUN chown -R nonroot:nonroot /evmd + +USER nonroot + +ENTRYPOINT ["/usr/bin/wrapper.sh"] +CMD ["start", "--log_format", "plain", "--minimum-gas-prices", "0.0001atest", "--json-rpc.api", "eth,txpool,personal,net,debug,web3", "--chain-id", "local-4221"] \ No newline at end of file diff --git a/contrib/images/evmd-env/wrapper.sh b/contrib/images/evmd-env/wrapper.sh new file mode 100755 index 000000000..9562acc7e --- /dev/null +++ b/contrib/images/evmd-env/wrapper.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh +set -x + +BINARY=/evmd/${BINARY:-evmd} +ID=${ID:-0} +LOG=${LOG:-evmd.log} + +if ! [ -f "${BINARY}" ]; then + echo "The binary $(basename "${BINARY}") cannot be found. Please add the binary to the shared folder. Please use the BINARY environment variable if the name of the binary is not 'evmd'" + exit 1 +fi + +export EVMDHOME="/data/node${ID}/evmd" + +if [ -d "$(dirname "${EVMDHOME}"/"${LOG}")" ]; then + "${BINARY}" --home "${EVMDHOME}" "$@" | tee "${EVMDHOME}/${LOG}" +else + "${BINARY}" --home "${EVMDHOME}" "$@" +fi diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..a02f9489b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,106 @@ +version: "3" + +services: + evmdnode0: + container_name: evmdnode0 + image: "cosmos/evmd" + environment: + - DEBUG=0 + - ID=0 + - LOG=${LOG:-evmd.log} + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + ports: + - "26656-26657:26656-26657" + - "1317:1317" + - "9090:9090" + - "2345:2345" + - "6065:6065" + - "8545-8546:8545-8546" + volumes: + - ./.testnets:/data:Z + networks: + localnet: + ipv4_address: 192.168.10.2 + + evmdnode1: + container_name: evmdnode1 + image: "cosmos/evmd" + environment: + - DEBUG=0 + - ID=1 + - LOG=${LOG:-evmd.log} + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + ports: + - "26666-26667:26656-26657" + - "1318:1317" + - "9091:9090" + - "2346:2345" + - "6075:6065" + - "8555-8556:8545-8546" + volumes: + - ./.testnets:/data:Z + networks: + localnet: + ipv4_address: 192.168.10.3 + + evmdnode2: + container_name: evmdnode2 + image: "cosmos/evmd" + environment: + - DEBUG=0 + - ID=2 + - LOG=${LOG:-evmd.log} + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + ports: + - "26676-26677:26656-26657" + - "1319:1317" + - "9092:9090" + - "2347:2345" + - "6085:6065" + - "8565-8566:8545-8546" + volumes: + - ./.testnets:/data:Z + networks: + localnet: + ipv4_address: 192.168.10.4 + + evmdnode3: + container_name: evmdnode3 + image: "cosmos/evmd" + environment: + - DEBUG=0 + - ID=3 + - LOG=${LOG:-evmd.log} + cap_add: + - SYS_PTRACE + security_opt: + - seccomp:unconfined + ports: + - "26686-26687:26656-26657" + - "1320:1317" + - "9093:9090" + - "2348:2345" + - "6095:6065" + - "8575-8576:8545-8546" + volumes: + - ./.testnets:/data:Z + networks: + localnet: + ipv4_address: 192.168.10.5 + +networks: + localnet: + driver: bridge + ipam: + driver: default + config: + - subnet: 192.168.10.0/25 diff --git a/evmd/cmd/evmd/cmd/creator.go b/evmd/cmd/evmd/cmd/creator.go new file mode 100644 index 000000000..c6238998d --- /dev/null +++ b/evmd/cmd/evmd/cmd/creator.go @@ -0,0 +1,151 @@ +package cmd + +import ( + "errors" + "io" + "path/filepath" + + dbm "github.com/cosmos/cosmos-db" + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/evm/evmd" + evmdconfig "github.com/cosmos/evm/evmd/cmd/evmd/config" + "github.com/spf13/cast" + "github.com/spf13/viper" + + "cosmossdk.io/log" + "cosmossdk.io/store" + "cosmossdk.io/store/snapshots" + snapshottypes "cosmossdk.io/store/snapshots/types" + storetypes "cosmossdk.io/store/types" +) + +type appCreator struct{} + +func (a appCreator) newApp( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + appOpts servertypes.AppOptions, +) servertypes.Application { + var cache storetypes.MultiStorePersistentCache + if cast.ToBool(appOpts.Get(server.FlagInterBlockCache)) { + cache = store.NewCommitKVStoreCacheManager() + } + pruningOpts, err := server.GetPruningOptionsFromFlags(appOpts) + if err != nil { + panic(err) + } + + skipUpgradeHeights := make(map[int64]bool) + for _, h := range cast.ToIntSlice(appOpts.Get(server.FlagUnsafeSkipUpgrades)) { + skipUpgradeHeights[int64(h)] = true + } + + homeDir := cast.ToString(appOpts.Get(flags.FlagHome)) + chainID := cast.ToString(appOpts.Get(flags.FlagChainID)) + if chainID == "" { + // fallback to genesis chain-id + genDocFile := filepath.Join(homeDir, cast.ToString(appOpts.Get("genesis_file"))) + appGenesis, err := genutiltypes.AppGenesisFromFile(genDocFile) + if err != nil { + panic(err) + } + + chainID = appGenesis.ChainID + } + + snapshotDir := filepath.Join(homeDir, "data", "snapshots") + snapshotDB, err := dbm.NewDB("metadata", server.GetAppDBBackend(appOpts), snapshotDir) + if err != nil { + panic(err) + } + snapshotStore, err := snapshots.NewStore(snapshotDB, snapshotDir) + if err != nil { + panic(err) + } + + // BaseApp Opts + snapshotOptions := snapshottypes.NewSnapshotOptions( + cast.ToUint64(appOpts.Get(server.FlagStateSyncSnapshotInterval)), + cast.ToUint32(appOpts.Get(server.FlagStateSyncSnapshotKeepRecent)), + ) + baseappOptions := []func(*baseapp.BaseApp){ + baseapp.SetChainID(chainID), + baseapp.SetPruning(pruningOpts), + baseapp.SetMinGasPrices(cast.ToString(appOpts.Get(server.FlagMinGasPrices))), + baseapp.SetHaltHeight(cast.ToUint64(appOpts.Get(server.FlagHaltHeight))), + baseapp.SetHaltTime(cast.ToUint64(appOpts.Get(server.FlagHaltTime))), + baseapp.SetMinRetainBlocks(cast.ToUint64(appOpts.Get(server.FlagMinRetainBlocks))), + baseapp.SetInterBlockCache(cache), + baseapp.SetTrace(cast.ToBool(appOpts.Get(server.FlagTrace))), + baseapp.SetIndexEvents(cast.ToStringSlice(appOpts.Get(server.FlagIndexEvents))), + baseapp.SetSnapshot(snapshotStore, snapshotOptions), + baseapp.SetIAVLCacheSize(cast.ToInt(appOpts.Get(server.FlagIAVLCacheSize))), + } + + return evmd.NewExampleApp( + logger, + db, + traceStore, + true, + simtestutil.EmptyAppOptions{}, + evmdconfig.EVMChainID, + evmdconfig.EvmAppOptions, + baseappOptions..., + ) +} + +func (a appCreator) appExport( + logger log.Logger, + db dbm.DB, + traceStore io.Writer, + height int64, + forZeroHeight bool, + jailAllowedAddrs []string, + appOpts servertypes.AppOptions, + modulesToExport []string, +) (servertypes.ExportedApp, error) { + var evmApp *evmd.EVMD + + homePath, ok := appOpts.Get(flags.FlagHome).(string) + if !ok || homePath == "" { + return servertypes.ExportedApp{}, errors.New("application home is not set") + } + + // InvCheckPeriod + viperAppOpts, ok := appOpts.(*viper.Viper) + if !ok { + return servertypes.ExportedApp{}, errors.New("appOpts is not viper.Viper") + } + // overwrite the FlagInvCheckPeriod + viperAppOpts.Set(server.FlagInvCheckPeriod, 1) + appOpts = viperAppOpts + + var loadLatest bool + if height == -1 { + loadLatest = true + } + + evmApp = evmd.NewExampleApp( + logger, + db, + traceStore, + loadLatest, + appOpts, + evmdconfig.EVMChainID, + evmdconfig.EvmAppOptions, + ) + + if height != -1 { + if err := evmApp.LoadHeight(height); err != nil { + return servertypes.ExportedApp{}, err + } + } + + return evmApp.ExportAppStateAndValidators(forZeroHeight, jailAllowedAddrs, modulesToExport) +} diff --git a/evmd/cmd/evmd/cmd/root.go b/evmd/cmd/evmd/cmd/root.go index a44a6581f..8d994543f 100644 --- a/evmd/cmd/evmd/cmd/root.go +++ b/evmd/cmd/evmd/cmd/root.go @@ -5,6 +5,7 @@ import ( "io" "os" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/spf13/cast" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -73,7 +74,6 @@ func NewRootCmd() *cobra.Command { TxConfig: tempApp.GetTxConfig(), Amino: tempApp.LegacyAmino(), } - initClientCtx := client.Context{}. WithCodec(encodingConfig.Codec). WithInterfaceRegistry(encodingConfig.InterfaceRegistry). @@ -169,19 +169,20 @@ func initTendermintConfig() *tmcfg.Config { return cfg } -func initRootCmd(rootCmd *cobra.Command, osApp *evmd.EVMD) { +func initRootCmd(rootCmd *cobra.Command, evmApp *evmd.EVMD) { cfg := sdk.GetConfig() cfg.Seal() defaultNodeHome := evmdconfig.MustGetDefaultNodeHome() rootCmd.AddCommand( - genutilcli.InitCmd(osApp.BasicModuleManager, defaultNodeHome), - genutilcli.Commands(osApp.TxConfig(), osApp.BasicModuleManager, defaultNodeHome), + genutilcli.InitCmd(evmApp.BasicModuleManager, defaultNodeHome), + genutilcli.Commands(evmApp.TxConfig(), evmApp.BasicModuleManager, defaultNodeHome), cmtcli.NewCompletionCmd(rootCmd, true), debug.Cmd(), confixcmd.ConfigCommand(), pruning.Cmd(newApp, defaultNodeHome), snapshot.Cmd(newApp), + NewTestnetCmd(evmApp.BasicModuleManager, banktypes.GenesisBalancesIterator{}, appCreator{}), ) // add Cosmos EVM' flavored TM commands to start server, etc. diff --git a/evmd/cmd/evmd/cmd/testnet.go b/evmd/cmd/evmd/cmd/testnet.go new file mode 100644 index 000000000..b1a945e0f --- /dev/null +++ b/evmd/cmd/evmd/cmd/testnet.go @@ -0,0 +1,609 @@ +package cmd + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + + cosmosevmhd "github.com/cosmos/evm/crypto/hd" + cosmosevmkeyring "github.com/cosmos/evm/crypto/keyring" + "github.com/cosmos/evm/evmd" + evmdconfig "github.com/cosmos/evm/evmd/cmd/evmd/config" + cosmosevmserverconfig "github.com/cosmos/evm/server/config" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + cmtconfig "github.com/cometbft/cometbft/config" + "github.com/cometbft/cometbft/types" + tmtime "github.com/cometbft/cometbft/types/time" + + dbm "github.com/cosmos/cosmos-db" + + "cosmossdk.io/log" + "cosmossdk.io/math" + + "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" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/server" + 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" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +var ( + flagNodeDirPrefix = "node-dir-prefix" + flagNumValidators = "v" + flagOutputDir = "output-dir" + flagNodeDaemonHome = "node-daemon-home" + flagStartingIPAddress = "starting-ip-address" + flagsUseDocker = "use-docker" + flagEnableLogging = "enable-logging" + flagGRPCAddress = "grpc.address" + flagRPCAddress = "rpc.address" + flagAPIAddress = "api.address" + flagPrintMnemonic = "print-mnemonic" + unsafeStartValidatorFn UnsafeStartValidatorCmdCreator +) + +type UnsafeStartValidatorCmdCreator func(ac appCreator) *cobra.Command + +type initArgs struct { + algo string + chainID string + keyringBackend string + minGasPrices string + nodeDaemonHome string + nodeDirPrefix string + numValidators int + outputDir string + startingIPAddress string + useDocker bool +} + +type startArgs struct { + algo string + apiAddress string + chainID string + enableLogging bool + grpcAddress string + minGasPrices string + numValidators int + outputDir string + printMnemonic bool + rpcAddress string +} + +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(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") + + // support old flags name for backwards compatibility + cmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { + if name == "algo" { + name = flags.FlagKeyType + } + + return pflag.NormalizedName(name) + }) +} + +// NewTestnetCmd creates a root testnet command with subcommands to: +// 1. run an in-process testnet or +// 2. initialize validator configuration files for running a multi-validator testnet in a separate process or +// 3. update application and consensus state with the local validator info +func NewTestnetCmd(mbm module.BasicManager, genBalIterator banktypes.GenesisBalancesIterator, appCreator appCreator) *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)) + // if the binary is built with the unsafe_start_local_validator tag, unsafeStartValidatorFn will be set + // and the subcommand will be added + if unsafeStartValidatorFn != nil { + testnetCmd.AddCommand(unsafeStartValidatorFn(appCreator)) + } + + return testnetCmd +} + +// testnetInitFilesCmd returns a 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)", + 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: + evmd 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 + } + + clientCtx = clientCtx.WithKeyringOptions(cosmosevmkeyring.Option()) + + serverCtx := server.GetServerContextFromCmd(cmd) + config := serverCtx.Config + + args := initArgs{} + args.outputDir, _ = cmd.Flags().GetString(flagOutputDir) + args.keyringBackend, _ = cmd.Flags().GetString(flags.FlagKeyringBackend) + args.chainID, _ = cmd.Flags().GetString(flags.FlagChainID) + args.useDocker, _ = cmd.Flags().GetBool(flagsUseDocker) + args.minGasPrices, _ = cmd.Flags().GetString(server.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.FlagKeyType) + + return initTestnetFiles(clientCtx, cmd, 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, "evmd", "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)") + cmd.Flags().Bool(flagsUseDocker, false, "test network via docker") + + return cmd +} + +// testnetStartCmd returns a 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: + evmd 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(server.FlagMinGasPrices) + args.numValidators, _ = cmd.Flags().GetInt(flagNumValidators) + args.algo, _ = cmd.Flags().GetString(flags.FlagKeyType) + args.enableLogging, _ = cmd.Flags().GetBool(flagEnableLogging) + args.rpcAddress, _ = cmd.Flags().GetString(flagRPCAddress) + args.apiAddress, _ = cmd.Flags().GetString(flagAPIAddress) + args.grpcAddress, _ = cmd.Flags().GetString(flagGRPCAddress) + 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(flagGRPCAddress, "0.0.0.0:9090", "the gRPC 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 *cmtconfig.Config, + mbm module.BasicManager, + genBalIterator banktypes.GenesisBalancesIterator, + args initArgs, +) error { + if args.chainID == "" { + args.chainID = "local-4221" + } + nodeIDs := make([]string, args.numValidators) + valPubKeys := make([]cryptotypes.PubKey, args.numValidators) + + serverCfg := srvconfig.DefaultConfig() + serverCfg.MinGasPrices = args.minGasPrices + serverCfg.API.Enable = true + serverCfg.Telemetry.Enabled = true + serverCfg.Telemetry.PrometheusRetentionTime = 60 + serverCfg.Telemetry.EnableHostnameLabel = false + serverCfg.Telemetry.GlobalLabels = [][]string{{"chain_id", args.chainID}} + evm := cosmosevmserverconfig.DefaultEVMConfig() + evm.EVMChainID = evmdconfig.EVMChainID + evmCfg := evmdconfig.EVMAppConfig{ + Config: *serverCfg, + EVM: *evm, + JSONRPC: *cosmosevmserverconfig.DefaultJSONRPCConfig(), + TLS: *cosmosevmserverconfig.DefaultTLSConfig(), + } + + 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.Moniker = nodeDirName + 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 + } + + 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, cosmosevmkeyring.Option()) + 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 := writeFile(fmt.Sprintf("%v.json", "key_seed"), nodeDir, cliPrint); err != nil { + return err + } + + accTokens := sdk.TokensFromConsensusPower(1000, sdk.DefaultPowerReduction) + accStakingTokens := sdk.TokensFromConsensusPower(500, sdk.DefaultPowerReduction) + coins := sdk.Coins{ + sdk.NewCoin("testtoken", accTokens), + sdk.NewCoin(sdk.DefaultBondDenom, accStakingTokens), + } + + genBalances = append(genBalances, banktypes.Balance{Address: addr.String(), Coins: coins.Sort()}) + genAccounts = append(genAccounts, authtypes.NewBaseAccount(addr, nil, 0, 0)) + + valTokens := sdk.TokensFromConsensusPower(100, sdk.DefaultPowerReduction) + createValMsg, err := stakingtypes.NewMsgCreateValidator( + sdk.ValAddress(addr).String(), + valPubKeys[i], + sdk.NewCoin(sdk.DefaultBondDenom, valTokens), + stakingtypes.NewDescription(nodeDirName, "", "", "", ""), + stakingtypes.NewCommissionRates(math.LegacyOneDec(), math.LegacyOneDec(), math.LegacyOneDec()), + math.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(cmd.Context(), txFactory, nodeDirName, txBuilder, true); err != nil { + return err + } + + txBz, err := clientCtx.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + if err != nil { + return err + } + + if err := writeFile(fmt.Sprintf("%v.json", nodeDirName), gentxsDir, txBz); err != nil { + return err + } + + srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), evmCfg) + } + + if err := initGenFiles(clientCtx, mbm, args.chainID, 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, clientCtx.TxConfig.SigningContext().ValidatorAddressCodec(), + ) + 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 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 = banktypes.SanitizeGenesisBalances(genBalances) + for _, bal := range bankGenState.Balances { + bankGenState.Supply = bankGenState.Supply.Add(bal.Coins...) + } + appGenState[banktypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&bankGenState) + + 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 *cmtconfig.Config, chainID string, + nodeIDs []string, valPubKeys []cryptotypes.PubKey, numValidators int, + outputDir, nodeDirPrefix, nodeDaemonHome string, genBalIterator banktypes.GenesisBalancesIterator, valAddrCodec runtime.ValidatorAddressCodec, +) 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) + + appGenesis, err := genutiltypes.AppGenesisFromFile(nodeConfig.GenesisFile()) + if err != nil { + return err + } + + nodeAppState, err := genutil.GenAppStateFromConfig(clientCtx.Codec, clientCtx.TxConfig, nodeConfig, initCfg, appGenesis, genBalIterator, genutiltypes.DefaultMessageValidator, + valAddrCodec) + 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 = server.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 +} + +func writeFile(name string, dir string, contents []byte) error { + file := filepath.Join(dir, name) + + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("could not create directory %q: %w", dir, err) + } + + if err := os.WriteFile(file, contents, 0o644); err != nil { //nolint: gosec + return err + } + + return nil +} + +// startTestnet starts an in-process testnet +func startTestnet(cmd *cobra.Command, args startArgs) error { + networkConfig := network.DefaultConfig(NewTestNetworkFixture) + + // 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.EnableLogging = args.enableLogging + networkConfig.RPCAddress = args.rpcAddress + networkConfig.APIAddress = args.apiAddress + networkConfig.GRPCAddress = args.grpcAddress + 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 + } + + if _, err := testnet.WaitForHeight(1); err != nil { + return err + } + cmd.Println("press the Enter Key to terminate") + if _, err := fmt.Scanln(); err != nil { // wait for Enter Key + return err + } + testnet.Cleanup() + + return nil +} + +// NewTestNetworkFixture returns a new evmd AppConstructor for network simulation tests +func NewTestNetworkFixture() network.TestFixture { + dir, err := os.MkdirTemp("", "evm") + if err != nil { + panic(fmt.Sprintf("failed creating temporary directory: %v", err)) + } + defer os.RemoveAll(dir) + + app := evmd.NewExampleApp( + log.NewNopLogger(), + dbm.NewMemDB(), + nil, + true, + simtestutil.EmptyAppOptions{}, + evmdconfig.EVMChainID, + evmdconfig.EvmAppOptions, + ) + + appCtr := func(val network.ValidatorI) servertypes.Application { + return evmd.NewExampleApp( + log.NewNopLogger(), + dbm.NewMemDB(), + nil, + true, + simtestutil.EmptyAppOptions{}, + evmdconfig.EVMChainID, + evmdconfig.EvmAppOptions, + ) + } + + return network.TestFixture{ + AppConstructor: appCtr, + GenesisState: app.DefaultGenesis(), + EncodingConfig: moduletestutil.TestEncodingConfig{ + InterfaceRegistry: app.InterfaceRegistry(), + Codec: app.AppCodec(), + TxConfig: app.GetTxConfig(), + Amino: app.LegacyAmino(), + }, + } +} diff --git a/evmd/cmd/evmd/config/evmd_config.go b/evmd/cmd/evmd/config/evmd_config.go index de13ea629..8882da5e0 100644 --- a/evmd/cmd/evmd/config/evmd_config.go +++ b/evmd/cmd/evmd/config/evmd_config.go @@ -87,16 +87,17 @@ func GetMaccPerms() map[string][]string { return maps.Clone(maccPerms) } +type EVMAppConfig struct { + serverconfig.Config + + EVM cosmosevmserverconfig.EVMConfig + JSONRPC cosmosevmserverconfig.JSONRPCConfig + TLS cosmosevmserverconfig.TLSConfig +} + // InitAppConfig helps to override default appConfig template and configs. // return "", nil if no custom configuration is required for the application. func InitAppConfig(denom string, evmChainID uint64) (string, interface{}) { - type CustomAppConfig struct { - serverconfig.Config - - EVM cosmosevmserverconfig.EVMConfig - JSONRPC cosmosevmserverconfig.JSONRPCConfig - TLS cosmosevmserverconfig.TLSConfig - } // Optionally allow the chain developer to overwrite the SDK's default // server config. @@ -118,7 +119,7 @@ func InitAppConfig(denom string, evmChainID uint64) (string, interface{}) { evmCfg := cosmosevmserverconfig.DefaultEVMConfig() evmCfg.EVMChainID = evmChainID - customAppConfig := CustomAppConfig{ + customAppConfig := EVMAppConfig{ Config: *srvCfg, EVM: *evmCfg, JSONRPC: *cosmosevmserverconfig.DefaultJSONRPCConfig(),