Alessio/server refactor (#2472)

* Add arg to PrintUnsignedStdTx() to actually operate in offline mode
* WIP simplifying server module
* Expose ExternalIP()
* Move {GenTx,Init}Cmd into gaia's new init package
This commit is contained in:
Alessio Treglia
2018-10-10 15:45:41 -07:00
committed by Jae Kwon
parent e251088672
commit 0f4a03b44e
22 changed files with 533 additions and 563 deletions
+6 -3
View File
@@ -16,6 +16,7 @@ import (
tmtypes "github.com/tendermint/tendermint/types"
"github.com/cosmos/cosmos-sdk/cmd/gaia/app"
gaiaInit "github.com/cosmos/cosmos-sdk/cmd/gaia/init"
"github.com/cosmos/cosmos-sdk/server"
)
@@ -28,10 +29,12 @@ func main() {
Short: "Gaia Daemon (server)",
PersistentPreRunE: server.PersistentPreRunEFn(ctx),
}
appInit := app.GaiaAppInit()
rootCmd.AddCommand(gaiaInit.InitCmd(ctx, cdc, appInit))
rootCmd.AddCommand(gaiaInit.TestnetFilesCmd(ctx, cdc, appInit))
server.AddCommands(ctx, cdc, rootCmd, app.GaiaAppInit(),
server.ConstructAppCreator(newApp, "gaia"),
server.ConstructAppExporter(exportAppStateAndTMValidators, "gaia"))
server.AddCommands(ctx, cdc, rootCmd, appInit,
newApp, exportAppStateAndTMValidators)
// prepare and add flags
executor := cli.PrepareBaseCmd(rootCmd, "GA", app.DefaultNodeHome)
+321
View File
@@ -0,0 +1,321 @@
package init
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server"
servercfg "github.com/cosmos/cosmos-sdk/server/config"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cfg "github.com/tendermint/tendermint/config"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/libs/cli"
"github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/p2p"
"github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/types"
)
// get cmd to initialize all files for tendermint and application
func GenTxCmd(ctx *server.Context, cdc *codec.Codec, appInit server.AppInit) *cobra.Command {
cmd := &cobra.Command{
Use: "gen-tx",
Short: "Create genesis transaction file (under [--home]/config/gentx/gentx-[nodeID].json)",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, args []string) error {
config := ctx.Config
config.SetRoot(viper.GetString(cli.HomeFlag))
ip := viper.GetString(server.FlagIP)
if len(ip) == 0 {
eip, err := server.ExternalIP()
if err != nil {
return err
}
ip = eip
}
genTxConfig := servercfg.GenTx{
viper.GetString(server.FlagName),
viper.GetString(server.FlagClientHome),
viper.GetBool(server.FlagOWK),
ip,
}
cliPrint, genTxFile, err := gentxWithConfig(cdc, appInit, config, genTxConfig)
if err != nil {
return err
}
toPrint := struct {
AppMessage json.RawMessage `json:"app_message"`
GenTxFile json.RawMessage `json:"gen_tx_file"`
}{
cliPrint,
genTxFile,
}
out, err := codec.MarshalJSONIndent(cdc, toPrint)
if err != nil {
return err
}
fmt.Println(string(out))
return nil
},
}
cmd.Flags().String(server.FlagIP, "", "external facing IP to use if left blank IP will be retrieved from this machine")
cmd.Flags().AddFlagSet(appInit.FlagsAppGenTx)
return cmd
}
// NOTE: This will update (write) the config file with
// updated name (moniker) for node.
func gentxWithConfig(cdc *codec.Codec, appInit server.AppInit, config *cfg.Config, genTxConfig servercfg.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 := server.GenesisTx{
NodeID: nodeID,
IP: genTxConfig.IP,
Validator: validator,
AppGenTx: appGenTx,
}
bz, err := codec.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 = common.EnsureDir(writePath, 0700)
if err != nil {
return
}
err = common.WriteFile(file, bz, 0644)
if err != nil {
return
}
// Write updated config with moniker
config.Moniker = genTxConfig.Name
configFilePath := filepath.Join(config.RootDir, "config", "config.toml")
cfg.WriteConfigFile(configFilePath, config)
return
}
// get cmd to initialize all files for tendermint and application
// nolint: golint
func InitCmd(ctx *server.Context, cdc *codec.Codec, appInit server.AppInit) *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize genesis config, priv-validator file, and p2p-node file",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
config := ctx.Config
config.SetRoot(viper.GetString(cli.HomeFlag))
initConfig := server.InitConfig{
viper.GetString(server.FlagChainID),
viper.GetBool(server.FlagWithTxs),
filepath.Join(config.RootDir, "config", "gentx"),
viper.GetBool(server.FlagOverwrite),
}
chainID, nodeID, appMessage, err := initWithConfig(cdc, appInit, config, initConfig)
if err != nil {
return err
}
// print out some key information
toPrint := struct {
ChainID string `json:"chain_id"`
NodeID string `json:"node_id"`
AppMessage json.RawMessage `json:"app_message"`
}{
chainID,
nodeID,
appMessage,
}
out, err := codec.MarshalJSONIndent(cdc, toPrint)
if err != nil {
return err
}
fmt.Println(string(out))
return nil
},
}
cmd.Flags().BoolP(server.FlagOverwrite, "o", false, "overwrite the genesis.json file")
cmd.Flags().String(server.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created")
cmd.Flags().Bool(server.FlagWithTxs, false, "apply existing 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(cdc *codec.Codec, appInit server.AppInit, config *cfg.Config, initConfig server.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", common.RandStr(6))
}
chainID = initConfig.ChainID
genFile := config.GenesisFile()
if !initConfig.Overwrite && common.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 []types.GenesisValidator
var persistentPeers string
if initConfig.GenTxs {
validators, appGenTxs, persistentPeers, err = processGenTxs(initConfig.GenTxsDir, cdc)
if err != nil {
return
}
config.P2P.PersistentPeers = persistentPeers
configFilePath := filepath.Join(config.RootDir, "config", "config.toml")
cfg.WriteConfigFile(configFilePath, config)
} else {
genTxConfig := servercfg.GenTx{
viper.GetString(server.FlagName),
viper.GetString(server.FlagClientHome),
viper.GetBool(server.FlagOWK),
"127.0.0.1",
}
// Write updated config with moniker
config.Moniker = genTxConfig.Name
configFilePath := filepath.Join(config.RootDir, "config", "config.toml")
cfg.WriteConfigFile(configFilePath, config)
appGenTx, am, validator, err := appInit.AppGenTx(cdc, pubKey, genTxConfig)
appMessage = am
if err != nil {
return "", "", nil, err
}
validators = []types.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 *codec.Codec) (
validators []types.GenesisValidator, appGenTxs []json.RawMessage, persistentPeers string, err error) {
var fos []os.FileInfo
fos, err = ioutil.ReadDir(genTxsDir)
if err != nil {
return
}
genTxs := make(map[string]server.GenesisTx)
var nodeIDs []string
for _, fo := range fos {
filename := path.Join(genTxsDir, fo.Name())
if !fo.IsDir() && (path.Ext(filename) != ".json") {
continue
}
// get the genTx
var bz []byte
bz, err = ioutil.ReadFile(filename)
if err != nil {
return
}
var genTx server.GenesisTx
err = cdc.UnmarshalJSON(bz, &genTx)
if err != nil {
return
}
genTxs[genTx.NodeID] = genTx
nodeIDs = append(nodeIDs, genTx.NodeID)
}
sort.Strings(nodeIDs)
for _, nodeID := range nodeIDs {
genTx := genTxs[nodeID]
// combine some stuff
validators = append(validators, genTx.Validator)
appGenTxs = append(appGenTxs, genTx.AppGenTx)
// Add a persistent peer
comma := ","
if len(persistentPeers) == 0 {
comma = ""
}
persistentPeers += fmt.Sprintf("%s%s@%s:26656", comma, genTx.NodeID, genTx.IP)
}
return
}
// read of create the private key file for this config
func readOrCreatePrivValidator(tmConfig *cfg.Config) crypto.PubKey {
// private validator
privValFile := tmConfig.PrivValidatorFile()
var privValidator *privval.FilePV
if common.FileExists(privValFile) {
privValidator = privval.LoadFilePV(privValFile)
} else {
privValidator = privval.GenFilePV(privValFile)
privValidator.Save()
}
return privValidator.GetPubKey()
}
// writeGenesisFile creates and writes the genesis configuration to disk. An
// error is returned if building or writing the configuration to file fails.
// nolint: unparam
func writeGenesisFile(cdc *codec.Codec, genesisFile, chainID string, validators []types.GenesisValidator, appState json.RawMessage) error {
genDoc := types.GenesisDoc{
ChainID: chainID,
Validators: validators,
AppState: appState,
}
if err := genDoc.ValidateAndComplete(); err != nil {
return err
}
return genDoc.SaveAs(genesisFile)
}
+111
View File
@@ -0,0 +1,111 @@
package init
import (
"bytes"
"io"
"io/ioutil"
"os"
"testing"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/server"
"github.com/cosmos/cosmos-sdk/server/mock"
"github.com/stretchr/testify/require"
abciServer "github.com/tendermint/tendermint/abci/server"
tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands"
"github.com/tendermint/tendermint/libs/log"
)
func TestInitCmd(t *testing.T) {
defer server.SetupViper(t)()
logger := log.NewNopLogger()
cfg, err := tcmd.ParseConfig()
require.Nil(t, err)
ctx := server.NewContext(cfg, logger)
cdc := codec.New()
appInit := server.AppInit{
AppGenState: mock.AppGenState,
AppGenTx: mock.AppGenTx,
}
cmd := InitCmd(ctx, cdc, appInit)
err = cmd.RunE(nil, nil)
require.NoError(t, err)
}
func TestEmptyState(t *testing.T) {
defer server.SetupViper(t)()
logger := log.NewNopLogger()
cfg, err := tcmd.ParseConfig()
require.Nil(t, err)
ctx := server.NewContext(cfg, logger)
cdc := codec.New()
appInit := server.AppInit{
AppGenTx: mock.AppGenTx,
AppGenState: mock.AppGenStateEmpty,
}
cmd := InitCmd(ctx, cdc, appInit)
err = cmd.RunE(nil, nil)
require.NoError(t, err)
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
cmd = server.ExportCmd(ctx, cdc, nil)
err = cmd.RunE(nil, nil)
require.NoError(t, err)
outC := make(chan string)
go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
outC <- buf.String()
}()
w.Close()
os.Stdout = old
out := <-outC
require.Contains(t, out, "WARNING: State is not initialized")
require.Contains(t, out, "genesis_time")
require.Contains(t, out, "chain_id")
require.Contains(t, out, "consensus_params")
require.Contains(t, out, "validators")
require.Contains(t, out, "app_hash")
}
func TestStartStandAlone(t *testing.T) {
home, err := ioutil.TempDir("", "mock-sdk-cmd")
require.Nil(t, err)
defer func() {
os.RemoveAll(home)
}()
logger := log.NewNopLogger()
cfg, err := tcmd.ParseConfig()
require.Nil(t, err)
ctx := server.NewContext(cfg, logger)
cdc := codec.New()
appInit := server.AppInit{
AppGenState: mock.AppGenState,
AppGenTx: mock.AppGenTx,
}
initCmd := InitCmd(ctx, cdc, appInit)
err = initCmd.RunE(nil, nil)
require.NoError(t, err)
app, err := mock.NewApp(home, logger)
require.Nil(t, err)
svrAddr, _, err := server.FreeTCPAddr()
require.Nil(t, err)
svr, err := abciServer.NewServer(svrAddr, "socket", app)
require.Nil(t, err, "error creating listener")
svr.SetLogger(logger.With("module", "abci-server"))
svr.Start()
timer := time.NewTimer(time.Duration(2) * time.Second)
select {
case <-timer.C:
svr.Stop()
}
}
+197
View File
@@ -0,0 +1,197 @@
package init
import (
"fmt"
"github.com/cosmos/cosmos-sdk/server"
"net"
"path/filepath"
"github.com/spf13/cobra"
gc "github.com/cosmos/cosmos-sdk/server/config"
"os"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/spf13/viper"
cfg "github.com/tendermint/tendermint/config"
cmn "github.com/tendermint/tendermint/libs/common"
)
var (
nodeDirPrefix = "node-dir-prefix"
nValidators = "v"
outputDir = "output-dir"
nodeDaemonHome = "node-daemon-home"
nodeCliHome = "node-cli-home"
startingIPAddress = "starting-ip-address"
)
const nodeDirPerm = 0755
// get cmd to initialize all files for tendermint testnet and application
func TestnetFilesCmd(ctx *server.Context, cdc *codec.Codec, appInit server.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 --o ./output --starting-ip-address 192.168.10.2
`,
RunE: func(_ *cobra.Command, _ []string) error {
config := ctx.Config
err := testnetWithConfig(config, cdc, appInit)
return err
},
}
cmd.Flags().Int(nValidators, 4,
"Number of validators to initialize the testnet with")
cmd.Flags().StringP(outputDir, "o", "./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(nodeDaemonHome, "gaiad",
"Home directory of the node's daemon configuration")
cmd.Flags().String(nodeCliHome, "gaiacli",
"Home directory of the node's cli configuration")
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, cdc *codec.Codec, appInit server.AppInit) error {
outDir := viper.GetString(outputDir)
numValidators := viper.GetInt(nValidators)
// Generate private key, node ID, initial transaction
for i := 0; i < numValidators; i++ {
nodeDirName := fmt.Sprintf("%s%d", viper.GetString(nodeDirPrefix), i)
nodeDaemonHomeName := viper.GetString(nodeDaemonHome)
nodeCliHomeName := viper.GetString(nodeCliHome)
nodeDir := filepath.Join(outDir, nodeDirName, nodeDaemonHomeName)
clientDir := filepath.Join(outDir, nodeDirName, nodeCliHomeName)
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, err := getIP(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(cdc, appInit, config, genTxConfig)
if err != nil {
return err
}
// Save private key seed words
name := fmt.Sprintf("%v.json", "key_seed")
err = writeFile(name, clientDir, cliPrint)
if err != nil {
return err
}
// Gather gentxs folder
name = fmt.Sprintf("%v.json", nodeDirName)
err = writeFile(name, gentxsDir, genTxFile)
if err != nil {
return err
}
}
// Generate genesis.json and config.toml
chainID := "chain-" + cmn.RandStr(6)
for i := 0; i < numValidators; i++ {
nodeDirName := fmt.Sprintf("%s%d", viper.GetString(nodeDirPrefix), i)
nodeDaemonHomeName := viper.GetString(nodeDaemonHome)
nodeDir := filepath.Join(outDir, nodeDirName, nodeDaemonHomeName)
gentxsDir := filepath.Join(outDir, "gentxs")
initConfig := server.InitConfig{
chainID,
true,
gentxsDir,
true,
}
config.Moniker = nodeDirName
config.SetRoot(nodeDir)
// Run `init` and generate genesis.json and config.toml
_, _, _, err := initWithConfig(cdc, appInit, config, initConfig)
if err != nil {
return err
}
}
fmt.Printf("Successfully initialized %v node directories\n", viper.GetInt(nValidators))
return nil
}
func getIP(i int) (ip string, err error) {
ip = viper.GetString(startingIPAddress)
if len(ip) == 0 {
ip, err = server.ExternalIP()
if err != nil {
return "", err
}
} else {
ip, err = calculateIP(ip, i)
if err != nil {
return "", err
}
}
return ip, nil
}
func writeFile(name string, dir string, contents []byte) error {
writePath := filepath.Join(dir)
file := filepath.Join(writePath, name)
err := cmn.EnsureDir(writePath, 0700)
if err != nil {
return err
}
err = cmn.WriteFile(file, contents, 0600)
if err != nil {
return err
}
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
}