From 4c5e536b31c184d38b2edf820efcc8bccea041ae Mon Sep 17 00:00:00 2001 From: Greg Szabo Date: Tue, 5 Jun 2018 15:17:04 -0400 Subject: [PATCH] Added testnet command and localnet targets Finished testnet command and introduced localnet targets in Makefile, together with gaiadnode Docker image Fixed function parameter list - now starts with ctx Separated GenTxConfig into a server/config package so both the server package and the mock package can use it Adding server/config to app package gaiadnode Docker image Separated GenTxConfig into a server/config package so both the server package and the mock package can use it Adding server/config to app package Fixes requested by Rigel Removed commented code Global flag fixes --- .gitignore | 2 + CHANGELOG.md | 93 ++++------- Makefile | 19 ++- cmd/gaia/app/genesis.go | 42 ++--- docker-compose.yml | 68 ++++++++ networks/local/Makefile | 7 + networks/local/README.md | 79 +++++++++ networks/local/gaiadnode/Dockerfile | 16 ++ networks/local/gaiadnode/wrapper.sh | 35 ++++ server/config/config.go | 14 ++ server/init.go | 249 ++++++++++++++++++---------- server/init_test.go | 4 + server/mock/app.go | 3 +- server/testnet.go | 177 ++++++++++++++++++++ server/util.go | 1 + 15 files changed, 630 insertions(+), 179 deletions(-) create mode 100644 docker-compose.yml create mode 100644 networks/local/Makefile create mode 100644 networks/local/README.md create mode 100644 networks/local/gaiadnode/Dockerfile create mode 100755 networks/local/gaiadnode/wrapper.sh create mode 100644 server/config/config.go create mode 100644 server/testnet.go diff --git a/.gitignore b/.gitignore index da467c151f12..f528ccc8fec2 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 2485093891a2..8fa2ca3b2ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,50 @@ # Changelog +## Pending + BREAKING CHANGES -* msg.GetSignBytes() now returns bech32-encoded addresses in all cases -* [lcd] REST end-points now include gas +* [cli] rearranged commands under subcommands +* [stake] remove Tick and add EndBlocker FEATURES IMPROVEMENTS -* export command now writes current validator set for Tendermint -* [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 +* bank module uses go-wire codec instead of 'encoding/json' +* auth module uses go-wire codec instead of 'encoding/json' +* revised use of endblock and beginblock +* added testnet command +* added localnet commands to Makefile (docker-based local testnet) FIXES -* [lcd] Switch to bech32 for addresses on all human readable inputs and outputs -* [lcd] fixed tx indexing/querying -* [cli] Added `--gas` flag to specify transaction gas limit +* [cli] fixed cli-bash tests +* [ci] added cli-bash tests +* [basecoin] updated basecoin for stake and slashing + +## 0.18.1 + +BREAKING CHANGES + +* [x/auth] move stuff specific to auth anteHandler to the auth module rather than the types folder. This includes: + * StdTx (and its related stuff i.e. StdSignDoc, etc) + * StdFee + * StdSignature + * Account interface + * Related to this organization, I also: +* [x/auth] got rid of AccountMapper interface (in favor of the struct already in auth module) +* [x/auth] removed the FeeHandler function from the AnteHandler, Replaced with FeeKeeper +* [x/auth] Removed GetSignatures() from Tx interface (as different Tx styles might use something different than StdSignature) +* [store] Removed SubspaceIterator and ReverseSubspaceIterator from KVStore interface and replaced them with helper functions in /types +* Switch to bech32cosmos on all human readable inputs and outputs + +BUG FIXES + +* auto-sequencing transactions correctly +* query sequence via account store +* fixed duplicate pub_key in stake.Validator ## 0.18.0 -_2018-06-05_ +_TBD_ BREAKING CHANGES @@ -38,20 +63,6 @@ BREAKING CHANGES * Introduction of Unbonding fields, lowlevel logic throughout (not fully implemented with queue) * Introduction of PoolShares type within validators, replaces three rational fields (BondedShares, UnbondingShares, UnbondedShares -* [x/auth] move stuff specific to auth anteHandler to the auth module rather than the types folder. This includes: - * StdTx (and its related stuff i.e. StdSignDoc, etc) - * StdFee - * StdSignature - * Account interface - * Related to this organization, I also: -* [x/auth] got rid of AccountMapper interface (in favor of the struct already in auth module) -* [x/auth] removed the FeeHandler function from the AnteHandler, Replaced with FeeKeeper -* [x/auth] Removed GetSignatures() from Tx interface (as different Tx styles might use something different than StdSignature) -* [store] Removed SubspaceIterator and ReverseSubspaceIterator from KVStore interface and replaced them with helper functions in /types -* [cli] rearranged commands under subcommands -* [stake] remove Tick and add EndBlocker -* Switch to bech32cosmos on all human readable inputs and outputs - FEATURES @@ -68,41 +79,12 @@ FEATURES * [stake] Added REST API * [Makefile] Added terraform/ansible playbooks to easily create remote testnets on Digital Ocean - BUG FIXES +* Auto-sequencing now works correctly * [stake] staking delegator shares exchange rate now relative to equivalent-bonded-tokens the validator has instead of bonded tokens ^ this is important for unbonded validators in the power store! -* [cli] fixed cli-bash tests -* [ci] added cli-bash tests -* [basecoin] updated basecoin for stake and slashing -* [docs] fixed references to old cli commands * [docs] Downgraded Swagger to v2 for downstream compatibility -* auto-sequencing transactions correctly -* query sequence via account store -* fixed duplicate pub_key in stake.Validator -* Auto-sequencing now works correctly - - - -## 0.17.5 - -*June 5, 2018* - -Update to Tendermint v0.19.9 (Fix evidence reactor, mempool deadlock, WAL panic, -memory leak) - -## 0.17.4 - -*May 31, 2018* - -Update to Tendermint v0.19.7 (WAL fixes and more) - -## 0.17.3 - -*May 29, 2018* - -Update to Tendermint v0.19.6 (fix fast-sync halt) ## 0.17.2 @@ -142,7 +124,6 @@ BUG FIXES * Auto-sequencing now works correctly - ## 0.16.0 (May 14th, 2018) BREAKING CHANGES @@ -177,14 +158,12 @@ BUG FIXES * Gaia now uses stake, ported from github.com/cosmos/gaia - ## 0.15.1 (April 29, 2018) IMPROVEMENTS: * Update Tendermint to v0.19.1 (includes many rpc fixes) - ## 0.15.0 (April 29, 2018) NOTE: v0.15.0 is a large breaking change that updates the encoding scheme to use diff --git a/Makefile b/Makefile index d6444b0b627a..fbcadfcabc5b 100644 --- a/Makefile +++ b/Makefile @@ -130,12 +130,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 @@ -157,4 +172,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 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 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 813796c0d8e4..3f44a7187479 100644 --- a/cmd/gaia/app/genesis.go +++ b/cmd/gaia/app/genesis.go @@ -3,13 +3,12 @@ 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" + gc "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" @@ -50,25 +49,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 +75,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 gc.GenTxConfig) ( appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { - clientRoot := viper.GetString(flagClientHome) - overwrite := viper.GetBool(flagOWK) - name := viper.GetString(flagName) - if name == "" { + 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 +93,10 @@ func GaiaAppGenTx(cdc *wire.Codec, pk crypto.PubKey) ( if err != nil { return } + cliPrint = json.RawMessage(bz) - return GaiaAppGenTxNF(cdc, pk, addr, name, overwrite) + appGenTx, _, validator, err = GaiaAppGenTxNF(cdc, pk, addr, genTxConfig.Name, genTxConfig.Overwrite) + return } // Generate a gaia genesis transaction without flags @@ -129,7 +117,7 @@ func GaiaAppGenTxNF(cdc *wire.Codec, pk crypto.PubKey, addr sdk.Address, name st validator = tmtypes.GenesisValidator{ PubKey: pk, - Power: freeFermionVal, + Power: server.FreeFermionVal, } return } @@ -160,21 +148,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", server.FreeFermionsAcc}, } acc := NewGenesisAccount(&accAuth) genaccs[i] = acc - stakeData.Pool.LooseUnbondedTokens += freeFermionsAcc // increase the supply + stakeData.Pool.LooseUnbondedTokens += server.FreeFermionsAcc // 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(server.FreeFermionVal)) stakeData.Validators = append(stakeData.Validators, validator) // pool logic - stakeData.Pool.BondedTokens += freeFermionVal + stakeData.Pool.BondedTokens += server.FreeFermionVal stakeData.Pool.BondedShares = sdk.NewRat(stakeData.Pool.BondedTokens) } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000000..83e473a7dbdb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,68 @@ +version: '3' + +services: + node0: + container_name: node0 + image: "tendermint/gaiadnode" + ports: + - "46656-46657:46656-46657" + environment: + - ID=0 + - LOG=$${LOG:-gaiad.log} + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.2 + + node1: + container_name: node1 + image: "tendermint/gaiadnode" + ports: + - "46659-46660:46656-46657" + environment: + - ID=1 + - LOG=$${LOG:-gaiad.log} + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.3 + + node2: + container_name: node2 + image: "tendermint/gaiadnode" + environment: + - ID=2 + - LOG=$${LOG:-gaiad.log} + ports: + - "46661-46662:46656-46657" + volumes: + - ./build:/gaiad:Z + networks: + localnet: + ipv4_address: 192.168.10.4 + + node3: + container_name: node3 + image: "tendermint/gaiadnode" + environment: + - ID=3 + - LOG=$${LOG:-gaiad.log} + ports: + - "46663-46664:46656-46657" + 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 000000000000..c707a168eed9 --- /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 000000000000..e16d947ab1a9 --- /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 000000000000..fc2c0d4a0c9b --- /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 000000000000..b3e90a2a0c1d --- /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 000000000000..26d038f4757d --- /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 GenTxConfig struct { + Name string + CliRoot string + Overwrite bool + IP string +} diff --git a/server/init.go b/server/init.go index 512751bed0ad..9eefda94c592 100644 --- a/server/init.go +++ b/server/init.go @@ -19,13 +19,14 @@ 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" + gc "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" ) @@ -39,10 +40,36 @@ type GenesisTx struct { } var ( - flagOverwrite = "overwrite" - flagGenTxs = "gen-txs" - flagIP = "ip" - flagChainID = "chain-id" + //--name parameter name, init gen-tx command + FlagName = "name" + //--home-client parameter name, init gen-tx command + FlagClientHome = "home-client" + //--owk parameter name, init gen-tx command + FlagOWK = "owk" + + // bonded tokens given to genesis validators/accounts + FreeFermionVal = int64(100) + // ... + FreeFermionsAcc = int64(50) +) + +// Storage for init command input parameters +type InitConfig struct { + ChainID string + GenTxs bool + GenTxsDir string + Overwrite bool +} + +var ( + //--overwrite parameter name, init command + FlagOverwrite = "overwrite" + //--gen-txs parameter name, init command + FlagGenTxs = "gen-txs" + //--ip parameter name, init command + FlagIP = "ip" + //--chain-id parameter name, init command + FlagChainID = "chain-id" ) // get cmd to initialize all files for tendermint and application @@ -54,50 +81,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) - - appGenTx, cliPrint, validator, err := appInit.AppGenTx(cdc, pubKey) - if err != nil { - return err - } + config.SetRoot(viper.GetString(tmcli.HomeFlag)) - 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, - } - bz, err := wire.MarshalJSONIndent(cdc, tx) - 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 + genTxConfig := gc.GenTxConfig{ + viper.GetString(FlagName), + viper.GetString(FlagClientHome), + viper.GetBool(FlagOWK), + ip, } - err = cmn.WriteFile(file, bz, 0644) + cliPrint, genTxFile, err := gentxWithConfig(ctx, cdc, appInit, config, genTxConfig) 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 +117,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 gc.GenTxConfig) ( + 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 +171,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()) - 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)) + config.SetRoot(viper.GetString(tmcli.HomeFlag)) + initConfig := InitConfig{ + viper.GetString(FlagChainID), + viper.GetBool(FlagGenTxs), + filepath.Join(config.RootDir, "config", "gentx"), + viper.GetBool(FlagOverwrite), } - 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) + chainID, nodeID, appMessage, err := initWithConfig(ctx, cdc, appInit, config, initConfig) if err != nil { return err } - // print out some key information toPrint := struct { ChainID string `json:"chain_id"` @@ -194,19 +198,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 := gc.GenTxConfig{ + 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 +380,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 gc.GenTxConfig) ( appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) // AppGenState creates the core parameters initialization. It takes in a @@ -337,7 +402,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 gc.GenTxConfig) ( 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 300decf33456..f05bcb54a15b 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 ab1a8447a544..6c553e2be4f5 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.GenTxConfig) ( 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 000000000000..2ddf31d980d8 --- /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.GenTxConfig{ + 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 9e705f87927b..4bf29cd7df7f 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,