diff --git a/cmd/realio-networkd/root.go b/cmd/realio-networkd/root.go index 184a18ed..4ee690b9 100644 --- a/cmd/realio-networkd/root.go +++ b/cmd/realio-networkd/root.go @@ -116,7 +116,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { genutilcli.ValidateGenesisCmd(app.ModuleBasics), AddGenesisAccountCmd(app.DefaultNodeHome), tmcli.NewCompletionCmd(rootCmd, true), - ethermintclient.NewTestnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{}), + NewTestnetCmd(app.ModuleBasics, banktypes.GenesisBalancesIterator{}), debug.Cmd(), config.Cmd(), // this line is used by starport scaffolding # stargate/root/commands diff --git a/cmd/realio-networkd/testnet.go b/cmd/realio-networkd/testnet.go new file mode 100644 index 00000000..760c6d93 --- /dev/null +++ b/cmd/realio-networkd/testnet.go @@ -0,0 +1,564 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + minttypes "github.com/realiotech/realio-network/x/mint/types" + "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" + 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" + + cmdcfg "github.com/realiotech/realio-network/cmd/config" + "github.com/realiotech/realio-network/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", cmdcfg.BaseDenom), "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01ario)") + 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)", + 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: + realio-networkd 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, "realio-network", "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: + realio-networkd 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("realionetworklocal_%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(cmdcfg.BaseDenom, 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(cmdcfg.BaseDenom, 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(cmdcfg.BaseDenom) + srvconfig.SetConfigTemplate(customAppTemplate) + customTMConfig := initTendermintConfig() + + if err := sdkserver.InterceptConfigsPreRunHandler(cmd, customAppTemplate, customAppConfig, customTMConfig); err != nil { + return err + } + + srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config/app.toml"), appConfig) + } + + if err := initGenFiles(clientCtx, mbm, args.chainID, cmdcfg.BaseDenom, 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 mintGenState = *minttypes.DefaultGenesisState() + clientCtx.Codec.MustUnmarshalJSON(appGenState[minttypes.ModuleName], &mintGenState) + appGenState[minttypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&mintGenState) + + 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 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 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ac2cc4af --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3' + +services: + node0: + container_name: realionetworknode0 + image: "realio-network/node" + ports: + - "26656-26657:26656-26657" + volumes: + - ./build/node0/realio-network:/realio-network:Z + command: + - /realio-networkd start --home /realio-network + networks: + localnet: + ipv4_address: 192.167.10.2 + + node1: + container_name: realionetworknode1 + image: "realio-network/node" + ports: + - "26659-26660:26656-26657" + volumes: + - ./build/node1/realio-network:/realio-network:Z + command: + - /realio-networkd start --home /realio-network + networks: + localnet: + ipv4_address: 192.167.10.3 + + node2: + container_name: realionetworknode2 + image: "realio-network/node" + ports: + - "26661-26662:26656-26657" + volumes: + - ./build/node2/realio-network:/realio-network:Z + command: + - /realio-networkd start --home /realio-network + networks: + localnet: + ipv4_address: 192.167.10.4 + + node3: + container_name: realionetworknode3 + image: "realio-network/node" + ports: + - "26663-26664:26656-26657" + volumes: + - ./build/node3/realio-network:/realio-network:Z + command: + - /realio-networkd start --home /realio-network + networks: + localnet: + ipv4_address: 192.167.10.5 + +networks: + localnet: + driver: bridge + ipam: + driver: default + config: + - + subnet: 192.167.10.0/16 \ No newline at end of file diff --git a/networks/local/Makefile b/networks/local/Makefile new file mode 100644 index 00000000..91d66553 --- /dev/null +++ b/networks/local/Makefile @@ -0,0 +1,4 @@ +all: + docker build --no-cache --tag realio-network/node ../.. -f realio-network/Dockerfile + +.PHONY: all \ No newline at end of file diff --git a/networks/local/realio-network/Dockerfile b/networks/local/realio-network/Dockerfile new file mode 100644 index 00000000..9185afec --- /dev/null +++ b/networks/local/realio-network/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.19-bullseye as build-env + +# Install minimum necessary dependencies +ENV PACKAGES curl make git libc-dev bash gcc +RUN apt-get update && apt-get upgrade -y && \ + apt-get install -y $PACKAGES + +# Set working directory for the build +WORKDIR /go/src/github.com/realiotech/realio-network + +# Add source files +COPY . . + +# build realio-networkd +RUN make build + +# Final image +FROM golang:1.19-bullseye as final + +WORKDIR / + +RUN apt-get update + +# Copy over binaries from the build-env +COPY --from=build-env /go/src/github.com/realiotech/realio-network/build/realio-networkd / +COPY --from=build-env /go/src/github.com/realiotech/realio-network/scripts/start-docker.sh / + +EXPOSE 26656 26657 1317 9090 8545 8546 + +# Run realio-networkd by default, omit entrypoint to ease using container with realio-networkd +ENTRYPOINT ["/bin/bash", "-c"] \ No newline at end of file diff --git a/scripts/start-docker.sh b/scripts/start-docker.sh new file mode 100644 index 00000000..2fe89fe8 --- /dev/null +++ b/scripts/start-docker.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +KEY="dev0" +CHAINID="realionetworklocal_7777-1" +MONIKER="mymoniker" +DATA_DIR=$(mktemp -d -t realionetwork-datadir.XXXXX) + +echo "create and add new keys" +./realio-networkd keys add $KEY --home $DATA_DIR --no-backup --chain-id $CHAINID --algo "eth_secp256k1" --keyring-backend test +echo "init RealioNetwork with moniker=$MONIKER and chain-id=$CHAINID" +./realio-networkd init $MONIKER --chain-id $CHAINID --home $DATA_DIR +echo "prepare genesis: Allocate genesis accounts" +./realio-networkd add-genesis-account \ +"$(./realio-networkd keys show $KEY -a --home $DATA_DIR --keyring-backend test)" 1000000000000000000ario,1000000000000000000arst \ +--home $DATA_DIR --keyring-backend test +echo "prepare genesis: Sign genesis transaction" +./realio-networkd gentx $KEY 1000000000000000000ario--keyring-backend test --home $DATA_DIR --keyring-backend test --chain-id $CHAINID +echo "prepare genesis: Collect genesis tx" +./realio-networkd collect-gentxs --home $DATA_DIR +echo "prepare genesis: Run validate-genesis to ensure everything worked and that the genesis file is setup correctly" +./realio-networkd validate-genesis --home $DATA_DIR + +echo "starting RealioNetwork node $i in background ..." +./realio-networkd start --pruning=nothing --rpc.unsafe \ +--keyring-backend test --home $DATA_DIR \ +>$DATA_DIR/node.log 2>&1 & disown + +echo "started RealioNetwork node" +tail -f /dev/null \ No newline at end of file diff --git a/testutil/network/doc.go b/testutil/network/doc.go new file mode 100644 index 00000000..9e44f371 --- /dev/null +++ b/testutil/network/doc.go @@ -0,0 +1,65 @@ +/* +Package network implements and exposes a fully operational in-process Tendermint +test network that consists of at least one or potentially many validators. This +test network can be used primarily for integration tests or unit test suites. + +The test network utilizes SimApp as the ABCI application and uses all the modules +defined in the Cosmos SDK. An in-process test network can be configured with any +number of validators as well as account funds and even custom genesis state. + +When creating a test network, a series of Validator objects are returned. Each +Validator object has useful information such as their address and public key. A +Validator will also provide its RPC, P2P, and API addresses that can be useful +for integration testing. In addition, a Tendermint local RPC client is also provided +which can be handy for making direct RPC calls to Tendermint. + +Note, due to limitations in concurrency and the design of the RPC layer in +Tendermint, only the first Validator object will have an RPC and API client +exposed. Due to this exact same limitation, only a single test network can exist +at a time. A caller must be certain it calls Cleanup after it no longer needs +the network. + +A typical testing flow might look like the following: + + type IntegrationTestSuite struct { + suite.Suite + + cfg testutil.Config + network *testutil.Network + } + + func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + + cfg := testutil.DefaultConfig() + cfg.NumValidators = 1 + + s.cfg = cfg + s.network = testutil.New(s.T(), cfg) + + _, err := s.network.WaitForHeight(1) + s.Require().NoError(err) + } + + func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + + // This is important and must be called to ensure other tests can create + // a network! + s.network.Cleanup() + } + + func (s *IntegrationTestSuite) TestQueryBalancesRequestHandlerFn() { + val := s.network.Validators[0] + baseURL := val.APIAddress + + // Use baseURL to make API HTTP requests or use val.RPCClient to make direct + // Tendermint RPC calls. + // ... + } + + func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) + } +*/ +package network diff --git a/testutil/network/network.go b/testutil/network/network.go new file mode 100644 index 00000000..ff95af22 --- /dev/null +++ b/testutil/network/network.go @@ -0,0 +1,698 @@ +package network + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "github.com/realiotech/realio-network/types" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "cosmossdk.io/math" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/spf13/cobra" + tmcfg "github.com/tendermint/tendermint/config" + tmflags "github.com/tendermint/tendermint/libs/cli/flags" + "github.com/tendermint/tendermint/libs/log" + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/node" + tmclient "github.com/tendermint/tendermint/rpc/client" + dbm "github.com/tendermint/tm-db" + "google.golang.org/grpc" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + pruningtypes "github.com/cosmos/cosmos-sdk/pruning/types" + "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/api" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/simapp/params" + "github.com/cosmos/cosmos-sdk/testutil" + 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" + "github.com/cosmos/cosmos-sdk/x/genutil" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/evmos/ethermint/crypto/hd" + "github.com/realiotech/realio-network/app" + + "github.com/evmos/ethermint/encoding" + "github.com/evmos/ethermint/server/config" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +// package-wide network lock to only allow one test network at a time +var lock = new(sync.Mutex) + +// AppConstructor defines a function which accepts a network configuration and +// creates an ABCI Application to provide to Tendermint. +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 simapp.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 + 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) + EnableTMLogging bool // enable Tendermint 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 +// testing requirements. +func DefaultConfig() Config { + encCfg := encoding.MakeConfig(app.ModuleBasics) + + return Config{ + Codec: encCfg.Codec, + TxConfig: encCfg.TxConfig, + LegacyAmino: encCfg.Amino, + InterfaceRegistry: encCfg.InterfaceRegistry, + AccountRetriever: authtypes.AccountRetriever{}, + AppConstructor: NewAppConstructor(encCfg), + GenesisState: app.ModuleBasics.DefaultGenesis(encCfg.Codec), + TimeoutCommit: 2 * time.Second, + ChainID: fmt.Sprintf("realionetworklocal%d-1", tmrand.Int63n(9999999999999)+1), + NumValidators: 4, + BondDenom: types.BaseDenom, + MinGasPrices: fmt.Sprintf("0.000006%s", types.BaseDenom), + AccountTokens: sdk.TokensFromConsensusPower(1000000000000000000, ethermint.PowerReduction), + StakingTokens: sdk.TokensFromConsensusPower(500000000000000000, ethermint.PowerReduction), + BondedTokens: sdk.TokensFromConsensusPower(100000000000000000, ethermint.PowerReduction), + PruningStrategy: pruningtypes.PruningOptionNothing, + CleanupDir: true, + SigningAlgo: string(hd.EthSecp256k1Type), + KeyringOptions: []keyring.Option{hd.EthSecp256k1Option()}, + PrintMnemonic: false, + } +} + +// NewAppConstructor returns a new Realio Network AppConstructor +func NewAppConstructor(encodingCfg params.EncodingConfig) AppConstructor { + return func(val Validator) servertypes.Application { + return app.New( + val.Ctx.Logger, dbm.NewMemDB(), nil, true, make(map[int64]bool), val.Ctx.Config.RootDir, 0, + encodingCfg, + simapp.EmptyAppOptions{}, + baseapp.SetPruning(pruningtypes.NewPruningOptionsFromString(val.AppConfig.Pruning)), + baseapp.SetMinGasPrices(val.AppConfig.MinGasPrices), + ) + } +} + +type ( + // Network defines a local in-process testing network using SimApp. It can be + // configured to start any number of validators, each with its own RPC and API + // clients. Typically, this test network would be used in client and integration + // testing where user input is expected. + // + // Note, due to Tendermint constraints in regards to RPC functionality, there + // may only be one test network running at a time. Thus, any caller must be + // sure to Cleanup after testing is finished in order to allow other tests + // to create networks. In addition, only the first validator will have a valid + // RPC and API server/client. + Network struct { + Logger Logger + BaseDir string + Validators []*Validator + + Config Config + } + + // Validator defines an in-process Tendermint validator node. Through this object, + // a client can make RPC and API calls and interact with any client command + // or handler. + Validator struct { + AppConfig *config.Config + ClientCtx client.Context + Ctx *server.Context + Dir string + NodeID string + PubKey cryptotypes.PubKey + Moniker string + APIAddress string + RPCAddress string + P2PAddress string + Address sdk.AccAddress + ValAddress sdk.ValAddress + RPCClient tmclient.Client + JSONRPCClient *ethclient.Client + + tmNode *node.Node + api *api.Server + grpc *grpc.Server + grpcWeb *http.Server + jsonrpc *http.Server + jsonrpcDone chan struct{} + } +) + +// Logger is a network logger interface that exposes testnet-level Log() methods for an in-process testing network +// This is not to be confused with logging that may happen at an individual node or validator level +type Logger interface { + Log(args ...interface{}) + Logf(format string, args ...interface{}) +} + +var ( + _ Logger = (*testing.T)(nil) + _ Logger = (*CLILogger)(nil) +) + +type CLILogger struct { + cmd *cobra.Command +} + +func (s CLILogger) Log(args ...interface{}) { + s.cmd.Println(args...) +} + +func (s CLILogger) Logf(format string, args ...interface{}) { + s.cmd.Printf(format, args...) +} + +func NewCLILogger(cmd *cobra.Command) CLILogger { + return CLILogger{cmd} +} + +// New creates a new Network for integration tests or in-process testnets run via the CLI +func New(l Logger, baseDir string, cfg Config) (*Network, error) { + // only one caller/test can create and use a network at a time + l.Log("acquiring test network lock") + lock.Lock() + + if !ethermint.IsValidChainID(cfg.ChainID) { + return nil, fmt.Errorf("invalid chain-id: %s", cfg.ChainID) + } + + network := &Network{ + Logger: l, + BaseDir: baseDir, + Validators: make([]*Validator, cfg.NumValidators), + Config: cfg, + } + + l.Logf("preparing test network with chain-id \"%s\"\n", cfg.ChainID) + + monikers := make([]string, cfg.NumValidators) + nodeIDs := make([]string, cfg.NumValidators) + valPubKeys := make([]cryptotypes.PubKey, cfg.NumValidators) + + var ( + genAccounts []authtypes.GenesisAccount + genBalances []banktypes.Balance + genFiles []string + ) + + buf := bufio.NewReader(os.Stdin) + + // generate private keys, node IDs, and initial transactions + for i := 0; i < cfg.NumValidators; i++ { + appCfg := config.DefaultConfig() + appCfg.Pruning = cfg.PruningStrategy + appCfg.MinGasPrices = cfg.MinGasPrices + appCfg.API.Enable = true + appCfg.API.Swagger = false + appCfg.Telemetry.Enabled = false + appCfg.Telemetry.GlobalLabels = [][]string{{"chain_id", cfg.ChainID}} + + ctx := server.NewDefaultContext() + tmCfg := ctx.Config + tmCfg.Consensus.TimeoutCommit = cfg.TimeoutCommit + + // Only allow the first validator to expose an RPC, API and gRPC + // server/client due to Tendermint in-process constraints. + apiAddr := "" + tmCfg.RPC.ListenAddress = "" + appCfg.GRPC.Enable = false + appCfg.GRPCWeb.Enable = false + appCfg.JSONRPC.Enable = false + apiListenAddr := "" + if i == 0 { + fmt.Println("hello") + if cfg.APIAddress != "" { + apiListenAddr = cfg.APIAddress + } else { + var err error + apiListenAddr, _, err = server.FreeTCPAddr() + if err != nil { + return nil, err + } + } + + appCfg.API.Address = apiListenAddr + apiURL, err := url.Parse(apiListenAddr) + if err != nil { + return nil, err + } + apiAddr = fmt.Sprintf("http://%s:%s", apiURL.Hostname(), apiURL.Port()) + + if cfg.RPCAddress != "" { + tmCfg.RPC.ListenAddress = cfg.RPCAddress + } else { + rpcAddr, _, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + tmCfg.RPC.ListenAddress = rpcAddr + } + + if cfg.GRPCAddress != "" { + appCfg.GRPC.Address = cfg.GRPCAddress + } else { + _, grpcPort, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + appCfg.GRPC.Address = fmt.Sprintf("0.0.0.0:%s", grpcPort) + } + appCfg.GRPC.Enable = true + + _, grpcWebPort, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + appCfg.GRPCWeb.Address = fmt.Sprintf("0.0.0.0:%s", grpcWebPort) + appCfg.GRPCWeb.Enable = true + + if cfg.JSONRPCAddress != "" { + appCfg.JSONRPC.Address = cfg.JSONRPCAddress + } else { + _, jsonRPCPort, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + appCfg.JSONRPC.Address = fmt.Sprintf("0.0.0.0:%s", jsonRPCPort) + } + appCfg.JSONRPC.Enable = true + appCfg.JSONRPC.API = config.GetAPINamespaces() + } + + logger := log.NewNopLogger() + if cfg.EnableTMLogging { + logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + logger, _ = tmflags.ParseLogLevel("info", logger, tmcfg.DefaultLogLevel) + } + + ctx.Logger = logger + + nodeDirName := fmt.Sprintf("node%d", i) + nodeDir := filepath.Join(network.BaseDir, nodeDirName, "realio-networkd") + clientDir := filepath.Join(network.BaseDir, nodeDirName, "realionetworkcli") + gentxsDir := filepath.Join(network.BaseDir, "gentxs") + + err := os.MkdirAll(filepath.Join(nodeDir, "config"), 0o750) + if err != nil { + return nil, err + } + + err = os.MkdirAll(clientDir, 0o750) + if err != nil { + return nil, err + } + + tmCfg.SetRoot(nodeDir) + tmCfg.Moniker = nodeDirName + monikers[i] = nodeDirName + + proxyAddr, _, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + tmCfg.ProxyApp = proxyAddr + + p2pAddr, _, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + tmCfg.P2P.ListenAddress = p2pAddr + tmCfg.P2P.AddrBookStrict = false + tmCfg.P2P.AllowDuplicateIP = true + + nodeID, pubKey, err := genutil.InitializeNodeValidatorFiles(tmCfg) + if err != nil { + return nil, err + } + nodeIDs[i] = nodeID + valPubKeys[i] = pubKey + + kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, clientDir, buf, cfg.Codec, cfg.KeyringOptions...) + if err != nil { + return nil, err + } + + keyringAlgos, _ := kb.SupportedAlgorithms() + algo, err := keyring.NewSigningAlgoFromString(cfg.SigningAlgo, keyringAlgos) + if err != nil { + return nil, err + } + + addr, secret, err := testutil.GenerateSaveCoinKey(kb, nodeDirName, "", true, algo) + if err != nil { + return nil, err + } + + // if PrintMnemonic is set to true, we print the first validator node's secret to the network's logger + // for debugging and manual testing + if cfg.PrintMnemonic && i == 0 { + printMnemonic(l, secret) + } + + info := map[string]string{"secret": secret} + infoBz, err := json.Marshal(info) + if err != nil { + return nil, err + } + + // save private key seed words + err = WriteFile(fmt.Sprintf("%v.json", "key_seed"), clientDir, infoBz) + if err != nil { + return nil, err + } + + balances := sdk.NewCoins( + sdk.NewCoin(fmt.Sprintf("%stoken", nodeDirName), cfg.AccountTokens), + sdk.NewCoin(cfg.BondDenom, cfg.StakingTokens), + ) + + genFiles = append(genFiles, tmCfg.GenesisFile()) + genBalances = append(genBalances, banktypes.Balance{Address: addr.String(), Coins: balances.Sort()}) + genAccounts = append(genAccounts, ðermint.EthAccount{ + BaseAccount: authtypes.NewBaseAccount(addr, nil, 0, 0), + CodeHash: common.BytesToHash(evmtypes.EmptyCodeHash).Hex(), + }) + + commission, err := sdk.NewDecFromStr("0.5") + if err != nil { + return nil, err + } + + createValMsg, err := stakingtypes.NewMsgCreateValidator( + sdk.ValAddress(addr), + valPubKeys[i], + sdk.NewCoin(cfg.BondDenom, cfg.BondedTokens), + stakingtypes.NewDescription(nodeDirName, "", "", "", ""), + stakingtypes.NewCommissionRates(commission, sdk.OneDec(), sdk.OneDec()), + sdk.OneInt(), + ) + if err != nil { + return nil, err + } + + p2pURL, err := url.Parse(p2pAddr) + if err != nil { + return nil, err + } + + memo := fmt.Sprintf("%s@%s:%s", nodeIDs[i], p2pURL.Hostname(), p2pURL.Port()) + fee := sdk.NewCoins(sdk.NewCoin(cfg.BondDenom, sdk.NewInt(0))) + txBuilder := cfg.TxConfig.NewTxBuilder() + err = txBuilder.SetMsgs(createValMsg) + if err != nil { + return nil, err + } + txBuilder.SetFeeAmount(fee) // Arbitrary fee + txBuilder.SetGasLimit(1000000) // Need at least 100386 + txBuilder.SetMemo(memo) + + txFactory := tx.Factory{} + txFactory = txFactory. + WithChainID(cfg.ChainID). + WithMemo(memo). + WithKeybase(kb). + WithTxConfig(cfg.TxConfig) + + if err := tx.Sign(txFactory, nodeDirName, txBuilder, true); err != nil { + return nil, err + } + + txBz, err := cfg.TxConfig.TxJSONEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, err + } + + if err := WriteFile(fmt.Sprintf("%v.json", nodeDirName), gentxsDir, txBz); err != nil { + return nil, err + } + + customAppTemplate, _ := config.AppConfig(types.BaseDenom) + srvconfig.SetConfigTemplate(customAppTemplate) + srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config/app.toml"), appCfg) + + ctx.Viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + ctx.Viper.SetConfigFile(filepath.Join(nodeDir, "config/app.toml")) + err = ctx.Viper.ReadInConfig() + if err != nil { + return nil, err + } + + clientCtx := client.Context{}. + WithKeyringDir(clientDir). + WithKeyring(kb). + WithHomeDir(tmCfg.RootDir). + WithChainID(cfg.ChainID). + WithInterfaceRegistry(cfg.InterfaceRegistry). + WithCodec(cfg.Codec). + WithLegacyAmino(cfg.LegacyAmino). + WithTxConfig(cfg.TxConfig). + WithAccountRetriever(cfg.AccountRetriever) + + network.Validators[i] = &Validator{ + AppConfig: appCfg, + ClientCtx: clientCtx, + Ctx: ctx, + Dir: filepath.Join(network.BaseDir, nodeDirName), + NodeID: nodeID, + PubKey: pubKey, + Moniker: nodeDirName, + RPCAddress: tmCfg.RPC.ListenAddress, + P2PAddress: tmCfg.P2P.ListenAddress, + APIAddress: apiAddr, + Address: addr, + ValAddress: sdk.ValAddress(addr), + } + } + + err := initGenFiles(cfg, genAccounts, genBalances, genFiles) + if err != nil { + return nil, err + } + err = collectGenFiles(cfg, network.Validators, network.BaseDir) + if err != nil { + return nil, err + } + + l.Log("starting test network...") + for _, v := range network.Validators { + err := startInProcess(cfg, v) + if err != nil { + return nil, err + } + } + + l.Log("started test network") + + // Ensure we cleanup incase any test was abruptly halted (e.g. SIGINT) as any + // defer in a test would not be called. + server.TrapSignal(network.Cleanup) + + return network, nil +} + +// LatestHeight returns the latest height of the network or an error if the +// query fails or no validators exist. +func (n *Network) LatestHeight() (int64, error) { + if len(n.Validators) == 0 { + return 0, errors.New("no validators available") + } + + status, err := n.Validators[0].RPCClient.Status(context.Background()) + if err != nil { + return 0, err + } + + return status.SyncInfo.LatestBlockHeight, nil +} + +// WaitForHeight performs a blocking check where it waits for a block to be +// committed after a given block. If that height is not reached within a timeout, +// an error is returned. Regardless, the latest height queried is returned. +func (n *Network) WaitForHeight(h int64) (int64, error) { + return n.WaitForHeightWithTimeout(h, 10*time.Second) +} + +// WaitForHeightWithTimeout is the same as WaitForHeight except the caller can +// provide a custom timeout. +func (n *Network) WaitForHeightWithTimeout(h int64, t time.Duration) (int64, error) { + ticker := time.NewTicker(time.Second) + timeout := time.After(t) + + if len(n.Validators) == 0 { + return 0, errors.New("no validators available") + } + + var latestHeight int64 + val := n.Validators[0] + + for { + select { + case <-timeout: + ticker.Stop() + return latestHeight, errors.New("timeout exceeded waiting for block") + case <-ticker.C: + status, err := val.RPCClient.Status(context.Background()) + if err == nil && status != nil { + latestHeight = status.SyncInfo.LatestBlockHeight + if latestHeight >= h { + return latestHeight, nil + } + } + } + } +} + +// WaitForNextBlock waits for the next block to be committed, returning an error +// upon failure. +func (n *Network) WaitForNextBlock() error { + lastBlock, err := n.LatestHeight() + if err != nil { + return err + } + + _, err = n.WaitForHeight(lastBlock + 1) + if err != nil { + return err + } + + return err +} + +// Cleanup removes the root testing (temporary) directory and stops both the +// Tendermint and API services. It allows other callers to create and start +// test networks. This method must be called when a test is finished, typically +// in a defer. +func (n *Network) Cleanup() { + defer func() { + lock.Unlock() + n.Logger.Log("released test network lock") + }() + + n.Logger.Log("cleaning up test network...") + + for _, v := range n.Validators { + if v.tmNode != nil && v.tmNode.IsRunning() { + _ = v.tmNode.Stop() + } + + if v.api != nil { + _ = v.api.Close() + } + + if v.grpc != nil { + v.grpc.Stop() + if v.grpcWeb != nil { + _ = v.grpcWeb.Close() + } + } + + if v.jsonrpc != nil { + shutdownCtx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + if err := v.jsonrpc.Shutdown(shutdownCtx); err != nil { + v.tmNode.Logger.Error("HTTP server shutdown produced a warning", "error", err.Error()) + } else { + v.tmNode.Logger.Info("HTTP server shut down, waiting 5 sec") + select { + case <-time.Tick(5 * time.Second): + case <-v.jsonrpcDone: + } + } + } + } + + if n.Config.CleanupDir { + _ = os.RemoveAll(n.BaseDir) + } + + n.Logger.Log("finished cleaning up test network") +} + +// printMnemonic prints a provided mnemonic seed phrase on a network logger +// for debugging and manual testing +func printMnemonic(l Logger, secret string) { + lines := []string{ + "THIS MNEMONIC IS FOR TESTING PURPOSES ONLY", + "DO NOT USE IN PRODUCTION", + "", + strings.Join(strings.Fields(secret)[0:8], " "), + strings.Join(strings.Fields(secret)[8:16], " "), + strings.Join(strings.Fields(secret)[16:24], " "), + } + + lineLengths := make([]int, len(lines)) + for i, line := range lines { + lineLengths[i] = len(line) + } + + maxLineLength := 0 + for _, lineLen := range lineLengths { + if lineLen > maxLineLength { + maxLineLength = lineLen + } + } + + l.Log("\n") + l.Log(strings.Repeat("+", maxLineLength+8)) + for _, line := range lines { + l.Logf("++ %s ++\n", centerText(line, maxLineLength)) + } + l.Log(strings.Repeat("+", maxLineLength+8)) + l.Log("\n") +} + +// centerText centers text across a fixed width, filling either side with whitespace buffers +func centerText(text string, width int) string { + textLen := len(text) + leftBuffer := strings.Repeat(" ", (width-textLen)/2) + rightBuffer := strings.Repeat(" ", (width-textLen)/2+(width-textLen)%2) + + return fmt.Sprintf("%s%s%s", leftBuffer, text, rightBuffer) +} diff --git a/testutil/network/util.go b/testutil/network/util.go new file mode 100644 index 00000000..086d12db --- /dev/null +++ b/testutil/network/util.go @@ -0,0 +1,256 @@ +package network + +import ( + "encoding/json" + "fmt" + "path/filepath" + "time" + + "github.com/ethereum/go-ethereum/ethclient" + tmos "github.com/tendermint/tendermint/libs/os" + "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" + pvm "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/rpc/client/local" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/cosmos/cosmos-sdk/server/api" + servergrpc "github.com/cosmos/cosmos-sdk/server/grpc" + srvtypes "github.com/cosmos/cosmos-sdk/server/types" + 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" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/evmos/ethermint/server" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +func startInProcess(cfg Config, val *Validator) error { + logger := val.Ctx.Logger + tmCfg := val.Ctx.Config + tmCfg.Instrumentation.Prometheus = false + + if err := val.AppConfig.ValidateBasic(); err != nil { + return err + } + + nodeKey, err := p2p.LoadOrGenNodeKey(tmCfg.NodeKeyFile()) + if err != nil { + return err + } + + app := cfg.AppConstructor(*val) + + genDocProvider := node.DefaultGenesisDocProviderFunc(tmCfg) + tmNode, err := node.NewNode( + tmCfg, + pvm.LoadOrGenFilePV(tmCfg.PrivValidatorKeyFile(), tmCfg.PrivValidatorStateFile()), + nodeKey, + proxy.NewLocalClientCreator(app), + genDocProvider, + node.DefaultDBProvider, + node.DefaultMetricsProvider(tmCfg.Instrumentation), + logger.With("module", val.Moniker), + ) + if err != nil { + return err + } + + if err := tmNode.Start(); err != nil { + return err + } + + val.tmNode = tmNode + + if val.RPCAddress != "" { + val.RPCClient = local.New(tmNode) + } + + // We'll need a RPC client if the validator exposes a gRPC or REST endpoint. + if val.APIAddress != "" || val.AppConfig.GRPC.Enable { + val.ClientCtx = val.ClientCtx. + WithClient(val.RPCClient) + + // Add the tx service in the gRPC router. + app.RegisterTxService(val.ClientCtx) + + // Add the tendermint queries service in the gRPC router. + app.RegisterTendermintService(val.ClientCtx) + } + + if val.AppConfig.API.Enable && val.APIAddress != "" { + apiSrv := api.New(val.ClientCtx, logger.With("module", "api-server")) + app.RegisterAPIRoutes(apiSrv, val.AppConfig.API) + + errCh := make(chan error) + + go func() { + if err := apiSrv.Start(val.AppConfig.Config); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-time.After(srvtypes.ServerStartTime): // assume server started successfully + } + + val.api = apiSrv + } + + if val.AppConfig.GRPC.Enable { + grpcSrv, err := servergrpc.StartGRPCServer(val.ClientCtx, app, val.AppConfig.GRPC) + if err != nil { + return err + } + + val.grpc = grpcSrv + + if val.AppConfig.GRPCWeb.Enable { + val.grpcWeb, err = servergrpc.StartGRPCWeb(grpcSrv, val.AppConfig.Config) + if err != nil { + return err + } + } + } + + if val.AppConfig.JSONRPC.Enable && val.AppConfig.JSONRPC.Address != "" { + if val.Ctx == nil || val.Ctx.Viper == nil { + return fmt.Errorf("validator %s context is nil", val.Moniker) + } + + tmEndpoint := "/websocket" + tmRPCAddr := fmt.Sprintf("tcp://%s", val.AppConfig.GRPC.Address) + + val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, nil) + if err != nil { + return err + } + + address := fmt.Sprintf("http://%s", val.AppConfig.JSONRPC.Address) + + val.JSONRPCClient, err = ethclient.Dial(address) + if err != nil { + return fmt.Errorf("failed to dial JSON-RPC at %s: %w", val.AppConfig.JSONRPC.Address, err) + } + } + + return nil +} + +func collectGenFiles(cfg Config, vals []*Validator, outputDir string) error { + genTime := tmtime.Now() + + for i := 0; i < cfg.NumValidators; i++ { + tmCfg := vals[i].Ctx.Config + + nodeDir := filepath.Join(outputDir, vals[i].Moniker, "realio-networkd") + gentxsDir := filepath.Join(outputDir, "gentxs") + + tmCfg.Moniker = vals[i].Moniker + tmCfg.SetRoot(nodeDir) + + initCfg := genutiltypes.NewInitConfig(cfg.ChainID, gentxsDir, vals[i].NodeID, vals[i].PubKey) + + genFile := tmCfg.GenesisFile() + genDoc, err := types.GenesisDocFromFile(genFile) + if err != nil { + return err + } + + appState, err := genutil.GenAppStateFromConfig(cfg.Codec, cfg.TxConfig, + tmCfg, initCfg, *genDoc, banktypes.GenesisBalancesIterator{}) + if err != nil { + return err + } + + // overwrite each validator's genesis file to have a canonical genesis time + if err := genutil.ExportGenesisFileWithTime(genFile, cfg.ChainID, nil, appState, genTime); err != nil { + return err + } + } + + return nil +} + +func initGenFiles(cfg Config, genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, genFiles []string) error { + // set the accounts in the genesis state + var authGenState authtypes.GenesisState + cfg.Codec.MustUnmarshalJSON(cfg.GenesisState[authtypes.ModuleName], &authGenState) + + accounts, err := authtypes.PackAccounts(genAccounts) + if err != nil { + return err + } + + authGenState.Accounts = append(authGenState.Accounts, accounts...) + cfg.GenesisState[authtypes.ModuleName] = cfg.Codec.MustMarshalJSON(&authGenState) + + // set the balances in the genesis state + var bankGenState banktypes.GenesisState + bankGenState.Balances = genBalances + cfg.GenesisState[banktypes.ModuleName] = cfg.Codec.MustMarshalJSON(&bankGenState) + + var stakingGenState stakingtypes.GenesisState + cfg.Codec.MustUnmarshalJSON(cfg.GenesisState[stakingtypes.ModuleName], &stakingGenState) + + stakingGenState.Params.BondDenom = cfg.BondDenom + cfg.GenesisState[stakingtypes.ModuleName] = cfg.Codec.MustMarshalJSON(&stakingGenState) + + var govGenState govv1.GenesisState + cfg.Codec.MustUnmarshalJSON(cfg.GenesisState[govtypes.ModuleName], &govGenState) + + govGenState.DepositParams.MinDeposit[0].Denom = cfg.BondDenom + cfg.GenesisState[govtypes.ModuleName] = cfg.Codec.MustMarshalJSON(&govGenState) + + var crisisGenState crisistypes.GenesisState + cfg.Codec.MustUnmarshalJSON(cfg.GenesisState[crisistypes.ModuleName], &crisisGenState) + + crisisGenState.ConstantFee.Denom = cfg.BondDenom + cfg.GenesisState[crisistypes.ModuleName] = cfg.Codec.MustMarshalJSON(&crisisGenState) + + var evmGenState evmtypes.GenesisState + cfg.Codec.MustUnmarshalJSON(cfg.GenesisState[evmtypes.ModuleName], &evmGenState) + + evmGenState.Params.EvmDenom = cfg.BondDenom + cfg.GenesisState[evmtypes.ModuleName] = cfg.Codec.MustMarshalJSON(&evmGenState) + + appGenStateJSON, err := json.MarshalIndent(cfg.GenesisState, "", " ") + if err != nil { + return err + } + + genDoc := types.GenesisDoc{ + ChainID: cfg.ChainID, + AppState: appGenStateJSON, + Validators: nil, + } + + // generate empty genesis files for each validator and save + for i := 0; i < cfg.NumValidators; i++ { + if err := genDoc.SaveAs(genFiles[i]); err != nil { + return err + } + } + + return nil +} + +func WriteFile(name string, dir string, contents []byte) error { + file := filepath.Join(dir, name) + + err := tmos.EnsureDir(dir, 0o755) + if err != nil { + return err + } + + return tmos.WriteFile(file, contents, 0o644) +}