laconicd/client/testnet.go

570 lines
19 KiB
Go
Raw Permalink Normal View History

package client
// DONTCOVER
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"github.com/ethereum/go-ethereum/common"
2021-04-18 15:54:18 +00:00
"github.com/spf13/cobra"
tmconfig "github.com/tendermint/tendermint/config"
2022-10-10 10:38:33 +00:00
tmrand "github.com/tendermint/tendermint/libs/rand"
2021-04-17 10:00:07 +00:00
"github.com/tendermint/tendermint/types"
2022-10-10 10:38:33 +00:00
tmtime "github.com/tendermint/tendermint/types/time"
2021-04-17 10:00:07 +00:00
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
2021-04-17 10:00:07 +00:00
"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"
2021-04-17 10:00:07 +00:00
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"
2022-10-10 10:38:33 +00:00
govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
2021-04-17 10:00:07 +00:00
mintypes "github.com/cosmos/cosmos-sdk/x/mint/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/cerc-io/laconicd/crypto/hd"
"github.com/cerc-io/laconicd/server/config"
srvflags "github.com/cerc-io/laconicd/server/flags"
ethermint "github.com/cerc-io/laconicd/types"
evmtypes "github.com/cerc-io/laconicd/x/evm/types"
"github.com/cerc-io/laconicd/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")
2022-10-10 10:38:33 +00:00
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")
2022-10-10 10:38:33 +00:00
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",
2022-10-10 10:38:33 +00:00
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:
2022-10-10 10:38:33 +00:00
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
2021-04-17 10:00:07 +00:00
}
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, ...)")
2022-10-10 10:38:33 +00:00
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:
2022-10-10 10:38:33 +00:00
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(
2021-04-17 10:00:07 +00:00
clientCtx client.Context,
cmd *cobra.Command,
2021-04-17 10:00:07 +00:00
nodeConfig *tmconfig.Config,
mbm module.BasicManager,
2021-04-17 10:00:07 +00:00
genBalIterator banktypes.GenesisBalancesIterator,
args initArgs,
) error {
if args.chainID == "" {
2022-10-10 10:38:33 +00:00
args.chainID = fmt.Sprintf("ethermint_%d-1", tmrand.Int63n(9999999999999)+1)
}
nodeIDs := make([]string, args.numValidators)
valPubKeys := make([]cryptotypes.PubKey, args.numValidators)
2021-04-18 15:54:18 +00:00
appConfig := config.DefaultConfig()
appConfig.MinGasPrices = args.minGasPrices
2021-04-17 10:00:07 +00:00
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 (
2021-04-17 10:00:07 +00:00
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")
2021-04-17 10:00:07 +00:00
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
}
2021-04-17 10:00:07 +00:00
nodeConfig.Moniker = nodeDirName
ip, err := getIP(i, args.startingIPAddress)
if err != nil {
_ = os.RemoveAll(args.outputDir)
return err
}
2021-04-17 10:00:07 +00:00
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)
2021-04-17 10:00:07 +00:00
genFiles = append(genFiles, nodeConfig.GenesisFile())
kb, err := keyring.New(sdk.KeyringServiceName(), args.keyringBackend, nodeDir, inBuf, clientCtx.Codec, hd.EthSecp256k1Option())
if err != nil {
return err
}
2021-04-17 10:00:07 +00:00
keyringAlgos, _ := kb.SupportedAlgorithms()
algo, err := keyring.NewSigningAlgoFromString(args.algo, keyringAlgos)
2021-04-17 10:00:07 +00:00
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, &ethermint.EthAccount{
2021-04-17 10:00:07 +00:00
BaseAccount: authtypes.NewBaseAccount(addr, nil, 0, 0),
CodeHash: common.BytesToHash(evmtypes.EmptyCodeHash).Hex(),
})
valTokens := sdk.TokensFromConsensusPower(100, ethermint.PowerReduction)
2021-04-17 10:00:07 +00:00
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
}
2021-04-17 10:00:07 +00:00
txBuilder := clientCtx.TxConfig.NewTxBuilder()
if err := txBuilder.SetMsgs(createValMsg); err != nil {
return err
}
txBuilder.SetMemo(memo)
txFactory := tx.Factory{}
txFactory = txFactory.
WithChainID(args.chainID).
2021-04-17 10:00:07 +00:00
WithMemo(memo).
WithKeybase(kb).
WithTxConfig(clientCtx.TxConfig)
if err := tx.Sign(txFactory, nodeDirName, txBuilder, true); err != nil {
2021-04-17 10:00:07 +00:00
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)
2022-10-10 10:38:33 +00:00
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(
2021-04-17 10:00:07 +00:00
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
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[authtypes.ModuleName], &authGenState)
2021-04-17 10:00:07 +00:00
accounts, err := authtypes.PackAccounts(genAccounts)
if err != nil {
return err
}
authGenState.Accounts = accounts
2021-08-26 15:45:45 +00:00
appGenState[authtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&authGenState)
2021-04-17 10:00:07 +00:00
// set the balances in the genesis state
var bankGenState banktypes.GenesisState
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[banktypes.ModuleName], &bankGenState)
2021-04-17 10:00:07 +00:00
bankGenState.Balances = genBalances
2021-08-26 15:45:45 +00:00
appGenState[banktypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&bankGenState)
var stakingGenState stakingtypes.GenesisState
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[stakingtypes.ModuleName], &stakingGenState)
stakingGenState.Params.BondDenom = coinDenom
2021-08-26 15:45:45 +00:00
appGenState[stakingtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&stakingGenState)
2022-10-10 10:38:33 +00:00
var govGenState govv1.GenesisState
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[govtypes.ModuleName], &govGenState)
govGenState.DepositParams.MinDeposit[0].Denom = coinDenom
2021-08-26 15:45:45 +00:00
appGenState[govtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&govGenState)
2021-04-17 10:00:07 +00:00
var mintGenState mintypes.GenesisState
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[mintypes.ModuleName], &mintGenState)
mintGenState.Params.MintDenom = coinDenom
2021-08-26 15:45:45 +00:00
appGenState[mintypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&mintGenState)
2021-04-17 10:00:07 +00:00
var crisisGenState crisistypes.GenesisState
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[crisistypes.ModuleName], &crisisGenState)
crisisGenState.ConstantFee.Denom = coinDenom
2021-08-26 15:45:45 +00:00
appGenState[crisistypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&crisisGenState)
var evmGenState evmtypes.GenesisState
2021-08-26 15:45:45 +00:00
clientCtx.Codec.MustUnmarshalJSON(appGenState[evmtypes.ModuleName], &evmGenState)
evmGenState.Params.EvmDenom = coinDenom
2021-08-26 15:45:45 +00:00
appGenState[evmtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&evmGenState)
2021-04-17 10:00:07 +00:00
appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ")
if err != nil {
return err
}
2021-04-17 10:00:07 +00:00
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(
2021-04-17 10:00:07 +00:00
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
2022-10-10 10:38:33 +00:00
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")
2021-04-17 10:00:07 +00:00
nodeConfig.Moniker = nodeDirName
2021-04-17 10:00:07 +00:00
nodeConfig.SetRoot(nodeDir)
nodeID, valPubKey := nodeIDs[i], valPubKeys[i]
2021-04-17 10:00:07 +00:00
initCfg := genutiltypes.NewInitConfig(chainID, gentxsDir, nodeID, valPubKey)
2021-04-17 10:00:07 +00:00
genDoc, err := types.GenesisDocFromFile(nodeConfig.GenesisFile())
if err != nil {
return err
}
2021-08-26 15:45:45 +00:00
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
}
2021-04-17 10:00:07 +00:00
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
}