diff --git a/Makefile b/Makefile index 79aa31e8..d5aa3912 100644 --- a/Makefile +++ b/Makefile @@ -118,6 +118,21 @@ format: find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" -not -name '*.pb.go' | xargs goimports -w -local github.com/kava-labs/kava .PHONY: format +############################################################################### +### Localnet ### +############################################################################### + +build-docker-local-kava: + @$(MAKE) -C networks/local + +# Run a 4-node testnet locally +localnet-start: build-linux localnet-stop + @if ! [ -f build/node0/kvd/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/kvd:Z kava/kavanode testnet --v 4 -o . --starting-ip-address 192.168.10.2 --keyring-backend=test ; fi + docker-compose up -d + +localnet-stop: + docker-compose down + ######################################## ### Testing diff --git a/cmd/kvd/main.go b/cmd/kvd/main.go index 769a7549..65c1c245 100644 --- a/cmd/kvd/main.go +++ b/cmd/kvd/main.go @@ -67,6 +67,7 @@ func main() { app.DefaultCLIHome), genutilcli.ValidateGenesisCmd(ctx, cdc, app.ModuleBasics), AddGenesisAccountCmd(ctx, cdc, app.DefaultNodeHome, app.DefaultCLIHome), + testnetCmd(ctx, cdc, app.ModuleBasics, auth.GenesisAccountIterator{}), flags.NewCompletionCmd(rootCmd, true), ) diff --git a/cmd/kvd/testnet.go b/cmd/kvd/testnet.go new file mode 100644 index 00000000..1a897a7e --- /dev/null +++ b/cmd/kvd/testnet.go @@ -0,0 +1,376 @@ +package main + +// DONTCOVER + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + + "github.com/cosmos/cosmos-sdk/client/flags" + clientkeys "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/server" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/spf13/cobra" + "github.com/spf13/viper" + tmconfig "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/crypto" + tmos "github.com/tendermint/tendermint/libs/os" + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +var ( + flagNodeDirPrefix = "node-dir-prefix" + flagNumValidators = "v" + flagOutputDir = "output-dir" + flagNodeDaemonHome = "node-daemon-home" + flagNodeCLIHome = "node-cli-home" + flagStartingIPAddress = "starting-ip-address" +) + +func testnetCmd( + ctx *server.Context, cdc *codec.Codec, mbm module.BasicManager, genAccIterator genutiltypes.GenesisAccountsIterator, +) *cobra.Command { + + cmd := &cobra.Command{ + Use: "testnet", + Short: "Initialize files for a local kava 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: + $ kvd testnet --v 4 --output-dir ./output --starting-ip-address 192.168.10.2 + `, + RunE: func(cmd *cobra.Command, _ []string) error { + config := ctx.Config + + outputDir := viper.GetString(flagOutputDir) + chainID := viper.GetString(flags.FlagChainID) + minGasPrices := viper.GetString(server.FlagMinGasPrices) + nodeDirPrefix := viper.GetString(flagNodeDirPrefix) + nodeDaemonHome := viper.GetString(flagNodeDaemonHome) + nodeCLIHome := viper.GetString(flagNodeCLIHome) + startingIPAddress := viper.GetString(flagStartingIPAddress) + numValidators := viper.GetInt(flagNumValidators) + + return InitTestnet( + cmd, config, cdc, mbm, genAccIterator, outputDir, chainID, + minGasPrices, nodeDirPrefix, nodeDaemonHome, nodeCLIHome, startingIPAddress, numValidators, + ) + }, + } + + cmd.Flags().Int(flagNumValidators, 4, + "Number of validators to initialize the testnet with") + cmd.Flags().StringP(flagOutputDir, "o", "./kavatestnet", + "Directory to store initialization data for the testnet") + cmd.Flags().String(flagNodeDirPrefix, "node", + "Prefix the directory name for each node with (node results in node0, node1, ...)") + cmd.Flags().String(flagNodeDaemonHome, "kvd", + "Home directory of the node's daemon configuration") + cmd.Flags().String(flagNodeCLIHome, "kvcli", + "Home directory of the node's cli 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.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") + cmd.Flags().String( + server.FlagMinGasPrices, fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), + "Minimum gas prices to accept for transactions; All fees in a tx must meet this minimum (e.g. 0.01photino,0.001stake)") + cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") + + return cmd +} + +const nodeDirPerm = 0755 + +// Initialize the testnet +func InitTestnet( + cmd *cobra.Command, config *tmconfig.Config, cdc *codec.Codec, + mbm module.BasicManager, genAccIterator genutiltypes.GenesisAccountsIterator, + outputDir, chainID, minGasPrices, nodeDirPrefix, nodeDaemonHome, + nodeCLIHome, startingIPAddress string, numValidators int, +) error { + + if chainID == "" { + chainID = "chain-" + tmrand.NewRand().Str(6) + } + + monikers := make([]string, numValidators) + nodeIDs := make([]string, numValidators) + valPubKeys := make([]crypto.PubKey, numValidators) + + kavaConfig := srvconfig.DefaultConfig() + kavaConfig.MinGasPrices = minGasPrices + + //nolint:prealloc + var ( + genAccounts []authexported.GenesisAccount + genFiles []string + ) + + inBuf := bufio.NewReader(cmd.InOrStdin()) + // generate private keys, node IDs, and initial transactions + for i := 0; i < numValidators; i++ { + nodeDirName := fmt.Sprintf("%s%d", nodeDirPrefix, i) + nodeDir := filepath.Join(outputDir, nodeDirName, nodeDaemonHome) + clientDir := filepath.Join(outputDir, nodeDirName, nodeCLIHome) + gentxsDir := filepath.Join(outputDir, "gentxs") + + config.SetRoot(nodeDir) + config.RPC.ListenAddress = "tcp://0.0.0.0:26657" + + if err := os.MkdirAll(filepath.Join(nodeDir, "config"), nodeDirPerm); err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + if err := os.MkdirAll(clientDir, nodeDirPerm); err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + monikers = append(monikers, nodeDirName) + config.Moniker = nodeDirName + + ip, err := getIP(i, startingIPAddress) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + nodeIDs[i], valPubKeys[i], err = genutil.InitializeNodeValidatorFiles(config) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + memo := fmt.Sprintf("%s@%s:26656", nodeIDs[i], ip) + genFiles = append(genFiles, config.GenesisFile()) + + kb, err := keys.NewKeyring( + sdk.KeyringServiceName(), + viper.GetString(flags.FlagKeyringBackend), + clientDir, + inBuf, + ) + + keyPass := clientkeys.DefaultKeyPass + addr, secret, err := server.GenerateSaveCoinKey(kb, nodeDirName, keyPass, true) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + info := map[string]string{"secret": secret} + + cliPrint, err := json.Marshal(info) + if err != nil { + return err + } + + // save private key seed words + if err := writeFile(fmt.Sprintf("%v.json", "key_seed"), clientDir, cliPrint); err != nil { + return err + } + + accTokens := sdk.TokensFromConsensusPower(1000) + accStakingTokens := sdk.TokensFromConsensusPower(500) + coins := sdk.Coins{ + sdk.NewCoin(fmt.Sprintf("%stoken", nodeDirName), accTokens), + sdk.NewCoin(sdk.DefaultBondDenom, accStakingTokens), + } + genAccounts = append(genAccounts, auth.NewBaseAccount(addr, coins.Sort(), nil, 0, 0)) + + valTokens := sdk.TokensFromConsensusPower(100) + msg := staking.NewMsgCreateValidator( + sdk.ValAddress(addr), + valPubKeys[i], + sdk.NewCoin(sdk.DefaultBondDenom, valTokens), + staking.NewDescription(nodeDirName, "", "", "", ""), + staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + sdk.OneInt(), + ) + + tx := auth.NewStdTx([]sdk.Msg{msg}, auth.StdFee{}, []auth.StdSignature{}, memo) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithChainID(chainID).WithMemo(memo).WithKeybase(kb) + + signedTx, err := txBldr.SignStdTx(nodeDirName, clientkeys.DefaultKeyPass, tx, false) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + txBytes, err := cdc.MarshalJSON(signedTx) + if err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + // gather gentxs folder + if err := writeFile(fmt.Sprintf("%v.json", nodeDirName), gentxsDir, txBytes); err != nil { + _ = os.RemoveAll(outputDir) + return err + } + + appConfigFilePath := filepath.Join(nodeDir, "config/app.toml") + srvconfig.WriteConfigFile(appConfigFilePath, kavaConfig) + } + + if err := initGenFiles(cdc, mbm, chainID, genAccounts, genFiles, numValidators); err != nil { + return err + } + + err := collectGenFiles( + cdc, config, chainID, monikers, nodeIDs, valPubKeys, numValidators, + outputDir, nodeDirPrefix, nodeDaemonHome, genAccIterator, + ) + if err != nil { + return err + } + + cmd.PrintErrf("Successfully initialized %d node directories\n", numValidators) + return nil +} + +func initGenFiles( + cdc *codec.Codec, mbm module.BasicManager, chainID string, + genAccounts []authexported.GenesisAccount, genFiles []string, numValidators int, +) error { + + appGenState := mbm.DefaultGenesis() + + // set the accounts in the genesis state + authDataBz := appGenState[auth.ModuleName] + var authGenState auth.GenesisState + cdc.MustUnmarshalJSON(authDataBz, &authGenState) + authGenState.Accounts = genAccounts + appGenState[auth.ModuleName] = cdc.MustMarshalJSON(authGenState) + + appGenStateJSON, err := codec.MarshalJSONIndent(cdc, 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( + cdc *codec.Codec, config *tmconfig.Config, chainID string, + monikers, nodeIDs []string, valPubKeys []crypto.PubKey, + numValidators int, outputDir, nodeDirPrefix, nodeDaemonHome string, + genAccIterator genutiltypes.GenesisAccountsIterator, +) 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") + moniker := monikers[i] + config.Moniker = nodeDirName + + config.SetRoot(nodeDir) + + nodeID, valPubKey := nodeIDs[i], valPubKeys[i] + initCfg := genutil.NewInitConfig(chainID, gentxsDir, moniker, nodeID, valPubKey) + + genDoc, err := types.GenesisDocFromFile(config.GenesisFile()) + if err != nil { + return err + } + + nodeAppState, err := genutil.GenAppStateFromConfig(cdc, config, initCfg, *genDoc, genAccIterator) + if err != nil { + return err + } + + if appState == nil { + // set the canonical application state (they should not differ) + appState = nodeAppState + } + + genFile := config.GenesisFile() + + // overwrite each validator's genesis file to have a canonical genesis time + if err := genutil.ExportGenesisFileWithTime(genFile, chainID, nil, appState, genTime); err != nil { + return err + } + } + + return nil +} + +func getIP(i int, startingIPAddr string) (ip string, err error) { + if len(startingIPAddr) == 0 { + ip, err = server.ExternalIP() + if err != nil { + return "", err + } + + return ip, nil + } + + return calculateIP(startingIPAddr, i) +} + +func calculateIP(ip string, i int) (string, error) { + ipv4 := net.ParseIP(ip).To4() + if ipv4 == nil { + return "", fmt.Errorf("%v: non ipv4 address", ip) + } + + for j := 0; j < i; j++ { + ipv4[3]++ + } + + return ipv4.String(), nil +} + +func writeFile(name string, dir string, contents []byte) error { + writePath := filepath.Join(dir) + file := filepath.Join(writePath, name) + + err := tmos.EnsureDir(writePath, 0700) + if err != nil { + return err + } + + err = tmos.WriteFile(file, contents, 0600) + if err != nil { + return err + } + + return nil +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..62ebd1ec --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +version: '3' + +services: + kvdnode0: + container_name: kvdnode0 + image: "kava/kavanode" + ports: + - "26656-26657:26656-26657" + environment: + - ID=0 + - LOG=${LOG:-kvd.log} + volumes: + - ./build:/kvd:Z + networks: + localnet: + ipv4_address: 192.168.10.2 + + kvdnode1: + container_name: kvdnode1 + image: "kava/kavanode" + ports: + - "26659-26660:26656-26657" + environment: + - ID=1 + - LOG=${LOG:-kvd.log} + volumes: + - ./build:/kvd:Z + networks: + localnet: + ipv4_address: 192.168.10.3 + + kvdnode2: + container_name: kvdnode2 + image: "kava/kavanode" + environment: + - ID=2 + - LOG=${LOG:-kvd.log} + ports: + - "26661-26662:26656-26657" + volumes: + - ./build:/kvd:Z + networks: + localnet: + ipv4_address: 192.168.10.4 + + kvdnode3: + container_name: kvdnode3 + image: "kava/kavanode" + environment: + - ID=3 + - LOG=${LOG:-kvd.log} + ports: + - "26663-26664:26656-26657" + volumes: + - ./build:/kvd: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/go.sum b/go.sum index 0e043c7b..42e636b3 100644 --- a/go.sum +++ b/go.sum @@ -414,6 +414,7 @@ github.com/tendermint/iavl v0.13.2/go.mod h1:vE1u0XAGXYjHykd4BLp8p/yivrw2PF1Tuol github.com/tendermint/tendermint v0.33.2/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk= github.com/tendermint/tendermint v0.33.3 h1:6lMqjEoCGejCzAghbvfQgmw87snGSqEhDTo/jw+W8CI= github.com/tendermint/tendermint v0.33.3/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk= +github.com/tendermint/tendermint v0.33.5 h1:jYgRd9ImkzA9iOyhpmgreYsqSB6tpDa6/rXYPb8HKE8= github.com/tendermint/tm-db v0.4.1/go.mod h1:JsJ6qzYkCGiGwm5GHl/H5GLI9XLb6qZX7PRe425dHAY= github.com/tendermint/tm-db v0.5.0 h1:qtM5UTr1dlRnHtDY6y7MZO5Di8XAE2j3lc/pCnKJ5hQ= github.com/tendermint/tm-db v0.5.0/go.mod h1:lSq7q5WRR/njf1LnhiZ/lIJHk2S8Y1Zyq5oP/3o9C2U= diff --git a/networks/local/Makefile b/networks/local/Makefile new file mode 100644 index 00000000..7ab650be --- /dev/null +++ b/networks/local/Makefile @@ -0,0 +1,4 @@ +all: + docker build --tag kava/kavanode kavanode + +.PHONY: all diff --git a/networks/local/kavanode/Dockerfile b/networks/local/kavanode/Dockerfile new file mode 100644 index 00000000..fae6dd0a --- /dev/null +++ b/networks/local/kavanode/Dockerfile @@ -0,0 +1,23 @@ +#FROM alpine:3.10.2 +# +#RUN apk update && \ +# apk upgrade && \ +# apk --no-cache add curl jq file + +# Changed from Alpine to Ubuntu because the keyring PR is linking to libc +# Alpine uses muslc instead of libc + +FROM ubuntu:18.04 + +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get -y install curl jq file + +VOLUME [ /kvd ] +WORKDIR /kvd +EXPOSE 26656 26657 +ENTRYPOINT ["/usr/bin/wrapper.sh"] +CMD ["start"] +STOPSIGNAL SIGTERM + +COPY wrapper.sh /usr/bin/wrapper.sh diff --git a/networks/local/kavanode/wrapper.sh b/networks/local/kavanode/wrapper.sh new file mode 100755 index 00000000..2ebd9c26 --- /dev/null +++ b/networks/local/kavanode/wrapper.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +BINARY=/kvd/${BINARY:-kvd} +ID=${ID:-0} +LOG=${LOG:-kvd.log} + +if ! [ -f "${BINARY}" ]; then + echo "The binary $(basename "${BINARY}") cannot be found. Please add the binary to the shared folder. Please use the BINARY environment variable if the name of the binary is not 'kvd' E.g.: -e BINARY=kvd_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 + +export KVDHOME="/kvd/node${ID}/kvd" + +if [ -d "$(dirname "${KVDHOME}"/"${LOG}")" ]; then + "${BINARY}" --home "${KVDHOME}" "$@" | tee "${KVDHOME}/${LOG}" +else + "${BINARY}" --home "${KVDHOME}" "$@" +fi