diff --git a/cmd/basecli/commands/cmds.go b/cmd/basecli/commands/cmds.go index 5f5325489f..19e685e3b0 100644 --- a/cmd/basecli/commands/cmds.go +++ b/cmd/basecli/commands/cmds.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/pflag" - txcmd "github.com/tendermint/light-client/commands/txs" + txcmd "github.com/tendermint/basecoin/commands/txs" cmn "github.com/tendermint/tmlibs/common" ctypes "github.com/tendermint/tendermint/rpc/core/types" diff --git a/cmd/basecli/main.go b/cmd/basecli/main.go index be87d2d583..acaaa1a7e7 100644 --- a/cmd/basecli/main.go +++ b/cmd/basecli/main.go @@ -8,12 +8,12 @@ import ( "github.com/tendermint/abci/version" keycmd "github.com/tendermint/go-crypto/cmd" - "github.com/tendermint/light-client/commands" - "github.com/tendermint/light-client/commands/proofs" - "github.com/tendermint/light-client/commands/proxy" - rpccmd "github.com/tendermint/light-client/commands/rpc" - "github.com/tendermint/light-client/commands/seeds" - "github.com/tendermint/light-client/commands/txs" + "github.com/tendermint/basecoin/commands" + "github.com/tendermint/basecoin/commands/proofs" + "github.com/tendermint/basecoin/commands/proxy" + rpccmd "github.com/tendermint/basecoin/commands/rpc" + "github.com/tendermint/basecoin/commands/seeds" + "github.com/tendermint/basecoin/commands/txs" "github.com/tendermint/tmlibs/cli" bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" diff --git a/commands/common.go b/commands/common.go new file mode 100644 index 0000000000..a022ff4089 --- /dev/null +++ b/commands/common.go @@ -0,0 +1,73 @@ +/* +Package commands contains any general setup/helpers valid for all subcommands +*/ +package commands + +import ( + "errors" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/tmlibs/cli" + + rpcclient "github.com/tendermint/tendermint/rpc/client" + + "github.com/tendermint/light-client/certifiers" + "github.com/tendermint/light-client/certifiers/client" + "github.com/tendermint/light-client/certifiers/files" +) + +var ( + trustedProv certifiers.Provider + sourceProv certifiers.Provider +) + +const ( + ChainFlag = "chain-id" + NodeFlag = "node" +) + +func AddBasicFlags(cmd *cobra.Command) { + cmd.PersistentFlags().String(ChainFlag, "", "Chain ID of tendermint node") + cmd.PersistentFlags().String(NodeFlag, "", ": to tendermint rpc interface for this chain") +} + +func GetChainID() string { + return viper.GetString(ChainFlag) +} + +func GetNode() rpcclient.Client { + return rpcclient.NewHTTP(viper.GetString(NodeFlag), "/websocket") +} + +func GetProviders() (trusted certifiers.Provider, source certifiers.Provider) { + if trustedProv == nil || sourceProv == nil { + // initialize provider with files stored in homedir + rootDir := viper.GetString(cli.HomeFlag) + trustedProv = certifiers.NewCacheProvider( + certifiers.NewMemStoreProvider(), + files.NewProvider(rootDir), + ) + node := viper.GetString(NodeFlag) + sourceProv = client.NewHTTP(node) + } + return trustedProv, sourceProv +} + +func GetCertifier() (*certifiers.InquiringCertifier, error) { + // load up the latest store.... + trust, source := GetProviders() + + // this gets the most recent verified seed + seed, err := certifiers.LatestSeed(trust) + if certifiers.IsSeedNotFoundErr(err) { + return nil, errors.New("Please run init first to establish a root of trust") + } + if err != nil { + return nil, err + } + cert := certifiers.NewInquiring( + viper.GetString(ChainFlag), seed.Validators, trust, source) + return cert, nil +} diff --git a/commands/init.go b/commands/init.go new file mode 100644 index 0000000000..d53b03cab9 --- /dev/null +++ b/commands/init.go @@ -0,0 +1,346 @@ +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/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" + + "github.com/tendermint/tendermint/types" + + "github.com/tendermint/light-client/certifiers" + "github.com/tendermint/light-client/certifiers/files" +) + +var ( + dirPerm = os.FileMode(0700) +) + +const ( + SeedFlag = "seed" + HashFlag = "valhash" + GenesisFlag = "genesis" + + 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(SeedFlag, "", "Seed 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 = initSeed() + 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 { + // 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 initSeed() (err error) { + // create a provider.... + trust, source := GetProviders() + + // load a seed file, or get data from the provider + var seed certifiers.Seed + seedFile := viper.GetString(SeedFlag) + if seedFile == "" { + fmt.Println("Loading validator set from tendermint rpc...") + seed, err = certifiers.LatestSeed(source) + } else { + fmt.Printf("Loading validators from file %s\n", seedFile) + seed, err = certifiers.LoadSeed(seedFile) + } + // can't load the seed? abort! + if err != nil { + return err + } + + // make sure it is a proper seed + err = seed.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, seed.Hash()) { + err = errors.Errorf("Seed hash doesn't match expectation: %X", seed.Hash()) + } + } else { + err = validateHash(seed) + } + + if err != nil { + return err + } + + // if accepted, store seed as current state + trust.StoreSeed(seed) + return nil +} + +func validateHash(seed certifiers.Seed) 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", seed.Hash()) + fmt.Println("Is this valid (y/n)?") + valid := askForConfirmation() + if !valid { + return errors.New("Invalid validator hash, try init with proper seed 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() + } +} diff --git a/commands/proofs/get.go b/commands/proofs/get.go new file mode 100644 index 0000000000..846a4e7504 --- /dev/null +++ b/commands/proofs/get.go @@ -0,0 +1,119 @@ +package proofs + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/viper" + + wire "github.com/tendermint/go-wire" + "github.com/tendermint/go-wire/data" + + "github.com/tendermint/tendermint/rpc/client" + + lc "github.com/tendermint/light-client" + "github.com/tendermint/basecoin/commands" + "github.com/tendermint/light-client/proofs" +) + +// GetAndParseAppProof does most of the work of the query commands, but is quite +// opinionated, so if you want more control, set up the items and call GetProof +// directly. Notably, it always uses go-wire.ReadBinaryBytes to deserialize, +// and Height and Node from standard flags. +// +// It will try to get the proof for the given key. If it is successful, +// it will return the proof and also unserialize proof.Data into the data +// argument (so pass in a pointer to the appropriate struct) +func GetAndParseAppProof(key []byte, data interface{}) (lc.Proof, error) { + height := GetHeight() + node := commands.GetNode() + prover := proofs.NewAppProver(node) + + proof, err := GetProof(node, prover, key, height) + if err != nil { + return proof, err + } + + err = wire.ReadBinaryBytes(proof.Data(), data) + return proof, err +} + +// GetProof performs the get command directly from the proof (not from the CLI) +func GetProof(node client.Client, prover lc.Prover, key []byte, height int) (proof lc.Proof, err error) { + proof, err = prover.Get(key, uint64(height)) + if err != nil { + return + } + ph := int(proof.BlockHeight()) + // here is the certifier, root of all knowledge + cert, err := commands.GetCertifier() + if err != nil { + return + } + + // get and validate a signed header for this proof + + // FIXME: cannot use cert.GetByHeight for now, as it also requires + // Validators and will fail on querying tendermint for non-current height. + // When this is supported, we should use it instead... + client.WaitForHeight(node, ph, nil) + commit, err := node.Commit(ph) + if err != nil { + return + } + check := lc.Checkpoint{ + Header: commit.Header, + Commit: commit.Commit, + } + err = cert.Certify(check) + if err != nil { + return + } + + // validate the proof against the certified header to ensure data integrity + err = proof.Validate(check) + if err != nil { + return + } + + return proof, err +} + +// ParseHexKey parses the key flag as hex and converts to bytes or returns error +// argname is used to customize the error message +func ParseHexKey(args []string, argname string) ([]byte, error) { + if len(args) == 0 { + return nil, errors.Errorf("Missing required argument [%s]", argname) + } + if len(args) > 1 { + return nil, errors.Errorf("Only accepts one argument [%s]", argname) + } + rawkey := args[0] + if rawkey == "" { + return nil, errors.Errorf("[%s] argument must be non-empty ", argname) + } + // with tx, we always just parse key as hex and use to lookup + return proofs.ParseHexKey(rawkey) +} + +func GetHeight() int { + return viper.GetInt(heightFlag) +} + +type proof struct { + Height uint64 `json:"height"` + Data interface{} `json:"data"` +} + +// OutputProof prints the proof to stdout +// reuse this for printing proofs and we should enhance this for text/json, +// better presentation of height +func OutputProof(info interface{}, height uint64) error { + wrap := proof{height, info} + res, err := data.ToJSON(wrap) + if err != nil { + return err + } + fmt.Println(string(res)) + return nil +} diff --git a/commands/proofs/root.go b/commands/proofs/root.go new file mode 100644 index 0000000000..15b49b8c90 --- /dev/null +++ b/commands/proofs/root.go @@ -0,0 +1,23 @@ +package proofs + +import "github.com/spf13/cobra" + +const ( + heightFlag = "height" +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "query", + Short: "Get and store merkle proofs for blockchain data", + Long: `Proofs allows you to validate data and merkle proofs. + +These proofs tie the data to a checkpoint, which is managed by "seeds". +Here we can validate these proofs and import/export them to prove specific +data to other peers as needed. +`, +} + +func init() { + RootCmd.Flags().Int(heightFlag, 0, "Height to query (skip to use latest block)") +} diff --git a/commands/proofs/state.go b/commands/proofs/state.go new file mode 100644 index 0000000000..f6dce1dc03 --- /dev/null +++ b/commands/proofs/state.go @@ -0,0 +1,46 @@ +package proofs + +import ( + "github.com/spf13/cobra" + + "github.com/tendermint/go-wire/data" + + "github.com/tendermint/basecoin/commands" + "github.com/tendermint/light-client/proofs" +) + +var KeyCmd = &cobra.Command{ + Use: "key [key]", + Short: "Handle proofs for state of abci app", + Long: `This will look up a given key in the abci app, verify the proof, +and output it as hex. + +If you want json output, use an app-specific command that knows key and value structure.`, + RunE: commands.RequireInit(doKeyQuery), +} + +// Note: we cannot yse GetAndParseAppProof here, as we don't use go-wire to +// parse the object, but rather return the raw bytes +func doKeyQuery(cmd *cobra.Command, args []string) error { + // parse cli + height := GetHeight() + key, err := ParseHexKey(args, "key") + if err != nil { + return err + } + + // get the proof -> this will be used by all prover commands + node := commands.GetNode() + prover := proofs.NewAppProver(node) + proof, err := GetProof(node, prover, key, height) + if err != nil { + return err + } + + // state just returns raw hex.... + info := data.Bytes(proof.Data()) + + // we can reuse this output for other commands for text/json + // unless they do something special like store a file to disk + return OutputProof(info, proof.BlockHeight()) +} diff --git a/commands/proofs/tx.go b/commands/proofs/tx.go new file mode 100644 index 0000000000..5009a8149f --- /dev/null +++ b/commands/proofs/tx.go @@ -0,0 +1,49 @@ +package proofs + +import ( + "github.com/spf13/cobra" + + "github.com/tendermint/basecoin/commands" + "github.com/tendermint/light-client/proofs" +) + +var TxPresenters = proofs.NewPresenters() + +var TxCmd = &cobra.Command{ + Use: "tx [txhash]", + Short: "Handle proofs of commited txs", + Long: `Proofs allows you to validate abci state with merkle proofs. + +These proofs tie the data to a checkpoint, which is managed by "seeds". +Here we can validate these proofs and import/export them to prove specific +data to other peers as needed. +`, + RunE: commands.RequireInit(doTxQuery), +} + +func doTxQuery(cmd *cobra.Command, args []string) error { + // parse cli + height := GetHeight() + bkey, err := ParseHexKey(args, "txhash") + if err != nil { + return err + } + + // get the proof -> this will be used by all prover commands + node := commands.GetNode() + prover := proofs.NewTxProver(node) + proof, err := GetProof(node, prover, bkey, height) + if err != nil { + return err + } + + // auto-determine which tx it was, over all registered tx types + info, err := TxPresenters.BruteForce(proof.Data()) + if err != nil { + return err + } + + // we can reuse this output for other commands for text/json + // unless they do something special like store a file to disk + return OutputProof(info, proof.BlockHeight()) +} diff --git a/commands/proxy/root.go b/commands/proxy/root.go new file mode 100644 index 0000000000..4897df816e --- /dev/null +++ b/commands/proxy/root.go @@ -0,0 +1,111 @@ +package proxy + +import ( + "net/http" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/tendermint/rpc/client" + "github.com/tendermint/tendermint/rpc/core" + rpc "github.com/tendermint/tendermint/rpc/lib/server" + + certclient "github.com/tendermint/light-client/certifiers/client" + "github.com/tendermint/basecoin/commands" +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "proxy", + Short: "Run proxy server, verifying tendermint rpc", + Long: `This node will run a secure proxy to a tendermint rpc server. + +All calls that can be tracked back to a block header by a proof +will be verified before passing them back to the caller. Other that +that it will present the same interface as a full tendermint node, +just with added trust and running locally.`, + RunE: commands.RequireInit(runProxy), + SilenceUsage: true, +} + +const ( + bindFlag = "serve" + wsEndpoint = "/websocket" +) + +func init() { + RootCmd.Flags().String(bindFlag, ":8888", "Serve the proxy on the given port") +} + +// TODO: pass in a proper logger +var logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + +func init() { + logger = logger.With("module", "main") + logger = log.NewFilter(logger, log.AllowInfo()) +} + +func runProxy(cmd *cobra.Command, args []string) error { + // First, connect a client + c := commands.GetNode() + cert, err := commands.GetCertifier() + if err != nil { + return err + } + sc := certclient.Wrap(c, cert) + sc.Start() + r := routes(sc) + + // build the handler... + mux := http.NewServeMux() + rpc.RegisterRPCFuncs(mux, r, logger) + wm := rpc.NewWebsocketManager(r, c) + wm.SetLogger(logger) + core.SetLogger(logger) + mux.HandleFunc(wsEndpoint, wm.WebsocketHandler) + + _, err = rpc.StartHTTPServer(viper.GetString(bindFlag), mux, logger) + if err != nil { + return err + } + + cmn.TrapSignal(func() { + // TODO: close up shop + }) + + return nil +} + +// First step, proxy with no checks.... +func routes(c client.Client) map[string]*rpc.RPCFunc { + + return map[string]*rpc.RPCFunc{ + // Subscribe/unsubscribe are reserved for websocket events. + // We can just use the core tendermint impl, which uses the + // EventSwitch we registered in NewWebsocketManager above + "subscribe": rpc.NewWSRPCFunc(core.Subscribe, "event"), + "unsubscribe": rpc.NewWSRPCFunc(core.Unsubscribe, "event"), + + // info API + "status": rpc.NewRPCFunc(c.Status, ""), + "blockchain": rpc.NewRPCFunc(c.BlockchainInfo, "minHeight,maxHeight"), + "genesis": rpc.NewRPCFunc(c.Genesis, ""), + "block": rpc.NewRPCFunc(c.Block, "height"), + "commit": rpc.NewRPCFunc(c.Commit, "height"), + "tx": rpc.NewRPCFunc(c.Tx, "hash,prove"), + "validators": rpc.NewRPCFunc(c.Validators, ""), + + // broadcast API + "broadcast_tx_commit": rpc.NewRPCFunc(c.BroadcastTxCommit, "tx"), + "broadcast_tx_sync": rpc.NewRPCFunc(c.BroadcastTxSync, "tx"), + "broadcast_tx_async": rpc.NewRPCFunc(c.BroadcastTxAsync, "tx"), + + // abci API + "abci_query": rpc.NewRPCFunc(c.ABCIQuery, "path,data,prove"), + "abci_info": rpc.NewRPCFunc(c.ABCIInfo, ""), + } +} diff --git a/commands/rpc/helpers.go b/commands/rpc/helpers.go new file mode 100644 index 0000000000..8fa374916e --- /dev/null +++ b/commands/rpc/helpers.go @@ -0,0 +1,49 @@ +package rpc + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin/commands" + + "github.com/tendermint/tendermint/rpc/client" +) + +var waitCmd = &cobra.Command{ + Use: "wait", + Short: "Wait until a given height, or number of new blocks", + RunE: commands.RequireInit(runWait), +} + +func init() { + waitCmd.Flags().Int(FlagHeight, -1, "wait for block height") + waitCmd.Flags().Int(FlagDelta, -1, "wait for given number of nodes") +} + +func runWait(cmd *cobra.Command, args []string) error { + c := commands.GetNode() + h := viper.GetInt(FlagHeight) + if h == -1 { + // read from delta + d := viper.GetInt(FlagDelta) + if d == -1 { + return errors.New("Must set --height or --delta") + } + status, err := c.Status() + if err != nil { + return err + } + h = status.LatestBlockHeight + d + } + + // now wait + err := client.WaitForHeight(c, h, nil) + if err != nil { + return err + } + fmt.Printf("Chain now at height %d\n", h) + return nil +} diff --git a/commands/rpc/insecure.go b/commands/rpc/insecure.go new file mode 100644 index 0000000000..c8054fe1b3 --- /dev/null +++ b/commands/rpc/insecure.go @@ -0,0 +1,66 @@ +package rpc + +import ( + "github.com/spf13/cobra" + "github.com/tendermint/basecoin/commands" +) + +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Query the status of the node", + RunE: commands.RequireInit(runStatus), +} + +func runStatus(cmd *cobra.Command, args []string) error { + c := commands.GetNode() + status, err := c.Status() + if err != nil { + return err + } + return printResult(status) +} + +var infoCmd = &cobra.Command{ + Use: "info", + Short: "Query info on the abci app", + RunE: commands.RequireInit(runInfo), +} + +func runInfo(cmd *cobra.Command, args []string) error { + c := commands.GetNode() + info, err := c.ABCIInfo() + if err != nil { + return err + } + return printResult(info) +} + +var genesisCmd = &cobra.Command{ + Use: "genesis", + Short: "Query the genesis of the node", + RunE: commands.RequireInit(runGenesis), +} + +func runGenesis(cmd *cobra.Command, args []string) error { + c := commands.GetNode() + genesis, err := c.Genesis() + if err != nil { + return err + } + return printResult(genesis) +} + +var validatorsCmd = &cobra.Command{ + Use: "validators", + Short: "Query the validators of the node", + RunE: commands.RequireInit(runValidators), +} + +func runValidators(cmd *cobra.Command, args []string) error { + c := commands.GetNode() + validators, err := c.Validators() + if err != nil { + return err + } + return printResult(validators) +} diff --git a/commands/rpc/root.go b/commands/rpc/root.go new file mode 100644 index 0000000000..2da71392ee --- /dev/null +++ b/commands/rpc/root.go @@ -0,0 +1,65 @@ +package rpc + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/tendermint/go-wire/data" + "github.com/tendermint/tendermint/rpc/client" + + certclient "github.com/tendermint/light-client/certifiers/client" + "github.com/tendermint/basecoin/commands" +) + +const ( + FlagDelta = "delta" + FlagHeight = "height" + FlagMax = "max" + FlagMin = "min" +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "rpc", + Short: "Query the tendermint rpc, validating everything with a proof", +} + +// TODO: add support for subscribing to events???? +func init() { + RootCmd.AddCommand( + statusCmd, + infoCmd, + genesisCmd, + validatorsCmd, + blockCmd, + commitCmd, + headersCmd, + waitCmd, + ) +} + +func getSecureNode() (client.Client, error) { + // First, connect a client + c := commands.GetNode() + cert, err := commands.GetCertifier() + if err != nil { + return nil, err + } + sc := certclient.Wrap(c, cert) + return sc, nil +} + +// printResult just writes the struct to the console, returns an error if it can't +func printResult(res interface{}) error { + // TODO: handle text mode + // switch viper.Get(cli.OutputFlag) { + // case "text": + // case "json": + json, err := data.ToJSON(res) + if err != nil { + return err + } + fmt.Println(string(json)) + return nil +} diff --git a/commands/rpc/secure.go b/commands/rpc/secure.go new file mode 100644 index 0000000000..da405e78a0 --- /dev/null +++ b/commands/rpc/secure.go @@ -0,0 +1,75 @@ +package rpc + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/basecoin/commands" +) + +func init() { + blockCmd.Flags().Int(FlagHeight, -1, "block height") + commitCmd.Flags().Int(FlagHeight, -1, "block height") + headersCmd.Flags().Int(FlagMin, -1, "minimum block height") + headersCmd.Flags().Int(FlagMax, -1, "maximum block height") +} + +var blockCmd = &cobra.Command{ + Use: "block", + Short: "Get a validated block at a given height", + RunE: commands.RequireInit(runBlock), +} + +func runBlock(cmd *cobra.Command, args []string) error { + c, err := getSecureNode() + if err != nil { + return err + } + + h := viper.GetInt(FlagHeight) + block, err := c.Block(h) + if err != nil { + return err + } + return printResult(block) +} + +var commitCmd = &cobra.Command{ + Use: "commit", + Short: "Get the header and commit signature at a given height", + RunE: commands.RequireInit(runCommit), +} + +func runCommit(cmd *cobra.Command, args []string) error { + c, err := getSecureNode() + if err != nil { + return err + } + + h := viper.GetInt(FlagHeight) + commit, err := c.Commit(h) + if err != nil { + return err + } + return printResult(commit) +} + +var headersCmd = &cobra.Command{ + Use: "headers", + Short: "Get all headers in the given height range", + RunE: commands.RequireInit(runHeaders), +} + +func runHeaders(cmd *cobra.Command, args []string) error { + c, err := getSecureNode() + if err != nil { + return err + } + + min := viper.GetInt(FlagMin) + max := viper.GetInt(FlagMax) + headers, err := c.BlockchainInfo(min, max) + if err != nil { + return err + } + return printResult(headers) +} diff --git a/commands/seeds/export.go b/commands/seeds/export.go new file mode 100644 index 0000000000..14b082bd4b --- /dev/null +++ b/commands/seeds/export.go @@ -0,0 +1,43 @@ +package seeds + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/basecoin/commands" +) + +var exportCmd = &cobra.Command{ + Use: "export ", + Short: "Export selected seeds to given file", + Long: `Exports the most recent seed to a binary file. +If desired, you can select by an older height or validator hash. +`, + RunE: commands.RequireInit(exportSeed), + SilenceUsage: true, +} + +func init() { + exportCmd.Flags().Int(heightFlag, 0, "Show the seed with closest height to this") + exportCmd.Flags().String(hashFlag, "", "Show the seed matching the validator hash") + RootCmd.AddCommand(exportCmd) +} + +func exportSeed(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a filepath to output") + } + path := args[0] + + // load the seed as specified + trust, _ := commands.GetProviders() + h := viper.GetInt(heightFlag) + hash := viper.GetString(hashFlag) + seed, err := loadSeed(trust, h, hash, "") + if err != nil { + return err + } + + // now get the output file and write it + return seed.Write(path) +} diff --git a/commands/seeds/import.go b/commands/seeds/import.go new file mode 100644 index 0000000000..add67a666a --- /dev/null +++ b/commands/seeds/import.go @@ -0,0 +1,57 @@ +package seeds + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/light-client/certifiers" + "github.com/tendermint/basecoin/commands" +) + +const ( + dryFlag = "dry-run" +) + +var importCmd = &cobra.Command{ + Use: "import ", + Short: "Imports a new seed from the given file", + Long: `Validate this file and update to the given seed if secure.`, + RunE: commands.RequireInit(importSeed), + SilenceUsage: true, +} + +func init() { + importCmd.Flags().Bool(dryFlag, false, "Test the import fully, but do not import") + RootCmd.AddCommand(importCmd) +} + +func importSeed(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide an input file") + } + + // prepare the certifier + cert, err := commands.GetCertifier() + if err != nil { + return err + } + + // parse the input file + path := args[0] + seed, err := certifiers.LoadSeed(path) + if err != nil { + return err + } + + // just do simple checks in --dry-run + if viper.GetBool(dryFlag) { + fmt.Printf("Testing seed %d/%X\n", seed.Height(), seed.Hash()) + err = seed.ValidateBasic(cert.ChainID()) + } else { + fmt.Printf("Importing seed %d/%X\n", seed.Height(), seed.Hash()) + err = cert.Update(seed.Checkpoint, seed.Validators) + } + return err +} diff --git a/commands/seeds/root.go b/commands/seeds/root.go new file mode 100644 index 0000000000..b4fc9d66f6 --- /dev/null +++ b/commands/seeds/root.go @@ -0,0 +1,15 @@ +package seeds + +import "github.com/spf13/cobra" + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "seeds", + Short: "Verify seeds from your local store", + Long: `Seeds allows you to inspect and update the validator set for the chain. + +Since all security in a PoS system is based on having the correct validator +set, it is important to inspect the seeds to maintain the security, which +is used to verify all header and merkle proofs. +`, +} diff --git a/commands/seeds/show.go b/commands/seeds/show.go new file mode 100644 index 0000000000..2b2c7c7169 --- /dev/null +++ b/commands/seeds/show.go @@ -0,0 +1,71 @@ +package seeds + +import ( + "encoding/hex" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/light-client/certifiers" + "github.com/tendermint/basecoin/commands" +) + +const ( + heightFlag = "height" + hashFlag = "hash" + fileFlag = "file" +) + +var showCmd = &cobra.Command{ + Use: "show", + Short: "Show the details of one selected seed", + Long: `Shows the most recent downloaded key by default. +If desired, you can select by height, validator hash, or a file. +`, + RunE: commands.RequireInit(showSeed), + SilenceUsage: true, +} + +func init() { + showCmd.Flags().Int(heightFlag, 0, "Show the seed with closest height to this") + showCmd.Flags().String(hashFlag, "", "Show the seed matching the validator hash") + showCmd.Flags().String(fileFlag, "", "Show the seed stored in the given file") + RootCmd.AddCommand(showCmd) +} + +func loadSeed(p certifiers.Provider, h int, hash, file string) (seed certifiers.Seed, err error) { + // load the seed from the proper place + if h != 0 { + seed, err = p.GetByHeight(h) + } else if hash != "" { + var vhash []byte + vhash, err = hex.DecodeString(hash) + if err == nil { + seed, err = p.GetByHash(vhash) + } + } else if file != "" { + seed, err = certifiers.LoadSeed(file) + } else { + // default is latest seed + seed, err = certifiers.LatestSeed(p) + } + return +} + +func showSeed(cmd *cobra.Command, args []string) error { + trust, _ := commands.GetProviders() + + h := viper.GetInt(heightFlag) + hash := viper.GetString(hashFlag) + file := viper.GetString(fileFlag) + seed, err := loadSeed(trust, h, hash, file) + if err != nil { + return err + } + + // now render it! + data, err := json.MarshalIndent(seed, "", " ") + fmt.Println(string(data)) + return err +} diff --git a/commands/seeds/update.go b/commands/seeds/update.go new file mode 100644 index 0000000000..ada8fe41f6 --- /dev/null +++ b/commands/seeds/update.go @@ -0,0 +1,42 @@ +package seeds + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/tendermint/light-client/certifiers" + "github.com/tendermint/basecoin/commands" +) + +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Update seed to current chain state if possible", + RunE: commands.RequireInit(updateSeed), + SilenceUsage: true, +} + +func init() { + RootCmd.AddCommand(updateCmd) +} + +func updateSeed(cmd *cobra.Command, args []string) error { + cert, err := commands.GetCertifier() + if err != nil { + return err + } + + // get the lastest from our source + seed, err := certifiers.LatestSeed(cert.SeedSource) + if err != nil { + return err + } + fmt.Printf("Trying to update to height: %d...\n", seed.Height()) + + // let the certifier do it's magic to update.... + err = cert.Update(seed.Checkpoint, seed.Validators) + if err != nil { + return err + } + fmt.Println("Success!") + return nil +} diff --git a/commands/txs/helpers.go b/commands/txs/helpers.go new file mode 100644 index 0000000000..9426fa7684 --- /dev/null +++ b/commands/txs/helpers.go @@ -0,0 +1,174 @@ +package txs + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/bgentry/speakeasy" + "github.com/mattn/go-isatty" + "github.com/pkg/errors" + "github.com/spf13/viper" + + "github.com/tendermint/basecoin/commands" + crypto "github.com/tendermint/go-crypto" + keycmd "github.com/tendermint/go-crypto/cmd" + "github.com/tendermint/go-crypto/keys" + + ctypes "github.com/tendermint/tendermint/rpc/core/types" + + lc "github.com/tendermint/light-client" +) + +type Validatable interface { + ValidateBasic() error +} + +// GetSigner returns the pub key that will sign the tx +// returns empty key if no name provided +func GetSigner() crypto.PubKey { + name := viper.GetString(NameFlag) + manager := keycmd.GetKeyManager() + info, _ := manager.Get(name) // error -> empty pubkey + return info.PubKey +} + +// Sign if it is Signable, otherwise, just convert it to bytes +func Sign(tx interface{}) (packet []byte, err error) { + name := viper.GetString(NameFlag) + manager := keycmd.GetKeyManager() + + if sign, ok := tx.(keys.Signable); ok { + if name == "" { + return nil, errors.New("--name is required to sign tx") + } + packet, err = signTx(manager, sign, name) + } else if val, ok := tx.(lc.Value); ok { + packet = val.Bytes() + } else { + err = errors.Errorf("Reader returned invalid tx type: %#v\n", tx) + } + return +} + +// SignAndPostTx does all work once we construct a proper struct +// it validates the data, signs if needed, transforms to bytes, +// and posts to the node. +func SignAndPostTx(tx Validatable) (*ctypes.ResultBroadcastTxCommit, error) { + // validate tx client-side + err := tx.ValidateBasic() + if err != nil { + return nil, err + } + + // sign the tx if needed + packet, err := Sign(tx) + if err != nil { + return nil, err + } + + // post the bytes + node := commands.GetNode() + return node.BroadcastTxCommit(packet) +} + +// LoadJSON will read a json file from disk if --input is passed in +// template is a pointer to a struct that can hold the expected data (&MyTx{}) +// +// If not data is provided, returns (false, nil) +// If data is provided and passes, returns (true, nil) +// If data is provided but not parsable, returns (true, err) +func LoadJSON(template interface{}) (bool, error) { + input := viper.GetString(InputFlag) + if input == "" { + return false, nil + } + + // load the input + raw, err := readInput(input) + if err != nil { + return true, err + } + + // parse the input + err = json.Unmarshal(raw, template) + if err != nil { + return true, err + } + return true, nil +} + +// OutputTx prints the tx result to stdout +// TODO: something other than raw json? +func OutputTx(res *ctypes.ResultBroadcastTxCommit) error { + js, err := json.MarshalIndent(res, "", " ") + if err != nil { + return err + } + fmt.Println(string(js)) + return nil +} + +func signTx(manager keys.Manager, tx keys.Signable, name string) ([]byte, error) { + prompt := fmt.Sprintf("Please enter passphrase for %s: ", name) + pass, err := getPassword(prompt) + if err != nil { + return nil, err + } + err = manager.Sign(name, pass, tx) + if err != nil { + return nil, err + } + return tx.TxBytes() +} + +func readInput(file string) ([]byte, error) { + var reader io.Reader + // get the input stream + if file == "-" { + reader = os.Stdin + } else { + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + reader = f + } + + // and read it all! + data, err := ioutil.ReadAll(reader) + return data, errors.WithStack(err) +} + +// if we read from non-tty, we just need to init the buffer reader once, +// in case we try to read multiple passwords +var buf *bufio.Reader + +func inputIsTty() bool { + return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) +} + +func stdinPassword() (string, error) { + if buf == nil { + buf = bufio.NewReader(os.Stdin) + } + pass, err := buf.ReadString('\n') + if err != nil { + return "", err + } + return strings.TrimSpace(pass), nil +} + +func getPassword(prompt string) (pass string, err error) { + if inputIsTty() { + pass, err = speakeasy.Ask(prompt) + } else { + pass, err = stdinPassword() + } + return +} diff --git a/commands/txs/root.go b/commands/txs/root.go new file mode 100644 index 0000000000..965f6f259d --- /dev/null +++ b/commands/txs/root.go @@ -0,0 +1,19 @@ +package txs + +import "github.com/spf13/cobra" + +const ( + NameFlag = "name" + InputFlag = "input" +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "tx", + Short: "Create and post transactions to the node", +} + +func init() { + RootCmd.PersistentFlags().String(NameFlag, "", "name to sign the tx") + RootCmd.PersistentFlags().String(InputFlag, "", "file with tx in json format") +} diff --git a/docs/guide/counter/cmd/countercli/commands/counter.go b/docs/guide/counter/cmd/countercli/commands/counter.go index 22e56cf0ac..c4336d1119 100644 --- a/docs/guide/counter/cmd/countercli/commands/counter.go +++ b/docs/guide/counter/cmd/countercli/commands/counter.go @@ -4,7 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - txcmd "github.com/tendermint/light-client/commands/txs" + txcmd "github.com/tendermint/basecoin/commands/txs" "github.com/tendermint/basecoin" bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" diff --git a/docs/guide/counter/cmd/countercli/commands/query.go b/docs/guide/counter/cmd/countercli/commands/query.go index 489a8a01d5..07a8620e84 100644 --- a/docs/guide/counter/cmd/countercli/commands/query.go +++ b/docs/guide/counter/cmd/countercli/commands/query.go @@ -3,7 +3,7 @@ package commands import ( "github.com/spf13/cobra" - proofcmd "github.com/tendermint/light-client/commands/proofs" + proofcmd "github.com/tendermint/basecoin/commands/proofs" "github.com/tendermint/basecoin/docs/guide/counter/plugins/counter" "github.com/tendermint/basecoin/stack" diff --git a/docs/guide/counter/cmd/countercli/main.go b/docs/guide/counter/cmd/countercli/main.go index 8d5ae608bc..b3cd915b0c 100644 --- a/docs/guide/counter/cmd/countercli/main.go +++ b/docs/guide/counter/cmd/countercli/main.go @@ -6,11 +6,11 @@ import ( "github.com/spf13/cobra" keycmd "github.com/tendermint/go-crypto/cmd" - "github.com/tendermint/light-client/commands" - "github.com/tendermint/light-client/commands/proofs" - "github.com/tendermint/light-client/commands/proxy" - "github.com/tendermint/light-client/commands/seeds" - "github.com/tendermint/light-client/commands/txs" + "github.com/tendermint/basecoin/commands" + "github.com/tendermint/basecoin/commands/proofs" + "github.com/tendermint/basecoin/commands/proxy" + "github.com/tendermint/basecoin/commands/seeds" + "github.com/tendermint/basecoin/commands/txs" "github.com/tendermint/tmlibs/cli" bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" diff --git a/glide.lock b/glide.lock index 71ebac8c7a..9157e3fc72 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 6eb1119dccf2ab4d0adb870a14cb4408047119be53c8ec4afeaa281bd1d2b457 -updated: 2017-06-28T13:09:42.542992443+02:00 +hash: 2fec08220d5d8cbc791523583b85f3fb68e3d65ead6802198d9c879a9e295b46 +updated: 2017-07-18T21:21:05.336445544+02:00 imports: - name: github.com/bgentry/speakeasy version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd @@ -111,6 +111,7 @@ imports: - example/dummy - server - types + - version - name: github.com/tendermint/ed25519 version: 1f52c6f8b8a5c7908aff4497c186af344b428925 subpackages: @@ -125,6 +126,7 @@ imports: - keys/server - keys/server/types - keys/storage/filestorage + - keys/storage/memstorage - keys/wordlist - name: github.com/tendermint/go-wire version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb @@ -132,17 +134,11 @@ imports: - data - data/base58 - name: github.com/tendermint/light-client - version: 489b726d8b358dbd9d8f6a15d18e8b9fe0a39269 + version: d63415027075bc5d74a98a718393b59b5c4279a5 subpackages: - certifiers - certifiers/client - certifiers/files - - commands - - commands/proofs - - commands/proxy - - commands/rpc - - commands/seeds - - commands/txs - proofs - name: github.com/tendermint/merkleeyes version: 102aaf5a8ffda1846413fb22805a94def2045b9f @@ -151,7 +147,7 @@ imports: - client - iavl - name: github.com/tendermint/tendermint - version: 3065059da7bb57714f08c7a6fcb97e4b36be0194 + version: 695ad5fe2d70ec7b6fcfe0b46a73cc1b2d55e0ac subpackages: - blockchain - cmd/tendermint/commands diff --git a/glide.yaml b/glide.yaml index 7e7727aac4..d9072f4824 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,13 +22,12 @@ import: subpackages: - data - package: github.com/tendermint/light-client - version: develop + version: unstable subpackages: - - commands - - commands/proofs - - commands/seeds - - commands/txs - proofs + - certifiers + - certifiers/client + - certifiers/files - package: github.com/tendermint/merkleeyes version: develop subpackages: diff --git a/modules/base/commands/wrap.go b/modules/base/commands/wrap.go index a1042eb765..3367cc70ff 100644 --- a/modules/base/commands/wrap.go +++ b/modules/base/commands/wrap.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/tendermint/light-client/commands" + "github.com/tendermint/basecoin/commands" "github.com/tendermint/basecoin" bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" diff --git a/modules/coin/commands/query.go b/modules/coin/commands/query.go index 9a1308b2bb..8edd96c2d4 100644 --- a/modules/coin/commands/query.go +++ b/modules/coin/commands/query.go @@ -5,8 +5,8 @@ import ( "github.com/spf13/cobra" lc "github.com/tendermint/light-client" - lcmd "github.com/tendermint/light-client/commands" - proofcmd "github.com/tendermint/light-client/commands/proofs" + lcmd "github.com/tendermint/basecoin/commands" + proofcmd "github.com/tendermint/basecoin/commands/proofs" "github.com/tendermint/basecoin/modules/auth" "github.com/tendermint/basecoin/modules/coin" diff --git a/modules/coin/commands/tx.go b/modules/coin/commands/tx.go index 81e786d4a7..566901f2ff 100644 --- a/modules/coin/commands/tx.go +++ b/modules/coin/commands/tx.go @@ -4,8 +4,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/tendermint/light-client/commands" - txcmd "github.com/tendermint/light-client/commands/txs" + "github.com/tendermint/basecoin/commands" + txcmd "github.com/tendermint/basecoin/commands/txs" "github.com/tendermint/basecoin" bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" diff --git a/modules/nonce/commands/query.go b/modules/nonce/commands/query.go index 4c4b6cff55..79e7695c45 100644 --- a/modules/nonce/commands/query.go +++ b/modules/nonce/commands/query.go @@ -7,8 +7,8 @@ import ( "github.com/spf13/cobra" lc "github.com/tendermint/light-client" - lcmd "github.com/tendermint/light-client/commands" - proofcmd "github.com/tendermint/light-client/commands/proofs" + lcmd "github.com/tendermint/basecoin/commands" + proofcmd "github.com/tendermint/basecoin/commands/proofs" "github.com/tendermint/basecoin/modules/nonce" "github.com/tendermint/basecoin/stack"