diff --git a/Makefile b/Makefile index 84c4e11f40..906b3b6015 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,9 @@ test_unit: test_cli: tests/cli/shunit2 # sudo apt-get install jq + ./tests/cli/keys.sh + ./tests/cli/rpc.sh + ./tests/cli/init.sh ./tests/cli/basictx.sh ./tests/cli/counter.sh ./tests/cli/restart.sh diff --git a/cmd/basecli/commands/auto.go b/client/commands/auto/cmd.go similarity index 97% rename from cmd/basecli/commands/auto.go rename to client/commands/auto/cmd.go index 040eb6ac38..704ba17851 100644 --- a/cmd/basecli/commands/auto.go +++ b/client/commands/auto/cmd.go @@ -1,4 +1,4 @@ -package commands +package auto import ( "os" diff --git a/client/commands/common.go b/client/commands/common.go new file mode 100644 index 0000000000..f14a11f384 --- /dev/null +++ b/client/commands/common.go @@ -0,0 +1,116 @@ +/* +Package commands contains any general setup/helpers valid for all subcommands +*/ +package commands + +import ( + "encoding/hex" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/tendermint/light-client/certifiers" + "github.com/tendermint/light-client/certifiers/client" + "github.com/tendermint/light-client/certifiers/files" + "github.com/tendermint/tmlibs/cli" + cmn "github.com/tendermint/tmlibs/common" + + rpcclient "github.com/tendermint/tendermint/rpc/client" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/modules/auth" +) + +var ( + trustedProv certifiers.Provider + sourceProv certifiers.Provider +) + +const ( + ChainFlag = "chain-id" + NodeFlag = "node" +) + +// AddBasicFlags adds --node and --chain-id, which we need for everything +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") +} + +// GetChainID reads ChainID from the flags +func GetChainID() string { + return viper.GetString(ChainFlag) +} + +// GetNode prepares a simple rpc.Client from the flags +func GetNode() rpcclient.Client { + return rpcclient.NewHTTP(viper.GetString(NodeFlag), "/websocket") +} + +// GetProviders creates a trusted (local) seed provider and a remote +// provider based on configuration. +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 +} + +// GetCertifier constructs a dynamic certifier from the config info +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 +} + +// ParseAddress parses an address of form: +// [:][:] +// into a basecoin.Actor. +// If app is not specified or "", then assume auth.NameSigs +func ParseAddress(input string) (res basecoin.Actor, err error) { + chain, app := "", auth.NameSigs + input = strings.TrimSpace(input) + spl := strings.SplitN(input, ":", 3) + + if len(spl) == 3 { + chain = spl[0] + spl = spl[1:] + } + if len(spl) == 2 { + if spl[0] != "" { + app = spl[0] + } + spl = spl[1:] + } + + addr, err := hex.DecodeString(cmn.StripHex(spl[0])) + if err != nil { + return res, errors.Errorf("Address is invalid hex: %v\n", err) + } + res = basecoin.Actor{ + ChainID: chain, + App: app, + Address: addr, + } + return +} diff --git a/client/commands/init.go b/client/commands/init.go new file mode 100644 index 0000000000..d53b03cab9 --- /dev/null +++ b/client/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/client/commands/proofs/get.go b/client/commands/proofs/get.go new file mode 100644 index 0000000000..c670e7b3ee --- /dev/null +++ b/client/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/client/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/client/commands/proofs/root.go b/client/commands/proofs/root.go new file mode 100644 index 0000000000..15b49b8c90 --- /dev/null +++ b/client/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/client/commands/proofs/state.go b/client/commands/proofs/state.go new file mode 100644 index 0000000000..6f7109d013 --- /dev/null +++ b/client/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/client/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/client/commands/proofs/tx.go b/client/commands/proofs/tx.go new file mode 100644 index 0000000000..7dc21af050 --- /dev/null +++ b/client/commands/proofs/tx.go @@ -0,0 +1,49 @@ +package proofs + +import ( + "github.com/spf13/cobra" + + "github.com/tendermint/basecoin/client/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/client/commands/proxy/root.go b/client/commands/proxy/root.go new file mode 100644 index 0000000000..0284f4dbe8 --- /dev/null +++ b/client/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/client/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/client/commands/rpc/helpers.go b/client/commands/rpc/helpers.go new file mode 100644 index 0000000000..5ef0f4f3ef --- /dev/null +++ b/client/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/client/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/client/commands/rpc/insecure.go b/client/commands/rpc/insecure.go new file mode 100644 index 0000000000..80432f95e7 --- /dev/null +++ b/client/commands/rpc/insecure.go @@ -0,0 +1,66 @@ +package rpc + +import ( + "github.com/spf13/cobra" + "github.com/tendermint/basecoin/client/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/client/commands/rpc/root.go b/client/commands/rpc/root.go new file mode 100644 index 0000000000..471744b2fe --- /dev/null +++ b/client/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/client/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/client/commands/rpc/secure.go b/client/commands/rpc/secure.go new file mode 100644 index 0000000000..d49504143c --- /dev/null +++ b/client/commands/rpc/secure.go @@ -0,0 +1,75 @@ +package rpc + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/basecoin/client/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/client/commands/seeds/export.go b/client/commands/seeds/export.go new file mode 100644 index 0000000000..a4c453526e --- /dev/null +++ b/client/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/client/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/client/commands/seeds/import.go b/client/commands/seeds/import.go new file mode 100644 index 0000000000..2731324d90 --- /dev/null +++ b/client/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/client/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/client/commands/seeds/root.go b/client/commands/seeds/root.go new file mode 100644 index 0000000000..b4fc9d66f6 --- /dev/null +++ b/client/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/client/commands/seeds/show.go b/client/commands/seeds/show.go new file mode 100644 index 0000000000..5ea8287e8a --- /dev/null +++ b/client/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/client/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/client/commands/seeds/update.go b/client/commands/seeds/update.go new file mode 100644 index 0000000000..575ede4ac2 --- /dev/null +++ b/client/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/client/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/client/commands/txs/helpers.go b/client/commands/txs/helpers.go new file mode 100644 index 0000000000..dd7999dd74 --- /dev/null +++ b/client/commands/txs/helpers.go @@ -0,0 +1,188 @@ +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" + + crypto "github.com/tendermint/go-crypto" + keycmd "github.com/tendermint/go-crypto/cmd" + "github.com/tendermint/go-crypto/keys" + lc "github.com/tendermint/light-client" + + ctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + "github.com/tendermint/basecoin/modules/auth" +) + +// Validatable represents anything that can be Validated +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 +} + +// GetSignerAct returns the address of the signer of the tx +// (as we still only support single sig) +func GetSignerAct() (res basecoin.Actor) { + // this could be much cooler with multisig... + signer := GetSigner() + if !signer.Empty() { + res = auth.SigPerm(signer.Address()) + } + return res +} + +// 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/client/commands/txs/presenter.go b/client/commands/txs/presenter.go new file mode 100644 index 0000000000..b13f694e5a --- /dev/null +++ b/client/commands/txs/presenter.go @@ -0,0 +1,34 @@ +package txs + +import ( + "github.com/pkg/errors" + wire "github.com/tendermint/go-wire" + "github.com/tendermint/light-client/proofs" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/tendermint/basecoin" +) + +// BaseTxPresenter this decodes all basecoin tx +type BaseTxPresenter struct { + proofs.RawPresenter // this handles MakeKey as hex bytes +} + +// ParseData - unmarshal raw bytes to a basecoin tx +func (BaseTxPresenter) ParseData(raw []byte) (interface{}, error) { + var tx basecoin.Tx + err := wire.ReadBinaryBytes(raw, &tx) + return tx, err +} + +// ValidateResult returns an appropriate error if the server rejected the +// tx in CheckTx or DeliverTx +func ValidateResult(res *ctypes.ResultBroadcastTxCommit) error { + if res.CheckTx.IsErr() { + return errors.Errorf("CheckTx: (%d): %s", res.CheckTx.Code, res.CheckTx.Log) + } + if res.DeliverTx.IsErr() { + return errors.Errorf("DeliverTx: (%d): %s", res.DeliverTx.Code, res.DeliverTx.Log) + } + return nil +} diff --git a/client/commands/txs/root.go b/client/commands/txs/root.go new file mode 100644 index 0000000000..965f6f259d --- /dev/null +++ b/client/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/client/commands/txs/wrapper.go b/client/commands/txs/wrapper.go new file mode 100644 index 0000000000..3d711f948b --- /dev/null +++ b/client/commands/txs/wrapper.go @@ -0,0 +1,45 @@ +package txs + +import ( + "github.com/spf13/pflag" + + "github.com/tendermint/basecoin" +) + +var ( + // Middleware must be set in main.go to defined the wrappers we should apply + Middleware Wrapper +) + +// Wrapper defines the information needed for each middleware package that +// wraps the data. They should read all configuration out of bounds via viper. +type Wrapper interface { + Wrap(basecoin.Tx) (basecoin.Tx, error) + Register(*pflag.FlagSet) +} + +// Wrappers combines a list of wrapper middlewares. +// The first one is the inner-most layer, eg. Fee, Nonce, Chain, Auth +type Wrappers []Wrapper + +var _ Wrapper = Wrappers{} + +// Wrap applies the wrappers to the passed in tx in order, +// aborting on the first error +func (ws Wrappers) Wrap(tx basecoin.Tx) (basecoin.Tx, error) { + var err error + for _, w := range ws { + tx, err = w.Wrap(tx) + if err != nil { + break + } + } + return tx, err +} + +// Register adds any needed flags to the command +func (ws Wrappers) Register(fs *pflag.FlagSet) { + for _, w := range ws { + w.Register(fs) + } +} diff --git a/cmd/basecli/commands/cmds.go b/cmd/basecli/commands/cmds.go deleted file mode 100644 index 5f5325489f..0000000000 --- a/cmd/basecli/commands/cmds.go +++ /dev/null @@ -1,111 +0,0 @@ -package commands - -import ( - "encoding/hex" - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/spf13/pflag" - - txcmd "github.com/tendermint/light-client/commands/txs" - cmn "github.com/tendermint/tmlibs/common" - - ctypes "github.com/tendermint/tendermint/rpc/core/types" - - "github.com/tendermint/basecoin" - "github.com/tendermint/basecoin/modules/auth" -) - -var ( - // Middleware must be set in main.go to defined the wrappers we should apply - Middleware Wrapper -) - -// Wrapper defines the information needed for each middleware package that -// wraps the data. They should read all configuration out of bounds via viper. -type Wrapper interface { - Wrap(basecoin.Tx) (basecoin.Tx, error) - Register(*pflag.FlagSet) -} - -// Wrappers combines a list of wrapper middlewares. -// The first one is the inner-most layer, eg. Fee, Nonce, Chain, Auth -type Wrappers []Wrapper - -var _ Wrapper = Wrappers{} - -// Wrap applies the wrappers to the passed in tx in order, -// aborting on the first error -func (ws Wrappers) Wrap(tx basecoin.Tx) (basecoin.Tx, error) { - var err error - for _, w := range ws { - tx, err = w.Wrap(tx) - if err != nil { - break - } - } - return tx, err -} - -// Register adds any needed flags to the command -func (ws Wrappers) Register(fs *pflag.FlagSet) { - for _, w := range ws { - w.Register(fs) - } -} - -// ValidateResult returns an appropriate error if the server rejected the -// tx in CheckTx or DeliverTx -func ValidateResult(res *ctypes.ResultBroadcastTxCommit) error { - if res.CheckTx.IsErr() { - return fmt.Errorf("CheckTx: (%d): %s", res.CheckTx.Code, res.CheckTx.Log) - } - if res.DeliverTx.IsErr() { - return fmt.Errorf("DeliverTx: (%d): %s", res.DeliverTx.Code, res.DeliverTx.Log) - } - return nil -} - -// ParseAddress parses an address of form: -// [:][:] -// into a basecoin.Actor. -// If app is not specified or "", then assume auth.NameSigs -func ParseAddress(input string) (res basecoin.Actor, err error) { - chain, app := "", auth.NameSigs - input = strings.TrimSpace(input) - spl := strings.SplitN(input, ":", 3) - - if len(spl) == 3 { - chain = spl[0] - spl = spl[1:] - } - if len(spl) == 2 { - if spl[0] != "" { - app = spl[0] - } - spl = spl[1:] - } - - addr, err := hex.DecodeString(cmn.StripHex(spl[0])) - if err != nil { - return res, errors.Errorf("Address is invalid hex: %v\n", err) - } - res = basecoin.Actor{ - ChainID: chain, - App: app, - Address: addr, - } - return -} - -// GetSignerAct returns the address of the signer of the tx -// (as we still only support single sig) -func GetSignerAct() (res basecoin.Actor) { - // this could be much cooler with multisig... - signer := txcmd.GetSigner() - if !signer.Empty() { - res = auth.SigPerm(signer.Address()) - } - return res -} diff --git a/cmd/basecli/commands/query.go b/cmd/basecli/commands/query.go deleted file mode 100644 index 2fd85a6c73..0000000000 --- a/cmd/basecli/commands/query.go +++ /dev/null @@ -1,20 +0,0 @@ -package commands - -import ( - wire "github.com/tendermint/go-wire" - "github.com/tendermint/light-client/proofs" - - "github.com/tendermint/basecoin" -) - -// BaseTxPresenter this decodes all basecoin tx -type BaseTxPresenter struct { - proofs.RawPresenter // this handles MakeKey as hex bytes -} - -// ParseData - unmarshal raw bytes to a basecoin tx -func (BaseTxPresenter) ParseData(raw []byte) (interface{}, error) { - var tx basecoin.Tx - err := wire.ReadBinaryBytes(raw, &tx) - return tx, err -} diff --git a/cmd/basecli/main.go b/cmd/basecli/main.go index be87d2d583..b827f7ffb3 100644 --- a/cmd/basecli/main.go +++ b/cmd/basecli/main.go @@ -8,15 +8,15 @@ 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/tmlibs/cli" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + "github.com/tendermint/basecoin/client/commands" + "github.com/tendermint/basecoin/client/commands/auto" + "github.com/tendermint/basecoin/client/commands/proofs" + "github.com/tendermint/basecoin/client/commands/proxy" + rpccmd "github.com/tendermint/basecoin/client/commands/rpc" + "github.com/tendermint/basecoin/client/commands/seeds" + txcmd "github.com/tendermint/basecoin/client/commands/txs" authcmd "github.com/tendermint/basecoin/modules/auth/commands" basecmd "github.com/tendermint/basecoin/modules/base/commands" coincmd "github.com/tendermint/basecoin/modules/coin/commands" @@ -56,19 +56,19 @@ func main() { coincmd.AccountQueryCmd, noncecmd.NonceQueryCmd, ) + proofs.TxPresenters.Register("base", txcmd.BaseTxPresenter{}) // set up the middleware - bcmd.Middleware = bcmd.Wrappers{ + txcmd.Middleware = txcmd.Wrappers{ feecmd.FeeWrapper{}, noncecmd.NonceWrapper{}, basecmd.ChainWrapper{}, authcmd.SigWrapper{}, } - bcmd.Middleware.Register(txs.RootCmd.PersistentFlags()) + txcmd.Middleware.Register(txcmd.RootCmd.PersistentFlags()) // you will always want this for the base send command - proofs.TxPresenters.Register("base", bcmd.BaseTxPresenter{}) - txs.RootCmd.AddCommand( + txcmd.RootCmd.AddCommand( // This is the default transaction, optional in your app coincmd.SendTxCmd, ) @@ -81,10 +81,10 @@ func main() { seeds.RootCmd, rpccmd.RootCmd, proofs.RootCmd, - txs.RootCmd, + txcmd.RootCmd, proxy.RootCmd, VersionCmd, - bcmd.AutoCompleteCmd, + auto.AutoCompleteCmd, ) cmd := cli.PrepareMainCmd(BaseCli, "BC", os.ExpandEnv("$HOME/.basecli")) diff --git a/docs/guide/counter/cmd/countercli/commands/counter.go b/docs/guide/counter/cmd/countercli/commands/counter.go index 22e56cf0ac..a8e473276d 100644 --- a/docs/guide/counter/cmd/countercli/commands/counter.go +++ b/docs/guide/counter/cmd/countercli/commands/counter.go @@ -4,10 +4,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - txcmd "github.com/tendermint/light-client/commands/txs" - "github.com/tendermint/basecoin" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" "github.com/tendermint/basecoin/docs/guide/counter/plugins/counter" "github.com/tendermint/basecoin/modules/coin" ) @@ -43,7 +41,7 @@ func counterTx(cmd *cobra.Command, args []string) error { return err } - tx, err = bcmd.Middleware.Wrap(tx) + tx, err = txcmd.Middleware.Wrap(tx) if err != nil { return err } @@ -53,7 +51,7 @@ func counterTx(cmd *cobra.Command, args []string) error { if err != nil { return err } - if err = bcmd.ValidateResult(bres); err != nil { + if err = txcmd.ValidateResult(bres); err != nil { return err } diff --git a/docs/guide/counter/cmd/countercli/commands/query.go b/docs/guide/counter/cmd/countercli/commands/query.go index 489a8a01d5..8debaf9c2b 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/client/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..8cc95f6677 100644 --- a/docs/guide/counter/cmd/countercli/main.go +++ b/docs/guide/counter/cmd/countercli/main.go @@ -6,14 +6,13 @@ 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/tmlibs/cli" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + "github.com/tendermint/basecoin/client/commands" + "github.com/tendermint/basecoin/client/commands/proofs" + "github.com/tendermint/basecoin/client/commands/proxy" + "github.com/tendermint/basecoin/client/commands/seeds" + txcmd "github.com/tendermint/basecoin/client/commands/txs" bcount "github.com/tendermint/basecoin/docs/guide/counter/cmd/countercli/commands" authcmd "github.com/tendermint/basecoin/modules/auth/commands" basecmd "github.com/tendermint/basecoin/modules/base/commands" @@ -50,17 +49,17 @@ func main() { ) // set up the middleware - bcmd.Middleware = bcmd.Wrappers{ + txcmd.Middleware = txcmd.Wrappers{ feecmd.FeeWrapper{}, noncecmd.NonceWrapper{}, basecmd.ChainWrapper{}, authcmd.SigWrapper{}, } - bcmd.Middleware.Register(txs.RootCmd.PersistentFlags()) + txcmd.Middleware.Register(txcmd.RootCmd.PersistentFlags()) // Prepare transactions - proofs.TxPresenters.Register("base", bcmd.BaseTxPresenter{}) - txs.RootCmd.AddCommand( + proofs.TxPresenters.Register("base", txcmd.BaseTxPresenter{}) + txcmd.RootCmd.AddCommand( // This is the default transaction, optional in your app coincmd.SendTxCmd, @@ -75,7 +74,7 @@ func main() { keycmd.RootCmd, seeds.RootCmd, proofs.RootCmd, - txs.RootCmd, + txcmd.RootCmd, proxy.RootCmd, ) 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/auth/commands/wrap.go b/modules/auth/commands/wrap.go index a02033c654..c93eecb702 100644 --- a/modules/auth/commands/wrap.go +++ b/modules/auth/commands/wrap.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/viper" "github.com/tendermint/basecoin" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" "github.com/tendermint/basecoin/modules/auth" ) @@ -17,7 +17,7 @@ const ( // SigWrapper wraps a tx with a signature layer to hold pubkey sigs type SigWrapper struct{} -var _ bcmd.Wrapper = SigWrapper{} +var _ txcmd.Wrapper = SigWrapper{} // Wrap will wrap the tx with OneSig or MultiSig depending on flags func (SigWrapper) Wrap(tx basecoin.Tx) (res basecoin.Tx, err error) { diff --git a/modules/base/commands/wrap.go b/modules/base/commands/wrap.go index a1042eb765..e199c4ed02 100644 --- a/modules/base/commands/wrap.go +++ b/modules/base/commands/wrap.go @@ -6,10 +6,10 @@ import ( "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/tendermint/light-client/commands" + "github.com/tendermint/basecoin/client/commands" "github.com/tendermint/basecoin" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" "github.com/tendermint/basecoin/modules/base" ) @@ -21,7 +21,7 @@ const ( // ChainWrapper wraps a tx with an chain info and optional expiration type ChainWrapper struct{} -var _ bcmd.Wrapper = ChainWrapper{} +var _ txcmd.Wrapper = ChainWrapper{} // Wrap will wrap the tx with a ChainTx from the standard flags func (ChainWrapper) Wrap(tx basecoin.Tx) (res basecoin.Tx, err error) { diff --git a/modules/coin/commands/query.go b/modules/coin/commands/query.go index 9a1308b2bb..8f5ef70409 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/client/commands" + proofcmd "github.com/tendermint/basecoin/client/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..9a338582c3 100644 --- a/modules/coin/commands/tx.go +++ b/modules/coin/commands/tx.go @@ -4,11 +4,9 @@ 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" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" "github.com/tendermint/basecoin/modules/coin" ) @@ -47,7 +45,7 @@ func doSendTx(cmd *cobra.Command, args []string) error { return err } - tx, err = bcmd.Middleware.Wrap(tx) + tx, err = txcmd.Middleware.Wrap(tx) if err != nil { return err } @@ -57,7 +55,7 @@ func doSendTx(cmd *cobra.Command, args []string) error { if err != nil { return err } - if err = bcmd.ValidateResult(bres); err != nil { + if err = txcmd.ValidateResult(bres); err != nil { return err } @@ -67,7 +65,7 @@ func doSendTx(cmd *cobra.Command, args []string) error { func readSendTxFlags() (tx basecoin.Tx, err error) { // parse to address - toAddr, err := bcmd.ParseAddress(viper.GetString(FlagTo)) + toAddr, err := commands.ParseAddress(viper.GetString(FlagTo)) if err != nil { return tx, err } @@ -98,7 +96,7 @@ func readSendTxFlags() (tx basecoin.Tx, err error) { func readFromAddr() (basecoin.Actor, error) { from := viper.GetString(FlagFrom) if from == "" { - return bcmd.GetSignerAct(), nil + return txcmd.GetSignerAct(), nil } - return bcmd.ParseAddress(from) + return commands.ParseAddress(from) } diff --git a/modules/fee/commands/wrap.go b/modules/fee/commands/wrap.go index c464c15c98..6f8c082f80 100644 --- a/modules/fee/commands/wrap.go +++ b/modules/fee/commands/wrap.go @@ -5,7 +5,8 @@ import ( "github.com/spf13/viper" "github.com/tendermint/basecoin" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" "github.com/tendermint/basecoin/modules/coin" "github.com/tendermint/basecoin/modules/fee" ) @@ -19,7 +20,7 @@ const ( // FeeWrapper wraps a tx with an optional fee payment type FeeWrapper struct{} -var _ bcmd.Wrapper = FeeWrapper{} +var _ txcmd.Wrapper = FeeWrapper{} // Wrap checks for FlagFee and if present wraps the tx with a // FeeTx of the given amount, paid by the signer @@ -52,7 +53,7 @@ func (FeeWrapper) Register(fs *pflag.FlagSet) { func readPayer() (basecoin.Actor, error) { payer := viper.GetString(FlagPayer) if payer == "" { - return bcmd.GetSignerAct(), nil + return txcmd.GetSignerAct(), nil } - return bcmd.ParseAddress(payer) + return commands.ParseAddress(payer) } diff --git a/modules/nonce/commands/query.go b/modules/nonce/commands/query.go index 4c4b6cff55..2bca14b99d 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/client/commands" + proofcmd "github.com/tendermint/basecoin/client/commands/proofs" "github.com/tendermint/basecoin/modules/nonce" "github.com/tendermint/basecoin/stack" diff --git a/modules/nonce/commands/wrap.go b/modules/nonce/commands/wrap.go index 7e29e8312a..bd3cc9999a 100644 --- a/modules/nonce/commands/wrap.go +++ b/modules/nonce/commands/wrap.go @@ -8,7 +8,8 @@ import ( "github.com/spf13/viper" "github.com/tendermint/basecoin" - bcmd "github.com/tendermint/basecoin/cmd/basecli/commands" + "github.com/tendermint/basecoin/client/commands" + txcmd "github.com/tendermint/basecoin/client/commands/txs" "github.com/tendermint/basecoin/modules/nonce" ) @@ -21,7 +22,7 @@ const ( // NonceWrapper wraps a tx with a nonce type NonceWrapper struct{} -var _ bcmd.Wrapper = NonceWrapper{} +var _ txcmd.Wrapper = NonceWrapper{} // Wrap grabs the sequence number from the flag and wraps // the tx with this nonce. Grabs the permission from the signer, @@ -49,7 +50,7 @@ func (NonceWrapper) Register(fs *pflag.FlagSet) { func readNonceKey() ([]basecoin.Actor, error) { nonce := viper.GetString(FlagNonceKey) if nonce == "" { - return []basecoin.Actor{bcmd.GetSignerAct()}, nil + return []basecoin.Actor{txcmd.GetSignerAct()}, nil } return parseActors(nonce) } @@ -57,7 +58,7 @@ func readNonceKey() ([]basecoin.Actor, error) { func parseActors(key string) (signers []basecoin.Actor, err error) { var act basecoin.Actor for _, k := range strings.Split(key, ",") { - act, err = bcmd.ParseAddress(k) + act, err = commands.ParseAddress(k) if err != nil { return } diff --git a/tests/cli/init.sh b/tests/cli/init.sh new file mode 100755 index 0000000000..a5a3bb387c --- /dev/null +++ b/tests/cli/init.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +CLIENT_EXE=basecli +SERVER_EXE=basecoin + +oneTimeSetUp() { + BASE=~/.bc_init_test + rm -rf "$BASE" + mkdir -p "$BASE" + + SERVER="${BASE}/server" + SERVER_LOG="${BASE}/${SERVER_EXE}.log" + + HEX="deadbeef1234deadbeef1234deadbeef1234aaaa" + ${SERVER_EXE} init ${HEX} --home="$SERVER" >> "$SERVER_LOG" + if ! assertTrue $?; then return 1; fi + + GENESIS_FILE=${SERVER}/genesis.json + CHAIN_ID=$(cat ${GENESIS_FILE} | jq .chain_id | tr -d \") + + printf "starting ${SERVER_EXE}...\n" + ${SERVER_EXE} start --home="$SERVER" >> "$SERVER_LOG" 2>&1 & + sleep 5 + PID_SERVER=$! + disown + if ! ps $PID_SERVER >/dev/null; then + echo "**STARTUP FAILED**" + cat $SERVER_LOG + return 1 + fi +} + +oneTimeTearDown() { + printf "\nstopping ${SERVER_EXE}..." + kill -9 $PID_SERVER >/dev/null 2>&1 + sleep 1 +} + +test01goodInit() { + export BCHOME=${BASE}/client-01 + assertFalse "ls ${BCHOME} 2>/dev/null >&2" + + echo y | ${CLIENT_EXE} init --node=tcp://localhost:46657 --chain-id="${CHAIN_ID}" > /dev/null + assertTrue "initialized light-client" $? + checkDir $BCHOME 3 +} + +test02badInit() { + export BCHOME=${BASE}/client-02 + assertFalse "ls ${BCHOME} 2>/dev/null >&2" + + # no node where we go + echo y | ${CLIENT_EXE} init --node=tcp://localhost:9999 --chain-id="${CHAIN_ID}" > /dev/null 2>&1 + assertFalse "invalid init" $? + # dir there, but empty... + checkDir $BCHOME 0 + + # try with invalid chain id + echo y | ${CLIENT_EXE} init --node=tcp://localhost:46657 --chain-id="bad-chain-id" > /dev/null 2>&1 + assertFalse "invalid init" $? + checkDir $BCHOME 0 + + # reject the response + echo n | ${CLIENT_EXE} init --node=tcp://localhost:46657 --chain-id="${CHAIN_ID}" > /dev/null 2>&1 + assertFalse "invalid init" $? + checkDir $BCHOME 0 +} + +test03noDoubleInit() { + export BCHOME=${BASE}/client-03 + assertFalse "ls ${BCHOME} 2>/dev/null >&2" + + # init properly + echo y | ${CLIENT_EXE} init --node=tcp://localhost:46657 --chain-id="${CHAIN_ID}" > /dev/null 2>&1 + assertTrue "initialized light-client" $? + checkDir $BCHOME 3 + + # try again, and we get an error + echo y | ${CLIENT_EXE} init --node=tcp://localhost:46657 --chain-id="${CHAIN_ID}" > /dev/null 2>&1 + assertFalse "warning on re-init" $? + checkDir $BCHOME 3 + + # unless we --force-reset + echo y | ${CLIENT_EXE} init --force-reset --node=tcp://localhost:46657 --chain-id="${CHAIN_ID}" > /dev/null 2>&1 + assertTrue "re-initialized light-client" $? + checkDir $BCHOME 3 +} + +test04acceptGenesisFile() { + export BCHOME=${BASE}/client-04 + assertFalse "ls ${BCHOME} 2>/dev/null >&2" + + # init properly + ${CLIENT_EXE} init --node=tcp://localhost:46657 --genesis=${GENESIS_FILE} > /dev/null 2>&1 + assertTrue "initialized light-client" $? + checkDir $BCHOME 3 +} + +# XXX Ex: checkDir $DIR $FILES +# Makes sure directory exists and has the given number of files +checkDir() { + assertTrue "ls ${1} 2>/dev/null >&2" + assertEquals "no files created" "$2" $(ls $1 | wc -l) +} + +# load and run these tests with shunit2! +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory +. $DIR/shunit2 diff --git a/tests/cli/keys.sh b/tests/cli/keys.sh new file mode 100755 index 0000000000..4cf9e61d12 --- /dev/null +++ b/tests/cli/keys.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +CLIENT_EXE=basecli + + +oneTimeSetUp() { + PASS=qwertyuiop + export BCHOME=$HOME/.bc_keys_test + ${CLIENT_EXE} reset_all + assertTrue $? +} + +newKey(){ + assertNotNull "keyname required" "$1" + KEYPASS=${2:-qwertyuiop} + echo $KEYPASS | ${CLIENT_EXE} keys new $1 >/dev/null 2>&1 + assertTrue "created $1" $? +} + +testMakeKeys() { + USER=demouser + assertFalse "already user $USER" "${CLIENT_EXE} keys get $USER" + newKey $USER + assertTrue "no user $USER" "${CLIENT_EXE} keys get $USER" +} + +# load and run these tests with shunit2! +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory +. $DIR/shunit2 diff --git a/tests/cli/rpc.sh b/tests/cli/rpc.sh new file mode 100755 index 0000000000..eb764089a5 --- /dev/null +++ b/tests/cli/rpc.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +CLIENT_EXE=basecli +SERVER_EXE=basecoin + +oneTimeSetUp() { + BASE=~/.bc_init_test + rm -rf "$BASE" + mkdir -p "$BASE" + + SERVER="${BASE}/server" + SERVER_LOG="${BASE}/${SERVER_EXE}.log" + + HEX="deadbeef1234deadbeef1234deadbeef1234aaaa" + ${SERVER_EXE} init ${HEX} --home="$SERVER" >> "$SERVER_LOG" + if ! assertTrue $?; then return 1; fi + + GENESIS_FILE=${SERVER}/genesis.json + CHAIN_ID=$(cat ${GENESIS_FILE} | jq .chain_id | tr -d \") + + printf "starting ${SERVER_EXE}...\n" + ${SERVER_EXE} start --home="$SERVER" >> "$SERVER_LOG" 2>&1 & + sleep 5 + PID_SERVER=$! + disown + if ! ps $PID_SERVER >/dev/null; then + echo "**STARTUP FAILED**" + cat $SERVER_LOG + return 1 + fi + + # this sets the base for all client queries in the tests + export BCHOME=${BASE}/client + ${CLIENT_EXE} init --node=tcp://localhost:46657 --genesis=${GENESIS_FILE} > /dev/null 2>&1 + if ! assertTrue "initialized light-client" "$?"; then + return 1 + fi +} + +oneTimeTearDown() { + printf "\nstopping ${SERVER_EXE}..." + kill -9 $PID_SERVER >/dev/null 2>&1 + sleep 1 +} + +test01getInsecure() { + GENESIS=$(${CLIENT_EXE} rpc genesis) + assertTrue "get genesis" "$?" + MYCHAIN=$(echo ${GENESIS} | jq .genesis.chain_id | tr -d \") + assertEquals "genesis chain matches" "${CHAIN_ID}" "${MYCHAIN}" + + STATUS=$(${CLIENT_EXE} rpc status) + assertTrue "get status" "$?" + SHEIGHT=$(echo ${STATUS} | jq .latest_block_height) + assertTrue "parsed status" "$?" + assertNotNull "has a height" "${SHEIGHT}" + + VALS=$(${CLIENT_EXE} rpc validators) + assertTrue "get validators" "$?" + VHEIGHT=$(echo ${VALS} | jq .block_height) + assertTrue "parsed validators" "$?" + assertTrue "sensible heights: $SHEIGHT / $VHEIGHT" "test $VHEIGHT -ge $SHEIGHT" + VCNT=$(echo ${VALS} | jq '.validators | length') + assertEquals "one validator" "1" "$VCNT" + + INFO=$(${CLIENT_EXE} rpc info) + assertTrue "get info" "$?" + DATA=$(echo $INFO | jq .response.data) + assertEquals "basecoin info" '"Basecoin v0.6.1"' "$DATA" +} + +test02getSecure() { + HEIGHT=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertTrue "get status" "$?" + + # check block produces something reasonable + assertFalse "missing height" "${CLIENT_EXE} rpc block" + BLOCK=$(${CLIENT_EXE} rpc block --height=$HEIGHT) + assertTrue "get block" "$?" + MHEIGHT=$(echo $BLOCK | jq .block_meta.header.height) + assertEquals "meta height" "${HEIGHT}" "${MHEIGHT}" + BHEIGHT=$(echo $BLOCK | jq .block.header.height) + assertEquals "meta height" "${HEIGHT}" "${BHEIGHT}" + + # check commit produces something reasonable + assertFalse "missing height" "${CLIENT_EXE} rpc commit" + let "CHEIGHT = $HEIGHT - 1" + COMMIT=$(${CLIENT_EXE} rpc commit --height=$CHEIGHT) + assertTrue "get commit" "$?" + HHEIGHT=$(echo $COMMIT | jq .header.height) + assertEquals "commit height" "${CHEIGHT}" "${HHEIGHT}" + assertEquals "canonical" "true" $(echo $COMMIT | jq .canonical) + BSIG=$(echo $BLOCK | jq .block.last_commit) + CSIG=$(echo $COMMIT | jq .commit) + assertEquals "block and commit" "$BSIG" "$CSIG" + + # now let's get some headers + # assertFalse "missing height" "${CLIENT_EXE} rpc headers" + HEADERS=$(${CLIENT_EXE} rpc headers --min=$CHEIGHT --max=$HEIGHT) + assertTrue "get headers" "$?" + assertEquals "proper height" "$HEIGHT" $(echo $HEADERS | jq '.last_height') + assertEquals "two headers" "2" $(echo $HEADERS | jq '.block_metas | length') + # should we check these headers? + CHEAD=$(echo $COMMIT | jq .header) + # most recent first, so the commit header is second.... + HHEAD=$(echo $HEADERS | jq .block_metas[1].header) + assertEquals "commit and header" "$CHEAD" "$HHEAD" +} + +test03waiting() { + START=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertTrue "get status" "$?" + + let "NEXT = $START + 5" + assertFalse "no args" "${CLIENT_EXE} rpc wait" + assertFalse "too long" "${CLIENT_EXE} rpc wait --height=1234" + assertTrue "normal wait" "${CLIENT_EXE} rpc wait --height=$NEXT" + + STEP=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertEquals "wait until height" "$NEXT" "$STEP" + + let "NEXT = $STEP + 3" + assertTrue "${CLIENT_EXE} rpc wait --delta=3" + STEP=$(${CLIENT_EXE} rpc status | jq .latest_block_height) + assertEquals "wait for delta" "$NEXT" "$STEP" +} + +# load and run these tests with shunit2! +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory +. $DIR/shunit2