cosmos-sdk/client/commands/init.go

353 lines
8.4 KiB
Go

package commands
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/tendermint/light-client/certifiers"
"github.com/tendermint/light-client/certifiers/files"
"github.com/tendermint/tmlibs/cli"
cmn "github.com/tendermint/tmlibs/common"
"github.com/tendermint/tendermint/types"
)
var (
dirPerm = os.FileMode(0700)
)
//nolint
const (
CommitFlag = "commit"
HashFlag = "valhash"
GenesisFlag = "genesis"
FlagTrustNode = "trust-node"
ConfigFile = "config.toml"
)
// InitCmd will initialize the basecli store
var InitCmd = &cobra.Command{
Use: "init",
Short: "Initialize the light client for a new chain",
RunE: runInit,
}
var ResetCmd = &cobra.Command{
Use: "reset_all",
Short: "DANGEROUS: Wipe out all client data, including keys",
RunE: runResetAll,
}
func init() {
InitCmd.Flags().Bool("force-reset", false, "Wipe clean an existing client store, except for keys")
InitCmd.Flags().String(CommitFlag, "", "Commit file to import (optional)")
InitCmd.Flags().String(HashFlag, "", "Trusted validator hash (must match to accept)")
InitCmd.Flags().String(GenesisFlag, "", "Genesis file with chainid and validators (optional)")
}
func runInit(cmd *cobra.Command, args []string) error {
root := viper.GetString(cli.HomeFlag)
if viper.GetBool("force-reset") {
resetRoot(root, true)
}
// make sure we don't have an existing client initialized
inited, err := WasInited(root)
if err != nil {
return err
}
if inited {
return errors.Errorf("%s already is initialized, --force-reset if you really want to wipe it out", root)
}
// clean up dir if init fails
err = doInit(cmd, root)
if err != nil {
resetRoot(root, true)
}
return err
}
// doInit actually creates all the files, on error, we should revert it all
func doInit(cmd *cobra.Command, root string) error {
// read the genesis file if present, and populate --chain-id and --valhash
err := checkGenesis(cmd)
if err != nil {
return err
}
err = initConfigFile(cmd)
if err != nil {
return err
}
err = initTrust()
return err
}
func runResetAll(cmd *cobra.Command, args []string) error {
root := viper.GetString(cli.HomeFlag)
resetRoot(root, false)
return nil
}
func resetRoot(root string, saveKeys bool) {
tmp := filepath.Join(os.TempDir(), cmn.RandStr(16))
keys := filepath.Join(root, "keys")
if saveKeys {
os.Rename(keys, tmp)
}
os.RemoveAll(root)
if saveKeys {
os.Mkdir(root, 0700)
os.Rename(tmp, keys)
}
}
type Runable func(cmd *cobra.Command, args []string) error
// Any commands that require and init'ed basecoin directory
// should wrap their RunE command with RequireInit
// to make sure that the client is initialized.
//
// This cannot be called during PersistentPreRun,
// as they are called from the most specific command first, and root last,
// and the root command sets up viper, which is needed to find the home dir.
func RequireInit(run Runable) Runable {
return func(cmd *cobra.Command, args []string) error {
// otherwise, run the wrappped command
if viper.GetBool(FlagTrustNode) {
return run(cmd, args)
}
// first check if we were Init'ed and if not, return an error
root := viper.GetString(cli.HomeFlag)
init, err := WasInited(root)
if err != nil {
return err
}
if !init {
return errors.Errorf("You must run '%s init' first", cmd.Root().Name())
}
// otherwise, run the wrappped command
return run(cmd, args)
}
}
// WasInited returns true if a basecoin was previously initialized
// in this directory. Important to ensure proper behavior.
//
// Returns error if we have filesystem errors
func WasInited(root string) (bool, error) {
// make sure there is a directory here in any case
os.MkdirAll(root, dirPerm)
// check if there is a config.toml file
cfgFile := filepath.Join(root, "config.toml")
_, err := os.Stat(cfgFile)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, errors.WithStack(err)
}
// check if there are non-empty checkpoints and validators dirs
dirs := []string{
filepath.Join(root, files.CheckDir),
filepath.Join(root, files.ValDir),
}
// if any of these dirs is empty, then we have no data
for _, d := range dirs {
empty, err := isEmpty(d)
if err != nil {
return false, err
}
if empty {
return false, nil
}
}
// looks like we have everything
return true, nil
}
func checkGenesis(cmd *cobra.Command) error {
genesis := viper.GetString(GenesisFlag)
if genesis == "" {
return nil
}
doc, err := types.GenesisDocFromFile(genesis)
if err != nil {
return err
}
flags := cmd.Flags()
flags.Set(ChainFlag, doc.ChainID)
hash := doc.ValidatorHash()
hexHash := hex.EncodeToString(hash)
flags.Set(HashFlag, hexHash)
return nil
}
// isEmpty returns false if we can read files in this dir.
// if it doesn't exist, read issues, etc... return true
//
// TODO: should we handle errors otherwise?
func isEmpty(dir string) (bool, error) {
// check if we can read the directory, missing is fine, other error is not
d, err := os.Open(dir)
if os.IsNotExist(err) {
return true, nil
}
if err != nil {
return false, errors.WithStack(err)
}
defer d.Close()
// read to see if any (at least one) files here...
files, err := d.Readdirnames(1)
if err == io.EOF {
return true, nil
}
if err != nil {
return false, errors.WithStack(err)
}
empty := len(files) == 0
return empty, nil
}
type Config struct {
Chain string `toml:"chain-id,omitempty"`
Node string `toml:"node,omitempty"`
Output string `toml:"output,omitempty"`
Encoding string `toml:"encoding,omitempty"`
}
func setConfig(flags *pflag.FlagSet, f string, v *string) {
if flags.Changed(f) {
*v = viper.GetString(f)
}
}
func initConfigFile(cmd *cobra.Command) error {
flags := cmd.Flags()
var cfg Config
required := []string{ChainFlag, NodeFlag}
for _, f := range required {
if !flags.Changed(f) {
return errors.Errorf(`"--%s" required`, f)
}
}
setConfig(flags, ChainFlag, &cfg.Chain)
setConfig(flags, NodeFlag, &cfg.Node)
setConfig(flags, cli.OutputFlag, &cfg.Output)
setConfig(flags, cli.EncodingFlag, &cfg.Encoding)
out, err := os.Create(filepath.Join(viper.GetString(cli.HomeFlag), ConfigFile))
if err != nil {
return errors.WithStack(err)
}
defer out.Close()
// save the config file
err = toml.NewEncoder(out).Encode(cfg)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func initTrust() (err error) {
// create a provider....
trust, source := GetProviders()
// load a commit file, or get data from the provider
var fc certifiers.FullCommit
commitFile := viper.GetString(CommitFlag)
if commitFile == "" {
fmt.Println("Loading validator set from tendermint rpc...")
fc, err = source.LatestCommit()
} else {
fmt.Printf("Loading validators from file %s\n", commitFile)
fc, err = files.LoadFullCommit(commitFile)
}
// can't load the commit? abort!
if err != nil {
return err
}
// make sure it is a proper commit
err = fc.ValidateBasic(viper.GetString(ChainFlag))
if err != nil {
return err
}
// validate hash interactively or not
hash := viper.GetString(HashFlag)
if hash != "" {
var hashb []byte
hashb, err = hex.DecodeString(hash)
if err == nil && !bytes.Equal(hashb, fc.ValidatorsHash()) {
err = errors.Errorf("Validator hash doesn't match expectation: %X", fc.ValidatorsHash())
}
} else {
err = validateHash(fc)
}
if err != nil {
return err
}
// if accepted, store commit as current state
trust.StoreCommit(fc)
return nil
}
func validateHash(fc certifiers.FullCommit) error {
// ask the user to verify the validator hash
fmt.Println("\nImportant: if this is incorrect, all interaction with the chain will be insecure!")
fmt.Printf(" Given validator hash valid: %X\n", fc.ValidatorsHash())
fmt.Println("Is this valid (y/n)?")
valid := askForConfirmation()
if !valid {
return errors.New("Invalid validator hash, try init with proper commit later")
}
return nil
}
func askForConfirmation() bool {
var resp string
_, err := fmt.Scanln(&resp)
if err != nil {
fmt.Println("Please type yes or no and then press enter:")
return askForConfirmation()
}
resp = strings.ToLower(resp)
if resp == "y" || resp == "yes" {
return true
} else if resp == "n" || resp == "no" {
return false
} else {
fmt.Println("Please type yes or no and then press enter:")
return askForConfirmation()
}
}