diff --git a/.gitignore b/.gitignore index da467c151f..f528ccc8fe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.swp *.swo .vscode +.idea # Build vendor @@ -15,6 +16,7 @@ docs/_build examples/basecoin/app/data baseapp/data/* client/lcd/keys/* +mytestnet # Testing coverage.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d3822227..1b55734035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 0.20.0 -*TBD* +*TBD* BREAKING CHANGES * Change default ports from 466xx to 266xx @@ -24,6 +24,8 @@ IMPROVEMENTS * [tests] Application module tests now use a mock application * [gaiacli] Fix error message when account isn't found when running gaiacli account * [lcd] refactored to eliminate use of global variables, and interdependent tests +* [tests] Added testnet command to gaiad +* [tests] Added localnet targets to Makefile * [x/stake] More stake tests added to test ByPower index FIXES diff --git a/Makefile b/Makefile index c0d18c4a39..f420446696 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PACKAGES=$(shell go list ./... | grep -v '/vendor/') PACKAGES_NOCLITEST=$(shell go list ./... | grep -v '/vendor/' | grep -v github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test) COMMIT_HASH := $(shell git rev-parse --short HEAD) -BUILD_FLAGS = -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=${COMMIT_HASH}" +BUILD_FLAGS = -tags netgo -ldflags "-X github.com/cosmos/cosmos-sdk/version.GitCommit=${COMMIT_HASH}" all: check_tools get_vendor_deps install install_examples test_lint test @@ -133,12 +133,27 @@ devdoc_update: ######################################## -### Remote validator nodes using terraform and ansible +### Local validator nodes using docker and docker-compose # Build linux binary build-linux: GOOS=linux GOARCH=amd64 $(MAKE) build +build-docker-gaiadnode: + $(MAKE) -C networks/local + +# Run a 4-node testnet locally +localnet-start: localnet-stop + @if ! [ -f build/node0/gaiad/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/gaiad:Z tendermint/gaiadnode testnet --v 4 --o . --starting-ip-address 192.168.10.2 ; fi + docker-compose up + +# Stop testnet +localnet-stop: + docker-compose down + +######################################## +### Remote validator nodes using terraform and ansible + TESTNET_NAME?=remotenet SERVERS?=4 BINARY=$(CURDIR)/build/gaiad @@ -160,4 +175,4 @@ remotenet-status: # To avoid unintended conflicts with file names, always add to .PHONY # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: build build_examples install install_examples install_debug dist check_tools get_tools get_vendor_deps draw_deps test test_cli test_unit test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update remotenet-start remotenet-stop remotenet-status +.PHONY: build build_examples install install_examples install_debug dist check_tools get_tools get_vendor_deps draw_deps test test_cli test_unit test_cover test_lint benchmark devdoc_init devdoc devdoc_save devdoc_update build-linux build-docker-gaiadnode localnet-start localnet-stop remotenet-start remotenet-stop remotenet-status diff --git a/cmd/gaia/app/genesis.go b/cmd/gaia/app/genesis.go index 06fdbbcc50..38ef53bbfd 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -3,19 +3,24 @@ package app import ( "encoding/json" "errors" - "github.com/spf13/pflag" - "github.com/spf13/viper" crypto "github.com/tendermint/go-crypto" tmtypes "github.com/tendermint/tendermint/types" "github.com/cosmos/cosmos-sdk/server" + "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/stake" ) +var ( + // bonded tokens given to genesis validators/accounts + freeTokenVal = int64(100) + freeTokensAcc = int64(50) +) + // State to Unmarshal type GenesisState struct { Accounts []GenesisAccount `json:"accounts"` @@ -50,25 +55,15 @@ func (ga *GenesisAccount) ToAccount() (acc *auth.BaseAccount) { } } -var ( - flagName = "name" - flagClientHome = "home-client" - flagOWK = "owk" - - // bonded tokens given to genesis validators/accounts - freeFermionVal = int64(100) - freeFermionsAcc = int64(50) -) - // get app init parameters for server init command func GaiaAppInit() server.AppInit { fsAppGenState := pflag.NewFlagSet("", pflag.ContinueOnError) fsAppGenTx := pflag.NewFlagSet("", pflag.ContinueOnError) - fsAppGenTx.String(flagName, "", "validator moniker, required") - fsAppGenTx.String(flagClientHome, DefaultCLIHome, + fsAppGenTx.String(server.FlagName, "", "validator moniker, required") + fsAppGenTx.String(server.FlagClientHome, DefaultCLIHome, "home directory for the client, used for key generation") - fsAppGenTx.Bool(flagOWK, false, "overwrite the accounts created") + fsAppGenTx.Bool(server.FlagOWK, false, "overwrite the accounts created") return server.AppInit{ FlagsAppGenState: fsAppGenState, @@ -86,18 +81,15 @@ type GaiaGenTx struct { } // Generate a gaia genesis transaction with flags -func GaiaAppGenTx(cdc *wire.Codec, pk crypto.PubKey) ( +func GaiaAppGenTx(cdc *wire.Codec, pk crypto.PubKey, genTxConfig config.GenTx) ( appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { - clientRoot := viper.GetString(flagClientHome) - overwrite := viper.GetBool(flagOWK) - name := viper.GetString(flagName) - if name == "" { - return nil, nil, tmtypes.GenesisValidator{}, errors.New("must specify --name (validator moniker)") + if genTxConfig.Name == "" { + return nil, nil, tmtypes.GenesisValidator{}, errors.New("Must specify --name (validator moniker)") } var addr sdk.Address var secret string - addr, secret, err = server.GenerateSaveCoinKey(clientRoot, name, "1234567890", overwrite) + addr, secret, err = server.GenerateSaveCoinKey(genTxConfig.CliRoot, genTxConfig.Name, "1234567890", genTxConfig.Overwrite) if err != nil { return } @@ -107,8 +99,9 @@ func GaiaAppGenTx(cdc *wire.Codec, pk crypto.PubKey) ( if err != nil { return } + cliPrint = json.RawMessage(bz) - appGenTx,_,validator,err = GaiaAppGenTxNF(cdc, pk, addr, name, overwrite) + appGenTx, _, validator, err = GaiaAppGenTxNF(cdc, pk, addr, genTxConfig.Name, genTxConfig.Overwrite) return } @@ -130,7 +123,7 @@ func GaiaAppGenTxNF(cdc *wire.Codec, pk crypto.PubKey, addr sdk.Address, name st validator = tmtypes.GenesisValidator{ PubKey: pk, - Power: freeFermionVal, + Power: freeTokenVal, } return } @@ -161,21 +154,21 @@ func GaiaAppGenState(cdc *wire.Codec, appGenTxs []json.RawMessage) (genesisState accAuth := auth.NewBaseAccountWithAddress(genTx.Address) accAuth.Coins = sdk.Coins{ {genTx.Name + "Token", 1000}, - {"steak", freeFermionsAcc}, + {"steak", freeTokensAcc}, } acc := NewGenesisAccount(&accAuth) genaccs[i] = acc - stakeData.Pool.LooseUnbondedTokens += freeFermionsAcc // increase the supply + stakeData.Pool.LooseUnbondedTokens += freeTokensAcc // increase the supply // add the validator if len(genTx.Name) > 0 { desc := stake.NewDescription(genTx.Name, "", "", "") validator := stake.NewValidator(genTx.Address, genTx.PubKey, desc) - validator.PoolShares = stake.NewBondedShares(sdk.NewRat(freeFermionVal)) + validator.PoolShares = stake.NewBondedShares(sdk.NewRat(freeTokenVal)) stakeData.Validators = append(stakeData.Validators, validator) // pool logic - stakeData.Pool.BondedTokens += freeFermionVal + stakeData.Pool.BondedTokens += freeTokenVal stakeData.Pool.BondedShares = sdk.NewRat(stakeData.Pool.BondedTokens) } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..fca518db4e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,68 @@ +version: '3' + +services: + gaiadnode0: + container_name: gaiadnode0 + image: "tendermint/gaiadnode" + ports: + - "26656-26657:26656-26657" + environment: + - ID=0 + - LOG=$${LOG:-gaiad.log} + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.2 + + gaiadnode1: + container_name: gaiadnode1 + image: "tendermint/gaiadnode" + ports: + - "26659-26660:26656-26657" + environment: + - ID=1 + - LOG=$${LOG:-gaiad.log} + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.3 + + gaiadnode2: + container_name: gaiadnode2 + image: "tendermint/gaiadnode" + environment: + - ID=2 + - LOG=$${LOG:-gaiad.log} + ports: + - "26661-26662:26656-26657" + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.4 + + gaiadnode3: + container_name: gaiadnode3 + image: "tendermint/gaiadnode" + environment: + - ID=3 + - LOG=$${LOG:-gaiad.log} + ports: + - "26663-26664:26656-26657" + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.5 + +networks: + localnet: + driver: bridge + ipam: + driver: default + config: + - + subnet: 192.168.10.0/16 + diff --git a/networks/local/Makefile b/networks/local/Makefile new file mode 100644 index 0000000000..c707a168ee --- /dev/null +++ b/networks/local/Makefile @@ -0,0 +1,7 @@ +# Makefile for the "gaiadnode" docker image. + +all: + docker build --tag tendermint/gaiadnode gaiadnode + +.PHONY: all + diff --git a/networks/local/README.md b/networks/local/README.md new file mode 100644 index 0000000000..e16d947ab1 --- /dev/null +++ b/networks/local/README.md @@ -0,0 +1,79 @@ +# Local Cluster with Docker Compose + +## Requirements + +- [Install gaia](/docs/index.rst) +- [Install docker](https://docs.docker.com/engine/installation/) +- [Install docker-compose](https://docs.docker.com/compose/install/) + +## Build + +Build the `gaiad` binary and the `tendermint/gaiadnode` docker image. + +Note the binary will be mounted into the container so it can be updated without +rebuilding the image. + +``` +cd $GOPATH/src/github.com/cosmos/cosmos-sdk + +# Build the linux binary in ./build +make build-linux + +# Build tendermint/gaiadnode image +make build-docker-gaiadnode +``` + + +## Run a testnet + +To start a 4 node testnet run: + +``` +make localnet-start +``` + +The nodes bind their RPC servers to ports 46657, 46660, 46662, and 46664 on the host. +This file creates a 4-node network using the gaiadnode image. +The nodes of the network expose their P2P and RPC endpoints to the host machine on ports 46656-46657, 46659-46660, 46661-46662, and 46663-46664 respectively. + +To update the binary, just rebuild it and restart the nodes: + +``` +make build-linux +make localnet-stop +make localnet-start +``` + +## Configuration + +The `make localnet-start` creates files for a 4-node testnet in `./build` by calling the `gaiad testnet` command. + +The `./build` directory is mounted to the `/gaiad` mount point to attach the binary and config files to the container. + +For instance, to create a single node testnet: + +``` +cd $GOPATH/src/github.com/cosmos/cosmos-sdk + +# Clear the build folder +rm -rf ./build + +# Build binary +make build-linux + +# Create configuration +docker run -v `pwd`/build:/gaiad tendermint/gaiadnode testnet --o . --v 1 + +#Run the node +docker run -v `pwd`/build:/gaiad tendermint/gaiadnode + +``` + +## Logging + +Log is saved under the attached volume, in the `gaiad.log` file and written on the screen. + +## Special binaries + +If you have multiple binaries with different names, you can specify which one to run with the BINARY environment variable. The path of the binary is relative to the attached volume. + diff --git a/networks/local/gaiadnode/Dockerfile b/networks/local/gaiadnode/Dockerfile new file mode 100644 index 0000000000..fc2c0d4a0c --- /dev/null +++ b/networks/local/gaiadnode/Dockerfile @@ -0,0 +1,16 @@ +FROM alpine:3.7 +MAINTAINER Greg Szabo + +RUN apk update && \ + apk upgrade && \ + apk --no-cache add curl jq file + +VOLUME [ /gaiad ] +WORKDIR /gaiad +EXPOSE 46656 46657 +ENTRYPOINT ["/usr/bin/wrapper.sh"] +CMD ["start"] +STOPSIGNAL SIGTERM + +COPY wrapper.sh /usr/bin/wrapper.sh + diff --git a/networks/local/gaiadnode/wrapper.sh b/networks/local/gaiadnode/wrapper.sh new file mode 100755 index 0000000000..b3e90a2a0c --- /dev/null +++ b/networks/local/gaiadnode/wrapper.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh + +## +## Input parameters +## +BINARY=/gaiad/${BINARY:-gaiad} +ID=${ID:-0} +LOG=${LOG:-gaiad.log} + +## +## Assert linux binary +## +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 'gaiad' E.g.: -e BINARY=gaiad_my_test_version" + exit 1 +fi +BINARY_CHECK="$(file "$BINARY" | grep 'ELF 64-bit LSB executable, x86-64')" +if [ -z "${BINARY_CHECK}" ]; then + echo "Binary needs to be OS linux, ARCH amd64" + exit 1 +fi + +## +## Run binary with all parameters +## +export GAIADHOME="/gaiad/node${ID}/gaiad" + +if [ -d "`dirname ${GAIADHOME}/${LOG}`" ]; then + "$BINARY" --home "$GAIADHOME" "$@" | tee "${GAIADHOME}/${LOG}" +else + "$BINARY" --home "$GAIADHOME" "$@" +fi + +chmod 777 -R /gaiad + diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000000..e6fc6a4de9 --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,14 @@ +package config + +//_____________________________________________________________________ + +// Configuration structure for command functions that share configuration. +// For example: init, init gen-tx and testnet commands need similar input and run the same code + +// Storage for init gen-tx command input parameters +type GenTx struct { + Name string + CliRoot string + Overwrite bool + IP string +} diff --git a/server/init.go b/server/init.go index 5e5a73fbeb..3424c73bc6 100644 --- a/server/init.go +++ b/server/init.go @@ -19,17 +19,33 @@ import ( "github.com/tendermint/go-crypto/keys/words" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/p2p" - tmtypes "github.com/tendermint/tendermint/types" pvm "github.com/tendermint/tendermint/privval" + tmtypes "github.com/tendermint/tendermint/types" tmcli "github.com/tendermint/tmlibs/cli" cmn "github.com/tendermint/tmlibs/common" dbm "github.com/tendermint/tmlibs/db" clkeys "github.com/cosmos/cosmos-sdk/client/keys" + serverconfig "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" ) +//Parameter names, for init gen-tx command +var ( + FlagName = "name" + FlagClientHome = "home-client" + FlagOWK = "owk" +) + +//parameter names, init command +var ( + FlagOverwrite = "overwrite" + FlagGenTxs = "gen-txs" + FlagIP = "ip" + FlagChainID = "chain-id" +) + // genesis piece structure for creating combined genesis type GenesisTx struct { NodeID string `json:"node_id"` @@ -38,12 +54,13 @@ type GenesisTx struct { AppGenTx json.RawMessage `json:"app_gen_tx"` } -var ( - flagOverwrite = "overwrite" - flagGenTxs = "gen-txs" - flagIP = "ip" - flagChainID = "chain-id" -) +// Storage for init command input parameters +type InitConfig struct { + ChainID string + GenTxs bool + GenTxsDir string + Overwrite bool +} // get cmd to initialize all files for tendermint and application func GenTxCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { @@ -54,50 +71,27 @@ func GenTxCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { RunE: func(_ *cobra.Command, args []string) error { config := ctx.Config - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) - if err != nil { - return err - } - nodeID := string(nodeKey.ID()) - pubKey := readOrCreatePrivValidator(config) + config.SetRoot(viper.GetString(tmcli.HomeFlag)) - appGenTx, cliPrint, validator, err := appInit.AppGenTx(cdc, pubKey) - if err != nil { - return err - } - - ip := viper.GetString(flagIP) + ip := viper.GetString(FlagIP) if len(ip) == 0 { - ip, err = externalIP() + eip, err := externalIP() if err != nil { return err } + ip = eip } - tx := GenesisTx{ - NodeID: nodeID, - IP: ip, - Validator: validator, - AppGenTx: appGenTx, + genTxConfig := serverconfig.GenTx{ + viper.GetString(FlagName), + viper.GetString(FlagClientHome), + viper.GetBool(FlagOWK), + ip, } - bz, err := wire.MarshalJSONIndent(cdc, tx) + cliPrint, genTxFile, err := gentxWithConfig(ctx, cdc, appInit, config, genTxConfig) if err != nil { return err } - genTxFile := json.RawMessage(bz) - name := fmt.Sprintf("gentx-%v.json", nodeID) - writePath := filepath.Join(viper.GetString(tmcli.HomeFlag), "config", "gentx") - file := filepath.Join(writePath, name) - err = cmn.EnsureDir(writePath, 0700) - if err != nil { - return err - } - err = cmn.WriteFile(file, bz, 0644) - if err != nil { - return err - } - - // print out some key information toPrint := struct { AppMessage json.RawMessage `json:"app_message"` GenTxFile json.RawMessage `json:"gen_tx_file"` @@ -113,11 +107,51 @@ func GenTxCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { return nil }, } - cmd.Flags().String(flagIP, "", "external facing IP to use if left blank IP will be retrieved from this machine") + cmd.Flags().String(FlagIP, "", "external facing IP to use if left blank IP will be retrieved from this machine") cmd.Flags().AddFlagSet(appInit.FlagsAppGenTx) return cmd } +func gentxWithConfig(ctx *Context, cdc *wire.Codec, appInit AppInit, config *cfg.Config, genTxConfig serverconfig.GenTx) ( + cliPrint json.RawMessage, genTxFile json.RawMessage, err error) { + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + return + } + nodeID := string(nodeKey.ID()) + pubKey := readOrCreatePrivValidator(config) + + appGenTx, cliPrint, validator, err := appInit.AppGenTx(cdc, pubKey, genTxConfig) + if err != nil { + return + } + + tx := GenesisTx{ + NodeID: nodeID, + IP: genTxConfig.IP, + Validator: validator, + AppGenTx: appGenTx, + } + bz, err := wire.MarshalJSONIndent(cdc, tx) + if err != nil { + return + } + genTxFile = json.RawMessage(bz) + name := fmt.Sprintf("gentx-%v.json", nodeID) + writePath := filepath.Join(config.RootDir, "config", "gentx") + file := filepath.Join(writePath, name) + err = cmn.EnsureDir(writePath, 0700) + if err != nil { + return + } + err = cmn.WriteFile(file, bz, 0644) + if err != nil { + return + } + + return +} + // get cmd to initialize all files for tendermint and application func InitCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { cmd := &cobra.Command{ @@ -127,58 +161,18 @@ func InitCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { RunE: func(_ *cobra.Command, _ []string) error { config := ctx.Config - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + config.SetRoot(viper.GetString(tmcli.HomeFlag)) + initConfig := InitConfig{ + viper.GetString(FlagChainID), + viper.GetBool(FlagGenTxs), + filepath.Join(config.RootDir, "config", "gentx"), + viper.GetBool(FlagOverwrite), + } + + chainID, nodeID, appMessage, err := initWithConfig(ctx, cdc, appInit, config, initConfig) if err != nil { return err } - nodeID := string(nodeKey.ID()) - pubKey := readOrCreatePrivValidator(config) - - chainID := viper.GetString(flagChainID) - if chainID == "" { - chainID = cmn.Fmt("test-chain-%v", cmn.RandStr(6)) - } - - genFile := config.GenesisFile() - if !viper.GetBool(flagOverwrite) && cmn.FileExists(genFile) { - return fmt.Errorf("genesis.json file already exists: %v", genFile) - } - - // process genesis transactions, or otherwise create one for defaults - var appMessage json.RawMessage - var appGenTxs []json.RawMessage - var validators []tmtypes.GenesisValidator - var persistentPeers string - - if viper.GetBool(flagGenTxs) { - genTxsDir := filepath.Join(viper.GetString(tmcli.HomeFlag), "config", "gentx") - validators, appGenTxs, persistentPeers, err = processGenTxs(genTxsDir, cdc, appInit) - if err != nil { - return err - } - config.P2P.PersistentPeers = persistentPeers - configFilePath := filepath.Join(viper.GetString(tmcli.HomeFlag), "config", "config.toml") - cfg.WriteConfigFile(configFilePath, config) - } else { - appGenTx, am, validator, err := appInit.AppGenTx(cdc, pubKey) - appMessage = am - if err != nil { - return err - } - validators = []tmtypes.GenesisValidator{validator} - appGenTxs = []json.RawMessage{appGenTx} - } - - appState, err := appInit.AppGenState(cdc, appGenTxs) - if err != nil { - return err - } - - err = writeGenesisFile(cdc, genFile, chainID, validators, appState) - if err != nil { - return err - } - // print out some key information toPrint := struct { ChainID string `json:"chain_id"` @@ -194,19 +188,80 @@ func InitCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { return err } fmt.Println(string(out)) - return nil }, } - cmd.Flags().BoolP(flagOverwrite, "o", false, "overwrite the genesis.json file") - cmd.Flags().String(flagChainID, "", "genesis file chain-id, if left blank will be randomly created") - cmd.Flags().Bool(flagGenTxs, false, "apply genesis transactions from [--home]/config/gentx/") + cmd.Flags().BoolP(FlagOverwrite, "o", false, "overwrite the genesis.json file") + cmd.Flags().String(FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") + cmd.Flags().Bool(FlagGenTxs, false, "apply genesis transactions from [--home]/config/gentx/") cmd.Flags().AddFlagSet(appInit.FlagsAppGenState) cmd.Flags().AddFlagSet(appInit.FlagsAppGenTx) // need to add this flagset for when no GenTx's provided cmd.AddCommand(GenTxCmd(ctx, cdc, appInit)) return cmd } +func initWithConfig(ctx *Context, cdc *wire.Codec, appInit AppInit, config *cfg.Config, initConfig InitConfig) ( + chainID string, nodeID string, appMessage json.RawMessage, err error) { + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + if err != nil { + return + } + nodeID = string(nodeKey.ID()) + pubKey := readOrCreatePrivValidator(config) + + if initConfig.ChainID == "" { + initConfig.ChainID = fmt.Sprintf("test-chain-%v", cmn.RandStr(6)) + } + chainID = initConfig.ChainID + + genFile := config.GenesisFile() + if !initConfig.Overwrite && cmn.FileExists(genFile) { + err = fmt.Errorf("genesis.json file already exists: %v", genFile) + return + } + + // process genesis transactions, or otherwise create one for defaults + var appGenTxs []json.RawMessage + var validators []tmtypes.GenesisValidator + var persistentPeers string + + if initConfig.GenTxs { + validators, appGenTxs, persistentPeers, err = processGenTxs(initConfig.GenTxsDir, cdc, appInit) + if err != nil { + return + } + config.P2P.PersistentPeers = persistentPeers + configFilePath := filepath.Join(config.RootDir, "config", "config.toml") + cfg.WriteConfigFile(configFilePath, config) + } else { + genTxConfig := serverconfig.GenTx{ + viper.GetString(FlagName), + viper.GetString(FlagClientHome), + viper.GetBool(FlagOWK), + "127.0.0.1", + } + appGenTx, am, validator, err := appInit.AppGenTx(cdc, pubKey, genTxConfig) + appMessage = am + if err != nil { + return "", "", nil, err + } + validators = []tmtypes.GenesisValidator{validator} + appGenTxs = []json.RawMessage{appGenTx} + } + + appState, err := appInit.AppGenState(cdc, appGenTxs) + if err != nil { + return + } + + err = writeGenesisFile(cdc, genFile, initConfig.ChainID, validators, appState) + if err != nil { + return + } + + return +} + // append a genesis-piece func processGenTxs(genTxsDir string, cdc *wire.Codec, appInit AppInit) ( validators []tmtypes.GenesisValidator, appGenTxs []json.RawMessage, persistentPeers string, err error) { @@ -315,7 +370,7 @@ type AppInit struct { FlagsAppGenTx *pflag.FlagSet // create the application genesis tx - AppGenTx func(cdc *wire.Codec, pk crypto.PubKey) ( + AppGenTx func(cdc *wire.Codec, pk crypto.PubKey, genTxConfig serverconfig.GenTx) ( appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) // AppGenState creates the core parameters initialization. It takes in a @@ -337,7 +392,7 @@ type SimpleGenTx struct { } // Generate a genesis transaction -func SimpleAppGenTx(cdc *wire.Codec, pk crypto.PubKey) ( +func SimpleAppGenTx(cdc *wire.Codec, pk crypto.PubKey, genTxConfig serverconfig.GenTx) ( appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { var addr sdk.Address diff --git a/server/init_test.go b/server/init_test.go index 300decf334..f05bcb54a1 100644 --- a/server/init_test.go +++ b/server/init_test.go @@ -34,6 +34,10 @@ func TestGenTxCmd(t *testing.T) { // TODO } +func TestTestnetFilesCmd(t *testing.T) { + // TODO +} + func TestSimpleAppGenTx(t *testing.T) { // TODO } diff --git a/server/mock/app.go b/server/mock/app.go index ab1a8447a5..42e8cf4b52 100644 --- a/server/mock/app.go +++ b/server/mock/app.go @@ -12,6 +12,7 @@ import ( "github.com/tendermint/tmlibs/log" bam "github.com/cosmos/cosmos-sdk/baseapp" + gc "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" ) @@ -124,7 +125,7 @@ func AppGenState(_ *wire.Codec, _ []json.RawMessage) (appState json.RawMessage, } // Return a validator, not much else -func AppGenTx(_ *wire.Codec, pk crypto.PubKey) ( +func AppGenTx(_ *wire.Codec, pk crypto.PubKey, genTxConfig gc.GenTx) ( appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { validator = tmtypes.GenesisValidator{ diff --git a/server/testnet.go b/server/testnet.go new file mode 100644 index 0000000000..4d7643750e --- /dev/null +++ b/server/testnet.go @@ -0,0 +1,177 @@ +package server + +import ( + "fmt" + "net" + "path/filepath" + + "github.com/spf13/cobra" + + gc "github.com/cosmos/cosmos-sdk/server/config" + + "github.com/cosmos/cosmos-sdk/wire" + "github.com/spf13/viper" + cfg "github.com/tendermint/tendermint/config" + cmn "github.com/tendermint/tmlibs/common" + "os" +) + +var ( + nodeDirPrefix = "node-dir-prefix" + nValidators = "v" + outputDir = "o" + + startingIPAddress = "starting-ip-address" +) + +const nodeDirPerm = 0755 + +// get cmd to initialize all files for tendermint testnet and application +func TestnetFilesCmd(ctx *Context, cdc *wire.Codec, appInit AppInit) *cobra.Command { + cmd := &cobra.Command{ + Use: "testnet", + Short: "Initialize files for a Gaiad testnet", + Long: `testnet will create "v" number of directories and populate each with +necessary files (private validator, genesis, config, etc.). + +Note, strict routability for addresses is turned off in the config file. + +Example: + + gaiad testnet --v 4 --output-dir ./output --starting-ip-address 192.168.10.2 + `, + RunE: func(_ *cobra.Command, _ []string) error { + config := ctx.Config + err := testnetWithConfig(config, ctx, cdc, appInit) + return err + }, + } + cmd.Flags().Int(nValidators, 4, + "Number of validators to initialize the testnet with") + cmd.Flags().String(outputDir, "./mytestnet", + "Directory to store initialization data for the testnet") + cmd.Flags().String(nodeDirPrefix, "node", + "Prefix the directory name for each node with (node results in node0, node1, ...)") + + cmd.Flags().String(startingIPAddress, "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, ...)") + return cmd +} + +func testnetWithConfig(config *cfg.Config, ctx *Context, cdc *wire.Codec, appInit AppInit) error { + + outDir := viper.GetString(outputDir) + // Generate private key, node ID, initial transaction + for i := 0; i < viper.GetInt(nValidators); i++ { + nodeDirName := fmt.Sprintf("%s%d", viper.GetString(nodeDirPrefix), i) + nodeDir := filepath.Join(outDir, nodeDirName, "gaiad") + clientDir := filepath.Join(outDir, nodeDirName, "gaiacli") + gentxsDir := filepath.Join(outDir, "gentxs") + config.SetRoot(nodeDir) + + err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm) + if err != nil { + _ = os.RemoveAll(outDir) + return err + } + + err = os.MkdirAll(clientDir, nodeDirPerm) + if err != nil { + _ = os.RemoveAll(outDir) + return err + } + + config.Moniker = nodeDirName + + ip := viper.GetString(startingIPAddress) + if len(ip) == 0 { + ip, err = externalIP() + if err != nil { + return err + } + } else { + ip, err = calculateIP(ip, i) + if err != nil { + return err + } + } + + genTxConfig := gc.GenTx{ + nodeDirName, + clientDir, + true, + ip, + } + + // Run `init gen-tx` and generate initial transactions + cliPrint, genTxFile, err := gentxWithConfig(ctx, cdc, appInit, config, genTxConfig) + if err != nil { + return err + } + + // Save private key seed words + name := fmt.Sprintf("%v.json", "key_seed") + writePath := filepath.Join(clientDir) + file := filepath.Join(writePath, name) + err = cmn.EnsureDir(writePath, 0700) + if err != nil { + return err + } + err = cmn.WriteFile(file, cliPrint, 0600) + if err != nil { + return err + } + + // Gather gentxs folder + name = fmt.Sprintf("%v.json", nodeDirName) + writePath = filepath.Join(gentxsDir) + file = filepath.Join(writePath, name) + err = cmn.EnsureDir(writePath, 0700) + if err != nil { + return err + } + err = cmn.WriteFile(file, genTxFile, 0644) + if err != nil { + return err + } + + } + + // Generate genesis.json and config.toml + chainID := "chain-" + cmn.RandStr(6) + for i := 0; i < viper.GetInt(nValidators); i++ { + + nodeDirName := fmt.Sprintf("%s%d", viper.GetString(nodeDirPrefix), i) + nodeDir := filepath.Join(outDir, nodeDirName, "gaiad") + gentxsDir := filepath.Join(outDir, "gentxs") + initConfig := InitConfig{ + chainID, + true, + gentxsDir, + true, + } + config.Moniker = nodeDirName + config.SetRoot(nodeDir) + + // Run `init` and generate genesis.json and config.toml + _, _, _, err := initWithConfig(ctx, cdc, appInit, config, initConfig) + if err != nil { + return err + } + } + + fmt.Printf("Successfully initialized %v node directories\n", viper.GetInt(nValidators)) + return nil +} + +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 +} diff --git a/server/util.go b/server/util.go index 9e705f8792..4bf29cd7df 100644 --- a/server/util.go +++ b/server/util.go @@ -85,6 +85,7 @@ func AddCommands( rootCmd.AddCommand( InitCmd(ctx, cdc, appInit), + TestnetFilesCmd(ctx, cdc, appInit), StartCmd(ctx, appCreator), UnsafeResetAllCmd(ctx), client.LineBreak,